[
  {
    "path": ".dockerignore",
    "content": ".git\n.env\n.github\n.vscode\nscripts/demo\ne2e\nelectron\nreleases\n**/node_modules\n**/dist\n**/prostgles_backups\n**/prostgles_media\n**/prostgles_storage\n**/prostgles_mcp"
  },
  {
    "path": ".eslintrc.common.js",
    "content": "/**\n * @type {import('eslint').Linter.Config}\n */\nmodule.exports = {\n  // \"root\": true,\n  parserOptions: {\n    ecmaVersion: \"latest\",\n    sourceType: \"module\",\n    allowImportExportEverywhere: true,\n    project: [\"./tsconfig.json\"],\n    // \"tsconfigRootDir\": __dirname,\n  },\n  // \"ingorePatterns\": \"**/*.d.ts, **/*.js\",\n\n  ignores: [\n    \"node_modules\",\n    \"dist\",\n    \"examples\",\n    \"**/*.d.ts\",\n    \"**/*.js\",\n    \"tests\",\n    \".eslintrc.js\",\n    \".eslint.config.js\",\n    \"*.json\",\n    \"**/*.json\",\n  ],\n  parser: \"@typescript-eslint/parser\",\n  plugins: [\"@typescript-eslint\"],\n  extends: [\"eslint:recommended\", \"plugin:@typescript-eslint/recommended\"],\n  rules: {\n    \"@typescript-eslint/no-misused-spread\": [\n      \"error\",\n      {\n        allow: [\n          \"CSSStyleDeclaration\",\n          \"Partial<CSSStyleDeclaration>\",\n          \"DOMStringMap\",\n        ],\n      },\n    ],\n    \"no-cond-assign\": \"error\",\n    \"@typescript-eslint/no-namespace\": \"off\",\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n    \"@typescript-eslint/no-non-null-assertion\": \"off\",\n    \"@typescript-eslint/ban-types\": \"off\",\n    \"@typescript-eslint/ban-ts-comment\": \"off\",\n    \"no-async-promise-executor\": \"off\",\n    \"@typescript-eslint/no-var-requires\": \"off\",\n    \"@typescript-eslint/no-unused-vars\": \"off\",\n    \"@typescript-eslint/no-empty-function\": \"off\",\n    \"@typescript-eslint/prefer-promise-reject-errors\": \"off\",\n    \"no-unused-vars\": \"off\",\n    \"no-empty\": \"off\",\n    \"no-constant-condition\": \"error\",\n    \"@typescript-eslint/no-unnecessary-condition\": \"error\",\n    \"@typescript-eslint/consistent-type-imports\": [\n      \"error\",\n      {\n        disallowTypeAnnotations: false,\n      },\n    ],\n    quotes: [\n      \"error\",\n      \"double\",\n      {\n        avoidEscape: true,\n        allowTemplateLiterals: true,\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Never modify line endings of bash scripts\n*.sh -crlf"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/BUG.yml",
    "content": "name: Bug Report\ndescription: File a bug report\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"triage\"]\nassignees:\n  - octocat\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: Also tell us, what did you expect to happen?\n      placeholder: Tell us what you see!\n      value: \"A bug happened!\"\n    validations:\n      required: true\n  - type: dropdown\n    id: product\n    attributes:\n      label: Which product are you seeing the problem on?\n      multiple: true\n      options:\n        - UI\n        - Prostgles-Desktop\n  - type: dropdown\n    id: os\n    attributes:\n      label: What OS are you seeing the problem on?\n      multiple: true\n      options:\n        - Android\n        - iOS\n        - Windows\n        - Linux\n  - type: dropdown\n    id: browsers\n    attributes:\n      label: What browsers are you seeing the problem on?\n      multiple: true\n      options:\n        - Firefox\n        - Chrome\n        - Safari\n        - Microsoft Edge\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/CUSTOM.yml",
    "content": "name: Custom issue\ndescription: File a feature request\ntitle: \"[Title]\"\nassignees:\n  - prostgles\nbody:\n  - type: textarea\n    id: details\n    attributes:\n      label: Issue details\n      value: \"Description ...\"\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/FEATURE.yml",
    "content": "name: Feature request\ndescription: File a feature request\ntitle: \"[Feature]: \"\nlabels: [\"feature\"]\nassignees:\n  - prostgles\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this feature request!\n  - type: textarea\n    id: details\n    attributes:\n      label: Requested Behavior\n      description: What do you want to be able to do\n      value: \"I need to \"\n    validations:\n      required: true\n  - type: textarea\n    id: current-approach\n    attributes:\n      label: How to achieve this now\n      description: Current work arounds to accomplish requested behavior\n      value: \"You have to \"\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "### Actual Behavior\n\n### Expected Behavior\n\n### Steps to reproduce the issue\n\n#### Data used\n\n#### Config\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/custom.md",
    "content": "## Issue title\n\n## Details\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "## Requested Behavior\n\n## Use Cases\n\n## Current work arounds to accomplish requested behavior\n"
  },
  {
    "path": ".github/workflows/docker_test.yml",
    "content": "name: Test docker scripts\non:\n  pull_request:\n    branches: [main, master]\n  merge_group:\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\njobs:\n  test_docker:\n    name: Test docker scripts\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Docker run\n        run: |\n          docker run -d -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=postgres postgres\n          TEST_SCRIPT=$(awk '/```docker-run.sh/{f=1;next} /```/{f=0} f' README.md)\n          echo $TEST_SCRIPT\n          eval \"$TEST_SCRIPT\"\n      - name: Test Docker run\n        run: |\n          sleep 15\n          docker ps \n          pg_container_name=$(docker ps --format '{{.Names}}\\t{{.Image}}' | grep 'prostgles' | head -1 | awk '{print $1}')\n          echo $pg_container_name\n          eval \"docker logs $pg_container_name\"\n          result=$(curl -b -i -L  -v  localhost:3004)\n          echo $result\n          echo $result | grep -q \"Prostgles UI\"; echo $?\n          docker stop $(docker ps -a -q)\n\n      - name: Docker compose\n        run: |\n          TEST_SCRIPT=$(awk '/```docker-compose.sh/{f=1;next} /```/{f=0} f' README.md)\n          echo $TEST_SCRIPT\n          eval \"$TEST_SCRIPT\"\n      - name: Test Docker compose\n        run: |\n          sleep 10\n          docker ps\n          result=$(curl -b -i -L  -v  localhost:3004)\n          echo $result | grep -q \"Prostgles UI\"; echo $?\n"
  },
  {
    "path": ".github/workflows/electron_build_linux.yml",
    "content": "name: Electron build & test linux\non: workflow_dispatch\njobs:\n  electron_build:\n    timeout-minutes: 20\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - name: Install dependencies, build and test\n        run: |\n          sudo apt-get update && \\\n          cd electron && npm run build-linux\n\n      - name: Install dependencies for testing\n        run: |\n          sudo apt-get update\n          cd electron && sudo apt-get install -y x11-xserver-utils scrot libnotify4  libnss3-dev libatk1.0-0 libatk-bridge2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 gconf-service gconf2\n          DEBIAN_FRONTEND=noninteractive sudo apt install -y x11-apps xvfb gdebi-core\n      - name: Install the app\n        run: |\n          cd electron\n          sha256sum ./dist/*.deb > ./dist/checksum.txt\n          cat ./dist/checksum.txt\n          sudo dpkg -i ./dist/*.deb\n      - name: Test installation file\n        run: |\n          cd electron\n          xvfb-run --server-num=98 --server-args=\"-screen 0 1920x1080x24\" -- bash -c \"QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu prostgles-desktop & sleep 5 && scrot ./dist/screenshot.png && exit\" && exit\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: installers\n          path: electron/dist\n          compression-level: 0\n          retention-days: 10\n"
  },
  {
    "path": ".github/workflows/electron_build_macos.yml",
    "content": "name: Electron build & test macOs\non: workflow_dispatch\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\njobs:\n  electron_build_macos:\n    timeout-minutes: 25\n    runs-on: macos-12-large\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 22 # node 20 is too slow\n      - name: Install dependencies\n        run: |\n          cd electron \n          npm run build-macos\n          rm -rf ./dist/mac-universal\n      - name: Test DMG file installations and run\n        run: |\n          shasum -a 256 electron/dist/*.dmg > electron/dist/checksum.txt\n          cat electron/dist/checksum.txt\n          hdiutil attach electron/dist/*.dmg\n          ls -la /Volumes\n          sudo cp -r /Volumes/Prostgles*/*.app /Applications\n          open /Applications/Prostgles*.app\n          sleep 10\n          screencapture electron/dist/picture.png\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: installers\n          path: electron/dist\n          compression-level: 0\n          retention-days: 10\n"
  },
  {
    "path": ".github/workflows/electron_build_windows.yml",
    "content": "name: Electron build & test windows\non: workflow_dispatch\njobs:\n  electron_build_windows:\n    timeout-minutes: 12\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - name: Build\n        run: |\n          cd electron && npm run build-win\n\n      - name: Test build\n        shell: cmd\n        run: |\n          cd electron\\dist\n          for /r %%f in (*.exe) DO certutil -hashfile \"%%f\" SHA256 > windows.checksums\n          type windows.checksums\n          for /r %%f in (*.exe) DO \"%%f\" /S --force-run\n          cd ..\n          waitfor SomethingThatIsNeverHappening /t 3 2>NUL\n          call screenCapture.bat .\\dist\\screen1.png\n\n      - uses: actions/upload-artifact@v3\n        if: always()\n        with:\n          name: installers\n          path: electron/dist\n          compression-level: 0\n          retention-days: 10\n"
  },
  {
    "path": ".github/workflows/electron_linux_test.yml",
    "content": "name: Electron test linux (deprecated)\non: workflow_dispatch\njobs:\n  electron_build:\n    timeout-minutes: 20\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - name: Install dependencies, build and test\n        run: |\n          sudo apt-get install xvfb -y &&\n          cd electron && npm run test-linux\n\n      - uses: actions/upload-artifact@v3\n        if: always()\n        with:\n          name: installers\n          path: electron/dist\n          retention-days: 10\n"
  },
  {
    "path": ".github/workflows/electron_macos_test.yml",
    "content": "name: Electron test macos (deprecated)\non: workflow_dispatch\njobs:\n  electron_build:\n    timeout-minutes: 20\n    runs-on: macos-12-large\n\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - name: Install dependencies, build and test\n        run: |\n          sleep 60 && \\\n          screencapture -v electron/dist/vid.mov & \\\n          cd electron && npm run test-macos\n\n      - uses: actions/upload-artifact@v3\n        if: always()\n        with:\n          name: test-results\n          path: electron/dist\n          retention-days: 10\n"
  },
  {
    "path": ".github/workflows/linux_test.yml",
    "content": "name: Linux & Electron test\non:\n  pull_request:\n    branches: [main, master]\n  merge_group:\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\njobs:\n  test_linux:\n    runs-on: ubuntu-latest\n    timeout-minutes: 45\n    services:\n      postgres:\n        image: postgis/postgis:15-3.3\n        env:\n          POSTGRES_DB: db\n          POSTGRES_PASSWORD: psw\n          POSTGRES_USER: usr\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 10\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n          fetch-depth: 2 # Fetch at least 2 commits to ensure doc checks work\n\n      - name: Install curl inside postgres container for video needed for demo\n        run: |\n          pg_container_name=$(docker ps --format '{{.Names}}\\t{{.Image}}' | grep 'postgis' | head -1 | awk '{print $1}') && \\\n          echo $pg_container_name && \\\n          docker ps -a && \\\n          docker exec $pg_container_name /bin/sh -c \"apt-get update && apt install curl -y\"\n\n      - name: Wait for PostgreSQL to be ready and create prostgles_desktop_db\n        run: |\n          until pg_isready -h localhost -U usr -d db; do\n            sleep 1\n          done\n          PGPASSWORD=psw psql -h localhost -U usr -d db -c \"CREATE DATABASE prostgles_desktop_db;\"\n\n      - name: Install dependencies (psql with pg_dump)\n          pg_dump version must be >= pg servers version to ensure backup/restore works\n        run: |\n          cd client && npm ci && \\\n          cd ../server && npm ci && \\\n          cd ../e2e && npm ci && npx playwright install && \\\n          sudo sh -c 'echo \"deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main\" > /etc/apt/sources.list.d/pgdg.list' && \\\n          sudo apt-get -y install wget ca-certificates && \\\n          wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - && \\\n          sudo apt-get update && \\\n          sudo apt-get -y install postgresql-client-16\n          # Install uv tool for MCP tools\n          curl -LsSf https://astral.sh/uv/install.sh | sh\n          npm cache clean --force\n\n      - name: Run test\n        run: |\n          npm test\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: |\n            e2e/electron-report/\n            e2e/playwright-report/\n          retention-days: 10\n  test_desktop_on_macos:\n    runs-on: macos-latest\n    timeout-minutes: 15\n    if: success()\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Install curl inside postgres container for video needed for demo\n        run: |\n          ls -la /Applications/\n\n      - name: Install dependencies (psql with pg_dump) pg_dump version must be >= pg servers version to ensure backup/restore works\n        run: |\n          /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\" && \\\n          brew install postgresql@14\n          brew services restart postgresql@14\n          export PATH=\"/opt/homebrew/opt/postgresql@14/bin:$PATH\"\n          /opt/homebrew/opt/postgresql@14/bin/psql --version\n          /opt/homebrew/opt/postgresql@14/bin/pg_dump --version\n          until pg_isready\n          do\n            echo \"Waiting for postgres to start...\"\n            sleep 1\n          done\n          /opt/homebrew/opt/postgresql@14/bin/createuser -s postgres\n          sudo psql -U postgres -c \"CREATE USER usr WITH LOGIN SUPERUSER ENCRYPTED PASSWORD 'psw'\"\n          sudo psql -U postgres -c \"CREATE DATABASE prostgles_desktop_db OWNER usr\"\n          brew install postgis\n          brew services restart postgresql \n\n          psql --version\n          cd electron\n          npm run test-macos\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report-electron\n          path: e2e/electron-report/\n          retention-days: 10\n"
  },
  {
    "path": ".github/workflows/macos_test.yml",
    "content": "name: MacOS test\non: workflow_dispatch\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\njobs:\n  test_macos:\n    runs-on: macos-latest\n    timeout-minutes: 30\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Install curl inside postgres container for video needed for demo\n        run: |\n          ls -la /Applications/\n\n      - name: Install dependencies (psql with pg_dump) pg_dump version must be >= pg servers version to ensure backup/restore works\n        run: |\n          /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\" && \\\n          brew install postgresql@14\n          brew services restart postgresql@14\n          export PATH=\"/opt/homebrew/opt/postgresql@14/bin:$PATH\"\n          /opt/homebrew/opt/postgresql@14/bin/psql --version\n          /opt/homebrew/opt/postgresql@14/bin/pg_dump --version\n          until pg_isready\n          do\n            echo \"Waiting for postgres to start...\"\n            sleep 1\n          done\n          /opt/homebrew/opt/postgresql@14/bin/createuser -s postgres\n          sudo psql -U postgres -c \"CREATE USER usr WITH LOGIN SUPERUSER ENCRYPTED PASSWORD 'psw'\"\n          sudo psql -U postgres -c \"CREATE DATABASE db OWNER usr\"\n          brew install postgis\n          brew services restart postgresql \n\n          psql --version\n          cd client && npm ci && \\\n          cd ../server && npm ci && \\\n          cd ../e2e && npm ci && npx playwright install\n      - name: Run test\n        run: |\n          npm test\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: e2e/playwright-report/\n          retention-days: 10\n\n          # brew install pcre\n          # sudo ln -s /opt/homebrew/Cellar/postgresql@16/16.0/bin/postgres /usr/local/bin/postgres\n          # wget https://download.osgeo.org/postgis/source/postgis-3.4.0.tar.gz\n          # tar -xvzf postgis-3.4.0.tar.gz\n          # rm postgis-3.4.0.tar.gz\n          # cd postgis-3.4.0\n          # ./configure --with-projdir=/opt/homebrew/opt/proj --with-protobufdir=/opt/homebrew/opt/protobuf-c --with-pgconfig=/opt/homebrew/opt/postgresql@16/bin/pg_config --with-jsondir=/opt/homebrew/opt/json-c --with-sfcgal=/opt/homebrew/opt/sfcgal/bin/sfcgal-config --with-pcredir=/opt/homebrew/opt/pcre \"LDFLAGS=$LDFLAGS -L/opt/homebrew/Cellar/gettext/0.22.2/lib\" \"CFLAGS=-I/opt/homebrew/Cellar/gettext/0.22.2/include\"\n          # make\n          # make install\n\n          # export PATH=\"/opt/homebrew/opt/postgresql@16/bin:$PATH\" && \\\n"
  },
  {
    "path": ".github/workflows/on_release.yml",
    "content": "name: On Release\non:\n  push:\n    tags:\n      - \"v*\"\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\njobs:\n  build_docker_images:\n    timeout-minutes: 20\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false # Optional: Prevent other matrix jobs from being cancelled if one fails\n      matrix:\n        include: # Define the specific combinations you need\n          - image_name: ui\n            dockerfile: Dockerfile\n          - image_name: ui-db\n            dockerfile: DB-Dockerfile\n          # Add more entries here if you have more images to build\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Extract version\n        id: extract_version\n        run: |\n          VERSION=$(./scripts/get_version.sh)\n          echo \"version=v$VERSION\" >> $GITHUB_OUTPUT\n          echo \"Extracted version: v$VERSION\"\n\n      - name: Ensure release file exists\n        run: |\n          pr_version=$(./scripts/get_version.sh) \n          base_ref=\"${{ github.base_ref }}\"\n          release_file_path=\"./releases/v${pr_version}.md\"\n\n          echo \"Checking for release file: ${release_file_path}\"\n\n          if [ -f \"$release_file_path\" ]; then\n            echo \"Release file found: ${release_file_path}\"\n          else\n\n            echo \"Release file not found for v${pr_version}\"\n            echo \"Ensure this file exists: ${release_file_path}\"\n\n            exit 1\n          fi\n\n      - name: Ensure docker-compose.yml image tags are updated\n        run: |\n          pr_version=$(./scripts/get_version.sh) \n          expected_tag=\"v${pr_version}\"\n          docker_compose_file=\"./docker-compose.yml\"\n\n          echo \"Checking docker-compose.yml for correct image tags...\"\n\n          if grep -q \"image: prostgles/ui:${expected_tag}\" \"$docker_compose_file\" && \\\n             grep -q \"image: prostgles/ui-db:${expected_tag}\" \"$docker_compose_file\"; then\n            echo \"docker-compose.yml uses the correct image tags: ${expected_tag}\"\n          else\n            echo \"docker-compose.yml does not use the correct image tags.\"\n            echo \"Expected tags: prostgles/ui:${expected_tag} and prostgles/ui-db:${expected_tag}\"\n            exit 1\n          fi\n\n      - name: Create Release with Notes\n        uses: softprops/action-gh-release@v2\n        with:\n          body_path: ./releases/${{ steps.extract_version.outputs.version }}.md\n          draft: false\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build and push ui\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ${{ matrix.dockerfile }}\n          platforms: linux/amd64\n          push: true\n          tags: |\n            prostgles/${{ matrix.image_name }}:latest\n            prostgles/${{ matrix.image_name }}:${{ steps.extract_version.outputs.version }}\n\n  build_linux:\n    timeout-minutes: 20\n    runs-on: ubuntu-latest\n    needs: build_docker_images\n    if: success()\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Build Linux\n        run: |\n          sudo apt-get install rpm -y && \\\n          cd electron && npm run build-linux\n          cd dist\n          sha256sum *.* > linux.checksums\n          echo linux.checksums\n      - name: Release Linux\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            electron/dist/**/*.deb\n            electron/dist/**/*.rpm\n            electron/dist/**/*.AppImage\n            electron/dist/**/*.checksums\n  build_windows:\n    timeout-minutes: 20\n    needs: build_docker_images\n    runs-on: windows-latest\n    if: success()\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Build Windows\n        shell: cmd\n        run: |\n          cd electron \n          npm run build-win\n          cd dist\n          for /r %%f in (*.exe) DO certutil -hashfile \"%%f\" SHA256 > windows.checksums\n          type windows.checksums\n      - name: Release Windows\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            electron/dist/**/*.exe\n            electron/dist/**/*.checksums\n  build_macos:\n    timeout-minutes: 20\n    needs: build_docker_images\n    runs-on: macos-latest\n    if: success()\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n      - name: Build MacOS\n        run: |\n          cd electron \n          npm run build-macos\n          rm -rf ./dist/mac-universal\n          cd dist\n          shasum -a 256 *.* > macos.checksums\n          cat macos.checksums\n      - name: Release MacOS\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            electron/dist/**/*.dmg\n            electron/dist/**/*.checksums\n"
  },
  {
    "path": ".github/workflows/package_version_increased.yml",
    "content": "name: Ensure package version is higher\non:\n  pull_request:\n    branches: [main, master]\n  merge_group:\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\njobs:\n  test_docker:\n    name: Run test suite\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Get versions\n        id: pr-version\n        run: |\n          sudo apt  install jq\n          ./scripts/get_version.sh > pr_version.txt\n          git fetch origin ${{ github.base_ref }} --depth=1\n          git checkout origin/${{ github.base_ref }} -- package.json ./electron/package.json\n          ./scripts/get_version.sh > current_version.txt\n\n      - name: Compare versions\n        run: |\n          current_version=$(cat current_version.txt)\n          pr_version=$(cat pr_version.txt) \n          if [ \"$(printf '%s\\n' \"$current_version\" \"$pr_version\" | sort -V | head -n1)\" = \"$pr_version\" ]; then\n            echo \"Version in package.json is not greater than the version in the base branch: $current_version <= $pr_version\"\n            exit 1\n          else\n            echo \"Version check passed: $current_version <= $pr_version\"\n          fi\n\n      - name: Ensure Changelog exists\n        if: success() # Ensure version comparison passed\n        run: |\n          pr_version=$(cat pr_version.txt) \n          base_ref=\"${{ github.base_ref }}\"\n          release_file_path=\"./changelog/v${pr_version}.md\"\n\n          echo \"Checking for changelog file: ${release_file_path}\"\n\n          if [ -f \"$release_file_path\" ]; then\n            echo \"Changelog file found: ${release_file_path}\"\n          else\n\n            echo \"Changelog file not found for v${pr_version}.\"\n            echo \"Ensure this file exists: ${release_file_path}\"\n\n            exit 1\n          fi\n"
  },
  {
    "path": ".github/workflows/windows_test.yml",
    "content": "name: Windows test\non: workflow_dispatch\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\njobs:\n  test_windows:\n    runs-on: windows-latest\n    timeout-minutes: 30\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - uses: nyurik/action-setup-postgis@v2\n        with:\n          username: postgres\n          password: postgres\n          database: postgres\n          port: 5432\n        id: postgres\n      - name: Install postgres\n        run: |\n          cd \"C:\\Program Files\\PostgreSQL\\14\\bin\"\n          .\\psql.exe -c \"CREATE EXTENSION postgis;\" \"postgresql://postgres:postgres@127.0.0.1:5432/postgres\"\n          .\\psql.exe -c \"CREATE USER usr WITH LOGIN SUPERUSER ENCRYPTED PASSWORD 'psw'\" \"postgresql://postgres:postgres@127.0.0.1:5432/postgres\"\n          .\\psql.exe -c \"CREATE DATABASE db OWNER usr\" \"postgresql://postgres:postgres@127.0.0.1:5432/postgres\"\n\n      - name: Install test dependencies\n        run: |\n          npm config set script-shell \"C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe\"\n          cd client\n          npm ci\n          cd ../server \n          npm ci\n          cd ../e2e \n          npm ci \n          npx playwright install\n      - name: Run test\n        run: |\n          npm test\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: e2e/playwright-report/\n          retention-days: 10\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\nprostgles_media\nprostgles_storage\nprostgles_backups\nprostgles_certificates\nprostgles_mcp\n.electron-auth.json\n**/.electron-auth.json\n.prostgles-desktop-config.json\n**/.prostgles-desktop-config.json\n\nscripts/demo/dist\nclient/build\nclient/static/icons\nserver/dist\nserver/media\n\nBACKUP\n.Trash-1000/\n.npmrc\n.npmignore\nmedia\nlog.txt\nui\n.aider*\n.env\nconfigs\ndebug\ndocs/screenshots/svgif-scenes"
  },
  {
    "path": ".prettierignore",
    "content": "e2e/playwright-report/trace/\nserver/*.json\nclient/*.json\n.vscode/\n*.d.ts\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"experimentalTernaries\": true\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"git.terminalAuthentication\": false,\n  \"git.autofetch\": false,\n  \"git.ignoreLimitWarning\": true,\n  \"typescript.tsdk\": \"./node_modules/typescript/lib\",\n  \"editor.formatOnSave\": true,\n  \"prettier.experimentalTernaries\": true,\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"prettier.printWidth\": 100\n}\n"
  },
  {
    "path": "DB-Dockerfile",
    "content": "FROM postgis/postgis:17-3.4\n\n# Switch to root user to install packages\nUSER root\n\n# procps needed for stat monitoring\nRUN apt-get update && apt-get install -y procps && \\\n    rm -rf /var/lib/apt/lists/*\n\n# Switch back to the default postgres user\nUSER postgres "
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:20-slim AS base\n\nWORKDIR /usr/src/app\n\nCOPY . .\n\n# Install latest pg_dump (psql v17) to ensure backup/restore works\nRUN apt-get update && \\\n    apt-get install -y --no-install-recommends gnupg wget ca-certificates lsb-release && \\ \n    apt-get upgrade -y && \\\n    sh -c 'echo \"deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main\" > /etc/apt/sources.list.d/pgdg.list'  && \\\n    wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \\\n    apt-get update && \\\n    apt-get install -y --no-install-recommends postgresql-client-17 && \\\n    pg_dump --version && \\\n    psql --version && \\\n    # Clean up\n    apt-get clean && \\\n    apt-get purge -y --auto-remove gnupg wget lsb-release && \\\n    rm -rf /var/lib/apt/lists/*\n\nFROM base AS deps \n\nWORKDIR /usr/src/app/client\n\nRUN npm run build && cd ../server && npm run build\n\nENV NODE_ENV=production\nENV IS_DOCKER=yes\n\nCMD [\"node\", \"/usr/src/app/server/dist/server/src/index.js\"]"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    Prostgles Desktop\n    Copyright (C) 2024  Stefan L\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "PRIVACY",
    "content": "<p>\nWith the exception of information you voluntarily submit via the \"Send feedback\" feature, Prostgles does not collect, transmit, or store any of your data on our servers.\n</p>\n\n<p>All your data is stored only in the following locations, depending on how you use the software:</p>\n<ol>\n  <li>\n    <strong>Your connected database:</strong> Metadata such as database connections, dashboard settings, SQL queries, etc., are stored within the database you provide and connect to. Depending on the variant:\n    <ul>\n      <li><strong>Electron/Desktop app:</strong> Data is in the connection titled <strong>Prostgles Desktop state</strong>.</li>\n      <li><strong>Source/Docker/Node.js app:</strong> Data is in the connection titled <strong>Prostgles UI state</strong>.</li>\n    </ul>\n  </li>\n  <li>\n    <strong>Your local machine:</strong> \n    <ul>\n      <li><strong>Electron/Desktop app:</strong> Stores files, backups and encrypted state database connection details in your user data directory (Windows: <code>%APPDATA%/prostgles-desktop</code>, macOS: <code>~/Library/Application Support/prostgles-desktop</code>, Linux: <code>~/.config/prostgles-desktop</code>).</li>\n      <li><strong>Source/Docker/Node.js app:</strong> Stores files, backups and state state database configuration in the directory where the software is run (current working directory) unless otherwise configured.</li>\n    </ul>\n  </li>\n  <li>\n    <strong>Optional cloud storage providers:</strong> If you configure file storage or backups, these providers store your data according to your setup.\n  </li>\n</ol>\n\n<p>\nTo the best of our knowledge, all third-party libraries used in Prostgles do not collect, transmit, or store your data on their servers.\n</p>\n"
  },
  {
    "path": "README.md",
    "content": "# Prostgles UI\n\nSQL Editor and internal tool builder for Postgres\n\n[Live demo](https://playground.prostgles.com/)\n\n### Screenshots\n\n[More](https://prostgles.com/ui)\n\n<p float=\"left\">\n  <img src=\"https://prostgles.com/static/images/screenshot_crypto.png\" width=\"100%\"/>  \n</p>\n\n### Features\n\n- SQL Editor with context-aware schema auto-completion and documentation extracts and hints\n- Realtime data exploration dashboard with versatile layout system (tab and side-by-side view)\n- Table view with controls to view related data, sort, filter and cross-filter\n- Map and Time charts with aggregations\n- Data insert/update forms with autocomplete\n- Role based access control rules\n- Isomorphic TypeScript API with schema types, end to end type safety and React hooks\n- File upload (locally or to cloud)\n- Search all tables from public schema\n- Media file display (audio/video/image/html/svg)\n- Data import (CSV, JSON and GeoJSON)\n- Backup/Restore (locally or to cloud)\n- TypeScript server-side functions (experimental)\n- Mobile friendly\n- LISTEN NOTIFY support\n\n### Installation - Docker compose (recommended)\n\nDownload the source code:\n\n```bash\ngit clone https://github.com/prostgles/ui.git\ncd ui\n```\n\nDocker setup. By default the app will be accessible at [localhost:3004](http://localhost:3004).\nOmit \"--build\" to use our published images.\n\n```docker-compose.sh\ndocker compose up -d --build\n```\n\nTo use a custom port (3099 for example) and/or a custom binding address (0.0.0.0 for example):\n\n```bash\nPRGL_DOCKER_IP=0.0.0.0 PRGL_DOCKER_PORT=3099 docker compose up --build\n```\n\nTo use with docker mcp experimental feature:\n\n```bash\ndocker compose --profile=docker-mcp up --build\n```\n\n### Installation - use existing PostgreSQL instance\n\nUse this method if you want to use your existing database to store Prostgles metadata\n\nDownload the source code:\n\n```bash\ngit clone https://github.com/prostgles/ui.git prostgles\ncd prostgles\n```\n\nBuild and run our docker image:\n\n```docker-run.sh\ndocker build -t prostgles .\ndocker run --network=host -d -p 127.0.0.1:3004:3004 \\\n  -e POSTGRES_HOST=127.0.0.1 \\\n  -e POSTGRES_PORT=5432 \\\n  -e POSTGRES_DB=postgres \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e PROSTGLES_UI_HOST=0.0.0.0 \\\n  -e IS_DOCKER=yes \\\n  -e NODE_ENV=production \\\n  prostgles\n\n```\n\nYour server will be running on [localhost:3004](http://localhost:3004).\n\n### Development\n\n#### 1. Install dependencies:\n\n- [NodeJS](https://nodejs.org/en/download)\n- [Postgres](https://www.postgresql.org/download/): For full features **postgresql-17-postgis-3.4** is recommended\n\n#### 2. Create a database and user update `.env`. All prostgles state and metadata will be stored in this database\n\n    sudo su - postgres\n    createuser --superuser usr\n    psql -c \"alter user usr with encrypted password 'psw'\"\n    createdb db -O usr\n\n#### 3. Start app in dev mode (will install npm packages)\n\n    npm run dev\n\n### Testing\n\nEnsure the app is running in development mode and:\n\n    cd e2e && npm test-local\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n### Reporting a Vulnerability\n\nPlease report (suspected) security vulnerabilities to security@prostgles.com\n\n- You will receive a response from us within 7 working days.\n- If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.\n"
  },
  {
    "path": "changelog/v1.0.0.md",
    "content": "Prostgles UI v1.0.0\n\nWorkspaces\nEach database connection now allows multiple workspaces. You can create and switch between different dashboards\n\n"
  },
  {
    "path": "changelog/v2.0.0.md",
    "content": "Prostgles UI v2\n\nImproved UI\n- Referenced tables tab\n- SQL code blocks\n- SQL snippets\n\nAccess control\n- Role-based access rules\n- API\n\nFile storage\n- Saved locally or to S3\n- File type and size rules\n\nBackup and Restore\n- Automatic or manual\n- Saved locally or to S3\n\nSecurity\n- App based 2 factor-authentication\n- IP subnet rules\n- CORS\n\n\n"
  },
  {
    "path": "changelog/v2.2.0.md",
    "content": "- Schema Diagram\n  - Link colouring modes to better understand related tables and foreign key properties\n  - Filtering by relationship type, schema\n- Command and settings quick search (Ctrl+K). Access any command, setting, or action through our new command palette\n- AI Assistant:\n  - Data access controls. Allow the assistant to execute sql with rollback, commit or specify allowed tables and commands for each chat\n  - Dashboard Generation: Create dashboards from your database schema and requirements\n  - Create task mode: Get suggested tools and data access permissions for the chat\n  - MCP Server Tools support: Install and allow the assistant to access Model Context Protocol server tools.\n  - File upload support\n  - Execute sql snippets directly in chat\n  - Real-time cost tracking per chat and configurable spending limits\n- Improved setup for Prostgles Desktop: \"Quick setup\" mode handles the creation of a state database and user.\n"
  },
  {
    "path": "changelog/v2.2.1.md",
    "content": "- Maintenance and fixes:\n  - Fixed docker release pipeline\n  - Updated electron packages\n  - Remove dead code\n"
  },
  {
    "path": "changelog/v2.2.2.md",
    "content": "- Improve documentation\n- Maintenance and fixes:\n  - Tidy release workflows\n"
  },
  {
    "path": "changelog/v2.2.3.md",
    "content": "- Improve documentation\n- Improve Docker MCP server\n"
  },
  {
    "path": "changelog/v2.2.4.md",
    "content": "- Improve documentation\n- Improve Tool use message UI\n- Fix parallel MCP requests bug\n"
  },
  {
    "path": "client/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@babel/env\",\n      {\n        \"targets\": {\n          \"esmodules\": true\n        },\n        \"modules\": false,\n        \"bugfixes\": true\n      }\n    ],\n    \"@babel/preset-react\",\n    \"@babel/preset-typescript\"\n  ],\n  \"plugins\": [\"dynamic-import-node\"],\n  \"env\": {\n    \"production\": {\n      \"presets\": [\n        [\n          \"minify\",\n          {\n            \"builtIns\": false,\n            \"evaluate\": false,\n            \"mangle\": false\n          }\n        ]\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "client/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n\nBACKUP\ndist\n/configs/last_compiled.txt"
  },
  {
    "path": "client/build.sh",
    "content": "npm i && export NODE_ENV=production && export BABEL_ENV=production && export NODE_OPTIONS=--max-old-space-size=2048 && npm run build-start\n"
  },
  {
    "path": "client/eslint.config.mjs",
    "content": "// @ts-check\nimport commonConfig from \"../.eslintrc.common.js\";\nimport eslint from \"@eslint/js\";\nimport tseslint from \"typescript-eslint\";\nimport { defineConfig } from \"eslint/config\";\nimport eslintPluginReact from \"eslint-plugin-react\";\nimport eslintPluginReactHooks from \"eslint-plugin-react-hooks\";\n// import typescriptEslint from \"@typescript-eslint/eslint-plugin\";\n\nexport default defineConfig(\n  eslint.configs.recommended,\n  tseslint.configs.recommended,\n  tseslint.configs.recommendedTypeChecked,\n  {\n    ignores: [\n      \"node_modules\",\n      \"dist\",\n      \"examples\",\n      \"**/*.d.ts\",\n      \"tests\",\n      \"docs\",\n      \"*.mjs\",\n      \"sample_schemas\",\n      \"**/*.d.ts\",\n      \"**/*.js\",\n    ],\n  },\n  {\n    files: [\"**/*.{ts,tsx}\"],\n    languageOptions: {\n      parser: tseslint.parser,\n      parserOptions: {\n        ecmaVersion: \"latest\",\n        sourceType: \"module\",\n        // projectService: {\n        //   allowDefaultProject: [\"*.js\", \"*.mjs\"],\n        // },\n        project: [\"./tsconfig.eslint.json\"],\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n    plugins: {\n      \"@typescript-eslint\": tseslint.plugin,\n      react: eslintPluginReact,\n      \"react-hooks\": eslintPluginReactHooks,\n    },\n    settings: {\n      react: {\n        version: \"detect\",\n      },\n    },\n    rules: {\n      ...commonConfig.rules,\n      \"react/no-unescaped-entities\": \"off\",\n      \"react/display-name\": \"off\",\n      \"react/no-children-prop\": \"off\",\n      \"react/prop-types\": \"off\",\n      \"react/no-unused-prop-types\": \"off\",\n      \"react-hooks/exhaustive-deps\": [\n        \"warn\",\n        {\n          additionalHooks:\n            \"(usePromise|useEffectAsync|useProstglesClient|useAsyncEffectQueue|useEffectDeep)\",\n        },\n      ],\n      \"no-cond-assign\": \"error\",\n      \"@typescript-eslint/no-namespace\": \"off\",\n      \"@typescript-eslint/no-explicit-any\": \"off\",\n      \"@typescript-eslint/no-non-null-assertion\": \"off\",\n      \"@typescript-eslint/ban-types\": \"off\",\n      \"@typescript-eslint/ban-ts-comment\": \"off\",\n      \"@typescript-eslint/no-unused-expressions\": \"off\",\n      \"@typescript-eslint/no-require-imports\": \"off\",\n      \"@typescript-eslint/no-empty-object-type\": \"off\",\n      \"no-async-promise-executor\": \"off\",\n      \"@typescript-eslint/no-var-requires\": \"off\",\n      \"@typescript-eslint/no-unnecessary-condition\": \"error\",\n      \"@typescript-eslint/no-floating-promises\": \"warn\",\n      \"no-unused-vars\": \"off\",\n      \"no-empty\": \"off\",\n      \"@typescript-eslint/only-throw-error\": \"off\",\n      \"@typescript-eslint/prefer-promise-reject-errors\": \"off\",\n      \"@typescript-eslint/restrict-template-expressions\": [\n        \"warn\",\n        {\n          allowNumber: true,\n          allowBoolean: true,\n          allowNullish: true,\n          allowArray: true,\n        },\n      ],\n      \"@typescript-eslint/no-misused-promises\": [\n        \"warn\",\n        { checksVoidReturn: false },\n      ],\n      \"@typescript-eslint/no-unsafe-assignment\": \"warn\",\n      \"@typescript-eslint/no-unsafe-argument\": \"warn\",\n      \"@typescript-eslint/no-unsafe-return\": \"warn\",\n      \"@typescript-eslint/await-thenable\": \"warn\",\n      \"@typescript-eslint/no-unsafe-member-access\": \"warn\",\n      \"@typescript-eslint/no-unsafe-call\": \"warn\",\n      \"@typescript-eslint/no-unused-vars\": [\n        \"warn\",\n        {\n          argsIgnorePattern: \"^_\",\n          varsIgnorePattern: \"^_\",\n          caughtErrorsIgnorePattern: \"^_\",\n        },\n      ],\n    },\n  },\n);\n"
  },
  {
    "path": "client/package.json",
    "content": "{\n  \"name\": \"prostgles-ui-client\",\n  \"dependencies\": {\n    \"@mdi/js\": \"^7.4.47\",\n    \"@types/dom-speech-recognition\": \"^0.0.6\",\n    \"d3\": \"^7.9.0\",\n    \"deck.gl\": \"^9.1.14\",\n    \"loaders.gl\": \"^0.3.5\",\n    \"monaco-editor\": \"^0.53.0\",\n    \"papaparse\": \"^5.4.1\",\n    \"prostgles-client\": \"^4.0.270\",\n    \"qrcode\": \"^1.5.1\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-flip-move\": \"^3.0.5\",\n    \"react-markdown\": \"^10.1.0\",\n    \"react-router-dom\": \"^7.5.2\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"sanitize-html\": \"^2.13.0\",\n    \"socket.io-client\": \"^4.8.1\",\n    \"sql-formatter\": \"^15.3.2\",\n    \"strip-ansi\": \"^7.1.2\",\n    \"tsconfig-paths-webpack-plugin\": \"^4.2.0\"\n  },\n  \"scripts\": {\n    \"prod\": \"NODE_ENV=production BABEL_ENV=production webpack\",\n    \"start\": \"node scripts/start.js --no-cache\",\n    \"build-start\": \"rm -rf ./build && webpack --config=configs/prod.js && rm -rf ./node_modules\",\n    \"build\": \"bash ./build.sh\",\n    \"dev\": \"npm i --no-audit --quiet && NODE_OPTIONS='--max-old-space-size=4048' && webpack --watch --config=configs/dev.js\",\n    \"lib\": \"rm -rf ./dist && tsc --project tsconfig_lib.json && npm run copyf\",\n    \"copyf\": \"cd ./src && find . -name '*.css' -type f -exec cp --parents {} ../dist/ \\\\; \",\n    \"copyf2\": \"find ./src/. -name '*.css' | cpio -pdm ./dist/\",\n    \"lint\": \"eslint . --quiet --fix\",\n    \"tsc\": \"tsc --noEmit\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.28.5\",\n    \"@babel/plugin-transform-runtime\": \"^7.28.5\",\n    \"@babel/plugin-transform-typescript\": \"^7.28.5\",\n    \"@babel/preset-env\": \"^7.28.5\",\n    \"@babel/preset-react\": \"^7.28.5\",\n    \"@babel/preset-typescript\": \"^7.28.5\",\n    \"@babel/runtime\": \"^7.28.4\",\n    \"@eslint/js\": \"^9.39.1\",\n    \"@types/d3\": \"^7.4.3\",\n    \"@types/dom-mediacapture-record\": \"^1.0.10\",\n    \"@types/papaparse\": \"^5.3.1\",\n    \"@types/qrcode\": \"^1.4.2\",\n    \"@types/react\": \"^18.3.7\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@types/react-router-dom\": \"^5.1.5\",\n    \"@types/resize-observer-browser\": \"^0.1.6\",\n    \"@types/sanitize-html\": \"^2.6.2\",\n    \"@types/webpack\": \"^4.41.25\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.48.0\",\n    \"@typescript-eslint/parser\": \"^8.48.0\",\n    \"babel-loader\": \"^9.1.3\",\n    \"babel-plugin-dynamic-import-node\": \"^2.3.3\",\n    \"babel-plugin-named-asset-import\": \"^0.3.8\",\n    \"babel-preset-minify\": \"^0.5.1\",\n    \"circular-dependency-plugin\": \"^5.2.2\",\n    \"copy-webpack-plugin\": \"^12.0.2\",\n    \"css-loader\": \"^6.8.1\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"file-loader\": \"^6.2.0\",\n    \"html-webpack-plugin\": \"^5.6.3\",\n    \"interpolate-html-plugin\": \"^4.0.0\",\n    \"mini-css-extract-plugin\": \"^2.7.1\",\n    \"monaco-editor-webpack-plugin\": \"^7.1.0\",\n    \"postcss\": \"^8.5.3\",\n    \"postcss-flexbugs-fixes\": \"^5.0.2\",\n    \"postcss-loader\": \"^7.3.4\",\n    \"postcss-normalize\": \"^10.0.1\",\n    \"postcss-preset-env\": \"^7.8.3\",\n    \"postcss-safe-parser\": \"^6.0.0\",\n    \"prettier\": \"^3.4.2\",\n    \"sass-loader\": \"^16.0.4\",\n    \"style-loader\": \"^4.0.0\",\n    \"ts-loader\": \"^9.5.1\",\n    \"typescript\": \"^5.9.3\",\n    \"typescript-eslint\": \"^8.48.0\",\n    \"url-loader\": \"^4.1.1\",\n    \"webpack\": \"^5.99.7\",\n    \"webpack-bundle-analyzer\": \"^4.10.2\",\n    \"webpack-cli\": \"^6.0.1\",\n    \"webpack-manifest-plugin\": \"^5.0.0\",\n    \"webpack-merge\": \"^6.0.1\",\n    \"webpack-shell-plugin-next\": \"^2.3.2\"\n  }\n}\n"
  },
  {
    "path": "client/public/manifest.json",
    "content": "{\n  \"name\": \"Prostgles UI\",\n  \"short_name\": \"Prostgles UI\",\n  \"description\": \"Dashboard and SQL editor for PostgreSQL.\",\n  \"display\": \"standalone\",\n  \"icons\": [\n    {\n      \"src\": \"/prostgles-logo.svg\",\n      \"sizes\": \"any\"\n    }\n  ]\n}\n"
  },
  {
    "path": "client/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/setup-icons.js",
    "content": "/** Save all mdi icons as xml */\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst icons = require(\"@mdi/js\");\n\nconst saveMdiIcons = () => {\n  const iconEntries = Object.entries(icons);\n  const iconsDestinationFolder = path.join(__dirname, \"/static/icons\");\n  if (fs.existsSync(iconsDestinationFolder)) {\n    const contents = fs.readdirSync(iconsDestinationFolder);\n    if (contents.length >= iconEntries.length) {\n      return;\n    }\n    fs.rm(iconsDestinationFolder, { recursive: true }, console.log);\n  }\n  fs.mkdirSync(iconsDestinationFolder, { recursive: true });\n  if (!iconEntries.length) {\n    console.error(\"No icons found. Did you run npm i ?!\");\n    process.exit(1);\n  }\n\n  const iconNames = [];\n  iconEntries.forEach(([name, iconPathD]) => {\n    const nameWithoutMdi = name.slice(3);\n    iconNames.push(nameWithoutMdi);\n    const iconSvg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" role=\"presentation\">\n      <path d=${JSON.stringify(iconPathD)} style=\"fill: currentcolor;\"></path>\n    </svg>`;\n    fs.writeFileSync(\n      path.join(iconsDestinationFolder, `${name.slice(3)}.svg`),\n      iconSvg,\n      { encoding: \"utf8\" },\n    );\n  });\n  console.log(` Saved ${iconEntries.length} icons`);\n  fs.writeFileSync(\n    path.join(iconsDestinationFolder, \"_meta.json\"),\n    JSON.stringify(iconNames, null, 2),\n    { encoding: \"utf8\" },\n  );\n};\n\nsetTimeout(saveMdiIcons, 1000);\n\nclass SaveMdiIcons {\n  apply(compiler) {\n    compiler.hooks.afterEmit.tap(\n      \"Save Mdi icons plugin afterEmit\",\n      (_stats) => {\n        setTimeout(saveMdiIcons, 1000);\n      },\n    );\n    compiler.hooks.done.tap(\"Save Mdi icons plugin done\", (_stats) => {\n      setTimeout(saveMdiIcons, 1000);\n    });\n  }\n}\n\nmodule.exports = { SaveMdiIcons };\n"
  },
  {
    "path": "client/src/App.css",
    "content": "*,\nbody,\nhtml {\n  box-sizing: border-box;\n}\n\nbody {\n  overflow: hidden;\n}\n\n.page-content {\n  padding-top: 1em;\n}\n\n.active-code-block-decoration {\n  background: lightblue;\n  width: 5px !important;\n  margin-left: 3px;\n}\n\n.active-code-block-play {\n  margin-left: 4px;\n}\n\n.dark-theme .active-code-block-decoration {\n  background: #14708f;\n}\n\n.dark-theme * {\n  color-scheme: dark;\n}\n"
  },
  {
    "path": "client/src/App.tsx",
    "content": "import type { ReactChild } from \"react\";\nimport React, { useMemo, useState } from \"react\";\nimport { Navigate, Route, Routes as Switch } from \"react-router-dom\";\nimport \"./App.css\";\nimport Loading from \"./components/Loader/Loading\";\nimport type { CommonWindowProps } from \"./dashboard/Dashboard/Dashboard\";\nimport { t } from \"./i18n/i18nUtils\";\nimport { Connections } from \"./pages/Connections/Connections\";\nimport NewConnnection from \"./pages/NewConnection/NewConnnectionForm\";\nimport { NotFound } from \"./pages/NotFound\";\nimport { ProjectConnection } from \"./pages/ProjectConnection/ProjectConnection\";\n\nimport ErrorComponent from \"./components/ErrorComponent\";\nimport UserManager from \"./dashboard/UserManager\";\nimport { Account } from \"./pages/Account/Account\";\nimport { ServerSettings } from \"./pages/ServerSettings/ServerSettings\";\n\nimport type { ProstglesState } from \"@common/electronInitTypes\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { fixIndent, ROUTES } from \"@common/utils\";\nimport type { AuthHandler } from \"prostgles-client/dist/getAuthHandler\";\nimport {\n  type DBHandlerClient,\n  type MethodHandler,\n} from \"prostgles-client/dist/prostgles\";\nimport { type Socket } from \"socket.io-client\";\nimport { CommandPalette } from \"./app/CommandPalette/CommandPalette\";\nimport { Documentation } from \"./app/CommandPalette/Documentation\";\nimport { XRealIpSpoofableAlert } from \"./app/XRealIpSpoofableAlert\";\nimport { createReactiveState, useReactiveState } from \"./appUtils\";\nimport { AlertProvider } from \"./components/AlertProvider\";\nimport { FlexCol, FlexRow } from \"./components/Flex\";\nimport { InfoRow } from \"./components/InfoRow\";\nimport { NavBarWrapper } from \"./components/NavBar/NavBarWrapper\";\nimport { PostgresInstallationInstructions } from \"./components/PostgresInstallationInstructions\";\nimport type { DBS, DBSMethods } from \"./dashboard/Dashboard/DBS\";\nimport { MousePointer } from \"./demo/MousePointer\";\nimport { ComponentList } from \"./pages/ComponentList\";\nimport { ElectronSetup } from \"./pages/ElectronSetup/ElectronSetup\";\nimport { Login } from \"./pages/Login/Login\";\nimport { NonHTTPSWarning } from \"./pages/NonHTTPSWarning\";\nimport { useAppTheme } from \"./theme/useAppTheme\";\nimport { useAppState } from \"./useAppState/useAppState\";\n\nexport type ClientUser = {\n  sid: string;\n  uid: string;\n  type: string;\n  has_2fa: boolean;\n} & DBSSchema[\"users\"];\n\nexport type ClientAuth = {\n  user?: ClientUser;\n};\nexport type Theme = \"dark\" | \"light\";\nexport type PrglReadyState = {\n  /**\n   * Used to re-render dashboard on dbs reconnect\n   */\n  dbsKey: string;\n  dbs: DBS;\n  dbsTables: CommonWindowProps[\"tables\"];\n  dbsMethods: DBSMethods;\n  dbsSocket: Socket;\n  auth: AuthHandler<ClientUser>;\n  isAdminOrSupport: boolean;\n  sid: string | undefined;\n};\nexport type ExtraProps = PrglReadyState & {\n  setTitle: (content: string | ReactChild) => void;\n  user: DBSSchema[\"users\"] | undefined;\n  dbsSocket: Socket;\n  theme: Theme;\n} & Pick<Required<AppState>, \"serverState\">;\n\nexport type PrglStateCore = Pick<\n  ExtraProps,\n  \"dbs\" | \"dbsMethods\" | \"dbsTables\"\n>;\nexport type PrglState = ExtraProps;\n\nexport type PrglCore = {\n  db: DBHandlerClient;\n  methods: MethodHandler;\n  tables: CommonWindowProps[\"tables\"];\n};\nexport type PrglProject = PrglCore & {\n  dbKey: string;\n  connectionId: string;\n  databaseId: number;\n  projectPath: string;\n  connection: DBSSchema[\"connections\"];\n};\nexport type Prgl = PrglState & PrglProject;\n\nexport type AppState = {\n  prglState?: PrglReadyState;\n  user: DBSSchema[\"users\"] | undefined;\n  serverState?: ProstglesState;\n  title: React.ReactNode;\n  isConnected: boolean;\n};\n\nexport const r_useAppVideoDemo = createReactiveState({ demoStarted: false });\n\nexport const App = () => {\n  const [isDisconnected, setIsDisconnected] = useState(false);\n  const state = useAppState(setIsDisconnected);\n  const [title, setTitle] = useState<React.ReactNode>(\"\");\n  const {\n    state: { demoStarted },\n  } = useReactiveState(r_useAppVideoDemo);\n\n  const { theme, userThemeOption } = useAppTheme(state);\n  const extraProps: PrglState | undefined = useMemo(\n    () =>\n      state.prglState &&\n      state.serverState && {\n        ...state.prglState,\n        setTitle: (content: ReactChild) => {\n          if (title !== content) setTitle(content);\n        },\n        user: state.user,\n        theme,\n        serverState: state.serverState,\n      },\n    [state, theme, title],\n  );\n\n  const { initState } = state.serverState ?? {};\n  const initStateError = initState?.state === \"error\" ? initState : undefined;\n  if (\n    state.serverState?.isElectron &&\n    ((state.state !== \"loading\" && state.state !== \"ok\") ||\n      !state.serverState.electronCredsProvided ||\n      initStateError)\n  ) {\n    return <ElectronSetup serverState={state.serverState} />;\n  }\n\n  const unknownErrorMessage =\n    \"Something went wrong with initialising the server. Check console for more details\";\n  const error =\n    state.dbsClientError ||\n    (initState?.state === \"error\" ?\n      initState.error || unknownErrorMessage\n    : undefined);\n\n  const { prglState, serverState, state: _state } = state;\n  if (!error && (!prglState || !serverState || _state === \"loading\")) {\n    return (\n      <div className=\"flex-row m-auto ai-center jc-center  p-2\">\n        <Loading id=\"main\" message=\"Connecting to state database...\" />\n      </div>\n    );\n  }\n\n  if (error || !prglState || !serverState || !extraProps) {\n    const hint =\n      state.dbsClientError ?\n        errorHints.dbsClientError\n      : initStateError?.errorType && errorHints[initStateError.errorType];\n    return (\n      <FlexCol className=\"m-auto ai-center jc-center max-w-700 p-2\">\n        <FlexRow>\n          <ErrorComponent\n            error={error}\n            variant=\"outlined\"\n            className=\"p-2\"\n            withIcon={true}\n          />\n          {initStateError?.errorType === \"connection\" && (\n            <PostgresInstallationInstructions placement=\"state-db\" os=\"linux\" />\n          )}\n        </FlexRow>\n        {hint && (\n          <InfoRow color=\"warning\" variant=\"naked\">\n            {hint}\n          </InfoRow>\n        )}\n      </FlexCol>\n    );\n  }\n  const isElectron = !!serverState.isElectron;\n  return (\n    <AlertProvider>\n      <FlexCol key={prglState.dbsKey} className={`App gap-0 f-1 min-h-0`}>\n        <CommandPalette isElectron={isElectron} />\n        <XRealIpSpoofableAlert {...state} />\n        {demoStarted && <MousePointer />}\n        {isDisconnected && (\n          <Loading\n            message={t.App[\"Reconnecting...\"]}\n            variant=\"cover\"\n            style={{ zIndex: 467887 }}\n          />\n        )}\n        <NonHTTPSWarning {...prglState} />\n        <Switch>\n          <Route\n            key=\"0\"\n            path=\"/\"\n            element={<Navigate to={ROUTES.CONNECTIONS} replace />}\n          />\n          <Route\n            key=\"1\"\n            path={ROUTES.CONNECTIONS}\n            element={\n              <NavBarWrapper\n                extraProps={extraProps}\n                needsUser={true}\n                userThemeOption={userThemeOption}\n              >\n                <Connections {...extraProps} />\n              </NavBarWrapper>\n            }\n          />\n          <Route\n            key=\"2\"\n            path={ROUTES.USERS}\n            element={\n              <NavBarWrapper\n                extraProps={extraProps}\n                needsUser={false}\n                userThemeOption={userThemeOption}\n              >\n                <UserManager {...extraProps} />\n              </NavBarWrapper>\n            }\n          />\n          <Route\n            key=\"3\"\n            path={ROUTES.ACCOUNT}\n            element={\n              <NavBarWrapper\n                extraProps={extraProps}\n                needsUser={false}\n                userThemeOption={userThemeOption}\n              >\n                <Account {...extraProps} />\n              </NavBarWrapper>\n            }\n          />\n          <Route\n            key=\"4\"\n            path={`${ROUTES.CONNECTIONS}/:connectionId`}\n            element={<ProjectConnection prglState={extraProps} />}\n          />\n          <Route\n            key=\"5\"\n            path={ROUTES.NEW_CONNECTION}\n            element={\n              <NewConnnection\n                connectionId={undefined}\n                db={undefined}\n                prglState={extraProps}\n                showTitle={true}\n              />\n            }\n          />\n          <Route\n            key=\"6\"\n            path={`${ROUTES.EDIT_CONNECTION}/:id`}\n            element={\n              <NewConnnection\n                connectionId={undefined}\n                db={undefined}\n                prglState={extraProps}\n                showTitle={true}\n              />\n            }\n          />\n          <Route\n            key=\"7\"\n            path={`${ROUTES.CONFIG}/:connectionId`}\n            element={\n              <ProjectConnection\n                prglState={extraProps}\n                showConnectionConfig={true}\n              />\n            }\n          />\n          <Route\n            key=\"8\"\n            path={ROUTES.SERVER_SETTINGS}\n            element={\n              <NavBarWrapper\n                extraProps={extraProps}\n                needsUser={true}\n                userThemeOption={userThemeOption}\n              >\n                <ServerSettings {...extraProps} />\n              </NavBarWrapper>\n            }\n          />\n          <Route\n            key=\"9\"\n            path={ROUTES.COMPONENT_LIST}\n            element={\n              <NavBarWrapper\n                extraProps={extraProps}\n                needsUser={false}\n                userThemeOption={userThemeOption}\n              >\n                <ComponentList />\n              </NavBarWrapper>\n            }\n          />\n          <Route\n            key=\"10\"\n            path={ROUTES.LOGIN}\n            element={<Login {...extraProps} />}\n          />\n          <Route\n            key=\"11\"\n            path={ROUTES.DOCUMENTATION}\n            element={\n              <NavBarWrapper\n                extraProps={extraProps}\n                needsUser={false}\n                userThemeOption={userThemeOption}\n              >\n                <Documentation isElectron={isElectron} />\n              </NavBarWrapper>\n            }\n          />\n          <Route key=\"12\" path=\"*\" element={<NotFound />} />\n        </Switch>\n      </FlexCol>\n    </AlertProvider>\n  );\n};\n\nconst errorHints = {\n  connection: fixIndent(`\n    Could not connect to state database. Ensure /server/.env file (or\n    environment variables) point to a running and accessible postgres\n    server database`),\n  init: \"Failed to start Prostgles\",\n  dbsClientError:\n    \"Failed to connect to state database. Try refreshing the page or restarting the app.\",\n};\n\nexport * from \"./appUtils\";\n"
  },
  {
    "path": "client/src/Testing.ts",
    "content": "export const COMMANDS = {\n  \"NewConnectionForm.connectionName\": \"Connection name input field\",\n  \"NewConnectionForm.connectionType\": \"Connection type select field\",\n  \"NewConnectionForm.db_conn\": \"Database connection input field\",\n  \"NewConnectionForm.db_host\": \"Database host input field\",\n  \"NewConnectionForm.db_port\": \"Database port input field\",\n  \"NewConnectionForm.db_user\": \"Database user input field\",\n  \"NewConnectionForm.db_pass\": \"Database password input field\",\n  \"NewConnectionForm.db_name\": \"Database name input field\",\n  \"NewConnectionForm.MoreOptionsToggle\": \"\",\n  \"NewConnectionForm.schemaFilter\": \"\",\n  \"NewConnectionForm.connectionTimeout\": \"Connection timeout input field\",\n  \"NewConnectionForm.sslMode\": \"SSL mode select field\",\n  \"NewConnectionForm.watchSchema\": \"Watch schema toggle\",\n  \"NewConnectionForm.realtime\": \"Realtime toggle\",\n  \"NewConnectionForm.testConnection\": \"Test connection button\",\n\n  \"config.goToConnDashboard\": \"Go to connection workspace \",\n  \"config.details\": \"\",\n  \"config.bkp\": \"\",\n  \"config.tableConfig\": \"\",\n  \"config.bkp.create\": \"\",\n  \"config.bkp.create.name\": \"Backup name input field\",\n  \"config.bkp.create.start\": \"\",\n  \"config.bkp.AutomaticBackups\": \"\",\n  \"config.bkp.AutomaticBackups.toggle\": \"\",\n  \"config.ac\": { desc: \"\", uiOnly: true },\n  \"config.status\": \"\",\n  \"config.ac.create\": \"\",\n  \"config.ac.save\": \"\",\n  \"config.ac.removeRule\": \"\",\n  \"config.ac.cancel\": \"\",\n  \"config.ac.createDefault\": \"\",\n  \"config.ac.edit.user\": \"Opens select/edit access rule user types\",\n  \"config.ac.edit.user.select\": \"User type select search box\",\n  \"config.ac.edit.user.select.create\":\n    \"Creates a user type when the search did not yield any results\",\n  \"config.ac.edit.user.select.done\": \"Closes create user popup\",\n\n  \"config.ac.edit.type\":\n    \"Rule type button group. Each button.value will contain the type\",\n\n  \"config.ac.edit.dataAccess\": \"Data access section with tables/runsql\",\n\n  \"config.ac.edit.createWorkspaces\": \"\",\n  \"config.ac.edit.publishedWorkspaces\": \"\",\n\n  \"config.ac.edit.typeCustom.tables\": \"\",\n  \"config.ac.edit.typeAll\": \"\",\n  \"config.ac.edit.typeSQL\": \"\",\n  \"config.files\": \"\",\n  \"config.files.toggle\": \"\",\n  \"config.files.toggle.confirm\": \"\",\n  \"config.api\": { desc: \"\", uiOnly: true },\n  \"config.methods\": \"\",\n\n  \"dashboard.window.rowInsert\": \"Open row insert panel\",\n  \"dashboard.window.rowInsertTop\": \"Open row insert panel from top filter bar\",\n\n  \"W_SQLMenu.name\": \"\",\n  \"W_SQLMenu.renderDisplayMode\": \"\",\n  \"W_SQLMenu.saveQuery\": \"\",\n  \"W_SQLMenu.openSQLFile\": \"\",\n  \"W_SQLMenu.deleteQuery\": \"\",\n\n  \"W_SQLEditor.executeStatement\": \"Executes the current SQL statement\",\n  W_SQLEditor: \"\",\n  W_SQLBottomBar: \"\",\n  \"W_SQLBottomBar.runQuery\":\n    \"Executes the current query (selected, block or full)\",\n  \"W_SQLBottomBar.limit\": \"\",\n  \"W_SQLBottomBar.queryDuration\": \"\",\n  \"W_SQLBottomBar.cancelQuery\": \"Cancels the current query\",\n  \"W_SQLBottomBar.terminateQuery\": \"Terminates the current query\",\n  \"W_SQLBottomBar.stopListen\": \"Terminates the current LISTEN query\",\n  \"W_SQLBottomBar.toggleTable\": \"\",\n  \"W_SQLBottomBar.toggleCodeEditor\": \"\",\n  \"W_SQLBottomBar.toggleNotices\": \"\",\n  \"W_SQLBottomBar.stopLoopQuery\": \"Stop loop query\",\n  \"W_SQLBottomBar.copyResults\": \"Copy results\",\n  \"W_SQLBottomBar.rowCount\": \"Row count\",\n  \"W_SQLBottomBar.sqlError\": \"SQL error\",\n\n  W_SQLResults: \"\",\n  \"Window.W_QuickMenu\": \"Quick menu for the current window\",\n  \"Window.ChildChart\": \"\",\n  \"Window.ChildChart.toolbar\": \"\",\n\n  \"dashboard.window.fullscreen\": \"fullscreen\",\n  \"dashboard.window.close\": \"close\",\n  \"dashboard.window.collapse\": \"collapse\",\n  \"dashboard.window.viewEditRow\": \"\",\n  \"dashboard.window.toggleFilterBar\": \"\",\n  \"dashboard.window.menu\": \"\",\n\n  \"dashboard.window.detachChart\": \"\",\n  \"dashboard.window.collapseChart\": \"\",\n  \"dashboard.window.closeChart\": \"\",\n  \"dashboard.window.chartMenu\": \"\",\n  \"dashboard.window.restoreMinimisedCharts\": \"\",\n\n  \"dashboard.goToConnConfig\": \"Go to connection config\",\n  \"dashboard.menu.settingsToggle\": \"\",\n  \"dashboard.menu.settings.defaultLayoutType\": \"\",\n  \"dashboard.menu.settings\": \"\",\n  \"dashboard.menu\": \"\",\n  \"dashboard.menu.sqlEditor\": \"\",\n  \"dashboard.menu.quickSearch\": \"\",\n  \"dashboard.menu.resize\": \"\",\n  \"dashboard.centered-layout.resize\": \"\",\n  \"dashboard.menu.fileTable\": \"\",\n  \"dashboard.menu.savedQueriesList\": \"\",\n  \"dashboard.menu.tablesSearchList\": \"\",\n  \"dashboard.menu.tablesSearchListInput\": \"\",\n  \"dashboard.menu.serverSideFunctionsList\": \"\",\n  \"dashboard.menu.create\": \"\",\n  \"dashboard.menu.createTable\": \"\",\n  \"dashboard.menu.createTable.tableName\": \"\",\n  \"dashboard.menu.createTable.addColumn\": \"\",\n  \"dashboard.menu.createTable.addColumn.confirm\": \"\",\n  \"dashboard.menu.createTable.confirm\": \"\",\n\n  \"W_TableMenu_TableInfo.name\": \"\",\n  \"W_TableMenu_TableInfo.comment\": \"\",\n  \"W_TableMenu_TableInfo.oid\": \"\",\n  \"W_TableMenu_TableInfo.type\": \"\",\n  \"W_TableMenu_TableInfo.owner\": \"\",\n  \"W_TableMenu_TableInfo.sizeInfo\": \"\",\n  \"W_TableMenu_TableInfo.viewDefinition\": \"\",\n  \"W_TableMenu_TableInfo.vacuum\": \"\",\n  \"W_TableMenu_TableInfo.vacuumFull\": \"\",\n  \"W_TableMenu_TableInfo.drop\": \"\",\n\n  \"W_TableMenu_ColumnList.alter\": \"\",\n  \"W_TableMenu_ColumnList.linkedColumnOptions\": \"\",\n  \"W_TableMenu_ColumnList.removeComputedColumn\": \"\",\n\n  TableHeader: \"\",\n  \"TableHeader.resizeHandle\": \"\",\n\n  \"FormField.clear\": \"Clear a FormField\",\n\n  SmartForm: \"\",\n  \"SmartForm.header.tableIconAndName\": \"\",\n  \"SmartForm.header.previousRow\": \"\",\n  \"SmartForm.header.nextRow\": \"\",\n\n  \"SmartForm.close\": \"Close dialog\",\n  \"SmartForm.delete\": \"Deletes row\",\n  \"SmartForm.delete.confirm\": \"Confirms Deleting a row\",\n  \"SmartForm.update\": \"update row\",\n  \"SmartForm.update.confirm\": \"Confirms update a row\",\n  \"SmartForm.insert\": \"Confirms Deleting a row\",\n  \"SmartForm.clone\": \"Confirms Deleting a row\",\n\n  \"SQLSmartEditor.Run\": \"Run the sql statement\",\n\n  \"SearchList.toggleAll\": \"\",\n  \"SearchList.List\": \"\",\n  \"SearchList.Input\": \"\",\n  ViewMoreSmartCardList: \"\",\n  \"Section.toggleFullscreen\": \"\",\n  FieldFilterControl: \"\",\n  \"FieldFilterControl.type\": \"\",\n  \"FieldFilterControl.type.custom\": \"\",\n  \"FieldFilterControl.type.except\": \"\",\n  \"FieldFilterControl.select\": \"\",\n  \"RenderFilter.edit\": \"\",\n  \"RenderFilter.done\": \"\",\n\n  ForcedFilterControl: \"\",\n  \"ForcedFilterControl.type\": \"\",\n  \"ForcedFilterControl.type.disabled\": \"\",\n  \"ForcedFilterControl.type.enabled\": \"\",\n\n  CheckFilterControl: \"\",\n  \"CheckFilterControl.type\": \"\",\n  \"CheckFilterControl.type.disabled\": \"\",\n  \"CheckFilterControl.type.enabled\": \"\",\n\n  selectRule: \"\",\n  selectRuleAdvanced: \"\",\n  updateRule: \"\",\n  updateRuleAdvanced: \"\",\n  deleteRule: \"\",\n  deleteRuleAdvanced: \"\",\n  insertRule: \"\",\n  insertRuleAdvanced: \"\",\n  syncRule: \"\",\n  syncRuleAdvanced: \"\",\n\n  SearchAll: \"\",\n  SmartAddFilter: \"\",\n  FilterWrapper: \"\",\n  \"FilterWrapper.typeSelect\": \"\",\n  FileBtn: \"\",\n\n  \"ForcedDataControl.toggle\": \"\",\n  \"ForcedDataControl.addColumn\": \"\",\n  \"TablePermissionControls.close\": \"\",\n  \"TablePermissionControls.done\": \"\",\n\n  Connections: \"\",\n  \"Connections.add\": \"add\",\n  \"Connections.new\": \" \",\n  \"Connection.openConnection\": \"Open connection\",\n  \"Connection.workspaceList\": \"Connection workspace list\",\n\n  \"Connection.closeAllWindows\": \"\",\n  \"Connection.statusMonitor\": \"\",\n  \"Connection.configure\": \"\",\n  \"Connection.edit\": \"\",\n  \"Connection.edit.updateOrCreateConfirm\": \"\",\n  \"Connection.edit.delete\": \"\",\n  \"Connection.edit.delete.dropDatabase\": \"\",\n  \"Connection.edit.delete.confirm\": \"\",\n\n  \"Connection.disconnect\": \"\",\n\n  \"ConnectionServer.add\": \"\",\n  \"ConnectionServer.add.newDatabase\": \"\",\n  \"ConnectionServer.add.existingDatabase\": \"\",\n  \"ConnectionServer.NewDbName\": \"\",\n  \"ConnectionServer.add.confirm\": \"\",\n\n  \"SmartFilterBar.toggle\": \"\",\n  \"SmartFilterBar.rightOptions.show\": \"\",\n  \"SmartFilterBar.rightOptions.update\": \"\",\n  \"SmartFilterBar.rightOptions.delete\": \"\",\n\n  \"ColumnEditor.name\": \"\",\n  \"ColumnEditor.dataType\": \"\",\n\n  AddColumnMenu: \"\",\n\n  WorkspaceAddBtn: \"\",\n  WorkspaceMenuDropDown: \"\",\n  \"WorkspaceMenuDropDown.WorkspaceAddBtn\": \"\",\n  \"WorkspaceMenu.SearchList\": \"\",\n  \"WorkspaceMenu.CloneWorkspace\": \"\",\n  \"WorkspaceMenu.toggleWorkspaceLayoutMode\": \"\",\n  WorkspaceDeleteBtn: \"\",\n  \"WorkspaceDeleteBtn.Confirm\": \"\",\n\n  JoinPathSelectorV2: \"\",\n  \"LinkedColumn.Add\": \"\",\n  \"LinkedColumn.ColumnList.toggle\": \"\",\n\n  \"SummariseColumn.toggle\": \"\",\n  FunctionSelector: \"\",\n  \"SummariseColumn.apply\": \"\",\n  \"Popup.header\": \"\",\n  \"Popup.close\": \"\",\n  \"Popup.content\": \"\",\n  \"Popup.footer\": \"\",\n  \"Popup.toggleFullscreen\": \"\",\n  \"PopupSection.fullscreen\": \"\",\n  \"PopupSection.content\": \"\",\n  \"LinkedColumn.ColumnListMenu\": \"\",\n  \"AddChartMenu.Map\": \"\",\n  \"AddChartMenu.Timechart\": \"\",\n  W_TimeChart: \"\",\n  \"W_TimeChart.ActiveRow\": \"\",\n  \"W_TimeChart.AddTimeChartFilter\": \"\",\n  SmartFormField: \"\",\n\n  \"CloseSaveSQLPopup.delete\": \"\",\n\n  SchemaGraph: \"\",\n  \"SchemaGraph.TopControls\": \"\",\n  \"SchemaGraph.TopControls.tableRelationsFilter\": \"\",\n  \"SchemaGraph.TopControls.columnRelationsFilter\": \"\",\n  \"SchemaGraph.TopControls.linkColorMode\": \"\",\n  \"SchemaGraph.TopControls.resetLayout\": \"\",\n  AddColumnReference: \"\",\n  \"SmartFormFieldOptions.AttachFile\": \"\",\n  RuleToggle: \"\",\n  \"table.options.displayMode\": \"\",\n  \"table.options.cardView.groupBy\": \"\",\n  \"table.options.cardView.orderBy\": \"\",\n  \"CardView.row\": \"\",\n  \"CardView.group\": \"\",\n  \"CardView.DragHeader\": \"\",\n\n  \"CreateFileColumn.confirm\": \"\",\n  \"AppDemo.start\": \"\",\n  MenuList: \"\",\n  ComparablePGPolicies: \"\",\n  \"ConnectionServer.SampleSchemas\": \"\",\n  \"dashboard.goToConnections\": \"\",\n  QuickAddComputedColumn: \"\",\n  \"SmartSelect.Done\": \"\",\n  \"SmartAddFilter.JoinTo\": \"\",\n  \"SmartAddFilter.toggleIncludeLinkedColumns\": \"\",\n  ContextDataSelector: \"\",\n  ClickCatchOverlay: \"\",\n  \"BackupControls.Restore\": \"\",\n  ChartLayerManager: \"\",\n  \"App.colorScheme\": \"\",\n  \"ElectronSetup.Next\": \"\",\n  \"ElectronSetup.Back\": \"\",\n  PostgresInstallationInstructions: \"\",\n  \"PostgresInstallationInstructions.Close\": \"\",\n  \"ElectronSetup.Done\": \"\",\n  SmartCardList: \"\",\n  \"AutomaticBackups.destination\": \"\",\n  \"AutomaticBackups.frequency\": \"\",\n  \"AutomaticBackups.hourOfDay\": \"\",\n  \"WorkspaceAddBtn.Create\": \"\",\n  MapOpacityMenu: \"\",\n  MapBasemapOptions: \"\",\n  \"InMapControls.goToDataBounds\": \"\",\n  \"InMapControls.showCursorCoords\": \"\",\n  LayerColorPicker: \"\",\n  \"W_TimeChart.resetExtent\": \"\",\n  TimeChartFilter: \"\",\n  \"ChartLayerManager.toggleLayer\": \"\",\n  \"ChartLayerManager.removeLayer\": \"\",\n  \"ChartLayerManager.AddChartLayer.addLayer\": \"\",\n  \"ChartLayerManager.AddChartLayer.addOSMLayer\": \"\",\n  ConnectionSelector: \"\",\n  \"Setup2FA.Enable\": \"\",\n  \"Setup2FA.Enable.GenerateQR\": \"\",\n  \"Setup2FA.Enable.CantScanQR\": \"\",\n  \"Setup2FA.Enable.Base64Secret\": \"\",\n  \"Setup2FA.Enable.ConfirmCode\": \"\",\n  \"Setup2FA.Enable.Confirm\": \"\",\n  \"Setup2FA.Disable\": \"\",\n  \"Setup2FA.error\": \"\",\n  \"DashboardMenuHeader.togglePinned\": \"\",\n  \"BackupControls.DeleteAll\": \"\",\n  \"BackupControls.DeleteAll.Confirm\": \"\",\n  \"ProjectConnection.error\": \"\",\n  NotFound: \"\",\n  \"NotFound.goHome\": \"\",\n  \"ConnectionServer.NewUserName\": \"\",\n  \"ConnectionServer.NewUserPassword\": \"\",\n  \"ConnectionServer.NewUserPermissionType\": \"\",\n  \"ConnectionServer.withNewOwnerToggle\": \"\",\n  \"W_Table.TableNotFound\": \"\",\n  JoinedRecords: \"\",\n  \"JoinedRecords.AddRow\": \"\",\n  \"JoinedRecords.SectionToggle\": \"\",\n  \"JoinedRecords.Section\": \"\",\n  \"SmartCard.viewEditRow\": \"\",\n  \"TimeChartLayerOptions.yAxis\": \"\",\n  \"TimeChartLayerOptions.aggFunc\": \"\",\n  \"TimeChartLayerOptions.aggFunc.select\": \"\",\n  \"TimeChartLayerOptions.groupBy\": \"\",\n  \"TimeChartLayerOptions.numericColumn\": \"\",\n  \"AgeFilter.comparator\": \"\",\n  \"AgeFilter.argsLeftToRight\": \"\",\n  \"Login.error\": \"\",\n  AskLLMAccessControl: \"\",\n  \"AskLLMAccessControl.AllowAll\": \"\",\n  \"Chat.messageList\": \"\",\n  \"Chat.sendWrapper\": \"\",\n  \"Chat.send\": \"\",\n  \"Chat.sendStop\": \"\",\n  \"Chat.addFiles\": \"\",\n  \"Chat.textarea\": \"\",\n  \"Chat.speech\": \"\",\n  AskLLM: \"\",\n  \"AskLLM.popup\": \"\",\n  SetupLLMCredentials: \"\",\n  \"SetupLLMCredentials.free\": \"\",\n  \"SetupLLMCredentials.api\": \"\",\n  \"AskLLMAccessControl.llm_daily_limit\": \"\",\n\n  DeckGLFeatureEditor: \"\",\n  \"MapBasemapOptions.Projection\": \"\",\n\n  SmartFilterBar: \"\",\n  SearchList: \"\",\n  \"SearchList.MatchCase\": \"\",\n  AddJoinFilter: \"\",\n  Pagination: \"\",\n  \"Pagination.page\": \"\",\n  \"Pagination.lastPage\": \"\",\n  \"Pagination.nextPage\": \"\",\n  \"Pagination.prevPage\": \"\",\n  \"Pagination.firstPage\": \"\",\n  \"Pagination.pageCountInfo\": \"\",\n  \"Pagination.pageSize\": \"\",\n  MapExtentBehavior: \"\",\n  AddLLMCredentialForm: \"\",\n  \"AddLLMCredentialForm.Save\": \"\",\n  \"AddLLMCredentialForm.Provider\": \"\",\n  \"App.LanguageSelector\": \"\",\n  \"EmailAuthSetup.SignupType\": \"\",\n  EmailAuthSetup: \"\",\n  \"EmailAuthSetup.error\": \"\",\n  \"EmailAuthSetup.toggle\": \"\",\n  EmailSMTPAndTemplateSetup: \"\",\n  \"EmailSMTPAndTemplateSetup.save\": \"\",\n  \"Login.toggle\": \"\",\n  AuthNotifPopup: \"\",\n  \"ProstglesSignup.continue\": \"\",\n  \"PublishedMethods.deleteFunction\": \"\",\n  \"SmartFormFieldOptions.NestedInsert\": \"\",\n\n  \"Btn.ClickConfirmation\": \"\",\n  \"Btn.ClickConfirmation.Confirm\": \"\",\n  AddMCPServer: \"\",\n  \"AddMCPServer.Open\": \"\",\n  \"AddMCPServer.Add\": \"\",\n  \"LLMChatOptions.MCPTools\": \"\",\n  \"LLMChatOptions.DatabaseAccess\": \"\",\n  \"MCPServersToolbar.stopAllToggle\": \"\",\n  \"MCPServersToolbar.searchTools\": \"\",\n  ConnectionsOptions: \"\",\n  \"ConnectionsOptions.showStateDatabase\": \"\",\n  \"ConnectionsOptions.showDatabaseNames\": \"\",\n  \"AuthProviderSetup.websiteURL\": \"\",\n  \"AuthProviderSetup.defaultUserType\": \"\",\n  \"AuthProviders.list\": \"\",\n  EmailSMTPSetup: \"\",\n  EmailTemplateSetup: \"\",\n  \"WorkspaceMenu.list\": \"\",\n  WorkspaceSettings: \"\",\n  \"LLMChatOptions.toggle\": \"\",\n  \"LLMChat.select\": \"\",\n  \"LLMChatOptions.Prompt\": \"\",\n  \"LLMChatOptions.Model\": \"\",\n  \"AskLLMChat.NewChat\": \"\",\n  \"AskLLMChat.LoadSuggestedToolsAndPrompt\": \"\",\n  \"AskLLMChat.LoadSuggestedDashboards\": \"\",\n  \"AskLLMChat.UnloadSuggestedDashboards\": \"\",\n  \"AskLLMToolApprover.AllowAlways\": \"\",\n  \"AskLLMToolApprover.AllowOnce\": \"\",\n  \"AskLLMToolApprover.Deny\": \"\",\n  MonacoEditor: \"\",\n  MCPServerTools: \"\",\n  \"MCPServerFooterActions.logs\": \"\",\n  \"MCPServerFooterActions.config\": \"\",\n  \"MCPServerFooterActions.enableToggle\": \"\",\n  \"MCPServerFooterActions.refreshTools\": \"\",\n  MCPServerConfigButton: \"\",\n  MCPServerConfig: \"\",\n  \"MCPServerConfig.save\": \"\",\n  \"MCPServers.toggleAutoApprove\": \"\",\n  Feedback: \"\",\n  \"FileImporterFooter.import\": \"\",\n\n  Sessions: \"Active sessions list\",\n  \"Account.ChangePassword\": \"Change password\",\n  NavBar: \"\",\n  \"NavBar.mobileMenuToggle\": \"\",\n  \"NavBar.logout\": \"\",\n  CommandPalette: \"\",\n  \"Chat.attachedFiles\": \"\",\n  \"Window.W_QuickMenu.addCrossFilteredTable\": \"Add cross-filtered table\",\n  Alert: \"Alert popup\",\n  ErrorComponent: \"\",\n  ToolUseMessage: \"\",\n  \"ToolUseMessage.toggle\": \"\",\n  \"ToolUseMessage.Popup\": \"\",\n  MarkdownMonacoCode: \"\",\n  \"MCPServersInstall.install\": \"\",\n  NewConnectionForm: \"\",\n  \"BackupsControls.Completed\": \"Completed backups list\",\n  \"AllowedOriginCheck.FormField\": \"\",\n  \"APIDetailsWs.Examples\": \"\",\n  \"APIDetailsHttp.Examples\": \"\",\n  AllowedOriginCheck: \"\",\n  \"APIDetailsTokens.CreateToken\": \"\",\n  \"APIDetailsTokens.CreateToken.daysUntilExpiration\": \"\",\n  \"APIDetailsTokens.CreateToken.generate\": \"\",\n  APIDetailsTokens: \"\",\n  \"AskLLM.DeleteMessage\": \"\",\n  \"DockerSandboxCreateContainer.Logs\": \"\",\n  TableBody: \"\",\n  \"ServerSideFunctions.onMountEnabled\": \"\",\n  DashboardMenu: \"\",\n  \"SearchAll.Popup\": \"\",\n\n  AddComputedColMenu: \"\",\n  \"AddComputedColMenu.countOfAllRows\": \"\",\n  \"AddComputedColMenu.addBtn\": \"\",\n  \"AddComputedColMenu.name\": \"\",\n  \"AddComputedColMenu.addTo\": \"\",\n  \"LinkedColumn.joinType\": \"\",\n  \"LinkedColumn.layoutType\": \"\",\n  \"CreateColumn.next\": \"\",\n  FileColumnConfigEditor: \"\",\n  \"FileColumnConfigEditor.maxFileSizeMB\": \"\",\n  \"FileColumnConfigEditor.contentMode\": \"\",\n  CreateFileColumn: \"\",\n  ColumnQuickStats: \"\",\n  \"ColumnQuickStats.addFilter\": \"\",\n  \"QuickAddComputedColumn.Add\": \"\",\n  \"QuickAddComputedColumn.name\": \"\",\n  \"LLMChatOptions.Prompt.Preview\": \"\",\n  \"FunctionColumnList.SearchInput\": \"\",\n  \"LLMChatOptions.Model.AddCredentials\": \"\",\n  QuickFilterGroupsControl: \"\",\n  LinkedColumn: \"\",\n  CreateColumn: \"\",\n  \"ToolUseMessage.toggleGroup\": \"\",\n  \"PGDumpOptions.format\": \"\",\n  \"PGDumpOptions.destination\": \"\",\n  \"PGDumpOptions.numberOfJobs\": \"\",\n  \"PGDumpOptions.compressionLevel\": \"\",\n  \"PGDumpOptions.excludeSchema\": \"\",\n  \"PGDumpOptions.noOwner\": \"\",\n  \"PGDumpOptions.create\": \"\",\n  \"PGDumpOptions.globalsOnly\": \"\",\n  \"PGDumpOptions.rolesOnly\": \"\",\n  \"PGDumpOptions.schemaOnly\": \"\",\n  \"PGDumpOptions.encoding\": \"\",\n  \"PGDumpOptions.clean\": \"\",\n  \"PGDumpOptions.dataOnly\": \"\",\n  \"PGDumpOptions.ifExists\": \"\",\n  \"PGDumpOptions.keepLogs\": \"\",\n  \"BackupControls.backupsInProgress\": \"\",\n  \"BackupsControls.Completed.delete\": \"\",\n  \"BackupsControls.Completed.download\": \"\",\n  \"BackupsControls.Completed.restore\": \"\",\n  \"BackupsControls.Completed.deleteAll\": \"\",\n  \"BackupsControls.restoreFromFile\": \"\",\n  BackupLogs: \"\",\n  FilterWrapper_FieldName: \"\",\n  FilterWrapper_Field: \"\",\n  \"CloudStorageCredentialSelector.selectCredential\": \"\",\n  DashboardMenuContent: \"\",\n} as const satisfies Record<\n  string,\n  | string\n  | {\n      desc: string;\n      uiOnly?: true;\n    }\n>;\nexport type Command = keyof typeof COMMANDS;\n\nexport type TestSelectors = {\n  \"data-command\"?: Command;\n  \"data-key\"?: string;\n  id?: string;\n};\n\nexport const dataCommand = (cmd: Command): { \"data-command\": Command } => ({\n  \"data-command\": cmd,\n});\nexport const getCommandElemSelector = (cmd: Command) => {\n  return `[data-command=${JSON.stringify(cmd)}]`;\n};\nexport const getDataKeyElemSelector = (key: string) => {\n  return `[data-key=${JSON.stringify(key)}]`;\n};\nexport const getDataLabelElemSelector = (key: string) => {\n  return `[data-label=${JSON.stringify(key)}]`;\n};\n\nexport const COMMAND_SEARCH_ATTRIBUTE_NAME = \"data-command-search-ended\";\n\nexport const MOCK_ELECTRON_WINDOW_ATTR = \"MOCK_ELECTRON_WINDOW_ATTR\" as const;\n\ndeclare module \"react\" {\n  interface HTMLAttributes<T> {\n    \"data-command\"?: Command;\n  }\n}\n\nexport declare namespace SVGif {\n  export type CursorAnimation =\n    | {\n        elementSelector: string;\n        offset?: { x: number; y: number };\n        duration: number;\n        type: \"click\" | \"clickAppearOnHover\";\n\n        /**\n         * Time to wait before clicking after reaching the final position\n         * */\n        waitBeforeClick?: number;\n        /**\n         * Time to stay on the final position after clicking\n         */\n        lingerMs?: number;\n      }\n    | {\n        type: \"moveTo\";\n        xy: [number, number];\n        duration: number;\n      };\n  export type Animation =\n    | CursorAnimation\n    | {\n        elementSelector: string;\n        duration: number;\n        type: \"type\";\n        extraAnimation?:\n          | { type: \"zoomToElement\" }\n          | { type: \"bringToFront\"; elementSelector: string };\n        /**\n         * Maximum scale to zoom in while typing\n         */\n        maxScale?: number;\n      }\n    | {\n        elementSelector: string;\n        duration: number;\n        type: \"zoomToElement\" | \"bringToFront\";\n        bringToFrontSelector?: string;\n        /**\n         * Maximum scale to zoom in\n         */\n        maxScale?: number;\n      }\n    | {\n        elementSelector: string;\n        duration: number;\n        type: \"fadeIn\" | \"growIn\";\n      }\n    | {\n        type: \"wait\";\n        duration: number;\n      };\n  export type Scene = {\n    svgFileName: string;\n    caption?: string;\n    animations: Animation[];\n  };\n}\n\n/**\n * TODO: Forbid imports to ensure this file is portable\n */\n"
  },
  {
    "path": "client/src/WithPrgl.tsx",
    "content": "import React from \"react\";\nimport type { Prgl } from \"./App\";\nimport { createReactiveState, useReactiveState } from \"./appUtils\";\nexport const prgl_R = createReactiveState<Prgl | undefined>(undefined);\n\nexport const WithPrgl = ({\n  onRender,\n}: {\n  onRender: (prgl: Prgl) => React.ReactNode;\n}): JSX.Element => {\n  const { state: prgl } = useReactiveState(prgl_R);\n\n  if (!prgl) return <></>;\n  const res = onRender(prgl);\n  return <>{res}</>;\n};\n"
  },
  {
    "path": "client/src/app/CommandPalette/CommandPalette.css",
    "content": ".flicker {\n  animation: flicker .5s infinite ;\n}\n.flicker-slow {\n  animation: flicker 1.5s infinite ;\n}\n@keyframes flicker {\n  0% { opacity: .75; }\n  50% { opacity: 0.1; }\n  100% { opacity: .75; }\n}"
  },
  {
    "path": "client/src/app/CommandPalette/CommandPalette.tsx",
    "content": "import { FlashMessage } from \"@components/FlashMessage\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport {\n  mdiArrowSplitVertical,\n  mdiButtonPointer,\n  mdiCardTextOutline,\n  mdiChartLine,\n  mdiCheckboxOutline,\n  mdiFileUploadOutline,\n  mdiFormatListBulleted,\n  mdiFormSelect,\n  mdiFormTextbox,\n  mdiKeyboard,\n  mdiLink,\n  mdiListBoxOutline,\n  mdiMenu,\n  mdiNumeric,\n  mdiTextLong,\n} from \"@mdi/js\";\nimport React, { useEffect, useState } from \"react\";\nimport { NavLink } from \"react-router-dom\";\nimport { flatUIDocs, type UIDoc, type UIDocInputElement } from \"../UIDocs\";\nimport \"./CommandPalette.css\";\nimport { Documentation } from \"./Documentation\";\nimport { useGoToUI } from \"./useGoToUI\";\nimport { getItemSearchRank } from \"@components/SearchList/searchMatchUtils/getItemSearchRank\";\nimport { isPlaywrightTest } from \"src/i18n/i18nUtils\";\nimport { getProperty } from \"@common/utils\";\n\n/**\n * By pressing Ctrl+K, the user to search and go to functionality in the UI.\n */\nexport const CommandPalette = ({ isElectron }: { isElectron: boolean }) => {\n  const { showSection, setShowSection } = useOnKeyDown();\n  const [highlights, setHighlights] = useState<CommandSearchHighlight[]>([]);\n  const { message, setMessage, goToUIDocItem } = useGoToUI(setHighlights);\n\n  return (\n    <>\n      {highlights.map((h, i) => (\n        <div\n          key={i}\n          style={{\n            position: \"fixed\",\n            zIndex: 9999,\n            left: `${h.left}px`,\n            top: `${h.top}px`,\n            width: `${h.width}px`,\n            height: `${h.height}px`,\n            background: \"var(--active-hover)\",\n            borderRadius: h.borderRadius,\n            pointerEvents: \"none\",\n            touchAction: \"none\",\n          }}\n          className={\n            \"CommandPalette_Highlighter \" +\n            (h.flickerSlow ? \"flicker-slow\" : \"flicker\")\n          }\n        />\n      ))}\n      {message ?\n        <FlashMessage {...message} onFinished={() => setMessage(undefined)} />\n      : showSection && (\n          <Popup\n            key={showSection}\n            title={\n              showSection === \"commands\" ? undefined : (\n                <NavLink to={\"/documentation\"}>Documentation</NavLink>\n              )\n            }\n            data-command=\"CommandPalette\"\n            clickCatchStyle={{ opacity: 1 }}\n            positioning={showSection === \"commands\" ? \"top-center\" : \"center\"}\n            onClose={() => setShowSection(undefined)}\n            contentClassName={\n              \"flex-col gap-2 \" + (showSection === \"docs\" ? \" p-2\" : \"p-1\")\n            }\n            contentStyle={\n              showSection === \"docs\" ?\n                {\n                  textAlign: \"left\",\n                }\n              : {\n                  width: \"min(100vw, 700px)\",\n                  maxHeight: \"min(100vh, 500px)\",\n                }\n            }\n          >\n            {showSection === \"commands\" ?\n              <SearchList\n                placeholder=\"Search actions...\"\n                autoFocus={true}\n                limit={100}\n                items={flatUIDocs.map((data) => {\n                  const iconKey =\n                    data.type === \"input\" ?\n                      `${data.type}-${data.inputType}`\n                    : data.type;\n                  const iconPath =\n                    data.iconPath ?? getProperty(UIDocTypeToIcon, iconKey);\n                  if (!iconPath) {\n                    console.warn(\"No icon for UIDoc type\", iconKey, data);\n                  }\n                  return {\n                    key: data.title,\n                    parentLabels: data.parentTitles,\n                    label: data.title,\n                    subLabel: data.description,\n                    contentLeft:\n                      iconPath ?\n                        <Icon\n                          path={iconPath}\n                          title={data.type}\n                          className=\"text-1 f-0\"\n                        />\n                      : undefined,\n                    onPress: async () => {\n                      setShowSection(undefined);\n                      await goToUIDocItem(data);\n                    },\n                    ranking: (searchTerm) =>\n                      getItemSearchRank(\n                        {\n                          title: data.title,\n                          subTitle: data.description,\n                          level: data.parentTitles.length,\n                        },\n                        searchTerm,\n                      ),\n                    data,\n                  };\n                })}\n              />\n            : <Documentation isElectron={isElectron} />}\n          </Popup>\n        )\n      }\n    </>\n  );\n};\n\nexport type CommandSearchHighlight = {\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n  borderRadius: string;\n  flickerSlow?: boolean;\n};\n\nconst useOnKeyDown = () => {\n  const [showSection, setShowSection] = useState<\"commands\" | \"docs\">();\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"F1\") {\n        event.preventDefault();\n        setShowSection(\"docs\");\n      }\n      if (event.ctrlKey && event.key === \"k\") {\n        event.preventDefault();\n        setShowSection(\"commands\");\n      }\n      if (event.key === \"Escape\") {\n        setShowSection(undefined);\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [showSection]);\n  return { showSection, setShowSection };\n};\n\nconst UIDocTypeToIcon: Partial<\n  Record<\n    `${UIDocInputElement[\"type\"]}-${UIDocInputElement[\"inputType\"]}`,\n    string\n  > &\n    Record<Exclude<UIDoc[\"type\"], \"input\">, string>\n> = {\n  link: mdiLink,\n  button: mdiButtonPointer,\n  popup: mdiButtonPointer,\n  select: mdiButtonPointer,\n  \"input-text\": mdiFormTextbox,\n  \"input-checkbox\": mdiCheckboxOutline,\n  \"input-file\": mdiFileUploadOutline,\n  \"input-number\": mdiNumeric,\n  \"input-select\": mdiFormSelect,\n  smartform: mdiListBoxOutline,\n  \"smartform-popup\": mdiListBoxOutline,\n  list: mdiFormatListBulleted,\n  section: mdiCardTextOutline,\n  tab: mdiCardTextOutline,\n  \"accordion-item\": mdiCardTextOutline,\n  \"drag-handle\": mdiArrowSplitVertical,\n  \"hotkey-popup\": mdiKeyboard,\n  navbar: mdiMenu,\n  text: mdiTextLong,\n  canvas: mdiChartLine,\n  // page: mdiGrid,\n};\n\nif (isPlaywrightTest) {\n  flatUIDocs.forEach(({ title: searchTerm }) => {\n    let lowestRank = { value: Infinity, title: \"\" };\n    flatUIDocs.forEach(({ title, description, parentTitles }) => {\n      const rank = getItemSearchRank(\n        {\n          title,\n          subTitle: description,\n          level: parentTitles.length,\n        },\n        searchTerm,\n      );\n\n      if (rank < lowestRank.value) {\n        lowestRank = { value: rank, title };\n      }\n    });\n\n    if (searchTerm !== lowestRank.title) {\n      throw new Error(\n        `Search rank test failed for term \"${searchTerm}\". Expected \"${searchTerm}\" to rank highest, but got \"${lowestRank.title}\"`,\n      );\n    }\n  });\n}\n"
  },
  {
    "path": "client/src/app/CommandPalette/Documentation.css",
    "content": ".Documentation img {\n  max-width: 100%;\n}\n"
  },
  {
    "path": "client/src/app/CommandPalette/Documentation.tsx",
    "content": "import React, { useMemo } from \"react\";\nimport Markdown from \"react-markdown\";\nimport rehypeRaw from \"rehype-raw\";\nimport \"./Documentation.css\";\n\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { useTypedSearchParams } from \"src/hooks/useTypedSearchParams\";\nimport { getDocumentationFiles } from \"./getDocumentation\";\nimport remarkGfm from \"remark-gfm\";\n\ntype P = {\n  isElectron: boolean;\n};\nexport const Documentation = ({ isElectron }: P) => {\n  const { docFiles, docFilesMap } = useMemo(() => {\n    const docFiles = getDocumentationFiles(isElectron).map((d) => ({\n      ...d,\n      text: d.text.replaceAll(`=\"./screenshots/`, `=\"/screenshots/`),\n    }));\n    const docFilesMap = new Map(docFiles.map((d) => [d.id, d]));\n    return {\n      docFiles,\n      docFilesMap,\n    };\n  }, [isElectron]);\n\n  const [{ documentation_section: section = docFiles[0]?.id }, setParams] =\n    useTypedSearchParams({\n      documentation_section: { type: \"string\", optional: true },\n    });\n\n  const currentDocFile = section ? docFilesMap.get(section) : undefined;\n\n  const WrappingElement = window.isLowWidthScreen ? FlexCol : FlexRow;\n\n  return (\n    <FlexCol className=\"Documentation min-s-0 bg-color-0 p-1\">\n      <WrappingElement className=\"ai-start min-s-0 f-1\">\n        <ScrollFade role=\"navigation\">\n          {docFiles.map((docFile) => (\n            <Btn\n              role=\"menuitem\"\n              style={{ width: \"100%\" }}\n              key={docFile.id}\n              aria-current={section === docFile.id}\n              variant={section === docFile.id ? \"faded\" : undefined}\n              onClick={() => setParams({ documentation_section: docFile.id })}\n            >\n              {docFile.title}\n            </Btn>\n          ))}\n        </ScrollFade>\n        <ScrollFade\n          scrollRestore={true}\n          style={{\n            width: \"min(100vw, 850px)\",\n            gap: 0,\n            height: \"100%\",\n            overflowY: \"auto\",\n            lineHeight: 1.5,\n          }}\n        >\n          {!currentDocFile ? null : (\n            <Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>\n              {currentDocFile.text}\n            </Markdown>\n          )}\n        </ScrollFade>\n      </WrappingElement>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/app/CommandPalette/getDocumentation.ts",
    "content": "import { isObject } from \"@common/publishUtils\";\nimport { fixIndent } from \"../../demo/scripts/sqlVideoDemo\";\nimport { COMMANDS } from \"../../Testing\";\nimport { UIDocs, type UIDoc, type UIDocElement } from \"../UIDocs\";\n\ntype SeparatePage = { doc: UIDoc; parentDocs: UIDoc[]; depth: number };\n\nconst asList = (\n  children: UIDocElement[],\n  parentDocs: UIDoc[],\n  separatePageDepth: number | undefined,\n  isElectron: boolean,\n) => {\n  const depth = parentDocs.length;\n  const listItemDepth = depth - (separatePageDepth ?? 1);\n\n  const separatePages: SeparatePage[] = [];\n\n  const listContent: string = children\n    .map((child) => {\n      const moveToNewPage = Boolean(child.docs);\n      if (moveToNewPage) {\n        separatePages.push({ doc: child, depth, parentDocs });\n      }\n\n      const listTitle =\n        moveToNewPage ?\n          `<a href=${JSON.stringify(`#${toSnakeCase(child.title)}`)}>${child.title}</a>`\n        : `**${child.title}**`;\n      const listItem = `${\"  \".repeat(listItemDepth)}- ${listTitle}: ${child.description}  `;\n      if (moveToNewPage) {\n        return listItem;\n      }\n      const items = getChildren(child, isElectron);\n      if (items.length) {\n        const nestedList = asList(\n          items,\n          [...parentDocs, child],\n          separatePageDepth,\n          isElectron,\n        );\n        nestedList.separatePages.forEach((sp) => separatePages.push(sp));\n        return listItem + \"\\n\" + nestedList.listContent;\n      }\n      return listItem;\n    })\n    .join(\"\\n\");\n\n  return {\n    listContent,\n    separatePages,\n  };\n};\n\nconst getUIDocAsMarkdown = (\n  doc: UIDoc,\n  parentDocs: UIDoc[],\n  separatePageDepth: number | undefined,\n  isElectron: boolean,\n): {\n  title: string;\n  content: string;\n  doc: UIDoc;\n}[] => {\n  const { docOptions } = doc;\n  const depth = docOptions ? 0 : Math.min(3, parentDocs.length);\n  if (isElectron && doc.uiVersionOnly) {\n    return [];\n  }\n  const { listContent: childrenContent, separatePages } = asList(\n    getChildren(doc, isElectron),\n    [...parentDocs, doc],\n    separatePageDepth,\n    isElectron,\n  );\n\n  const separatePagesWithContent = separatePages.flatMap((sp) =>\n    getUIDocAsMarkdown(sp.doc, sp.parentDocs, sp.depth, isElectron),\n  );\n\n  if (doc.uiVersionOnly) {\n    const sel = \"selectorCommand\" in doc ? doc.selectorCommand : undefined;\n    const cmdInfo = sel && COMMANDS[sel];\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    if (!cmdInfo || !isObject(cmdInfo) || !cmdInfo.uiOnly) {\n      throw new Error(\n        `UI Version Only documentation for \"${doc.title}\" does not have a valid selectorCommand.`,\n      );\n    }\n  }\n\n  const hDepth = depth + 1;\n  const childrenTitle =\n    doc.childrenTitle ?? (doc.type === \"navbar\" ? \"Navbar items\" : undefined);\n\n  const displayTitle = isObject(docOptions) ? docOptions.title : doc.title;\n  const content = [\n    `<h${hDepth} id=${JSON.stringify(toSnakeCase(doc.title))}> ${displayTitle} </h${hDepth}> \\n`,\n    doc.uiVersionOnly ? `>  Not available on Desktop version\\n  ` : \"\",\n    `${doc.docs ? fixIndent(doc.docs) : doc.description}\\n`,\n    childrenTitle ? `### ${childrenTitle}` : \"\",\n    childrenContent,\n  ]\n    .filter(Boolean)\n    .join(\"\\n\");\n\n  return [\n    {\n      title: doc.title,\n      doc,\n      content,\n    },\n  ].concat(separatePagesWithContent);\n};\n\nexport type DocumentationFile = {\n  id: string;\n  fileName: string;\n  title: string;\n  text: string;\n};\nexport const getDocumentationFiles = (isElectron: boolean) => {\n  const documentationPages: DocumentationFile[] = [];\n  UIDocs.forEach((doc) => {\n    const docItems = getUIDocAsMarkdown(doc, [], undefined, isElectron);\n\n    const pushFile = (title: string, text: string) => {\n      const index = documentationPages.length + 1;\n      documentationPages.push({\n        fileName: `${index.toString().padStart(2, \"0\")}_${title.replaceAll(\" \", \"_\")}.md`,\n        title,\n        id: toSnakeCase(title),\n        text,\n      });\n    };\n    if (documentationPages.length) {\n      pushFile(doc.title, \"\");\n    }\n    do {\n      const lastFile = documentationPages.at(-1);\n      const currDocItem = docItems.shift();\n      if (!currDocItem) {\n        continue;\n      }\n      const asSeparateFile = currDocItem.doc.docOptions === \"asSeparateFile\";\n      if (!lastFile || asSeparateFile) {\n        const title = asSeparateFile ? currDocItem.doc.title : doc.title;\n        pushFile(title, currDocItem.content + \"\\n\\n\");\n      } else {\n        lastFile.text += currDocItem.content + \"\\n\\n\";\n      }\n    } while (docItems.length);\n  });\n\n  return documentationPages;\n};\n\nconst toSnakeCase = (str: string) =>\n  str.toLowerCase().trim().replaceAll(/ /g, \"_\");\n\nconst getChildren = (doc: UIDoc, isElectron: boolean) => {\n  if (doc.docOptions === \"hideChildren\") return [];\n  const items =\n    \"children\" in doc ? doc.children\n    : \"itemContent\" in doc ? doc.itemContent\n    : \"pageContent\" in doc ? (doc.pageContent ?? [])\n    : [];\n\n  if (isElectron) {\n    return items.filter((item) => !item.uiVersionOnly);\n  }\n  return items;\n};\n\nwindow.documentation = getDocumentationFiles(false);\n"
  },
  {
    "path": "client/src/app/CommandPalette/getUIDocShortestPath.ts",
    "content": "import { filterArr } from \"@common/llmUtils\";\nimport type { UIDoc, UIDocPage } from \"../UIDocs\";\nimport { getCommandElemSelector } from \"src/Testing\";\nimport { isDefined } from \"prostgles-types\";\n\nexport const getUIDocShortestPath = (\n  currentPage: UIDocPage,\n  prevParents: UIDoc[],\n): undefined | UIDoc[] => {\n  const currentPageLinks = filterArr(currentPage.children, {\n    type: \"link\",\n  } as const);\n  const shortcut = prevParents.slice().map((doc, index) => {\n    if (doc.type === \"popup\") {\n      if (\n        doc.contentSelectorCommand &&\n        document.querySelectorAll(\n          getCommandElemSelector(doc.contentSelectorCommand),\n        ).length === 1\n      ) {\n        return { index };\n      }\n      const selector =\n        doc.selector ?? getCommandElemSelector(doc.selectorCommand);\n      if (index > 0 && document.querySelectorAll(selector).length === 1) {\n        return { index: index - 1 };\n      }\n    } else if (doc.type === \"page\" || doc.type === \"link\") {\n      const matchingLink = currentPageLinks.find((link) => {\n        return (\n          link.path === doc.path &&\n          link.pathItem?.tableName === doc.pathItem?.tableName\n        );\n      });\n      if (!matchingLink) {\n        const isAlreadyOnPage =\n          currentPage.path === doc.path &&\n          currentPage.pathItem?.tableName === doc.pathItem?.tableName;\n        if (!isAlreadyOnPage) {\n          return undefined;\n        }\n        return { index };\n      }\n      return { matchingLink, index };\n    } else if (doc.type !== \"info\") {\n      if (\n        (doc.selector &&\n          document.querySelectorAll(doc.selector).length === 1) ||\n        (doc.selectorCommand &&\n          document.querySelectorAll(getCommandElemSelector(doc.selectorCommand))\n            .length === 1)\n      ) {\n        return { index };\n      }\n    }\n  });\n  const bestShortcut = shortcut.findLast(isDefined);\n  if (bestShortcut) {\n    const { matchingLink, index } = bestShortcut;\n    return [matchingLink, ...prevParents.slice(index + 1)].filter(isDefined);\n  }\n};\n"
  },
  {
    "path": "client/src/app/CommandPalette/useGoToUI.tsx",
    "content": "import { useCallback, useMemo } from \"react\";\nimport { useLocation, useNavigate } from \"react-router-dom\";\nimport { COMMAND_SEARCH_ATTRIBUTE_NAME } from \"../../Testing\";\nimport { useAlert } from \"@components/AlertProvider\";\nimport { click } from \"../../demo/demoUtils\";\nimport { isPlaywrightTest } from \"../../i18n/i18nUtils\";\nimport { tout } from \"../../utils/utils\";\nimport {\n  flatUIDocs,\n  type UIDoc,\n  type UIDocFlat,\n  type UIDocPage,\n} from \"../UIDocs\";\nimport type { CommandSearchHighlight } from \"./CommandPalette\";\nimport { useHighlightDocItem } from \"./useHighlightDocItem\";\nimport {\n  getDocPagePath,\n  getUIDocElements,\n  getUIDocElementsAndAlertIfEmpty,\n} from \"./utils\";\nimport { includes } from \"../../dashboard/W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\nimport { getUIDocShortestPath } from \"./getUIDocShortestPath\";\n\nexport type DocItemHighlightItemPosition = \"mid\" | \"last\";\n\nexport const useGoToUI = (\n  setHighlights: (h: CommandSearchHighlight[]) => void,\n) => {\n  const navigate = useNavigate();\n\n  const { addAlert } = useAlert();\n  const { highlight, message, setMessage, showMultiHighlight } =\n    useHighlightDocItem(setHighlights);\n\n  const location = useLocation();\n  const currentPage = useMemo(() => {\n    return flatUIDocs.find((doc) => {\n      if (doc.type === \"page\") {\n        const matchInfo = getDocPagePath(doc, location.pathname);\n        return matchInfo.isExactMatch;\n      }\n    }) as UIDocPage | undefined;\n  }, [location.pathname]);\n\n  const clickOneOrHighlight = useCallback(\n    async (doc: UIDoc, duration: DocItemHighlightItemPosition) => {\n      if (doc.type === \"info\") return;\n\n      const { items, fullSelector } = getUIDocElements(doc);\n      if (items.length === 1) {\n        await highlight(doc, duration);\n        await click(\"\", fullSelector);\n      } else if (items.length > 1) {\n        await showMultiHighlight(doc, duration);\n      }\n    },\n    [highlight, showMultiHighlight],\n  );\n\n  const goToUI = useCallback(\n    async (doc: UIDoc): Promise<undefined | boolean> => {\n      const nonIteractableContainers: UIDoc[\"type\"][] = [\n        \"info\",\n        \"list\",\n        \"page\",\n        \"navbar\",\n        \"section\",\n      ];\n      if (doc.type === \"info\") return;\n\n      if (doc.type === \"hotkey-popup\") {\n        const [maybeCtrl, charKey] = doc.hotkey;\n        const ctrlKEvent = new KeyboardEvent(\"keydown\", {\n          key: charKey.toLowerCase(),\n          code: \"Key\" + charKey,\n          ctrlKey: maybeCtrl === \"Ctrl\",\n          altKey: maybeCtrl === \"Alt\",\n          shiftKey: maybeCtrl === \"Shift\",\n          bubbles: true,\n        });\n\n        // Dispatch it on the document\n        document.dispatchEvent(ctrlKEvent);\n      } else if (doc.type === \"page\" || doc.type === \"navbar\") {\n        const { isExactMatch, paths } = getDocPagePath(doc, location.pathname);\n        if (!isExactMatch) {\n          navigate(paths[0]!);\n          await tout(400);\n          if (doc.type === \"page\" && doc.pathItem) {\n            await highlight(doc, \"mid\");\n          }\n          await tout(400);\n        }\n      } else if (nonIteractableContainers.includes(doc.type)) {\n        // Do not highlight non-interactable container types\n        const { items } = getUIDocElementsAndAlertIfEmpty(doc, addAlert);\n        return !items.length;\n      } else if (\n        doc.type === \"popup\" ||\n        doc.type === \"tab\" ||\n        doc.type === \"accordion-item\" ||\n        doc.type === \"link\" ||\n        doc.type === \"smartform-popup\"\n      ) {\n        await clickOneOrHighlight(doc, \"mid\");\n      } else {\n        await highlight(doc, \"mid\");\n      }\n    },\n    [location.pathname, navigate, clickOneOrHighlight, highlight, addAlert],\n  );\n\n  const goToUIDocItem = useCallback(\n    async (data: UIDocFlat) => {\n      const prevParents = data.parentDocs;\n      const shortcut =\n        currentPage ?\n          getUIDocShortestPath(currentPage, prevParents)\n        : undefined;\n      const pathItems = shortcut ?? prevParents;\n      const shouldBeOpened = includes(data.type, [\n        \"link\",\n        \"page\",\n        \"tab\",\n        \"popup\",\n        \"smartform-popup\",\n      ]);\n      const finalPathItems =\n        data.type === \"hotkey-popup\" ? [data]\n        : shouldBeOpened ? [...pathItems, data]\n        : pathItems;\n      for (const parent of finalPathItems) {\n        const shouldStop = await goToUI(parent);\n        if (!isPlaywrightTest && shouldStop) {\n          return;\n        }\n        await tout(200);\n      }\n      if (!shouldBeOpened) {\n        await highlight(data, \"last\");\n      }\n      window.document.body.setAttribute(\n        COMMAND_SEARCH_ATTRIBUTE_NAME,\n        data.title,\n      );\n    },\n    [currentPage, highlight, goToUI],\n  );\n\n  return {\n    message,\n    setMessage,\n    goToUIDocItem,\n  };\n};\n"
  },
  {
    "path": "client/src/app/CommandPalette/useHighlightDocItem.ts",
    "content": "import { useCallback, useState } from \"react\";\nimport { useAlert } from \"@components/AlertProvider\";\nimport { isDefined } from \"prostgles-types\";\nimport { getUIDocElementsAndAlertIfEmpty } from \"./utils\";\nimport type { UIDocNonInfo } from \"../UIDocs\";\nimport { scrollIntoViewIfNeeded, tout } from \"../../utils/utils\";\nimport type { CommandSearchHighlight } from \"./CommandPalette\";\nimport { isInParentViewport } from \"../domToSVG/utils/isElementVisible\";\nimport { isPlaywrightTest } from \"../../i18n/i18nUtils\";\nimport type { DocItemHighlightItemPosition } from \"./useGoToUI\";\n\nexport const useHighlightDocItem = (\n  setHighlights: (h: CommandSearchHighlight[]) => void,\n) => {\n  const { addAlert } = useAlert();\n  const [message, setMessage] = useState<{\n    text: string;\n    left: number;\n    top: number;\n  }>();\n  const highlight = useCallback(\n    async (doc: UIDocNonInfo, itemPosition: DocItemHighlightItemPosition) => {\n      const { items } = getUIDocElementsAndAlertIfEmpty(doc, addAlert);\n      const firstElem = items[0];\n      firstElem && scrollIntoViewIfNeeded(firstElem);\n      await tout(500);\n      const mustChooseOne = items.length > 1 && itemPosition !== \"last\";\n      const highlights = Array.from(items)\n        .map((el) => {\n          const rect = el.getBoundingClientRect();\n          const isVisible = isInParentViewport(el, rect);\n          if (!isVisible) {\n            return;\n          }\n          return {\n            left: rect.left,\n            top: rect.top,\n            width: rect.width,\n            height: rect.height,\n            borderRadius: getComputedStyle(el).borderRadius,\n            flickerSlow: itemPosition === \"last\" || mustChooseOne,\n          };\n        })\n        .filter(isDefined);\n\n      const firstItem = highlights[0];\n      setHighlights(highlights);\n      let waitedForClick = false;\n      if (firstItem && mustChooseOne) {\n        const { left, top } = firstItem;\n        setMessage({ text: \"Chose one\", left, top: top - 70 });\n        if (isPlaywrightTest) {\n          items[0]?.scrollIntoView();\n          items[0]?.click();\n        } else {\n          await new Promise((resolve) => {\n            window.addEventListener(\"click\", resolve, { once: true });\n            window.addEventListener(\"keydown\", resolve, { once: true });\n          });\n        }\n        waitedForClick = true;\n      }\n      if (!waitedForClick) {\n        await tout(\n          isPlaywrightTest ? 0\n          : itemPosition === \"mid\" ? 500\n          : 2000,\n        );\n      }\n      setHighlights([]);\n      setMessage(undefined);\n      return Array.from(items);\n    },\n    [addAlert, setHighlights],\n  );\n  const showMultiHighlight = useCallback(\n    async (doc: UIDocNonInfo, duration: DocItemHighlightItemPosition) => {\n      const [firstItem] = await highlight(doc, duration);\n      if (!firstItem) {\n        addAlert({\n          children: `No items found in the ${JSON.stringify(doc.title)} list.`,\n        });\n        return;\n      }\n    },\n    [highlight, addAlert],\n  );\n  return { highlight, message, setMessage, showMultiHighlight };\n};\n"
  },
  {
    "path": "client/src/app/CommandPalette/utils.ts",
    "content": "import { isObject } from \"@common/publishUtils\";\nimport type { AlertContext } from \"@components/AlertProvider\";\nimport { includes } from \"../../dashboard/W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\nimport { waitForElement } from \"../../demo/demoUtils\";\nimport { isPlaywrightTest } from \"../../i18n/i18nUtils\";\nimport { getCommandElemSelector, type Command } from \"../../Testing\";\nimport { isDefined } from \"../../utils/utils\";\nimport type { UIDoc, UIDocNonInfo } from \"../UIDocs\";\n\nexport const focusElement = async (\n  testId: Command | \"\",\n  endSelector?: string,\n) => {\n  const elem = await waitForElement<HTMLDivElement>(testId, endSelector);\n  elem.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n  elem.focus();\n  return elem;\n};\n\nexport const getUIDocElements = (doc: Exclude<UIDoc, { type: \"info\" }>) => {\n  const selectors = doc.type === \"page\" ? doc.pathItem : doc;\n  const { selectorCommand, selector = \"\" } = selectors ?? {};\n\n  const childSelector = doc.type === \"list\" ? doc.itemSelector : \"\";\n  let fullSelector =\n    (selectorCommand ? getCommandElemSelector(selectorCommand) : \"\") +\n    \" \" +\n    selector +\n    \" \" +\n    childSelector;\n  if (!fullSelector.trim() && doc.type === \"page\") {\n    fullSelector = \"body\";\n  }\n  const items = document.querySelectorAll<HTMLDivElement>(fullSelector);\n  return {\n    items,\n    fullSelector,\n  };\n};\n\nexport const highlightItems = (doc: Exclude<UIDoc, { type: \"info\" }>) => {\n  const listItems = getUIDocElements(doc).items;\n  listItems.forEach((el) => {\n    el.style.border = \"2px solid var(--text-warning)\";\n  });\n  return Array.from(listItems);\n};\n\nconst PATH_JOIN_CHARS = [\"/\", \"#\", \"?\"] as const;\nexport const getDocPagePath = (\n  doc: Extract<UIDoc, { type: \"page\" | \"navbar\" }>,\n  pathname: string,\n) => {\n  const paths =\n    doc.type === \"page\" ?\n      [doc.pathItem?.selectorPath, doc.path].filter(isDefined)\n    : doc.paths;\n  const matchedPage = paths.find((pathWopts) => {\n    const path = isObject(pathWopts) ? pathWopts.route : pathWopts;\n    const exact = isObject(pathWopts) && pathWopts.exact;\n    if (exact) {\n      return pathname === path;\n    }\n    const endChar = pathname[path.length];\n    return (\n      pathname.startsWith(path) &&\n      (!endChar || includes(endChar, PATH_JOIN_CHARS))\n    );\n  });\n  let isOnPage = !!matchedPage;\n  let currentPathItem: string | undefined;\n  if (matchedPage && doc.type === \"page\" && doc.pathItem) {\n    const matchedPagePath =\n      isObject(matchedPage) ? matchedPage.route : matchedPage;\n    currentPathItem = pathname\n      .slice(matchedPagePath.length + 1)\n      .split(`/[${PATH_JOIN_CHARS.join(\"\")}]/`)\n      .pop();\n    isOnPage = isOnPage && !!currentPathItem;\n  }\n\n  const isExactMatch =\n    !isOnPage ? false\n    : doc.type === \"navbar\" ? true\n    : doc.pathItem ? Boolean(currentPathItem)\n    : doc.path === pathname;\n  return {\n    isOnPage,\n    isExactMatch,\n    matchedPage,\n    paths: paths.map((pathWopts) =>\n      isObject(pathWopts) ? pathWopts.route : pathWopts,\n    ),\n  };\n};\n\nexport const getUIDocElementsAndAlertIfEmpty = (\n  doc: UIDocNonInfo,\n  addAlert: AlertContext[\"addAlert\"],\n) => {\n  const result = getUIDocElements(doc);\n  if (!result.items.length && !isPlaywrightTest) {\n    addAlert({\n      children: [\n        `Could not find a ${JSON.stringify(doc.title)} item.`,\n        `Selector used: ${JSON.stringify(result.fullSelector)}`,\n      ].join(\"\\n\"),\n    });\n  }\n  return result;\n};\n"
  },
  {
    "path": "client/src/app/UIDocs/UIInstallationUIDoc.ts",
    "content": "import type { UIDoc } from \"../UIDocs\";\n\nexport const UIInstallation = {\n  title: \"Installation\",\n  type: \"info\",\n  description: \"A guide to help you get started with the application.\",\n  docs: `\n    The quickest way to start is with Docker. \n    If you don't have Docker installed, please follow the official \n    <a href=\"https://docs.docker.com/engine/install/\" target=\"_blank\">Docker installation guide</a>\n \n    Download the source code:\n\n    \\`\\`\\`bash\n    git clone https://github.com/prostgles/ui.git\n    cd ui\n    \\`\\`\\`\n\n    Start the application:\n\n    \\`\\`\\`docker-compose.sh\n    docker compose up -d --build\n    \\`\\`\\` \n    \n    Once running, Prostgles UI will be available at [localhost:3004](http://localhost:3004)\n\n    ### Initial Setup & Authentication\n\n    When first launching Prostgles UI, an admin user will be created automatically:\n    - If \\`PRGL_USERNAME\\` and \\`PRGL_PASSWORD\\` environment variables are provided, an admin user is created with these credentials. \n    - Otherwise, a passwordless admin user is created. \n    It gets assigned to the first client that accesses the app. \n    Subsequent clients accessing the app will be rejected with an appropriate error message detailing that the passwordless admin user has already been assigned.\n\n    To setup multiple users, the passwordless admin user must be converted to a normal admin account by setting up a password.\n    This will allow accessing /users page where you can manage users.\n\n    Users login using their username and password. Two-factor authentication is provided through TOTP (Time-based One-Time Password) and can be enabled in the account section.\n\n    Email and third-party (OAuth) authentication can be configured in Server Settings section. It allows users to register and log in using their email address or third-party accounts like Google, GitHub, etc.\n   \n    `,\n} satisfies UIDoc;\n"
  },
  {
    "path": "client/src/app/UIDocs/accountUIDoc.ts",
    "content": "import { mdiAccountOutline, mdiApplicationBracesOutline } from \"@mdi/js\";\nimport { ROUTES } from \"@common/utils\";\nimport { getDataKeyElemSelector } from \"../../Testing\";\nimport type { UIDocContainers } from \"../UIDocs\";\n\nexport const accountUIDoc = {\n  type: \"page\",\n  path: ROUTES.ACCOUNT,\n  title: \"Account\",\n  iconPath: mdiAccountOutline,\n  description:\n    \"Manage your account settings, security preferences, and API access.\",\n  docs: `\n    Manage your account settings, security preferences, and API access.\n    \n    <img src=\"./screenshots/account.svgif.svg\" alt=\"Account Page\" />\n  `,\n  children: [\n    {\n      type: \"tab\",\n      title: \"Account details\",\n      selector: getDataKeyElemSelector(\"details\"),\n      description: \"View and update your account information.\",\n      children: [\n        {\n          type: \"smartform\",\n          title: \"Account information\",\n          description:\n            \"View all account details and associated data (workspaces, dashboards, views, etc.)\",\n          tableName: \"users\",\n          selectorCommand: \"SmartForm\",\n        },\n      ],\n    },\n    {\n      type: \"tab\",\n      title: \"Security\",\n      selector: getDataKeyElemSelector(\"security\"),\n      description: \"Manage your account security settings.\",\n      children: [\n        {\n          type: \"popup\",\n          title: \"Two-factor authentication\",\n          description:\n            \"Set up and manage two-factor authentication for enhanced security.\",\n          selectorCommand: \"Setup2FA.Enable\",\n          children: [\n            {\n              type: \"button\",\n              title: \"Generate QR Code\",\n              description:\n                \"Generate a QR code to set up 2FA with your authenticator app.\",\n              selectorCommand: \"Setup2FA.Enable.GenerateQR\",\n            },\n            {\n              type: \"button\",\n              title: \"Can't scan QR code\",\n              description:\n                \"View manual setup instructions if you can't scan the QR code.\",\n              selectorCommand: \"Setup2FA.Enable.CantScanQR\",\n            },\n            {\n              type: \"input\",\n              inputType: \"text\",\n              title: \"Confirm code\",\n              description:\n                \"Enter the code from your authenticator app to enable 2FA.\",\n              selectorCommand: \"Setup2FA.Enable.ConfirmCode\",\n            },\n            {\n              type: \"button\",\n              title: \"Enable 2FA\",\n              description:\n                \"Complete the 2FA setup process by confirming the code.\",\n              selectorCommand: \"Setup2FA.Enable.Confirm\",\n            },\n            {\n              type: \"text\",\n              title: \"Base64 secret\",\n              description:\n                \"Manual setup secret key for your authenticator app.\",\n              selectorCommand: \"Setup2FA.Enable.Base64Secret\",\n            },\n          ],\n        },\n        {\n          type: \"button\",\n          title: \"Disable 2FA\",\n          description: \"Turn off two-factor authentication for your account.\",\n          selectorCommand: \"Setup2FA.Disable\",\n        },\n        {\n          type: \"popup\",\n          title: \"Change password\",\n          description: \"Change your account password.\",\n          selectorCommand: \"Account.ChangePassword\",\n          children: [],\n        },\n        {\n          type: \"list\",\n          title: \"Active sessions\",\n          description: \"View and manage your active web sessions.\",\n          selectorCommand: \"Sessions\",\n          itemContent: [],\n          itemSelector: ``,\n        },\n      ],\n    },\n    {\n      type: \"tab\",\n      title: \"API\",\n      selector: getDataKeyElemSelector(\"api\"),\n      iconPath: mdiApplicationBracesOutline,\n      description: \"View and manage your API access settings.\",\n      children: [\n        {\n          type: \"section\",\n          title: \"API Details\",\n          description: \"View your API credentials and configuration.\",\n          selectorCommand: \"config.api\",\n          uiVersionOnly: true,\n          children: [],\n        },\n      ],\n    },\n  ],\n} satisfies UIDocContainers;\n"
  },
  {
    "path": "client/src/app/UIDocs/commandPaletteUIDoc.ts",
    "content": "import { getCommandElemSelector } from \"src/Testing\";\nimport type { UIDocContainers } from \"../UIDocs\";\n\nexport const commandPaletteUIDoc = {\n  type: \"hotkey-popup\",\n  hotkey: [\"Ctrl\", \"K\"],\n  title: \"Command Palette\",\n  description: \"Go to command/action quickly\",\n  docs: `\n    Keyboard-driven navigation to different parts of the application without having to browse through menus or panels. \n\n    Press <kbd>Ctrl+K</kbd> to open the command palette popup. Type to search through the documentation for functionality, settings, and other sections.\n    <img src=\"./screenshots/command_palette.svgif.svg\" alt=\"Command Palette\" />\n  `,\n  selectorCommand: \"CommandPalette\",\n  children: [\n    {\n      type: \"input\",\n      inputType: \"text\",\n      title: \"Search commands input\",\n      description: \"Type to search for commands and actions\",\n      selector:\n        getCommandElemSelector(\"CommandPalette\") +\n        \" \" +\n        getCommandElemSelector(\"SearchList.Input\"),\n    },\n    {\n      type: \"list\",\n      title: \"Search results\",\n      description:\n        \"List of matching commands and actions. Press Enter to execute/go to the selected command.\",\n      selector:\n        getCommandElemSelector(\"CommandPalette\") +\n        \" \" +\n        getCommandElemSelector(\"SearchList.List\"),\n      itemSelector: \"[data-key]\",\n      itemContent: [],\n    },\n  ],\n} satisfies UIDocContainers;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/AIAssistantUIDoc.ts",
    "content": "import { mdiMagnify, mdiPlus, mdiStop, mdiTools } from \"@mdi/js\";\nimport { fixIndent } from \"../../../demo/scripts/sqlVideoDemo\";\nimport { getCommandElemSelector } from \"../../../Testing\";\nimport type { UIDocElement } from \"../../UIDocs\";\nimport { DEFAULT_MCP_SERVER_NAMES } from \"@common/mcp\";\n\nexport const AIAssistantUIDoc = {\n  type: \"popup\",\n  selectorCommand: \"AskLLM\",\n  title: \"AI Assistant\",\n  description:\n    \"Opens an AI assistant to help generate SQL queries, understand database schema, or perform other tasks.\",\n  docs: fixIndent(`\n    The AI assistant is an intelligent companion that helps you work more efficiently with your PostgreSQL databases. \n    It can generate SQL queries, explain database schemas, analyze data patterns, and assist with various database-related tasks through a conversational interface.\n    MCP Servers can be used to extend the AI capabilities with custom tools and integrations.\n\n    <img src=\"./screenshots/ai_assistant.svgif.svg\" alt=\"AI assistant popup screenshot\" />\n \n    Supported AI Providers: OpenAI, Anthropic, Google Gemini, OpenRouter, and Local Models. \n\n    *Note: AI providers are configured by administrators in Server Settings > LLM Providers*\n  `),\n  docOptions: \"asSeparateFile\",\n  children: [\n    {\n      type: \"section\",\n      title: \"Header actions\",\n      description: \"Actions available in the header of the AI assistant popup.\",\n      selector: getCommandElemSelector(\"Popup.header\"),\n      children: [\n        {\n          type: \"smartform-popup\",\n          selector: getCommandElemSelector(\"LLMChatOptions.toggle\"),\n          title: \"Chat settings\",\n          tableName: \"llm_chats\",\n          description:\n            \"Allows editing all chat settings and data as well deleting or cloning the current chat.\",\n        },\n        {\n          type: \"input\",\n          inputType: \"select\",\n          selector: getCommandElemSelector(\"LLMChat.select\"),\n          title: \"Select chat\",\n          description:\n            \"Selects a chat from the list of available chats. Each chat represents a separate conversation with the AI assistant.\",\n        },\n        {\n          type: \"button\",\n          selector: getCommandElemSelector(\"AskLLMChat.NewChat\"),\n          title: \"New chat\",\n          description: \"Creates a new chat with the AI assistant.\",\n        },\n        {\n          type: \"button\",\n          selector: getCommandElemSelector(\"Popup.toggleFullscreen\"),\n          title: \"Toggle fullscreen\",\n          description:\n            \"Toggles the fullscreen mode for the AI assistant popup.\",\n        },\n        {\n          type: \"button\",\n          selector: getCommandElemSelector(\"Popup.close\"),\n          title: \"Close popup\",\n          description: \"Closes the AI assistant popup.\",\n        },\n      ],\n    },\n    {\n      type: \"list\",\n      title: \"Chat messages\",\n      description: \"List of messages in the current chat.\",\n      selector: getCommandElemSelector(\"Chat.messageList\"),\n      itemContent: [],\n      itemSelector: getCommandElemSelector(\"Chat.messageList\") + \" > .message\",\n    },\n    {\n      type: \"section\",\n      title: \"Message input\",\n      description:\n        \"Input field for entering messages to the AI assistant and quick actions.\",\n      docs: fixIndent(`\n        The message input area allows you to write text, attach files and control other aspects of the AI assistant (change model, add/remove tools, speech to text). \n\n      `),\n      selector: getCommandElemSelector(\"Chat.sendWrapper\"),\n      children: [\n        {\n          type: \"input\",\n          inputType: \"text\",\n          title: \"Message input\",\n          description:\n            \"Input field for entering messages to the AI assistant. Pressing Shift+Enter creates a new line.\",\n          selector: getCommandElemSelector(\"Chat.textarea\"),\n        },\n        {\n          type: \"popup\",\n          title: \"MCP tools allowed\",\n          iconPath: mdiTools,\n          description: `Opens the MCP tools menu for the current chat. Default tools: ${DEFAULT_MCP_SERVER_NAMES.join(\", \")}`,\n          docs: `\n            MCP Servers extend the capabilities of the AI assistant by providing custom tools and integrations.\n          `,\n          selector: getCommandElemSelector(\"LLMChatOptions.MCPTools\"),\n          children: [\n            {\n              type: \"popup\",\n              selector: getCommandElemSelector(\"AddMCPServer.Open\"),\n              title: \"Add MCP server\",\n              iconPath: mdiPlus,\n              description:\n                \"Opens the form to add a new MCP server for the current chat.\",\n              children: [\n                {\n                  type: \"input\",\n                  inputType: \"text\",\n                  title: \"MCP tool json config\",\n                  description:\n                    \"JSON configuration for the MCP tool to be added.\",\n                  selector:\n                    getCommandElemSelector(\"AddMCPServer\") +\n                    \" \" +\n                    getCommandElemSelector(\"MonacoEditor\"),\n                },\n                {\n                  type: \"popup\",\n                  title: \"Add MCP server\",\n                  description:\n                    \"Adds the specified MCP server to the current chat.\",\n                  selector: getCommandElemSelector(\"AddMCPServer.Add\"),\n                  children: [],\n                },\n              ],\n            },\n            {\n              type: \"button\",\n              title: \"Stop/Start all MCP Servers\",\n              description: \"Quick way to stop/restart all MCP servers.\",\n              selector: getCommandElemSelector(\n                \"MCPServersToolbar.stopAllToggle\",\n              ),\n              iconPath: mdiStop,\n            },\n            {\n              type: \"button\",\n              title: \"Search tools\",\n              description:\n                \"Searches for specific MCP tools in the list of available tools.\",\n              selector: getCommandElemSelector(\"MCPServersToolbar.searchTools\"),\n              iconPath: mdiMagnify,\n            },\n            {\n              type: \"list\",\n              title: \"MCP tools\",\n              iconPath: mdiTools,\n              description:\n                \"List of available MCP tools. To allow a tool to be used in the current chat it must be ticked. Each tool represents a specific functionality or integration.\",\n              selector:\n                getCommandElemSelector(\"LLMChatOptions.MCPTools\") +\n                \" \" +\n                getCommandElemSelector(\"SmartCardList\"),\n              itemSelector: \".SmartCard\",\n              itemContent: [\n                {\n                  type: \"text\",\n                  title: \"MCP server name\",\n                  description:\n                    \"Name of the parent MCP server associated with the tool.\",\n                  selector: \"div\",\n                },\n                {\n                  type: \"list\",\n                  title: \"MCP server tools\",\n                  description:\n                    \"List of available tools for the selected MCP server. Click to enable or disable a specific tool for the current chat.\",\n                  selector: getCommandElemSelector(\"MCPServerTools\"),\n                  itemSelector: \".SmartCard\",\n                  itemContent: [],\n                },\n                {\n                  type: \"popup\",\n                  title: \"MCP Server Logs\",\n                  description:\n                    \"Opens the logs for the selected MCP server, allowing you to view its activity and status.\",\n                  selector: getCommandElemSelector(\n                    \"MCPServerFooterActions.logs\",\n                  ),\n                  children: [],\n                },\n                {\n                  type: \"popup\",\n                  title: \"MCP Server Config\",\n                  description:\n                    \"Opens the configuration for the selected MCP server, allowing you to manage its settings.\",\n                  selector: getCommandElemSelector(\"MCPServerConfigButton\"),\n                  children: [\n                    {\n                      type: \"button\",\n                      title: \"Save config\",\n                      description:\n                        \"Saves the configuration for the selected MCP server.\",\n                      selector: getCommandElemSelector(\"MCPServerConfig.save\"),\n                    },\n                  ],\n                },\n                {\n                  type: \"button\",\n                  title: \"Reload MCP tools\",\n                  description:\n                    \"Reloads the MCP tools for the selected MCP server, updating the list of available tools.\",\n                  selectorCommand: \"MCPServerFooterActions.refreshTools\",\n                },\n                {\n                  type: \"button\",\n                  title: \"Enable/Disable MCP server\",\n                  description:\n                    \"Enables or disables the selected MCP server for all chats. If configuration is required a popup will be shown.\",\n                  selector: getCommandElemSelector(\n                    \"MCPServerFooterActions.enableToggle\",\n                  ),\n                },\n              ],\n            },\n          ],\n        },\n        {\n          type: \"smartform-popup\",\n          selector: getCommandElemSelector(\"LLMChatOptions.DatabaseAccess\"),\n          title: \"Database access\",\n          description:\n            \"Opens the database access settings for the current chat. This controls how the AI assistant can interact with the current database.\",\n          tableName: \"llm_chats\",\n          fieldNames: [\"db_data_permissions\", \"db_schema_permissions\"],\n        },\n        {\n          type: \"popup\",\n          title: \"Prompt Selector\",\n          description:\n            \"Opens the prompt details for the current chat, allowing you to manage the prompt template and other related settings.\",\n          selector: getCommandElemSelector(\"LLMChatOptions.Prompt\"),\n          children: [\n            {\n              type: \"popup\",\n              selectorCommand: \"LLMChatOptions.Prompt.Preview\",\n              title: \"Prompt preview\",\n              description:\n                \"Preview of the prompt with context variables filled in.\",\n              children: [],\n            },\n          ],\n        },\n        {\n          type: \"popup\",\n          title: \"LLM Model\",\n          description:\n            \"Selects the LLM model to be used for the current chat. Different models may have different capabilities and performance. \",\n          selector: getCommandElemSelector(\"LLMChatOptions.Model\"),\n          children: [\n            {\n              type: \"button\",\n              selector: `${getCommandElemSelector(\"LLMChatOptions.Model\")} .LABELWRAPPER`,\n              title: \"Select model\",\n              description: \"Selects this LLM model for the current chat.\",\n            },\n            {\n              type: \"smartform-popup\",\n              title: \"Add model credentials\",\n              description:\n                \"Opens the form to add llm provider credentials for the selected LLM model.\",\n              selectorCommand: \"LLMChatOptions.Model.AddCredentials\",\n              tableName: \"llm_credentials\",\n            },\n          ],\n        },\n        {\n          type: \"input\",\n          inputType: \"file\",\n          selectorCommand: \"Chat.addFiles\",\n          title: \"Attach files\",\n          description:\n            \"Attaches files to be sent to the AI assistant along with the message. Supported file types may vary depending on the AI model and configuration.\",\n        },\n        {\n          type: \"popup\",\n          title: \"Speech to Text\",\n          selectorCommand: \"Chat.speech\",\n          description:\n            \"Opens the speech-to-text input options, allowing you to send audio recordings or transcribe audio messages to send to the AI assistant. Right click to open speech-to-text settings.\",\n          children: [],\n        },\n        {\n          type: \"button\",\n          title: \"Send message\",\n          description: \"Sends the entered message to the AI assistant.\",\n          selector: getCommandElemSelector(\"Chat.send\"),\n        },\n      ],\n    },\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/config/accessControlUIDoc.ts",
    "content": "import { mdiAccountMultiple } from \"@mdi/js\";\nimport { fixIndent } from \"../../../../demo/scripts/sqlVideoDemo\";\nimport type { UIDoc } from \"../../../UIDocs\";\n\nexport const accessControlUIDoc = {\n  type: \"tab\",\n  selectorCommand: \"config.ac\",\n  uiVersionOnly: true,\n  title: \"Access control\",\n  iconPath: mdiAccountMultiple,\n  description:\n    \"Manage user permissions and access rules for this database connection.\",\n  docs: fixIndent(`\n    Manage user permissions and access rules for this database connection.\n    <img src=\"./screenshots/access_control.svgif.svg\" alt=\"Access control\" />\n  `),\n  docOptions: \"asSeparateFile\",\n  children: [\n    {\n      type: \"button\",\n      selectorCommand: \"config.ac.create\",\n      title: \"Create Access Rule\",\n      description: \"Add a new access control rule to define user permissions.\",\n    },\n    {\n      type: \"button\",\n      selectorCommand: \"config.ac.save\",\n      title: \"Save Access Rule\",\n      description: \"Save changes to the current access control rule.\",\n    },\n    {\n      type: \"button\",\n      selectorCommand: \"config.ac.cancel\",\n      title: \"Cancel Changes\",\n      description: \"Cancel editing the current access control rule.\",\n    },\n    {\n      type: \"button\",\n      selectorCommand: \"config.ac.removeRule\",\n      title: \"Remove Rule\",\n      description: \"Delete the selected access control rule.\",\n    },\n  ],\n} satisfies UIDoc;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/config/apiUIDoc.ts",
    "content": "import { mdiApplicationBracesOutline } from \"@mdi/js\";\nimport type { UIDocElement } from \"../../../UIDocs\";\n\nexport const apiUIDoc = {\n  type: \"tab\",\n  selectorCommand: \"config.api\",\n  iconPath: mdiApplicationBracesOutline,\n  title: \"API\",\n  description: \"Configure API access settings and view API documentation.\",\n  docs: `\n  The API section allows you to configure the API access settings for your application.\n  This enables programmatic access to your application's data and functionality via HTTP or WebSocket protocols.\n  Generated database types can be used to interact with the API in a type-safe manner through our TypeScript client library.\n  Code snippets are provided to help you get started with using the API in your applications.\n  You can also manage API tokens for authentication and access control.\n  \n  You can set the URL path for the API, manage allowed origins for CORS requests, and create or view API tokens for accessing the API. The API supports both WebSocket and HTTP protocols.`,\n  docOptions: \"asSeparateFile\",\n  children: [\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selector: \"input#url_path\",\n      title: \"API URL Path\",\n      description:\n        \"Set the URL path for the API. This is the base path for all API endpoints.\",\n    },\n    {\n      type: \"popup\",\n      title: \"Allowed Origin Alert\",\n      selectorCommand: \"AllowedOriginCheck\",\n      description: \"Will only appear if the allowed origin is not set.\",\n      children: [\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selectorCommand: \"AllowedOriginCheck.FormField\",\n          title: \"Allowed Origin\",\n          description:\n            \"Set the allowed origin for CORS requests. This controls which domains can make cross-origin requests to this app by setting the Access-Control-Allow-Origin header.\",\n        },\n      ],\n    },\n    {\n      type: \"popup\",\n      selectorCommand: \"APIDetailsWs.Examples\",\n      title: \"Websocket API usage examples\",\n      description:\n        \"View examples of how to use the API using typescript and javascript\",\n      children: [],\n    },\n    {\n      type: \"popup\",\n      selectorCommand: \"APIDetailsHttp.Examples\",\n      title: \"HTTP API usage examples\",\n      description:\n        \"View examples of how to use the API using typescript and javascript\",\n      children: [],\n    },\n    {\n      type: \"section\",\n      selectorCommand: \"APIDetailsTokens\",\n      title: \"API Tokens\",\n      description:\n        \"Shows existing API tokens and allows you to create new ones.\",\n      children: [\n        {\n          type: \"popup\",\n          selectorCommand: \"APIDetailsTokens.CreateToken\",\n          title: \"Create API Token\",\n          description:\n            \"Create a new API token for accessing the API. Tokens can be used for both WebSocket and HTTP API access.\",\n          children: [\n            {\n              type: \"input\",\n              inputType: \"number\",\n              selectorCommand:\n                \"APIDetailsTokens.CreateToken.daysUntilExpiration\",\n              title: \"Expires in (days)\",\n              description:\n                \"Set the number of days until the token expires. After expiration, the token will no longer be valid.\",\n            },\n            {\n              type: \"button\",\n              selectorCommand: \"APIDetailsTokens.CreateToken.generate\",\n              title: \"Generate Token\",\n              description:\n                \"Click to generate a new API token. The token will be displayed once generated. After generation, the code examples will be updated to include the new token.\",\n            },\n          ],\n        },\n      ],\n    },\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/config/backupAndRestoreUIDoc.ts",
    "content": "import { mdiDatabaseSync } from \"@mdi/js\";\nimport type { UIDoc } from \"../../../UIDocs\";\n\nexport const backupAndRestoreUIDoc = {\n  type: \"tab\",\n  selectorCommand: \"config.bkp\",\n  title: \"Backup and Restore\",\n  description: \"Manage database backups and restore operations.\",\n  docOptions: \"asSeparateFile\",\n  iconPath: mdiDatabaseSync,\n  docs: `\n    Manage database backups and restore operations for this PostgreSQL connection. \n    Create reliable backups using PostgreSQL's native tools and restore \n    your data when needed.\n\n    Backups can be saved to a local file system or to cloud storage to AWS S3.\n    Similarly, you can restore backups from local files or from AWS S3.\n\n    <img src=\"./screenshots/backup_and_restore.svgif.svg\" alt=\"Backup and Restore\" />\n  `,\n  children: [\n    {\n      type: \"popup\",\n      selectorCommand: \"config.bkp.create\",\n      title: \"Create Backup\",\n      description: \"Start a new database backup operation.\",\n      children: [\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selectorCommand: \"config.bkp.create.name\",\n          title: \"Backup Name\",\n          description:\n            \"Optional name for the backup to help identify it later.\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"PGDumpOptions.format\",\n          title: \"Backup Format\",\n          description:\n            \"Choose the backup format: Custom, Plain SQL, Tar, or Directory.\",\n        },\n        {\n          type: \"input\",\n          inputType: \"checkbox\",\n          selectorCommand: \"PGDumpOptions.schemaOnly\",\n          title: \"Schema Only\",\n          description: \"Backup only the database schema without data.\",\n        },\n        {\n          type: \"input\",\n          inputType: \"checkbox\",\n          selectorCommand: \"PGDumpOptions.dataOnly\",\n          title: \"Data Only\",\n          description: \"Backup only the data without schema.\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"PGDumpOptions.destination\",\n          title: \"Backup Destination\",\n          description:\n            \"Choose where to save the backup: Local filesystem or AWS S3.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.numberOfJobs\",\n          type: \"input\",\n          inputType: \"number\",\n          title: \"Number of Jobs\",\n          description:\n            \"Specify the number of parallel jobs to use for the backup process.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.compressionLevel\",\n          type: \"input\",\n          inputType: \"number\",\n          title: \"Compression Level\",\n          description:\n            \"Set the compression level for the backup (0-9). Higher values mean better compression but slower performance.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.excludeSchema\",\n          type: \"input\",\n          inputType: \"text\",\n          title: \"Exclude Schema\",\n          description: \"Specify a schema to exclude from the backup.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.noOwner\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"No Owner\",\n          description:\n            \"Do not output commands to set ownership of objects to match the original database. Useful when restoring to a different database user.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.create\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"Create\",\n          description: \"Include commands to create the database in the backup.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.globalsOnly\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"Globals Only\",\n          description:\n            \"Backup only global objects such as roles and tablespaces.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.rolesOnly\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"Roles Only\",\n          description: \"Backup only roles (users) from the database.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.schemaOnly\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"Schema Only\",\n          description: \"Backup only the database schema without data.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.encoding\",\n          type: \"input\",\n          inputType: \"text\",\n          title: \"Encoding\",\n          description: \"Specify the character encoding to use in the backup.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.clean\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"Clean\",\n          description:\n            \"Include commands to drop database objects before recreating them.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.dataOnly\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"Data Only\",\n          description: \"Backup only the data without schema.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.ifExists\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"If Exists\",\n          description:\n            \"Use IF EXISTS clauses in the backup to avoid errors when dropping objects that do not exist.\",\n        },\n        {\n          selectorCommand: \"PGDumpOptions.keepLogs\",\n          type: \"input\",\n          inputType: \"checkbox\",\n          title: \"Keep Logs\",\n          description: \"Retain log files generated during the backup process.\",\n        },\n\n        {\n          type: \"button\",\n          selectorCommand: \"config.bkp.create.start\",\n          title: \"Start Backup\",\n          description: \"Begin the backup process with the selected options.\",\n        },\n      ],\n    },\n    {\n      type: \"popup\",\n      selectorCommand: \"config.bkp.AutomaticBackups\",\n      title: \"Automatic Backups\",\n      description: \"Configure scheduled automatic backups for this database.\",\n      children: [\n        {\n          type: \"input\",\n          inputType: \"checkbox\",\n          selectorCommand: \"config.bkp.AutomaticBackups.toggle\",\n          title: \"Enable Automatic Backups\",\n          description: \"Enable or disable automatic backup scheduling.\",\n        },\n      ],\n    },\n    {\n      type: \"section\",\n      selectorCommand: \"BackupControls.backupsInProgress\",\n      title: \"Backups in Progress\",\n      description: \"Monitor and manage ongoing backup operations.\",\n      children: [\n        {\n          selectorCommand: \"BackupLogs\",\n          type: \"button\",\n          title: \"View Logs\",\n          description: \"View real-time logs of the ongoing backup operation.\",\n        },\n      ],\n    },\n    {\n      selectorCommand: \"BackupsControls.restoreFromFile\",\n      type: \"button\",\n      title: \"Restore from File\",\n      description:\n        \"Initiate a database restore operation from a local backup file.\",\n    },\n    {\n      type: \"list\",\n      selectorCommand: \"BackupsControls.Completed\",\n      title: \"Completed Backups\",\n      description: \"View and manage completed backup operations.\",\n      itemSelector: `[data-key]`,\n      itemContent: [\n        {\n          type: \"button\",\n          selectorCommand: \"BackupsControls.Completed.delete\",\n          title: \"Delete Backup\",\n          description:\n            \"Delete the selected backup file from storage. Will ask for confirmation.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"BackupsControls.Completed.download\",\n          title: \"Download Backup\",\n          description:\n            \"Download the selected backup file to your local system.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"BackupsControls.Completed.restore\",\n          title: \"Restore Backup\",\n          description:\n            \"Restore a database from a selected backup file. Will ask for confirmation.\",\n        },\n      ],\n    },\n  ],\n} satisfies UIDoc;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/config/fileStorageUIDoc.ts",
    "content": "import { mdiImage } from \"@mdi/js\";\nimport type { UIDoc } from \"../../../UIDocs\";\n\nexport const fileStorageUIDoc = {\n  type: \"tab\",\n  selectorCommand: \"config.files\",\n  title: \"File storage\",\n  description:\n    \"Configure file upload and storage settings for this connection.\",\n  docOptions: \"asSeparateFile\",\n  iconPath: mdiImage,\n  docs: `\n    Configure file upload and storage settings for this connection.\n\n    <img src=\"./screenshots/file_storage.svg\" alt=\"File Storage Configuration\" />\n  `,\n  children: [\n    {\n      type: \"input\",\n      inputType: \"checkbox\",\n      selectorCommand: \"config.files.toggle\",\n      title: \"Enable File Storage\",\n      description:\n        \"Enable file upload and storage capabilities for this connection.\",\n    },\n  ],\n} satisfies UIDoc;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/connectionConfigUIDoc.ts",
    "content": "import { mdiChartLine, mdiDatabaseCogOutline } from \"@mdi/js\";\nimport { fixIndent, ROUTES } from \"@common/utils\";\nimport type { UIDocContainers } from \"../../UIDocs\";\nimport { editConnectionUIDoc } from \"../editConnectionUIDoc\";\nimport { accessControlUIDoc } from \"./config/accessControlUIDoc\";\nimport { apiUIDoc } from \"./config/apiUIDoc\";\nimport { backupAndRestoreUIDoc } from \"./config/backupAndRestoreUIDoc\";\nimport { fileStorageUIDoc } from \"./config/fileStorageUIDoc\";\n\nexport const connectionConfigUIDoc = {\n  type: \"page\",\n  path: ROUTES.CONFIG,\n  iconPath: mdiDatabaseCogOutline,\n  pathItem: {\n    tableName: \"connections\",\n    selectorCommand: \"Connection.configure\",\n    selectorPath: ROUTES.CONNECTIONS,\n  },\n  title: \"Connection configuration\",\n  description:\n    \"Configure the selected database connection. Set connection details, manage users, and customize settings.\",\n  docs: fixIndent(`\n    Configure the selected database connection. Set connection details, manage users, and customize settings.\n    <img src=\"./screenshots/connection_config.svgif.svg\" alt=\"Connection configuration\" />\n  `),\n  children: [\n    {\n      type: \"tab\",\n      selectorCommand: \"config.details\",\n      title: \"Connection details\",\n      description:\n        \"Edit connection parameters such as host, port, database name, and other connection settings.\",\n      docs: editConnectionUIDoc.docs,\n      iconPath: mdiDatabaseCogOutline,\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selectorCommand: \"config.status\",\n      title: \"Status monitor\",\n      iconPath: mdiChartLine,\n      description:\n        \"View real-time connection status, running queries, and system resource usage.\",\n      children: [],\n    },\n    accessControlUIDoc,\n    fileStorageUIDoc,\n    backupAndRestoreUIDoc,\n    apiUIDoc,\n    {\n      type: \"tab\",\n      selectorCommand: \"config.tableConfig\",\n      title: \"Table config\",\n      description:\n        \"Advanced table configuration using TypeScript (experimental feature).\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selectorCommand: \"config.methods\",\n      title: \"Server-side functions\",\n      description:\n        \"Configure and manage server-side functions (experimental feature).\",\n      children: [],\n    },\n  ],\n} satisfies UIDocContainers;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/dashboardContentUIDoc.ts",
    "content": "import type { UIDocElement } from \"../../../UIDocs\";\nimport { mapUIDoc } from \"./mapUIDoc\";\nimport { sqlEditorUIDoc } from \"./sqlEditorUIDoc\";\nimport { tableUIDoc } from \"./table/tableUIDoc\";\nimport { timechartUIDoc } from \"./timechartUIDoc\";\n\nexport const dashboardContentUIDoc = {\n  type: \"section\",\n  selector: \".Dashboard_MainContentWrapper\",\n  title: \"Workspace area\",\n  description:\n    \"Main content area of the dashboard, where the tables, views, SQL editors and other visualisations are displayed.\",\n  docs: `\n    The workspace area is the main place for interacting with your data. \n    It includes the SQL editor, data tables, maps, and timecharts, allowing you to execute queries, visualize data, and manage database objects.`,\n  children: [sqlEditorUIDoc, tableUIDoc, mapUIDoc, timechartUIDoc],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/dashboardMenuUIDoc.ts",
    "content": "import {\n  getDataKeyElemSelector,\n  getDataLabelElemSelector,\n} from \"../../../../Testing\";\nimport type { UIDocElement } from \"../../../UIDocs\";\n\nexport const dashboardMenuUIDoc = {\n  type: \"popup\",\n  selectorCommand: \"dashboard.menu\",\n  title: \"Dashboard menu\",\n  description:\n    \"Allows opening tables and views, schema diagram, importing files, managing saved queries, and accessing dashboard settings.\",\n  docs: `\n    The dashboard menu is the entry point in exploring your database.\n    The layout adapts to the screen size by pinning the menu to keep it open when there is enough space. \n    For wider screens the centered layout mode can be enabled through the settings. \n\n    `,\n  contentSelectorCommand: \"DashboardMenuContent\",\n  children: [\n    {\n      type: \"button\",\n      selectorCommand: \"dashboard.menu.sqlEditor\",\n      title: \"Open an SQL editor\",\n      description: \"Opens an SQL editor view in the workspace area.\",\n    },\n    {\n      type: \"popup\",\n      selectorCommand: \"dashboard.menu.quickSearch\",\n      title: \"Quick search\",\n      description:\n        \"Opens the quick search menu for searching across all available tables and views from the current database.\",\n      children: [],\n    },\n    {\n      type: \"smartform-popup\",\n      tableName: \"workspaces\",\n      fieldNames: [\"options\"],\n      selectorCommand: \"dashboard.menu.settings\",\n      title: \"Settings\",\n      description:\n        \"Opens the settings menu for configuring dashboard layout preferences.\",\n    },\n    {\n      type: \"button\",\n      selectorCommand: \"DashboardMenuHeader.togglePinned\",\n      title: \"Pin/Unpin menu\",\n      description:\n        \"Toggles the pinning of the dashboard menu. Pinned menus remain open until unpinned or accessing from a low width screen.\",\n    },\n    {\n      type: \"drag-handle\",\n      direction: \"x\",\n      title: \"Resize menu\",\n      description:\n        \"Allows resizing the dashboard menu. Drag to adjust the width of the menu.\",\n      selectorCommand: \"dashboard.menu.resize\",\n    },\n    {\n      type: \"drag-handle\",\n      direction: \"x\",\n      selectorCommand: \"dashboard.centered-layout.resize\",\n      title: \"Resize centered layout\",\n      description:\n        \"Allows resizing the workspace area when centered layout is enabled. Drag to adjust the width of the centered layout.\",\n    },\n\n    {\n      type: \"list\",\n      selectorCommand: \"dashboard.menu.savedQueriesList\",\n      title: \"Saved queries\",\n      description:\n        \"List of saved queries of the current user from the current workspace. Click to open a saved query.\",\n      itemContent: [],\n      itemSelector: \"li\",\n    },\n    {\n      type: \"list\",\n      selectorCommand: \"dashboard.menu.tablesSearchList\",\n      title: \"Tables and views\",\n      description:\n        \"List of tables and views from the current database. Click to open a table or view. By default only the tables from the public schema are shown. Schema list from the connection settings controls which schemas are shown.\",\n      itemSelector: \"li\",\n      itemContent: [],\n    },\n    {\n      type: \"list\",\n      selectorCommand: \"dashboard.menu.serverSideFunctionsList\",\n      title: \"Server-side functions\",\n      description:\n        \"List of server-side functions for the current database. Click to open a function.\",\n      itemSelector: \"li\",\n      itemContent: [],\n    },\n    {\n      type: \"popup\",\n      selectorCommand: \"dashboard.menu.create\",\n      title: \"Create/Import\",\n      description:\n        \"Opens the menu for creating new tables, server-side functions or importing csv/json files.\",\n      docs: `\n        Create new tables, server-side functions or import files into the current database.`,\n      children: [\n        {\n          type: \"popup\",\n          selector: getDataKeyElemSelector(\"new table\"),\n          title: \"Create new table\",\n          description:\n            \"Opens the form to create a new table in the current database.\",\n          children: [],\n        },\n        {\n          type: \"popup\",\n          selector: getDataKeyElemSelector(\"import file\"),\n          title: \"Import file\",\n          description:\n            \"Opens the form to import a file into the current database.\",\n          docs: `\n            Import files into the current database. Supported file types include CSV, GeoJSON, and JSON.\n            The import process allows you to specify the table name, infer column data types, and choose how to insert JSON/GeoJSON data into the table.  \n            \n            <img src=\"./screenshots/file_importer.svgif.svg\" alt=\"File Importer screenshot\" />\n          `,\n          docOptions: \"asSeparateFile\",\n          childrenTitle: \"Import file options\",\n          children: [\n            {\n              type: \"input\",\n              inputType: \"file\",\n              selectorCommand: \"FileBtn\",\n              title: \"Import file\",\n              description:\n                \"Input field for selecting a file to import. Supported types: csv/geojson/json.\",\n            },\n            {\n              type: \"input\",\n              inputType: \"text\",\n              selector: getDataLabelElemSelector(\"Table name\"),\n              title: \"Table name\",\n              description:\n                \"New/existing table name into which data is to be imported.\",\n            },\n            {\n              type: \"input\",\n              inputType: \"checkbox\",\n              selector: getDataLabelElemSelector(\n                \"Try to infer and apply column data types\",\n              ),\n              title: \"Try to infer and apply column data types\",\n              description:\n                \"Checkbox for inferring and applying column data types during import. If checked, the system will attempt to determine the appropriate data types for each column based on the imported file. If unchecked, TEXT data type will be used for all columns.\",\n            },\n            {\n              type: \"input\",\n              inputType: \"checkbox\",\n              selector: getDataLabelElemSelector(\"Drop table if exists\"),\n              title: \"Drop table if exists\",\n              description:\n                \"Checkbox for dropping the table if it already exists in the database. If checked, the existing table will be deleted before importing the new file.\",\n            },\n            {\n              type: \"select\",\n              selector: getDataLabelElemSelector(\"Insert as\"),\n              title: \"Insert as\",\n              description:\n                \"Select list for choosing the method of inserting JSON/GeoJSON data into the table. Options include: Single text value, JSONB rows, and Properties with geometry.\",\n            },\n            {\n              type: \"button\",\n              selectorCommand: \"FileImporterFooter.import\",\n              title: \"Import\",\n              description:\n                \"Button to initiate the import process. Click to start importing the selected file into the specified table.\",\n            },\n          ],\n        },\n        {\n          type: \"popup\",\n          selector: getDataKeyElemSelector(\"new function\"),\n          title: \"Create TS Function\",\n          description:\n            \"Opens the form to create a new server-side TypeScript function for the current database.\",\n          children: [],\n        },\n      ],\n    },\n    {\n      type: \"popup\",\n      selectorCommand: \"SchemaGraph\",\n      title: \"Schema diagram\",\n      description:\n        \"Opens the schema diagram for visualizing the relationships between tables in the current database.\",\n      docs: `\n        Explore your database structure visually through the schema diagram. This tool lets you:\n        - **Select schemas** — Choose one or multiple schemas to display\n        - **Navigate freely** — Pan and zoom to focus on specific areas\n        - **View table relationships** — See how tables connect through foreign keys\n        - **Filter your view** — Show or hide tables and columns by relationship type\n        - **Color links by root table** — Trace relationships back to their source at a glance. Links inherit the color of the table that defines the relationship (e.g., all user_id foreign keys match the users table color)\n        - **Reset the layout** — Return to the default view which auto-arranges tables ensuring the most linked tables are central\n\n        It allows you to explore the schema structure, view table relationships, and manage the layout of the schema diagram.\n        You can pan and zoom the diagram, select schemas, filter tables and columns based on their relationship types, reset the layout.\n        Link color modes allow you to better understand related tables and foreign key properties.\n        \n        <img src=\"./screenshots/schema_diagram.svgif.svg\" alt=\"Schema diagram screenshot\" />\n\n        ## Controls\n      `,\n      docOptions: \"asSeparateFile\",\n      childrenTitle: \"Top controls\",\n      children: [\n        {\n          type: \"select\",\n          selectorCommand: \"SchemaGraph.TopControls.tableRelationsFilter\",\n          title: \"Table relationship filter\",\n          description:\n            \"Display tables based on their relationship type. Options include: all, linked (with relationships), orphaned (without relationships).\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"SchemaGraph.TopControls.columnRelationsFilter\",\n          title: \"Column relationship filter\",\n          description:\n            \"Display columns based on their relationship type. Options include: all, references (with relationships), none (no columns/only table names will be shown).\",\n        },\n        {\n          type: \"select\",\n          title: \"Link colour mode\",\n          selectorCommand: \"SchemaGraph.TopControls.linkColorMode\",\n          description:\n            \"Colour links by: default (fixed colour), root table (the colour of the table the relationship tree originates from), on-delete/on-update (colour based on constraint referential action).\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"SchemaGraph.TopControls.resetLayout\",\n          title: \"Reset layout\",\n          description:\n            \"Moving tables is persisted the state database. Clicking this resets the schema diagram layout to its initial state.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"Popup.close\",\n          title: \"Close schema diagram\",\n          description:\n            \"Closes the schema diagram and returns to the dashboard menu.\",\n        },\n      ],\n    },\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/mapUIDoc.ts",
    "content": "import { fixIndent } from \"../../../../demo/scripts/sqlVideoDemo\";\nimport {\n  getCommandElemSelector,\n  getDataKeyElemSelector,\n} from \"../../../../Testing\";\nimport type { UIDocElement } from \"../../../UIDocs\";\nimport { getCommonViewHeaderUIDoc } from \"../getCommonViewHeaderUIDoc\";\n\nexport const mapUIDoc = {\n  type: \"section\",\n  selector: `.SilverGridChild[data-view-type=\"map\"]`,\n  title: \"Map view\",\n  description:\n    \"Displays a map visualization based on the Table/SQL query results.\",\n  docOptions: \"asSeparateFile\",\n  docs: fixIndent(`\n    The map view allows you to visualize geographical data from your database.\n    It requires the [PostGIS](https://postgis.net/) extension to be installed on your PostgreSQL database.\n    It can display points, lines, and polygons based on geometry or geography columns in your tables or views.\n    It supports multiple layers, custom basemaps, and various map controls for interaction.\n\n    <img src=\"./screenshots/map.svgif.svg\" alt=\"Map view screenshot\" />\n    \n  `),\n  children: [\n    getCommonViewHeaderUIDoc(\n      \"Shows the table/view name together with the geometry/geography column name used for the map visualization.\",\n      {\n        title: \"Map view menu\",\n        docs: fixIndent(`\n          The map view menu provides options for configuring the map visualization, including data refresh, basemap settings, and layer management.\n          \n        `),\n        description: \"Data refresh and display options.\",\n        children: [\n          {\n            type: \"tab\",\n            selector:\n              getCommandElemSelector(\"MenuList\") +\n              \" \" +\n              getDataKeyElemSelector(\"Data refresh\"),\n            title: \"Data refresh\",\n            description:\n              \"Allows setting subscriptions or data refresh rates. By default every table subscribes to data changes.\",\n            children: [],\n          },\n          {\n            type: \"tab\",\n            selector:\n              getCommandElemSelector(\"MenuList\") +\n              \" \" +\n              getDataKeyElemSelector(\"Basemap\"),\n            title: \"Basemap\",\n            description: \"Allows setting the map tiles and projection.\",\n            children: [\n              {\n                type: \"select\",\n                selectorCommand: \"MapBasemapOptions.Projection\",\n                title: \"Projection\",\n                description:\n                  \"Allows setting the map projection: Mercator (default) or Orthographic (allows a setting custom tile image for plan drawings).\",\n              },\n            ],\n          },\n          {\n            type: \"tab\",\n            selector:\n              getCommandElemSelector(\"MenuList\") +\n              \" \" +\n              getDataKeyElemSelector(\"Layers\"),\n            title: \"Layers\",\n            description:\n              \"Allows setting the map layers data source and style. The map supports multiple layers.\",\n            children: [],\n          },\n          {\n            type: \"tab\",\n            selector:\n              getCommandElemSelector(\"MenuList\") +\n              \" \" +\n              getDataKeyElemSelector(\"Settings\"),\n            title: \"Settings\",\n            description:\n              \"Allows setting the map layout options: aggregation limit, click behavior, etc.\",\n            children: [],\n          },\n        ],\n      },\n      \"chart\",\n    ),\n    {\n      type: \"section\",\n      selector: \".Window\",\n      title: \"Map window with controls\",\n      description:\n        \"Map visualization and controls for interacting with the map.\",\n      docs: fixIndent(`\n        The map window contains the map visualization and controls for interacting with the map.\n        It allows you to add layers, set the map extent behavior, and toggle cursor coordinates display.\n        \n      `),\n      children: [\n        {\n          type: \"popup\",\n          selectorCommand: \"ChartLayerManager\",\n          title: \"Map layer manager\",\n          description:\n            \"Allows adding/removing layers to the map. Each layer can be configured with its own data source and style.\",\n          children: [\n            {\n              type: \"select\",\n              selectorCommand: \"ChartLayerManager.AddChartLayer.addLayer\",\n              title: \"Add layer\",\n              description:\n                \"Allows adding a new layer to the map. The available options are all the tables that have geometry or geography columns.\",\n            },\n            {\n              type: \"popup\",\n              selectorCommand: \"ChartLayerManager.AddChartLayer.addOSMLayer\",\n              title: \"Add OSM layer\",\n              description:\n                \"Allows adding a new layer based on OpenStreetMap data. This is useful for displaying additional map data like roads, restaurants, etc.\",\n              children: [],\n            },\n            {\n              type: \"popup\",\n              selectorCommand: \"MapBasemapOptions\",\n              title: \"Map basemap options\",\n              description: \"Allows setting the map tiles and projection.\",\n              children: [],\n            },\n            {\n              selectorCommand: \"MapOpacityMenu\",\n              type: \"section\",\n              title: \"Map opacity options\",\n              description: \"Allows setting the opacity of the map layers.\",\n              children: [],\n            },\n          ],\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"InMapControls.showCursorCoords\",\n          title: \"Show cursor coordinates\",\n          description:\n            \"Toggle displays the current cursor coordinates on the map. \",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"MapExtentBehavior\",\n          title: \"Map extent behavior\",\n          description:\n            \"Allows setting the map extent and auto-zoom behavior: Follow data, Follow map or Free roam.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"InMapControls.goToDataBounds\",\n          title: \"Zoom to data\",\n          description:\n            \"Zooms the map to fit the bounds of the data currently displayed.\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"DeckGLFeatureEditor\",\n          title: \"Add new feature\",\n          description:\n            \"Allows drawing and inserting a new feature: Point, Line or Polygon.\",\n        },\n      ],\n    },\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/serverSideFunctionUIDoc.ts",
    "content": "import { fixIndent } from \"../../../../demo/scripts/sqlVideoDemo\";\nimport type { UIDocElement } from \"../../../UIDocs\";\nimport { getCommonViewHeaderUIDoc } from \"../getCommonViewHeaderUIDoc\";\n\nexport const serverSideFunctionUIDoc = {\n  type: \"section\",\n  selector: `.SilverGridChild[data-view-type=\"method\"]`,\n  title: \"Server-side function view\",\n  description: \"Allows executing server-side functions and viewing results.\",\n  docs: fixIndent(`\n    The server-side functions is an experimental feature that allows you to specify and execute server-side Typescript functions directly from the dashboard.`),\n  children: [\n    getCommonViewHeaderUIDoc(\n      \"Function name.\",\n      {\n        children: [],\n        title: \"Server-side function view menu\",\n        description: \"Server-side function menu\",\n        docs: fixIndent(`\n          The server-side function view menu provides options for executing the function, viewing results, and managing function parameters.\n          \n        `),\n      },\n      \"method\",\n    ),\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/sqlEditorUIDoc.ts",
    "content": "import { fixIndent } from \"../../../../demo/scripts/sqlVideoDemo\";\nimport {\n  getCommandElemSelector,\n  getDataKeyElemSelector,\n} from \"../../../../Testing\";\nimport type { UIDocElement } from \"../../../UIDocs\";\nimport { getCommonViewHeaderUIDoc } from \"../getCommonViewHeaderUIDoc\";\n\nexport const sqlEditorUIDoc = {\n  type: \"section\",\n  selector: `.SilverGridChild[data-view-type=\"sql\"]`,\n  title: \"SQL editor\",\n  description:\n    \"The SQL editor allows users to write and execute SQL queries against the selected database. It provides a user-friendly interface for interacting with the database.\",\n  docs: `\n    The SQL editor is a powerful tool for executing SQL queries against your PostgreSQL database. \n\n    ### Core Features\n    - **Intelligent auto-completion** with context-aware suggestions based on your schema and data with JSONB property access support\n    - **Rich suggestion details** with related objects, usage examples and documentation extracts, reducing the need to switch context\n    - **Execute current statement** functionality to run only the SQL statement where the cursor is located\n    - **Charting options** to visualize query results as timecharts or maps directly from the editor\n    - **Multiple result display modes** including table, JSON, and CSV formats\n    \n    To make it easier working with multiple queries, the default query execution behaviour is to execute the current statement.\n    It is highlighted by the blue vertical line to the left of the code. Press <kbd>Alt+E</kbd> or <kbd>Ctrl+Enter</kbd> or <kbd>F5</kbd> to execute it.\n    \n    The editor is based on [Monaco Editor](https://microsoft.github.io/monaco-editor/), which powers VS Code. \n    It supports multi-cursor editing, find and replace, and many other advanced editing features.\n\n    Realtime query resource usage can be enabled in the settings to monitor CPU and memory consumption.\n    \n    <img src=\"./screenshots/sql_editor.svgif.svg\" alt=\"SQL editor screenshot\" />\n\n    ## Components:\n  `,\n  docOptions: \"asSeparateFile\",\n  children: [\n    getCommonViewHeaderUIDoc(\n      \"SQL editor query name, editable in the menu.\",\n      {\n        title: \"SQL editor menu\",\n        description:\n          \"The SQL editor menu provides access to various options and settings for the SQL editor.\",\n        docs: fixIndent(`\n          The SQL editor menu provides access to various options and settings for the SQL editor.`),\n        children: [\n          {\n            type: \"tab\",\n            selector:\n              getCommandElemSelector(\"MenuList\") +\n              \" \" +\n              getDataKeyElemSelector(\"General\"),\n            title: \"General\",\n            description: \"General settings for the SQL editor.\",\n            children: [\n              {\n                type: \"input\",\n                inputType: \"text\",\n                selectorCommand: \"W_SQLMenu.name\",\n                title: \"Query name\",\n                description:\n                  \"The name of the current SQL query. This is used for saving and managing queries.\",\n              },\n              {\n                type: \"select\",\n                selectorCommand: \"W_SQLMenu.renderDisplayMode\",\n                title: \"Result display mode\",\n                description:\n                  \"The mode in which the results of the SQL query will be displayed. Options include table, JSON, and CSV.\",\n              },\n              {\n                type: \"button\",\n                selectorCommand: \"W_SQLMenu.saveQuery\",\n                title: \"Save query as file\",\n                description:\n                  \"Saves the current SQL query to a file. This can also be accomplished by pressing Ctrl+S.\",\n              },\n              {\n                type: \"button\",\n                selectorCommand: \"W_SQLMenu.openSQLFile\",\n                title: \"Open SQL file\",\n                description:\n                  \"This allows loading the contents of an SQL file into the current query.\",\n              },\n              {\n                selectorCommand: \"W_SQLMenu.deleteQuery\",\n                type: \"button\",\n                title: \"Delete query\",\n                description:\n                  \"Deletes the current SQL query. If it has contents a confirmation dialog will appear.\",\n              },\n            ],\n          },\n          {\n            type: \"tab\",\n            selector:\n              getCommandElemSelector(\"MenuList\") +\n              \" \" +\n              getDataKeyElemSelector(\"Editor options\"),\n            title: \"Editor options\",\n            description:\n              \"Settings for the SQL editor's appearance and behavior.\",\n            children: [\n              {\n                type: \"smartform\",\n                tableName: \"windows\",\n                fieldNames: [\"sql_options\"],\n                title: \"Editor settings\",\n                selector:\n                  getCommandElemSelector(\"Popup.content\") + \" .MonacoEditor\",\n                description:\n                  \"Settings for the SQL editor's appearance and behavior. This includes font size, theme, and other preferences.\",\n              },\n            ],\n          },\n          {\n            type: \"tab\",\n            selector:\n              getCommandElemSelector(\"MenuList\") +\n              \" \" +\n              getDataKeyElemSelector(\"Hotkeys\"),\n            title: \"Hotkeys\",\n            description:\n              \"Keyboard shortcuts for common actions in the SQL editor. This includes executing queries, saving files, and more.\",\n            children: [],\n          },\n        ],\n      },\n      \"sql\",\n    ),\n    {\n      type: \"section\",\n      selectorCommand: \"W_SQLEditor\",\n      title: \"SQL editor component\",\n      description:\n        \"The main component for the SQL editor. It contains the SQL editor and statement action buttons. Being bsed on Monaco editor, it supports syntax highlighting, auto-completion and other editing functionality like multi-cursor editing.\",\n      children: [\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selectorCommand: \"MonacoEditor\",\n          title: \"SQL editor input\",\n          description:\n            \"The input field for writing SQL queries. It supports syntax highlighting and auto-completion.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_SQLEditor.executeStatement\",\n          title: \"Execute current statement\",\n          description:\n            \"Executes the current SQL statement highlighted by the blue vertical line to the left of the code. This button is only visible when the cursor is within a statement.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"AddChartMenu.Timechart\",\n          title: \"Add timechart\",\n          description:\n            \"Adds a timechart visualization based on the current SQL statement. This button is only visible when the cursor is within a statement that returns at least one timestamp column.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"AddChartMenu.Map\",\n          title: \"Add map\",\n          description:\n            \"Adds a map visualization based on the current SQL statement. This button is only visible when the cursor is within a statement that returns at least one geometry/geography column (postgis extension must be enabled).\",\n        },\n      ],\n    },\n    {\n      type: \"section\",\n      title: \"SQL editor toolbar\",\n      description:\n        \"The toolbar provides various options for executing and managing SQL queries.\",\n      selectorCommand: \"W_SQLBottomBar\",\n      children: [\n        {\n          type: \"button\",\n          selectorCommand: \"W_SQLBottomBar.runQuery\",\n          title: \"Run query\",\n          description:\n            \"Executes the current SQL query. The result will be displayed in the query results section.\",\n        },\n        {\n          selectorCommand: \"W_SQLBottomBar.limit\",\n          title: \"Limit\",\n          type: \"input\",\n          inputType: \"number\",\n          description:\n            \"Sets the maximum number of rows to return in the query results. This is useful for limiting the amount of data returned, especially for large datasets.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_SQLBottomBar.cancelQuery\",\n          title: \"Cancel query\",\n          description:\n            \"Cancels the currently running query. This button is only visible when a query is running.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_SQLBottomBar.terminateQuery\",\n          title: \"Terminate query\",\n          description:\n            \"Forcefully terminates the currently running query. This is more aggressive than cancel and is only visible when a query is running.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_SQLBottomBar.stopListen\",\n          title: \"Stop LISTEN\",\n          description:\n            \"Stops the active LISTEN operation. This button is only visible when a LISTEN query is active.\",\n        },\n        {\n          type: \"text\",\n          selectorCommand: \"W_SQLBottomBar.rowCount\",\n          title: \"Row count\",\n          description:\n            \"Displays the number of rows fetched by the query and the total number of rows if available.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_SQLBottomBar.toggleTable\",\n          title: \"Toggle table visibility\",\n          description:\n            \"Shows or hides the results table for the executed query.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_SQLBottomBar.toggleCodeEditor\",\n          title: \"Toggle code editor\",\n          description:\n            \"Shows or hides the SQL code editor, allowing users to focus on query results when needed.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_SQLBottomBar.toggleNotices\",\n          title: \"Toggle notices\",\n          description:\n            \"Shows or hides database notices. When enabled, it displays notifications from the database system.\",\n        },\n        {\n          type: \"section\",\n          selectorCommand: \"W_SQLBottomBar.queryDuration\",\n          title: \"Query duration\",\n          children: [],\n          description:\n            \"Displays the execution time of the last completed query or the current running time for an active query.\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"W_SQLBottomBar.copyResults\",\n          title: \"Copy results\",\n          description:\n            \"Copy/download query results as: CSV, TSV, JSON, Typescript definition, SQL SELECT INTO\",\n        },\n        {\n          type: \"text\",\n          selectorCommand: \"W_SQLBottomBar.sqlError\",\n          title: \"SQL error\",\n          description:\n            \"Displays any errors that occurred during the execution of the SQL query. This is useful for debugging and correcting SQL syntax.\",\n        },\n      ],\n    },\n    {\n      type: \"section\",\n      selectorCommand: \"W_SQLResults\",\n      title: \"Query results\",\n      description:\n        \"Displays the results of the executed SQL query. Results can be displayed as a table (default), JSON or CSV. Users can interact with the results, such as sorting and filtering.\",\n      children: [\n        {\n          type: \"section\",\n          selector: \".table-component\",\n          title: \"Results table\",\n          description:\n            \"The results table displays the data returned by the executed SQL query. It supports sorting, filtering, and pagination.\",\n          children: [],\n        },\n        {\n          type: \"section\",\n          selectorCommand: \"Window.ChildChart\",\n          title: \"Chart\",\n          description: \"Timechart/Map visualization of the SQL query results.\",\n          children: [],\n        },\n      ],\n    },\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/table/addColumnMenuUIDoc.ts",
    "content": "import type { UIDoc } from \"src/app/UIDocs\";\nimport { getCommandElemSelector } from \"src/Testing\";\n\nexport const addColumnMenuUIDoc = {\n  type: \"popup\",\n  selectorCommand: \"AddColumnMenu\",\n  title: \"Add column menu\",\n  description:\n    \"Opens the add column menu, allowing users to add computed columns, create new columns, add linked fields.\",\n  children: [\n    {\n      type: \"popup\",\n      selector: `${getCommandElemSelector(\"AddColumnMenu\")} [data-key=\"Computed\"]`,\n      contentSelectorCommand: \"QuickAddComputedColumn\",\n      title: \"Add Computed Field\",\n      description:\n        \"Opens a popup to create a computed column - calculations or transformations based on existing data using functions like aggregations, date formatting, string operations, etc.\",\n      children: [\n        {\n          type: \"list\",\n          selectorCommand: \"FunctionSelector\",\n          title: \"Function selector\",\n          description:\n            \"Choose a function to apply to the selected column (e.g., aggregates (min/max/avg/count), date formatting, string operations).\",\n          itemContent: [],\n          itemSelector: `li[role=\"option\"]`,\n        },\n        {\n          type: \"list\",\n          selectorCommand: \"SearchList.List\",\n          title: \"Column selection\",\n          description:\n            \"List of applicable columns to apply functions to. Columns from foreign tables are also shown with the join path in the header.\",\n          itemSelector: \"li\",\n          itemContent: [],\n        },\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selectorCommand: \"AddComputedColMenu.name\",\n          title: \"Column name\",\n          description:\n            \"Name for the new computed column. Auto-generated based on the function and column.\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"AddComputedColMenu.addTo\",\n          title: \"Add to position\",\n          description:\n            \"Choose whether to add the computed column at the start or end of the column list.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"AddComputedColMenu.addBtn\",\n          title: \"Add computed column\",\n          description:\n            \"Confirms and adds the new computed column to the table based on the selected function and parameters.\",\n        },\n      ],\n    },\n    {\n      type: \"popup\",\n      selector: `${getCommandElemSelector(\"AddColumnMenu\")} [data-key=\"Referenced\"]`,\n      contentSelectorCommand: \"LinkedColumn\",\n      title: \"Add Linked Data\",\n      description:\n        \"Opens a popup to display data from related tables via foreign key relationships. Disabled if no foreign keys exist or when using aggregates with nested columns.\",\n\n      children: [\n        {\n          type: \"select\",\n          selectorCommand: \"JoinPathSelectorV2\",\n          title: \"Join path selector\",\n          description:\n            \"Select which related table to join to via foreign key relationships. Shows available join paths from the current table.\",\n        },\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selector: \"#nested-col-name\",\n          title: \"Column label\",\n          description:\n            \"Custom name/label for this linked column field in the table. Defaults to the related table name.\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"LinkedColumn.ColumnList.toggle\",\n          title: \"Linked column selection\",\n          description:\n            \"Choose which columns from the related table to display in this linked field.\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"QuickAddComputedColumn\",\n          title: \"Quick add computed column\",\n          description:\n            \"Add a single computed column from the linked table. E.g., count, sum, avg.\",\n        },\n        {\n          type: \"section\",\n          selector: \".ExpandSection\",\n          title: \"More options\",\n          description:\n            \"Additional configuration for linked columns including layout, join type, filters, and limits.\",\n          children: [\n            {\n              type: \"select\",\n              selector: \"LinkedColumn.layoutType\",\n              title: \"Layout mode\",\n              description:\n                \"Choose how to display the linked data: as rows, columns, or without headers.\",\n            },\n            {\n              type: \"select\",\n              selectorCommand: \"LinkedColumn.joinType\",\n              title: \"Join type\",\n              description:\n                \"Select between inner join (discards parent rows without matches) or left join (keeps all parent rows).\",\n            },\n            {\n              type: \"input\",\n              inputType: \"number\",\n              selector: \"#nested-col-limit\",\n              title: \"Limit\",\n              description:\n                \"Maximum number of linked records to display (0-30). Optional.\",\n            },\n            {\n              type: \"section\",\n              selectorCommand: \"SmartFilterBar\",\n              title: \"Filters and sorting\",\n              description:\n                \"Apply filters and sorting to the linked table data.\",\n              children: [],\n            },\n          ],\n        },\n      ],\n    },\n    {\n      type: \"popup\",\n      selector: `${getCommandElemSelector(\"AddColumnMenu\")} [data-key=\"Create\"]`,\n      title: \"Create New Column\",\n      description:\n        \"Opens a popup to create a new physical column in the database table. Disabled for views or users without SQL privileges.\",\n      children: [\n        {\n          type: \"section\",\n          selectorCommand: \"ColumnEditor.name\",\n          title: \"Column editor\",\n          description:\n            \"Configure column properties including name, data type, constraints, default values, and foreign key references.\",\n          children: [],\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"AddColumnReference\",\n          title: \"Foreign key reference\",\n          description:\n            \"Optionally set up a foreign key relationship to another table/column in the database.\",\n        },\n        {\n          type: \"select\",\n          selectorCommand: \"ColumnEditor.dataType\",\n          title: \"Data type selection\",\n          description:\n            \"Appears after typing the column name. Choose the data type for the new column (e.g., integer, text, date, boolean, etc.).\",\n        },\n        {\n          type: \"tab\",\n          selectorCommand: \"CreateColumn.next\",\n          title: \"Show create column SQL\",\n          description:\n            \"Generates and displays the SQL query that will be executed to create the new column in the database.\",\n          children: [\n            {\n              type: \"input\",\n              inputType: \"text\",\n              selectorCommand: \"MonacoEditor\",\n              title: \"Generated SQL query\",\n              description:\n                \"The generated ALTER TABLE SQL query to create the new column based on the specified properties.\",\n            },\n            {\n              type: \"button\",\n              selectorCommand: \"SQLSmartEditor.Run\",\n              title: \"Execute create column\",\n              description:\n                \"Runs the generated ALTER TABLE query to create the new column in the database.\",\n            },\n          ],\n        },\n      ],\n    },\n    {\n      type: \"popup\",\n      selector: `${getCommandElemSelector(\"AddColumnMenu\")} [data-key=\"CreateFileColumn\"]`,\n      title: \"Create New File Column\",\n      description:\n        \"Opens a popup to create a new column for handling file uploads and attachments. Requires file storage to be enabled.\",\n      children: [\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selector: `[data-label=\"New column name\"] input`,\n          title: \"New column name\",\n          description: \"Name for the new file column.\",\n        },\n        {\n          type: \"input\",\n          inputType: \"checkbox\",\n          selector: \".SwitchToggle\",\n          title: \"Optional\",\n          description:\n            \"Whether the file column should allow NULL values (optional) or require a file (NOT NULL).\",\n        },\n        {\n          type: \"section\",\n          selectorCommand: \"FileColumnConfigEditor\",\n          title: \"File column configuration\",\n          description:\n            \"Configure accepted file types and other file handling options. Appears after entering the column name.\",\n          children: [\n            {\n              type: \"input\",\n              title: \"Maximum file size in megabytes\",\n              inputType: \"number\",\n              selectorCommand: \"FileColumnConfigEditor.maxFileSizeMB\",\n              description:\n                \"Set the maximum allowed file size for uploads in megabytes. Default is 1 MB. 0 means no limit.\",\n            },\n            {\n              type: \"select\",\n              selectorCommand: \"FileColumnConfigEditor.contentMode\",\n              title: \"Content filter mode\",\n              description:\n                \"Choose how to filter accepted files: by file extension, basic content type (e.g., image, audio, vide), by specific content type (e.g., image/png), by specific extension (jpg, png, pdf).\",\n            },\n          ],\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"CreateFileColumn.confirm\",\n          title: \"Create file column\",\n          description: \"Confirms and creates the new file column.\",\n        },\n      ],\n    },\n  ],\n} satisfies UIDoc;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/table/columnMenuUIDoc.ts",
    "content": "import type { UIDoc } from \"src/app/UIDocs\";\nimport { getDataKeyElemSelector } from \"src/Testing\";\n\nexport const columnMenuUIDoc = {\n  type: \"popup\",\n  title: \"Column menu\",\n  triggerMode: \"contextmenu\",\n  selector: `[role=\"columnheader\"]`,\n  description:\n    \"Opens the column menu, allowing users to change styling, render mode, view quick stats and other column related options.\",\n  children: [\n    {\n      type: \"tab\",\n      title: \"Sort\",\n      description:\n        \"Sort the table data based on the values in this column, either in ascending or descending order. Table can also be sorted by multiple columns by holding shift while clicking column headers.\",\n      selector: getDataKeyElemSelector(\"Sort\"),\n      children: [],\n    },\n    {\n      type: \"tab\",\n      title: \"Style\",\n      description:\n        \"Customize the appearance of this column, including text color and cell background. You can also set conditional formatting rules to highlight specific data patterns.\",\n      selector: getDataKeyElemSelector(\"Style\"),\n      children: [],\n    },\n    {\n      type: \"tab\",\n      title: \"Display format\",\n      description:\n        \"Choose how the data in this column is displayed, such as date formats, number formats, or custom render modes.\",\n      selector: getDataKeyElemSelector(\"Display format\"),\n      children: [],\n    },\n    {\n      type: \"button\",\n      title: \"Filter\",\n      description:\n        \"Open the filter panel to set up filters based on this column's values, helping you to quickly narrow down the data displayed in the table.\",\n      selector: getDataKeyElemSelector(\"Filter\"),\n    },\n    {\n      type: \"tab\",\n      title: \"Quick stats\",\n      description:\n        \"View quick statistics about the data in this column, such as count, unique values, and distribution.\",\n      selector: getDataKeyElemSelector(\"Quick stats\"),\n      children: [\n        {\n          type: \"section\",\n          title: \"Column Quick Stats\",\n          description:\n            \"The Column Quick Stats panel provides a summary of key statistics for the selected column, including distinct count, min/max values, and value distribution. You can also sort the distribution and add filters directly from this panel.\",\n          selectorCommand: \"ColumnQuickStats\",\n          children: [\n            {\n              type: \"button\",\n              title: \"Add filter\",\n              description:\n                \"Add a filter based on the selected value from top values list.\",\n              selectorCommand: \"ColumnQuickStats.addFilter\",\n            },\n          ],\n        },\n      ],\n    },\n    {\n      type: \"tab\",\n      title: \"Columns\",\n      description:\n        \"Shortcut to the column management panel to add, remove, or rearrange columns in the table.\",\n      selector: getDataKeyElemSelector(\"Columns\"),\n      children: [],\n    },\n    {\n      type: \"tab\",\n      title: \"Add Computed Column\",\n      description:\n        \"Add a computed field based on calculations or transformations of existing data in this column.\",\n      selector: getDataKeyElemSelector(\"Add Computed Column\"),\n      children: [],\n    },\n    {\n      type: \"tab\",\n      title: \"Apply function\",\n      description:\n        \"Apply a function to the data in this column, such as aggregations, string manipulations, or date transformations.\",\n      selector: getDataKeyElemSelector(\"Apply function\"),\n      children: [],\n    },\n    {\n      type: \"tab\",\n      title: \"Add Linked Columns\",\n      description:\n        \"Add linked data from related tables based on foreign key relationships.\",\n      selector: getDataKeyElemSelector(\"Add Linked Columns\"),\n      children: [],\n    },\n    {\n      type: \"tab\",\n      title: \"Alter\",\n      description:\n        \"Alter the column's properties, such as data type, default value, or constraints.\",\n      selector: getDataKeyElemSelector(\"Alter\"),\n      children: [],\n    },\n    {\n      type: \"button\",\n      title: \"Hide\",\n      description:\n        \"Hide this column from the table view without deleting it, allowing you to focus on the most relevant data.\",\n      selector: getDataKeyElemSelector(\"Hide\"),\n    },\n    {\n      type: \"button\",\n      title: \"Hide Others\",\n      description:\n        \"Hide all other columns except this one, providing a focused view of the data in this column.\",\n      selector: getDataKeyElemSelector(\"Hide Others\"),\n    },\n  ],\n} satisfies UIDoc;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/table/paginationUIDoc.ts",
    "content": "import type { UIDocElement } from \"../../../../UIDocs\";\n\nexport const paginationUIDoc: UIDocElement = {\n  selectorCommand: \"Pagination\",\n  type: \"section\",\n  title: \"Pagination Controls\",\n  description: \"Navigation controls for paginated data.\",\n  children: [\n    {\n      selectorCommand: \"Pagination.firstPage\",\n      type: \"button\",\n      title: \"First Page\",\n      description: \"Navigate to the first page of results.\",\n    },\n    {\n      selectorCommand: \"Pagination.prevPage\",\n      type: \"button\",\n      title: \"Previous Page\",\n      description: \"Navigate to the previous page of results.\",\n    },\n    {\n      selectorCommand: \"Pagination.page\",\n      type: \"input\",\n      inputType: \"number\",\n      title: \"Page Number\",\n      description:\n        \"Current page number. You can type a specific page number to jump directly to that page.\",\n    },\n    {\n      selectorCommand: \"Pagination.nextPage\",\n      type: \"button\",\n      title: \"Next Page\",\n      description: \"Navigate to the next page of results.\",\n    },\n    {\n      selectorCommand: \"Pagination.lastPage\",\n      type: \"button\",\n      title: \"Last Page\",\n      description: \"Navigate to the last page of results.\",\n    },\n    {\n      selectorCommand: \"Pagination.pageSize\",\n      type: \"select\",\n      title: \"Page Size\",\n      description:\n        \"Select how many rows to display per page. Changing this may adjust the current page if it would exceed the total number of pages.\",\n    },\n    {\n      selectorCommand: \"Pagination.pageCountInfo\",\n      type: \"text\",\n      title: \"Page Count Information\",\n      description:\n        \"Displays the total number of pages and rows in the current dataset.\",\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/table/smartFilterBarUIDoc.ts",
    "content": "import { fixIndent } from \"../../../../../demo/scripts/sqlVideoDemo\";\nimport { getDataKeyElemSelector } from \"../../../../../Testing\";\nimport type { UIDocElement } from \"../../../../UIDocs\";\n\nexport const smartFilterBarUIDoc = {\n  type: \"section\",\n  selectorCommand: \"SmartFilterBar\",\n  title: \"Table Toolbar\",\n  description:\n    \"Filtering and data add/edit interface for database tables and views.\",\n  docs: fixIndent(`\n    Table toolbar can be toggled through the show/hide filtering button (top left corner). \n    It provides a user-friendly interface to add filters, search for data, and perform various actions on the table data.\n    \n    <img src=\"./screenshots/smart_filter_bar.svg\" alt=\"Smart Filter Bar screenshot\" />\n  `),\n  children: [\n    {\n      type: \"popup\",\n      selectorCommand: \"SmartAddFilter\",\n      title: \"Add Filter\",\n      description:\n        \"Allows adding a filter by chosing a column from the current table or from linked tables.\",\n      children: [\n        {\n          type: \"button\",\n          selectorCommand: \"SmartAddFilter.toggleIncludeLinkedColumns\",\n          title: \"Include Linked Columns\",\n          description:\n            \"Toggle to include columns from linked tables in the column list.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"SmartAddFilter.JoinTo\",\n          title: \"Join To\",\n          description:\n            \"Toggles join view to specify which tables a join path to a specific table from which to select a column to filter by.\",\n        },\n        {\n          type: \"list\",\n          selector: \".SearchList\",\n          title: \"Filterable Columns\",\n          description:\n            \"List of columns available for filtering. Click to add a filter.\",\n          itemSelector: \"li\",\n          itemContent: [],\n        },\n      ],\n    },\n    {\n      type: \"section\",\n      selectorCommand: \"SearchList\",\n      title: \"Search Bar\",\n      description: \"Quick search functionality across the table data.\",\n      children: [\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selector: \".SmartSearch input\",\n          title: \"Search\",\n          description:\n            \"Type to search across all searchable fields in the table. The search is a simple contains search. Selecting a result will add a filter to the table.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"SearchList.MatchCase\",\n          title: \"Match Case\",\n          description:\n            \"Toggle to match the case of the search term with the data.\",\n        },\n      ],\n    },\n    {\n      type: \"section\",\n      selector: \".SmartFilterBarRightActions\",\n      title: \"Table actions\",\n      description: \"Additional table data actions.\",\n      children: [\n        {\n          type: \"button\",\n          selectorCommand: \"SmartFilterBar.rightOptions.show\",\n          title: \"Show additional actions\",\n          description:\n            \"Opens a menu with additional actions for the table data.\",\n        },\n        {\n          selectorCommand: \"SmartFilterBar.rightOptions.delete\",\n          type: \"popup\",\n          title: \"Delete action\",\n          description:\n            \"Opens a menu to delete the selected rows from the table.\",\n          children: [],\n        },\n        {\n          selectorCommand: \"SmartFilterBar.rightOptions.update\",\n          type: \"popup\",\n          title: \"Update action\",\n          description: \"Opens a menu to update the selected rows in the table.\",\n          children: [],\n        },\n        {\n          selectorCommand: \"dashboard.window.rowInsertTop\",\n          type: \"popup\",\n          title: \"Insert row\",\n          description:\n            \"Opens the row insert menu, allowing users to add new rows to the table.\",\n          children: [],\n        },\n      ],\n    },\n    {\n      type: \"section\",\n      title: \"Filters\",\n      description: \"Filters applied to the data\",\n      selector: getDataKeyElemSelector(\"where\"),\n      children: [],\n    },\n    {\n      type: \"section\",\n      title: \"Having Filters\",\n      description: \"Filters applied after aggregation (HAVING clause in SQL).\",\n      selector: getDataKeyElemSelector(\"having\"),\n      children: [],\n    },\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/table/smartFormUIDoc.ts",
    "content": "import type { UIDocElement } from \"src/app/UIDocs\";\nimport { getCommandElemSelector } from \"src/Testing\";\n\nexport const smartFormUIDoc: UIDocElement = {\n  type: \"popup\",\n  selectorCommand: \"dashboard.window.viewEditRow\",\n  title: \"View/edit data\",\n  description:\n    \"Opens the row card, allowing users to view/edit/delete the selected row.\",\n  docOptions: { title: \"Row card\" },\n  docs: `\n    Smart form is an intelligent, auto-generated form system that adapts to your database schema.\n    It provides a user-friendly interface for inserting and updating data with automatic validation,\n    foreign key handling, and support for complex data types.\n\n    <img src=\"./screenshots/smart_form.svgif.svg\" alt=\"SmartForm screenshot\" />\n\n    ## Features\n    - **Auto-generated fields** based on table schema\n    - **Data type validation** (text, numbers, dates, JSON, etc.)\n    - **Foreign key support** with searchable dropdowns\n    - **File upload** for media and document columns\n    - **Linked data management** - insert related records inline\n    - **JSON/JSONB editor** with syntax highlighting\n    - **Geometry/Geography** support for spatial data\n    - **Array types** with dynamic add/remove\n    - **Default values** and constraints enforcement\n    - **Required field** indicators\n    - **Custom field rendering** based on column configuration\n\n    ## Field Types\n    - Text inputs (single/multi-line)\n    - Number inputs (integer, decimal)\n    - Date/Time/Timestamp pickers\n    - Boolean checkboxes\n    - Select dropdowns (enums, foreign keys)\n    - File upload fields\n    - JSON/JSONB editors\n    - Geometry/Geography mappers\n    - Array editors\n \n  `,\n  children: [\n    {\n      type: \"section\",\n      selector: \".W_SmartForm\",\n      title: \"Smart Form\",\n      description:\n        \"The smart form displays the details of a single row from the table, allowing users to view and edit the data in a structured format.\",\n      children: [\n        {\n          selectorCommand: \"SmartForm.header.tableIconAndName\",\n          type: \"section\",\n          title: \"Table icon in header\",\n          children: [],\n          description:\n            \"Table icon and table name. Table icon is configurable through the table menu settings.\",\n        },\n        {\n          selectorCommand: \"SmartForm.header.previousRow\",\n          type: \"button\",\n          title: \"Previous row button\",\n          description:\n            \"Navigate to the previous row in the dataset. Only shown for tables with primary key. Disabled when there is no previous row available.\",\n        },\n        {\n          selectorCommand: \"SmartForm.header.nextRow\",\n          type: \"button\",\n          title: \"Next row button\",\n          description:\n            \"Navigate to the next row in the dataset. Only shown for tables with primary key. Disabled when there is no next row available.\",\n        },\n        {\n          selectorCommand: \"Popup.toggleFullscreen\",\n          type: \"button\",\n          title: \"Fullscreen toggle button\",\n          description:\n            \"Toggles the fullscreen mode of the SmartForm popup for an expanded view.\",\n        },\n        {\n          selectorCommand: \"Popup.close\",\n          type: \"button\",\n          title: \"Close button\",\n          description: \"Closes the SmartForm popup without saving any data.\",\n        },\n        {\n          type: \"section\",\n          selector: \".form-field\",\n          title: \"Form field\",\n          description:\n            \"Each form field represents a column from the table, displaying the column name and the corresponding value for the selected row. Users can edit the value if the field is editable.\",\n          children: [\n            {\n              type: \"section\",\n              selector: \"label\",\n              children: [],\n              title: \"Field label\",\n              description: \"Displays the name of the column.\",\n            },\n            {\n              type: \"section\",\n              selector: \".form-field__right-content-wrapper\",\n              title: \"Field input area\",\n              description:\n                \"The input area where users can view and edit the value of the column. The input type varies based on the column's data type.\",\n              children: [],\n            },\n            {\n              type: \"section\",\n              selector: \".FormFieldHint\",\n              title: \"Field hint\",\n              description:\n                \"Additional information or guidance about the field, displayed below the input area.\",\n              children: [],\n            },\n            {\n              selectorCommand: \"ViewMoreSmartCardList\",\n              type: \"popup\",\n              title: \"View more linked records\",\n              description:\n                \"For foreign key fields a 'View more' button appears to open a detailed list of all linked records in a separate popup. This is useful to browse and search all columns from the referenced table.\",\n              children: [],\n            },\n            {\n              type: \"popup\",\n              selectorCommand: \"SmartFormFieldOptions.NestedInsert\",\n              title: \"Insert linked record\",\n              description:\n                \"If the column is a foreign key a 'Insert new record' button will appear on hover which allows inserting data into the referenced table. Useful when the desired value does not exist yet (foreign key columns only show existing values).\",\n              children: [],\n            },\n            {\n              selectorCommand: \"FormField.clear\",\n              type: \"button\",\n              title: \"Clear field value button\",\n              description:\n                \"Clear the current value of the field, resetting it to null. Only shown if the field is nullable and the user is allowed to update it.\",\n            },\n          ],\n        },\n        {\n          selectorCommand: \"JoinedRecords\",\n          type: \"list\",\n          title: \"Joined records section\",\n          description:\n            \"If the current table has other tables referencing it via foreign keys, a section will appear at the bottom of the form showing lists of those related records. This allows users to view and manage data that is linked to the current row.\",\n          itemSelector: getCommandElemSelector(\"JoinedRecords.Section\"),\n          itemContent: [\n            {\n              selectorCommand: \"JoinedRecords.SectionToggle\",\n              type: \"button\",\n              title: \"Toggle joined records section\",\n              description:\n                \"Expand or collapse the joined records section to show or hide the list of related records.\",\n            },\n            {\n              selectorCommand: \"ViewMoreSmartCardList\",\n              type: \"popup\",\n              title: \"View more joined records\",\n              description:\n                \"Open a detailed list of all joined records in a separate popup. Useful to browse and search all columns from the joined table.\",\n              children: [],\n            },\n            {\n              selectorCommand: \"JoinedRecords.AddRow\",\n              type: \"popup\",\n              title: \"Add joined record\",\n              description:\n                \"Open the SmartForm to insert a new joined record into the related table, automatically linking it to the current row.\",\n              children: [],\n            },\n            {\n              selectorCommand: \"Section.toggleFullscreen\",\n              type: \"button\",\n              title: \"Toggle fullscreen mode\",\n              description:\n                \"Expand the section to fullscreen for better visibility and interaction with the joined records.\",\n            },\n          ],\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/table/tableMenuUIDoc.ts",
    "content": "import { fixIndent } from \"../../../../../demo/scripts/sqlVideoDemo\";\nimport {\n  getCommandElemSelector,\n  getDataKeyElemSelector,\n} from \"../../../../../Testing\";\nimport type { UIDocElement } from \"../../../../UIDocs\";\n\nexport const tableMenuUIDoc = {\n  selectorCommand: \"dashboard.window.menu\",\n  type: \"popup\",\n  title: \"Table menu\",\n  description: \"Opens the table menu, allowing users to manage the table view.\",\n  docs: fixIndent(`\n    The table menu provides options for managing the table view, including viewing table info, editing columns, and managing data refresh rates.\n  `),\n  children: [\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Table info\"),\n      title: \"Table info\",\n      description: \"Postgres specific table/view details.\",\n      children: [\n        {\n          type: \"section\",\n          selectorCommand: \"W_TableMenu_TableInfo.name\",\n          title: \"Table name\",\n          description:\n            \"Displays the name of the table with option to rename it.\",\n          children: [\n            {\n              type: \"button\",\n              selector: `${getCommandElemSelector(\"W_TableMenu_TableInfo.name\")} button`,\n              title: \"Edit name\",\n              description: \"Opens SQL editor to rename the table.\",\n            },\n          ],\n        },\n        {\n          type: \"section\",\n          selectorCommand: \"W_TableMenu_TableInfo.comment\",\n          title: \"Comment\",\n          description: \"Displays and allows editing of the table comment.\",\n          children: [\n            {\n              type: \"button\",\n              selector: `${getCommandElemSelector(\"W_TableMenu_TableInfo.comment\")} button`,\n              title: \"Edit comment\",\n              description: \"Opens SQL editor to modify the table comment.\",\n            },\n          ],\n        },\n        {\n          type: \"text\",\n          selectorCommand: \"W_TableMenu_TableInfo.oid\",\n          title: \"OID\",\n          description:\n            \"Displays the object identifier of the table in the database.\",\n        },\n        {\n          type: \"text\",\n          selectorCommand: \"W_TableMenu_TableInfo.type\",\n          title: \"Type\",\n          description: \"Shows whether this is a table or view.\",\n        },\n        {\n          type: \"text\",\n          selectorCommand: \"W_TableMenu_TableInfo.owner\",\n          title: \"Owner\",\n          description: \"Displays the database user who owns this table/view.\",\n        },\n        {\n          type: \"section\",\n          selectorCommand: \"W_TableMenu_TableInfo.sizeInfo\",\n          title: \"Size information\",\n          description: \"Provides details about the table's size and row count.\",\n          children: [\n            {\n              type: \"text\",\n              selector: `${getCommandElemSelector(\"W_TableMenu_TableInfo.sizeInfo\")} [label=\"Actual Size\"]`,\n              title: \"Actual Size\",\n              description: \"The physical size of the table data on disk.\",\n            },\n            {\n              type: \"text\",\n              selector: `${getCommandElemSelector(\"W_TableMenu_TableInfo.sizeInfo\")} [label=\"Index Size\"]`,\n              title: \"Index Size\",\n              description:\n                \"The size of all indexes associated with this table.\",\n            },\n            {\n              type: \"text\",\n              selector: `${getCommandElemSelector(\"W_TableMenu_TableInfo.sizeInfo\")} [label=\"Total Size\"]`,\n              title: \"Total Size\",\n              description: \"The combined size of table data and indexes.\",\n            },\n            {\n              type: \"text\",\n              selector: `${getCommandElemSelector(\"W_TableMenu_TableInfo.sizeInfo\")} [label=\"Row count\"]`,\n              title: \"Row count\",\n              description: \"The total number of rows in the table.\",\n            },\n          ],\n        },\n        {\n          type: \"text\",\n          selectorCommand: \"W_TableMenu_TableInfo.viewDefinition\",\n          title: \"View definition\",\n          description:\n            \"Shows the SQL definition for views. Only visible for views, not tables.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_TableMenu_TableInfo.vacuum\",\n          title: \"Vacuum\",\n          description:\n            \"Performs garbage collection and optionally analyzes the database. Only available for tables.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_TableMenu_TableInfo.vacuumFull\",\n          title: \"Vacuum Full\",\n          description:\n            \"Performs a more thorough vacuum that can reclaim more space but takes longer and locks the table. Only available for tables.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_TableMenu_TableInfo.drop\",\n          title: \"Drop\",\n          description:\n            \"Deletes the table or view from the database after confirmation.\",\n        },\n      ],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Columns\"),\n      title: \"Columns\",\n      description: \"Allows editing, reordering and toggling table columns.\",\n      children: [\n        {\n          type: \"list\",\n          selector: getCommandElemSelector(\"SearchList.List\"),\n          title: \"Columns list\",\n          description: \"Allows editing, reordering and toggling table columns.\",\n          itemSelector: \"li\",\n          itemContent: [\n            {\n              type: \"popup\",\n              selectorCommand: \"W_TableMenu_ColumnList.alter\",\n              title: \"Alter column\",\n              description: \"Opens a popup to edit the column properties.\",\n              children: [],\n            },\n            {\n              type: \"popup\",\n              selectorCommand: \"W_TableMenu_ColumnList.linkedColumnOptions\",\n              title: \"Edit linked field\",\n              description: \"Opens a popup to edit the linked field properties.\",\n              children: [],\n            },\n            {\n              type: \"button\",\n              selectorCommand: \"W_TableMenu_ColumnList.removeComputedColumn\",\n              title: \"Remove computed column\",\n              description:\n                \"Removes a computed column from the table. Only visible for computed columns.\",\n            },\n          ],\n        },\n      ],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Data refresh\"),\n      title: \"Data refresh\",\n      description:\n        \"Allows setting subscriptions or data refresh rates. By default every table subscribes to data changes.\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Triggers\"),\n      title: \"Triggers\",\n      description: \"Allows managing triggers. \",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Constraints\"),\n      title: \"Constraints\",\n      description: \"Allows managing constraints.\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Indexes\"),\n      title: \"Indexes\",\n      description: \"Allows managing indexes.\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Policies\"),\n      title: \"Policies\",\n      description: \"Allows managing policies.\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Access rules\"),\n      title: \"Access rules\",\n      description: \"Allows managing prostgles access rules.\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Current query\"),\n      title: \"Current query\",\n      description:\n        \"Allows viewing the SQL and data/layout info for the current table view.\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"Display options\"),\n      title: \"Display options\",\n      description: \"Layout preferences.\",\n      children: [],\n    },\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/table/tableUIDoc.ts",
    "content": "import { fixIndent } from \"src/demo/scripts/sqlVideoDemo\";\nimport type { UIDocElement } from \"../../../../UIDocs\";\nimport { getCommonViewHeaderUIDoc } from \"../../getCommonViewHeaderUIDoc\";\nimport { paginationUIDoc } from \"./paginationUIDoc\";\nimport { smartFilterBarUIDoc } from \"./smartFilterBarUIDoc\";\nimport { tableMenuUIDoc } from \"./tableMenuUIDoc\";\nimport { addColumnMenuUIDoc } from \"./addColumnMenuUIDoc\";\nimport { columnMenuUIDoc } from \"./columnMenuUIDoc\";\nimport { smartFormUIDoc } from \"./smartFormUIDoc\";\n\nexport const tableUIDoc = {\n  type: \"section\",\n  selector: `.SilverGridChild[data-view-type=\"table\"]`,\n  title: \"Table view\",\n  description: \"Allows interacting with a table/view from the database.\",\n  docs: fixIndent(`\n    The table view allows you to explore, filter, and edit your Postgres data with ease.\n    Instantly sort and search, build computed columns, pull in linked data through automatic joins, and create charts, maps, and cross-filtered views in a couple of clicks.\n    With smart forms for row editing, rich column controls, and deep schema-aware features, the table view turns your database into an interactive workspace for analysis, tooling, and rapid iteration.\n\n    \n    ## Features\n\n    - **Smart filtering**: Use the smart filter bar to quickly filter your data based on column types and values.\n    - **Computed columns**: Add calculations or transformations of existing data.\n    - **Automatic joins**: Show related data from linked tables with automatic joins and summarise if needed.\n    - **Charts and maps**: Timechart and map visualisations with multi-layer support.\n    - **Cross-filtered views**: Create additional table or chart views that are cross-filtered by the current table.\n    - **Smart forms**: Edit rows using smart forms that adapt to your schema and data types.\n    - **Conditional styling**: Style your columns based on row data.\n\n    <img src=\"./screenshots/table.svgif.svg\" alt=\"Table view screenshot\" />\n    \n    ## Components\n    `),\n  docOptions: \"asSeparateFile\",\n  children: [\n    getCommonViewHeaderUIDoc(\n      \"The name of the table/view together with the number of records matching the current filters. \",\n      tableMenuUIDoc,\n      \"table\",\n    ),\n    smartFilterBarUIDoc,\n    {\n      type: \"section\",\n      selector: \".W_Table\",\n      title: \"Table\",\n      description:\n        \"The main table view displaying the data from the database. It allows users to interact with the data, including sorting, filtering, and editing.\",\n      children: [\n        {\n          type: \"section\",\n          selectorCommand: \"TableHeader\",\n          title: \"Table header\",\n          description:\n            \"The header of the table, which contains the column names and allows users to sort the data by clicking on the column headers.\",\n          children: [\n            addColumnMenuUIDoc,\n            columnMenuUIDoc,\n            {\n              type: \"button\",\n              selector: `[role=\"columnheader\"]`,\n              title: \"Column header\",\n              description:\n                \"Pressing the header will toggle sorting state (if the column is sortable). Right clicking (or long press on mobile) will open column menu. Dragging the header will allow reordering the columns.\",\n            },\n            {\n              type: \"drag-handle\",\n              direction: \"x\",\n              selectorCommand: \"TableHeader.resizeHandle\",\n              title: \"Resize column\",\n              description:\n                \"Allows users to resize the column width by dragging the handle.\",\n            },\n            smartFormUIDoc,\n            {\n              selectorCommand: \"dashboard.window.rowInsert\",\n              type: \"popup\",\n              title: \"Insert row\",\n              description:\n                \"Opens the row insert menu, allowing users to add new rows to the table.\",\n              children: [],\n            },\n          ],\n        },\n      ],\n    },\n    paginationUIDoc,\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboard/timechartUIDoc.ts",
    "content": "import { fixIndent } from \"../../../../demo/scripts/sqlVideoDemo\";\nimport { getCommandElemSelector } from \"../../../../Testing\";\nimport type { UIDocElement } from \"../../../UIDocs\";\nimport { getCommonViewHeaderUIDoc } from \"../getCommonViewHeaderUIDoc\";\n\nexport const timechartUIDoc = {\n  type: \"section\",\n  selector: `.SilverGridChild[data-view-type=\"timechart\"]`,\n  title: \"Timechart view\",\n  description: \"Displays a timechart based on the Table/SQL query results.\",\n  docs: `\n    The timechart view allows you to visualize time-series data from your database.\n    It supports multiple layers, each with its own data source and style.\n    You can add filters to the timechart to narrow down the data displayed.\n\n    <img src=\"./screenshots/timechart.svgif.svg\" alt=\"Timechart view screenshot\" />\n  \n    ## Components\n    `,\n  docOptions: \"asSeparateFile\",\n  children: [\n    getCommonViewHeaderUIDoc(\n      \"Shows the table/view name together with the number of records matching the current filters.\",\n      {\n        description: \"Timechart view menu\",\n        children: [],\n        title: \"Timechart view menu\",\n        docs: fixIndent(`\n          The timechart view menu provides options for configuring the timechart.\n        `),\n      },\n      \"chart\",\n    ),\n    {\n      type: \"section\",\n      selector: \".Window\",\n      title: \"Chart area with controls\",\n      description: \"Timechart visualization.\",\n      children: [\n        {\n          type: \"popup\",\n          selectorCommand: \"ChartLayerManager\",\n          title: \"Layer manager\",\n          description:\n            \"Allows adding/removing layers from the chart. Each layer can be configured with its own data source and style.\",\n          children: [\n            {\n              type: \"list\",\n              selector: \".ChartLayerManager_LayerList\",\n              title: \"Layer list\",\n              itemSelector: \".ChartLayerManager_LayerList > .LayerQuery\",\n              itemContent: [\n                {\n                  type: \"popup\",\n                  title: \"Layer color picker\",\n                  selectorCommand: \"LayerColorPicker\",\n                  description:\n                    \"Allows setting the color for the layer. The color can be set for each column in the layer.\",\n                  children: [],\n                },\n                {\n                  selector: `[title=\"Table name\"]`,\n                  type: \"text\",\n                  title: \"Table name\",\n                  description:\n                    \"The name of the table used for the layer. This is the table that contains the data for the layer.\",\n                },\n                {\n                  type: \"popup\",\n                  selectorCommand: \"TimeChartLayerOptions.aggFunc\",\n                  title: \"Aggregation options\",\n                  description:\n                    \"Allows setting the y-axis options for the layer.\",\n                  children: [\n                    {\n                      type: \"select\",\n                      selectorCommand: \"TimeChartLayerOptions.aggFunc.select\",\n                      title: \"Aggregation function\",\n                      description:\n                        \"Selects the aggregation function to be used for the layer. The available options are: Sum, Average, Min, Max, Count.\",\n                    },\n                    {\n                      type: \"select\",\n                      selectorCommand: \"TimeChartLayerOptions.numericColumn\",\n                      title: \"Aggregation column\",\n                      description:\n                        \"Selects the numeric column to be used for the aggregation function. \",\n                    },\n                    {\n                      type: \"select\",\n                      selectorCommand: \"TimeChartLayerOptions.groupBy\",\n                      title: \"Group by\",\n                      description:\n                        \"Selects the column to group the data by. This is will create a line for each group by value.\",\n                    },\n                    {\n                      selectorCommand: \"Popup.close\",\n                      type: \"button\",\n                      title: \"Close\",\n                      description: \"Closes the aggregation function popup.\",\n                    },\n                  ],\n                },\n                {\n                  selectorCommand: \"ChartLayerManager.toggleLayer\",\n                  type: \"button\",\n                  title: \"Toggle layer on/off\",\n                  description:\n                    \"Toggles the visibility of the layer on the chart. This allows you to hide or show the layer without removing it.\",\n                },\n                {\n                  selectorCommand: \"ChartLayerManager.removeLayer\",\n                  type: \"button\",\n                  title: \"Remove layer\",\n                  description:\n                    \"Removes the layer from the chart. This will delete the layer and its configuration.\",\n                },\n              ],\n              description:\n                \"Displays the list of layers currently added to the chart. \",\n            },\n            {\n              type: \"select\",\n              selectorCommand: \"ChartLayerManager.AddChartLayer.addLayer\",\n              title: \"Add layer\",\n              description:\n                \"Allows adding a new layer to the chart. The available options are all the tables that have date or timestamp columns.\",\n            },\n          ],\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_TimeChart.resetExtent\",\n          title: \"Reset extent\",\n          description:\n            \"Resets the chart to the default extent, showing all data points. Visible when the chart was paned or zoomed.\",\n        },\n        {\n          type: \"list\",\n          selector: \".W_TimeChartLayerLegend\",\n          title: \"Layer legend\",\n          itemSelector: \".W_TimeChartLayerLegend_Item\",\n          itemContent: [],\n          description:\n            \"Displays the layers currently added to the chart. Quick access to changing the layer color, aggregation type and group by.\",\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"W_TimeChart.AddTimeChartFilter\",\n          title: \"Add/Edit time filter\",\n          description:\n            \"Allows adding a time filter to the timechart. This will filter the data points based on the selected time range.\",\n        },\n        {\n          type: \"canvas\",\n          selector: getCommandElemSelector(\"W_TimeChart\") + \" > .Canvas\",\n          title: \"Timechart canvas\",\n          description:\n            \"Zoomable and pannable canvas that displays the timechart. It shows the data points based on the selected layers and filters. Clicking on a point will add a filter with that time bucket.\",\n        },\n      ],\n    },\n  ],\n} satisfies UIDocElement;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/dashboardUIDoc.ts",
    "content": "import { mdiMonitorDashboard } from \"@mdi/js\";\nimport { ROUTES } from \"@common/utils\";\nimport { getCommandElemSelector } from \"../../../Testing\";\nimport type { UIDocContainers } from \"../../UIDocs\";\nimport { AIAssistantUIDoc } from \"./AIAssistantUIDoc\";\nimport { dashboardContentUIDoc } from \"./dashboard/dashboardContentUIDoc\";\nimport { dashboardMenuUIDoc } from \"./dashboard/dashboardMenuUIDoc\";\n\nexport const dashboardUIDoc = {\n  type: \"page\",\n  path: ROUTES.CONNECTIONS,\n  pathItem: {\n    tableName: \"connections\",\n    selectorCommand: \"Connection.openConnection\",\n  },\n  title: \"Connection dashboard\",\n  description: \"Database exploration and management interface\",\n  iconPath: mdiMonitorDashboard,\n  docs: `\n    The connection dashboard is your command center for exploring and managing your Postgres database. \n    Open tables, run SQL, visualize schema relationships, switch workspaces, and launch tools—all in one flexible, customizable workspace.\n    With quick search, saved queries, AI-powered assistance, and instant access to every database object, the dashboard gives you a fast, intuitive way to navigate your data and build the tools you need.\n    \n    ## Features\n    \n    - **Unified workspace**: View tables, SQL editors, charts, and tools together in a flexible layout. Save and switch between different layouts and sets of opened views for different tasks or projects.\n    - **AI Assistant**: Generate SQL, explore data, and get help directly within the dashboard.\n    - **Flexible layout**: Drag, resize, and arrange views in a tiled layout to create a workspace that fits your needs.\n    - **Global search**: Search across all tables, views, and functions in a single, fast search bar.\n    - **Schema diagram**: Visualize relationships between tables and schemas to better understand your database structure.\n    - **Import data**: Easily import data from CSV/JSON files into your database tables.\n\n    <img src=\"./screenshots/dashboard.svgif.svg\" alt=\"Connection dashboard\" />\n\n    ## Components\n    `,\n  childrenTitle: \"Dashboard elements\",\n  children: [\n    dashboardMenuUIDoc,\n    {\n      type: \"button\",\n      title: \"Dashboard menu toggle\",\n      description:\n        \"Opens or closes the dashboard menu unless the menu is pinned.\",\n      selectorCommand: \"dashboard.menu\",\n    },\n    {\n      type: \"link\",\n      title: \"Go to configuration\",\n      description: \"Opens the configuration page for the selected connection.\",\n      selectorCommand: \"dashboard.goToConnConfig\",\n      path: ROUTES.CONFIG,\n      pathItem: {\n        tableName: \"connections\",\n      },\n    },\n    {\n      type: \"input\",\n      inputType: \"select\",\n      title: \"Change connection\",\n      description: \"Switch to a different database connection.\",\n      selectorCommand: \"ConnectionSelector\",\n    },\n    {\n      type: \"select\",\n      title: \"Workspaces\",\n      description:\n        \"List of available workspaces for the selected connection. Each workspace stores opened views and their layout.\",\n      selectorCommand: \"WorkspaceMenu.list\",\n    },\n    {\n      type: \"popup\",\n      selectorCommand: \"WorkspaceMenuDropDown\",\n      title: \"Workspaces menu\",\n      description:\n        \"Opens the workspaces menu, allowing you to create, manage, and switch between workspaces.\",\n      docs: `\n        Workspaces are a powerful feature that allows you to organize your work within a connection.\n        The opened views and their layout is saved to the workspace, so you can switch between different sets of data and configurations without losing your progress.\n\n        The workspaces menu provides access to all available workspaces for the selected connection. You can create new workspaces, switch between existing ones, and manage workspace settings.\n        Each workspace allows you to work with a separate set of data and configurations, making it easier to organize your work and collaborate with others.\n        The menu also includes options to clone existing workspaces and delete them if they are no longer needed.\n      `,\n      children: [\n        {\n          type: \"list\",\n          title: \"Workspaces\",\n          description:\n            \"List of available workspaces. Click to switch to a different workspace.\",\n          selector: getCommandElemSelector(\"WorkspaceMenu.SearchList\") + \" ul\",\n          itemSelector: \"li\",\n          itemContent: [\n            {\n              type: \"popup\",\n              selectorCommand: \"WorkspaceDeleteBtn\",\n              title: \"Delete workspace\",\n              description: \"Opens the delete workspace confirmation dialog\",\n              children: [\n                {\n                  type: \"button\",\n                  title: \"Delete workspace\",\n                  description:\n                    \"Confirms the deletion of the selected workspace.\",\n                  selectorCommand: \"WorkspaceDeleteBtn.Confirm\",\n                },\n              ],\n            },\n            {\n              type: \"button\",\n              selectorCommand: \"WorkspaceMenu.CloneWorkspace\",\n              title: \"Clone workspace\",\n              description:\n                \"Creates a copy of the selected workspace with a new name.\",\n            },\n            {\n              selectorCommand: \"WorkspaceSettings\",\n              type: \"smartform-popup\",\n              tableName: \"workspaces\",\n              title: \"Workspace settings\",\n              description:\n                \"Opens the settings for the selected workspace, allowing you to manage its properties.\",\n            },\n          ],\n        },\n        {\n          type: \"popup\",\n          title: \"Create new workspace\",\n          description:\n            \"Opens the form to create a new workspace for the selected connection.\",\n          selectorCommand: \"WorkspaceMenuDropDown.WorkspaceAddBtn\",\n          children: [\n            {\n              type: \"input\",\n              title: \"Workspace name\",\n              description: \"Name of the new workspace.\",\n              selector: getCommandElemSelector(\"WorkspaceAddBtn\") + \" input\",\n              inputType: \"text\",\n            },\n            {\n              type: \"button\",\n              title: \"Create workspace\",\n              description:\n                \"Create and switch to the new workspace with the specified name.\",\n              selectorCommand: \"WorkspaceAddBtn.Create\",\n            },\n          ],\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"WorkspaceMenu.toggleWorkspaceLayoutMode\",\n          title: \"Toggle layout mode\",\n          description:\n            \"Switches between fixed and editable layout modes for the current workspace. Fixed mode locks the layout, preventing it from being changed by the user.\",\n        },\n      ],\n    },\n    dashboardContentUIDoc,\n    AIAssistantUIDoc,\n    {\n      type: \"popup\",\n      selectorCommand: \"Feedback\",\n      title: \"Feedback\",\n      description:\n        \"Opens the feedback form, allowing you to provide feedback about the application.\",\n      children: [],\n    },\n    {\n      type: \"link\",\n      selectorCommand: \"dashboard.goToConnections\",\n      title: \"Go to Connections\",\n      description: \"Opens the connections list page.\",\n      path: ROUTES.CONNECTIONS,\n    },\n  ],\n} satisfies UIDocContainers;\n"
  },
  {
    "path": "client/src/app/UIDocs/connection/getCommonViewHeaderUIDoc.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\nimport type { UIDocElement } from \"../../UIDocs\";\n\nexport const getCommonViewHeaderUIDoc = (\n  titleContentDescription: string,\n  menu: {\n    description: string;\n    children: UIDocElement[];\n    docs: string;\n    title: string;\n  },\n  viewType: \"table\" | \"sql\" | \"chart\" | \"method\",\n): UIDocElement => ({\n  type: \"section\",\n  selector: \".silver-grid-item-header\",\n  title: \"Header section\",\n  description:\n    \"Contains menu button, title and window minimise/fullscreen controls.\",\n  children: (\n    [\n      viewType === \"chart\" ? undefined : (\n        {\n          type: \"section\",\n          selectorCommand: \"Window.W_QuickMenu\",\n          title: \"Quick actions\",\n          description:\n            \"Quick actions for the view, providing easy access to charting and joins.\",\n          children: (\n            [\n              viewType === \"sql\" ? undefined : (\n                {\n                  selectorCommand: \"dashboard.window.toggleFilterBar\",\n                  type: \"button\",\n                  title: \"Toggle filter bar\",\n                  description:\n                    \"Shows or hides the filter bar, allowing users to filter the data displayed in the table.\",\n                }\n              ),\n              viewType === \"sql\" ? undefined : (\n                {\n                  selectorCommand: \"Window.W_QuickMenu.addCrossFilteredTable\",\n                  type: \"button\",\n                  title: \"Add cross-filtered table\",\n                  description:\n                    \"Adds a new table view that is cross-filtered by the current table. This allows you to explore related data in a new table view.\",\n                }\n              ),\n\n              {\n                type: \"button\",\n                selectorCommand: \"AddChartMenu.Timechart\",\n                title: \"Add timechart\",\n                description:\n                  viewType === \"sql\" ?\n                    \"Adds a timechart visualization based on the current SQL statement. Visible only when the last executed statement returned at least one timestamp column.\"\n                  : \"Adds a timechart visualization based on the current table. Visible only when the current table (or any linked table) has a timestamp/date column.\",\n              },\n              {\n                type: \"button\",\n                selectorCommand: \"AddChartMenu.Map\",\n                title: \"Add map\",\n                description:\n                  viewType === \"sql\" ?\n                    \"Adds a map visualization based on the current SQL statement. Visible only when the last executed statement returned at least one geometry/geography column (postgis extension must be enabled).\"\n                  : \"Adds a map visualization based on the current data. Visible only when the current table (or any linked table) has a geometry/geography column (postgis extension must be enabled).\",\n              },\n            ] satisfies (UIDocElement | undefined)[]\n          ).filter(isDefined),\n        }\n      ),\n      {\n        type: \"drag-handle\",\n        direction: \"x\",\n        selector: \".silver-grid-item-header--title\",\n        title: \"View title. Drag to re-arrange layout\",\n        description: titleContentDescription,\n      },\n      {\n        selectorCommand: \"dashboard.window.collapse\",\n        type: \"button\",\n        title: \"Collapse the view\",\n        description:\n          \"Collapses the view, minimizing it to temporarily save space on the dashboard. \",\n      },\n      {\n        selectorCommand: \"dashboard.window.fullscreen\",\n        type: \"button\",\n        title: \"Fullscreen\",\n        description: \"Expands the view to fill the entire screen.\",\n      },\n      {\n        selectorCommand: \"dashboard.window.close\",\n        type: \"button\",\n        title: \"Remove view\",\n        description:\n          viewType === \"sql\" ?\n            \"Removes the SQL editor from the dashboard. If there are unsaved changes, a confirmation dialog will appear.\"\n          : \"Removes the view from the dashboard.\",\n      },\n      viewType !== \"chart\" ? undefined : (\n        {\n          type: \"section\",\n          selectorCommand: \"Window.ChildChart.toolbar\",\n          title: \"Chart toolbar\",\n          description:\n            \"Toolbar for the chart view if not detached. By default, newly charts added will appear over the originating table/sql editor view. They can be detached to a separate window.\",\n          children: [\n            {\n              type: \"popup\",\n              selectorCommand: \"dashboard.window.chartMenu\",\n              title: \"Chart menu\",\n              description: \"Menu for the chart view.\",\n              children: [],\n            },\n            {\n              selectorCommand: \"dashboard.window.collapseChart\",\n              title: \"Collapse chart\",\n              type: \"button\",\n              description:\n                \"Collapses the chart window, minimizing it to save space on the dashboard. It can then be restored by clicking the chart icon in the SQL editor top left quick actions section.\",\n            },\n            {\n              selectorCommand: \"dashboard.window.detachChart\",\n              title: \"Detach chart\",\n              type: \"button\",\n              description:\n                \"Detaches the chart from the parent view, allowing it to be moved and resized independently. It keeps the connection the originating table view to cross filter it.\",\n            },\n            {\n              selectorCommand: \"dashboard.window.closeChart\",\n              title: \"Close chart\",\n              type: \"button\",\n              description:\n                \"Closes the chart view, returning to the originatine table/sql editor view.\",\n            },\n          ],\n        }\n      ),\n      {\n        selectorCommand: \"dashboard.window.menu\",\n        type: \"popup\",\n        ...menu,\n      },\n    ] satisfies (UIDocElement | undefined)[]\n  ).filter(isDefined),\n});\n"
  },
  {
    "path": "client/src/app/UIDocs/connectionsUIDoc.ts",
    "content": "import { ROUTES } from \"@common/utils\";\nimport { mdiDatabasePlusOutline, mdiFilter, mdiServerNetwork } from \"@mdi/js\";\nimport { getCommandElemSelector, getDataKeyElemSelector } from \"../../Testing\";\nimport type { UIDocContainers, UIDocElement } from \"../UIDocs\";\nimport { editConnectionUIDoc } from \"./editConnectionUIDoc\";\n\nconst newOwnerOrUserOptions = [\n  {\n    type: \"input\",\n    inputType: \"checkbox\",\n    title: \"Create database owner\",\n    description:\n      \"If checked, a new owner will be created for the database. Useful for ensuring the database is owned by a non-superuser account.\",\n    selectorCommand: \"ConnectionServer.withNewOwnerToggle\",\n  },\n  {\n    title: \"New Owner Name\",\n    description: \"The name of the new owner.\",\n    selectorCommand: \"ConnectionServer.NewUserName\",\n    type: \"input\",\n    inputType: \"text\",\n  },\n  {\n    selectorCommand: \"ConnectionServer.NewUserPassword\",\n    title: \"New Owner Password\",\n    description: \"The password of the new owner.\",\n    inputType: \"text\",\n    type: \"input\",\n  },\n  {\n    type: \"section\",\n    selectorCommand: \"ConnectionServer.NewUserPermissionType\",\n    title: \"New Owner Permission Type\",\n    description:\n      \"Apart from Owner it is possible to create a user with reduced permission types (SELECT/UPDATE/DELETE/INSERT).\",\n    children: [],\n  },\n] satisfies UIDocElement[];\n\nexport const connectionsUIDoc = {\n  type: \"page\",\n  path: ROUTES.CONNECTIONS,\n  title: \"Connections\",\n  iconPath: mdiServerNetwork,\n  description:\n    \"Manage your database connections. View, add, or edit connections to your databases.\",\n  docs: `\n    The Connections page serves as the central hub within Prostgles UI for managing all your PostgreSQL database connections. \n    From here, you can add and open connections, modify existing ones, and gain an immediate overview of their status and associated workspaces. \n\n    <img src=\"./screenshots/connections.svg\" alt=\"Connections page screenshot\" />  \n      \n`,\n  childrenTitle: \"Connection controls\",\n  children: [\n    {\n      type: \"link\",\n      title: \"New connection\",\n      iconPath: mdiDatabasePlusOutline,\n      description: \"Opens the form to add a new database connection.\",\n      selectorCommand: \"Connections.new\",\n      path: ROUTES.NEW_CONNECTION,\n      docs: `\n        Use the **New Connection** button to add a new database connection.\n        \n        This will open a form where you can enter the connection details such as host, port, database name, user, and password.\n        \n        <img src=\"./screenshots/new_connection.svgif.svg\" alt=\"New connection form screenshot\" />\n      `,\n      childrenTitle: \"New connection form fields\",\n      docOptions: { title: \"Adding a connection\" },\n      pageContent: editConnectionUIDoc.children,\n    },\n    {\n      type: \"popup\",\n      title: \"Display options\",\n      description:\n        \"Customize how the list of connections is displayed (e.g., show/hide state database, show database names).\",\n      selectorCommand: \"ConnectionsOptions\",\n      iconPath: mdiFilter,\n      docOptions: \"hideChildren\",\n      children: [\n        {\n          type: \"input\",\n          title: \"Show state connection\",\n          description:\n            \"If checked, displays the internal 'Prostgles UI state' connection which stores application metadata and dashboard data.\",\n          selectorCommand: \"ConnectionsOptions.showStateDatabase\",\n          inputType: \"checkbox\",\n        },\n        {\n          type: \"input\",\n          title: \"Show database names\",\n          description:\n            \"If checked, displays the specific database name along with the connection name.\",\n          selectorCommand: \"ConnectionsOptions.showDatabaseNames\",\n          inputType: \"checkbox\",\n        },\n      ],\n    },\n    {\n      type: \"list\",\n      title: \"Connection list\",\n      description: \"Controls to open and manage your database connections.\",\n      docs: `\n        The connection list displays all your database connections grouped by database host, port and user.\n        \n        <img src=\"./screenshots/connections.svg\" alt=\"Connections list screenshot\" />\n        `,\n      selector: getCommandElemSelector(\"Connections\") + \" .Connections_list\",\n      itemSelector: \".Connection\",\n      childrenTitle: \"Connection actions\",\n      itemContent: [\n        {\n          type: \"link\",\n          selectorCommand: \"Connection.openConnection\",\n          path: ROUTES.CONNECTIONS,\n          title: \"Open connection\",\n          description:\n            \"Opens the selected connection dashboard on the default workspace.\",\n          pathItem: {\n            tableName: \"connections\",\n          },\n        },\n        {\n          type: \"popup\",\n          selectorCommand: \"ConnectionServer.add\",\n          title: \"Add new database\",\n          description: \"Adds a new connection to the selected server. \",\n          children: [\n            {\n              type: \"popup\",\n              selectorCommand: \"ConnectionServer.add.newDatabase\",\n              title: \"Create new database\",\n              description: \"Create a new database within the server.\",\n              docs: `\n                Allows you to create a new database in the selected server.\n                It will use the first connection details from the group connection.\n                If no adequate account is found (no superuser or rolcreatedb), it will be greyed out with with an appropriate explanation tooltip text.\n\n              `,\n              childrenTitle: \"New database options\",\n              children: [\n                {\n                  type: \"input\",\n                  title: \"Database Name\",\n                  description: \"The name of the new database.\",\n                  inputType: \"text\",\n                  selectorCommand: \"ConnectionServer.NewDbName\",\n                },\n                {\n                  type: \"select\",\n                  title: \"Sample Schemas\",\n                  description:\n                    \"Select a sample schema to create the database with.\",\n                  selectorCommand: \"ConnectionServer.SampleSchemas\",\n                },\n                ...newOwnerOrUserOptions,\n                {\n                  selectorCommand: \"ConnectionServer.add.confirm\",\n                  type: \"button\",\n                  title: \"Create and connect\",\n                  description: \"Creates and connects to the new database.\",\n                },\n              ],\n            },\n            {\n              type: \"popup\",\n              selector: getDataKeyElemSelector(\"Select existing database\"),\n              title: \"Connect to an existing database\",\n              description: \"Selects a database from the server to connect to. \",\n              docs: `\n                Allows you to connect to an existing database in the selected server.\n                It will use the first connection details from the group connection. \n\n                <img src=\"./screenshots/connect_existing_database.svg\" alt=\"Connect existing database popup screenshot\" />\n\n                After selecting the database, you can choose to create a new owner or user for the connection should you need to.\n              `,\n              children: [\n                {\n                  type: \"select\",\n                  title: \"Select Database\",\n                  description: \"Choose a database from the server.\",\n                  selectorCommand: \"ConnectionServer.add.existingDatabase\",\n                },\n                ...newOwnerOrUserOptions,\n                {\n                  selectorCommand: \"ConnectionServer.add.confirm\",\n                  type: \"button\",\n                  title: \"Save and connect\",\n                  description:\n                    \"Connects to the selected database with the new owner.\",\n                },\n              ],\n            },\n          ],\n        },\n        {\n          type: \"button\",\n          title: \"Debug: Close All Windows\",\n          selectorCommand: \"Connection.closeAllWindows\",\n          description:\n            \"Force-closes all windows/tabs for this connection. Use if the workspace becomes unresponsive or encounters a bug.\",\n        },\n        {\n          type: \"popup\",\n          title: \"Status monitor\",\n          selectorCommand: \"Connection.statusMonitor\",\n          description:\n            \"View real-time statistics, running queries, and system resource usage (CPU, RAM, Disk) for this connection.\",\n          children: [],\n        },\n        {\n          type: \"link\",\n          title: \"Connection configuration\",\n          selectorCommand: \"Connection.configure\",\n          description:\n            \"Access and modify settings for this connection, such as access control, file storage, backup/restore options, and server-side functions.\",\n          path: ROUTES.CONFIG,\n          pathItem: {\n            tableName: \"connections\",\n          },\n          // TODO: we need to move shared pages to the end\n          // docs: connectionConfigUIDoc.docs,\n          // pageContent: connectionConfigUIDoc.children,\n        },\n        {\n          type: \"link\",\n          title: \"Edit connection details\",\n          selectorCommand: \"Connection.edit\",\n          description:\n            \"Modify the connection parameters (e.g., display name, database details like host and port). Also allows deleting or cloning the connection.\",\n          docs: editConnectionUIDoc.docs,\n          path: ROUTES.EDIT_CONNECTION,\n          pathItem: {\n            tableName: \"connections\",\n          },\n          pageContent: editConnectionUIDoc.children,\n        },\n        {\n          type: \"button\",\n          title: \"Status indicator/Disconnect\",\n          selectorCommand: \"Connection.disconnect\",\n          description:\n            \"Shows the current connection status (green indicates connected). Click to disconnect from the database.\",\n        },\n        {\n          type: \"list\",\n          selectorCommand: \"Connection.workspaceList\",\n          title: \"Workspaces\",\n          description:\n            \"List of workspaces associated with this connection. Click to switch to a specific workspace.\",\n          itemSelector: \" [data-key]\",\n          itemContent: [],\n        },\n      ],\n    },\n  ],\n} satisfies UIDocContainers;\n"
  },
  {
    "path": "client/src/app/UIDocs/desktopInstallation.ts",
    "content": "import { fixIndent } from \"../../demo/scripts/sqlVideoDemo\";\nimport type { UIDoc } from \"../UIDocs\";\n\nexport const desktopInstallationUIDoc = {\n  type: \"info\",\n  title: \"Installation (Desktop Version)\",\n  description: \"Instructions for installing Prostgles UI on your desktop.\",\n  docs: fixIndent(`\n    To get started with Prostgles Desktop, download and install the binary file that's appropriate for your operating system (Windows, macOS, or Linux) from [our website](/download).\n\n    - **Linux**: We provide **.deb**, **.rpm** or **.AppImage** files to suit your distribution,\n    - **macOS**: Open the downloaded **.dmg** file, drag the Prostgles Desktop icon into your Applications folder, and launch the application.\n    - **Windows** - Run the downloaded **.exe** file and follow the on-screen instructions to complete the installation.\n\n    Alternatively, you can visit the [releases page](https://github.com/prostgles/ui/releases) for checksums, release notes or older versions.\n\n    ## Setting up\n\n    When you open Prostgles Desktop, you see the Welcome screen while it loads.\n    You'll need to complete two initial setup steps:\n\n    1. Accept the privacy policy\n    2. Connect to a state database\n\n    #### State database\n\n    Prostgles Desktop stores its state and configuration data in a PostgreSQL database.\n    To maintain a secure and responsive environment, we highly recommend [installing Postgres](https://www.postgresql.org/download/) on your local machine.\n    You will need to create a dedicated database and superuser account with a strong password for Prostgles Desktop.\n\n    <img src=\"./screenshots/electron_setup.svgif.svg\" alt=\"Electron Setup\" />  \n`),\n} satisfies UIDoc;\n"
  },
  {
    "path": "client/src/app/UIDocs/editConnectionUIDoc.ts",
    "content": "import { ROUTES } from \"@common/utils\";\nimport type { UIDocContainers } from \"../UIDocs\";\n\nexport const editConnectionUIDoc = {\n  type: \"page\",\n  // path: ROUTES.EDIT_CONNECTION,\n  title: \"Add or Edit Connection\",\n  description:\n    \"Create a new connection or modify the details of an existing database connection. Update connection settings, credentials, and more.\",\n  docs: `\n    Connection settings, credentials, and other parameters to ensure your connection is configured correctly.\n    `,\n  children: [\n    {\n      type: \"popup\",\n      selectorCommand: \"PostgresInstallationInstructions\",\n      title: \"PostgreSQL Installation Instructions\",\n      description: \"Instructions for installing PostgreSQL on your system.\",\n      children: [],\n    },\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selectorCommand: \"NewConnectionForm.connectionName\",\n      title: \"Connection Name\",\n      description: \"The name of the connection.\",\n    },\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selectorCommand: \"NewConnectionForm.connectionType\",\n      title: \"Connection Type\",\n      description:\n        \"Allows you change the connection details format: standard or connection string.\",\n    },\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selectorCommand: \"NewConnectionForm.db_conn\",\n      title: \"Connection String\",\n      description: \"The connection string for the database. \",\n    },\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selectorCommand: \"NewConnectionForm.db_host\",\n      title: \"Database Host\",\n      description: \"The hostname or IP address of the database server.\",\n    },\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selectorCommand: \"NewConnectionForm.db_port\",\n      title: \"Database Port\",\n      description: \"The port number on which the database server is listening.\",\n    },\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selectorCommand: \"NewConnectionForm.db_user\",\n      title: \"Database User\",\n      description: \"The username used to connect to the database.\",\n    },\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selectorCommand: \"NewConnectionForm.db_pass\",\n      title: \"Database Password\",\n      description: \"The password for the database user.\",\n    },\n    {\n      type: \"input\",\n      inputType: \"text\",\n      selectorCommand: \"NewConnectionForm.db_name\",\n      title: \"Database Name\",\n      description: \"The name of the database to connect to.\",\n    },\n    {\n      type: \"section\",\n      selectorCommand: \"NewConnectionForm.MoreOptionsToggle\",\n      title: \"More Options\",\n      description: \"Additional connection options.\",\n      children: [\n        {\n          type: \"select\",\n          selectorCommand: \"NewConnectionForm.schemaFilter\",\n          title: \"Schema Filter\",\n          description:\n            \"Controls which schemas are visible in the dashboard (public by default).\",\n        },\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selectorCommand: \"NewConnectionForm.connectionTimeout\",\n          title: \"Connection Timeout\",\n          description:\n            \"The maximum time to wait for a connection to the database before timing out.\",\n        },\n        {\n          type: \"section\",\n          selectorCommand: \"NewConnectionForm.sslMode\",\n          title: \"SSL Mode\",\n          description: \"Configure SSL settings for the connection.\",\n          children: [],\n        },\n        {\n          type: \"input\",\n          inputType: \"checkbox\",\n          selectorCommand: \"NewConnectionForm.watchSchema\",\n          title: \"Watch Schema\",\n          description:\n            \"Enabled by default. Enables schema change tracking. Any changes made to the database schema are reflected in the API and UI.\",\n        },\n        {\n          type: \"input\",\n          inputType: \"checkbox\",\n          selectorCommand: \"NewConnectionForm.realtime\",\n          title: \"Enable Realtime\",\n          description:\n            \"Enabled by default. Enables realtime data change tracking for tables and views. Requires trigger permissions to the underlying tables.\",\n        },\n      ],\n    },\n    {\n      type: \"button\",\n      selectorCommand: \"Connection.edit.updateOrCreateConfirm\",\n      title: \"Update Connection\",\n      description: \"Save the changes made to the connection.\",\n    },\n  ],\n} satisfies Omit<UIDocContainers, \"path\">;\n"
  },
  {
    "path": "client/src/app/UIDocs/navbarUIDoc.ts",
    "content": "import { mdiThemeLightDark, mdiTranslate } from \"@mdi/js\";\nimport { ROUTES } from \"@common/utils\";\nimport type { UIDocNavbar } from \"../UIDocs\";\n\nexport const navbarUIDoc = {\n  type: \"navbar\",\n  selectorCommand: \"NavBar\",\n  title: \"Navigation bar\",\n  description:\n    \"The top navigation bar provides quick access to all major sections of Prostgles UI.\",\n  docs: `\n    The top navigation bar provides quick access to all major sections of Prostgles UI. \n    Located at the top of the interface, it allows you to switch between database connections, manage users and server settings, and access your account preferences. \n    The navigation adapts to your user role, showing admin-only sections like Users and Server Settings only to authorized users.\n\n    ## Key sections of the app\n\n    ### Connections\n\n    A connection represents a unique postgres database instance (unique host, port, user and database name).\n    The connection list page shows all available connections you can access based on your user permissions.\n    \n    ### Connection dashboard\n\n    Clicking a connection from the connection list will take you to the dashboard page where you can explore and interact with the database.\n    Available views and tools include SQL Editor, Table, Map, Schema Diagram, AI Assistant, and more.\n\n    ### Dashboard workspaces\n    \n    The views you open in the dashboard are saved automatically to the current workspace.\n    This allows you to return to the same views later, even after closing the application.\n    You can create multiple workspaces to organize your views by project, team, task, or any other criteria.\n\n\n    <img src=\"./screenshots/navbar.svgif.svg\" alt=\"Navigation\" />\n`,\n  children: [\n    // {\n    //   type: \"link\",\n    //   path: ROUTES.CONNECTIONS,\n    //   selector: \".prgl-brand-icon\",\n    //   title: \"Go to Homepage\",\n    //   description: \"Navigate to the home page (connection list). \",\n    // },\n    {\n      type: \"link\",\n      path: ROUTES.CONNECTIONS,\n      selector: '[href=\"/connections\"]',\n      title: \"Connections\",\n      description: \"Manage database connections\",\n    },\n    {\n      type: \"link\",\n      path: ROUTES.USERS,\n      selector: '[href=\"/users\"]',\n      title: \"Users\",\n      description: \"Manage user accounts (admin only)\",\n      docOptions: \"hideChildren\",\n      pageContent: [\n        {\n          type: \"input\",\n          inputType: \"text\",\n          selectorCommand: \"SearchList\",\n          title: \"Search Users\",\n          description: \"Search for users by name or email.\",\n        },\n        {\n          type: \"smartform-popup\",\n          selectorCommand: \"dashboard.window.rowInsertTop\",\n          title: \"Create User\",\n          description: \"Create a new user account.\",\n          tableName: \"users\",\n        },\n        {\n          type: \"list\",\n          selector: \".table-component\",\n          title: \"User List\",\n          description: \"List of all users in the system.\",\n          itemSelector: \".TableBody div[role='row']\",\n          itemContent: [\n            {\n              type: \"smartform-popup\",\n              selectorCommand: \"dashboard.window.viewEditRow\",\n              title: \"Edit User\",\n              description: \"Edit user details.\",\n              tableName: \"users\",\n            },\n          ],\n        },\n        {\n          type: \"button\",\n          selectorCommand: \"Pagination.lastPage\",\n          title: \"Go to Last Page\",\n          description: \"Navigate to the last page of the user list.\",\n        },\n      ],\n    },\n    {\n      type: \"link\",\n      path: ROUTES.SERVER_SETTINGS,\n      selector: '[href=\"/server-settings\"]',\n      title: \"Server Settings\",\n      description: \"Configure server settings (admin only)\",\n    },\n    {\n      type: \"link\",\n      path: ROUTES.ACCOUNT,\n      title: \"Account\",\n      selector: '[href=\"/account\"]',\n      description: \"Manage your account\",\n    },\n    {\n      type: \"button\",\n      title: \"Logout\",\n      selectorCommand: \"NavBar.logout\",\n      description: \"Logout of your account\",\n    },\n    {\n      type: \"select\",\n      selectorCommand: \"App.colorScheme\",\n      title: \"Theme Selector\",\n      description: \"Switch between light and dark themes\",\n      iconPath: mdiThemeLightDark,\n    },\n    {\n      type: \"select\",\n      selectorCommand: \"App.LanguageSelector\",\n      title: \"Language Selector\",\n      description: \"Change the interface language\",\n      iconPath: mdiTranslate,\n    },\n    {\n      type: \"button\",\n      selector: \".hamburger\",\n      title: \"Toggle Menu (visible on small screens)\",\n      description: \"Toggle the mobile navigation menu on smaller screens\",\n    },\n  ],\n  paths: [\n    { route: ROUTES.CONNECTIONS, exact: true },\n    ROUTES.USERS,\n    ROUTES.SERVER_SETTINGS,\n    ROUTES.ACCOUNT,\n  ],\n} satisfies UIDocNavbar;\n"
  },
  {
    "path": "client/src/app/UIDocs/overviewUIDoc.ts",
    "content": "import type { UIDoc } from \"../UIDocs\";\n\nexport const overviewUIDoc = {\n  type: \"info\",\n  title: \"Overview\",\n  description: \"Overview\",\n  docs: `\n    Prostgles UI is a user-friendly way for interacting with PostgreSQL, creating dashboards and internal tools.\n\n    <img src=\"./screenshots/overview.svgif.svg\" alt=\"Prostgles UI Overview\" width=\"100%\" />\n\n    ## Features\n    - SQL Editor with syntax highlighting and auto-completion\n    - Real-time dashboards with charts\n    - AI assistant with MCP support\n    - User authentication (email, third-party OAuth and two-factor authentication)\n    - Role-based access control\n    - Database management\n    - File storage and backups (locally or to AWS S3 compatible storage)\n    - TypeScript API with database schema types and end to end type safety\n    - LISTEN NOTIFY support\n    - Mobile friendly\n\n    It comes in two versions: \n    - **Prostgles UI** - a web-based application with the complete feature set accessible through any modern browser.\n    - **Prostgles Desktop** - a native desktop application based on Electron available for Linux, MacOS and Windows. \n    It has a subset of the core features from Prostgles UI for data exploration and database management. \n    User Management and other multi-user focused features are not available in the desktop version.\n  `,\n  docOptions: \"asSeparateFile\",\n} as const satisfies UIDoc;\n"
  },
  {
    "path": "client/src/app/UIDocs/serverSettingsUIDoc.ts",
    "content": "import { mdiServerSecurity, mdiTools } from \"@mdi/js\";\nimport { ROUTES } from \"@common/utils\";\nimport { getCommandElemSelector, getDataKeyElemSelector } from \"../../Testing\";\nimport type { UIDocContainers } from \"../UIDocs\";\n\nexport const serverSettingsUIDoc = {\n  type: \"page\",\n  path: ROUTES.SERVER_SETTINGS,\n  iconPath: mdiServerSecurity,\n  title: \"Server Settings\",\n  description:\n    \"Server Settings. Configure security, authentication, and LLM settings.\",\n  docs: `\n    Manage server settings to enhance security, configure authentication methods, and set up LLM providers.\n    <img src=\"./screenshots/server_settings.svg\" alt=\"Server Settings\" />\n  `,\n  children: [\n    {\n      type: \"tab\",\n      selector: getDataKeyElemSelector(\"security\"),\n      title: \"Security\",\n      description:\n        \"Security. Configure domain access, IP restrictions, session duration, and login rate limits to enhance security.\",\n      children: [\n        {\n          type: \"smartform\",\n          title: \"Settings form\",\n          description: \"Configure server settings.\",\n          selectorCommand: \"SmartForm\",\n          tableName: \"server_settings\",\n        },\n      ],\n    },\n    {\n      type: \"tab\",\n      title: \"Authentication\",\n      selector: getDataKeyElemSelector(\"auth\"),\n      description:\n        \"Manage user authentication methods, default user roles, and third-party login providers to control access.\",\n      children: [\n        {\n          type: \"input\",\n          title: \"Website URL\",\n          inputType: \"text\",\n          selector: getCommandElemSelector(\"AuthProviderSetup.websiteURL\"),\n          description:\n            \"Website URL. Used for email and third-party login redirect URL. When first visiting the app as an admin user, it is automatically set to the current URL which will trigger a page refresh.\",\n        },\n        {\n          type: \"input\",\n          title: \"Default user type\",\n          inputType: \"select\",\n          selector: getCommandElemSelector(\"AuthProviderSetup.defaultUserType\"),\n          description:\n            \"The default user type assigned to new users. Defaults to 'default'.\",\n        },\n        {\n          type: \"accordion-item\",\n          title: \"Email signup\",\n          description: \"Email signup/magic-link authentication setup.\",\n          selector: getCommandElemSelector(\"EmailAuthSetup\"),\n          docs: `\n            Provide SMTP or AWS SES credentials to enable email signup and magic-link authentication. \n            By default users authenticate using a password.`,\n          children: [\n            {\n              type: \"input\",\n              title: \"Enable/Disable email signup toggle\",\n              inputType: \"checkbox\",\n              selector: getCommandElemSelector(\"EmailAuthSetup.toggle\"),\n              description:\n                \"Enable email signup. This will allow users to sign up and log in using their email address.\",\n            },\n            {\n              type: \"input\",\n              title: \"Signup type\",\n              inputType: \"select\",\n              selector: getCommandElemSelector(\"EmailAuthSetup.SignupType\"),\n              description:\n                \"Signup type. Choose between 'withPassword' or 'withMagicLink'.\",\n            },\n            {\n              type: \"popup\",\n              title: \"Email verification\",\n              selector: getCommandElemSelector(\"EmailSMTPAndTemplateSetup\"),\n              description: \"SMTP and email template setup.\",\n              children: [\n                {\n                  type: \"accordion-item\",\n                  title: \"Email provider setup\",\n                  selector: getCommandElemSelector(\"EmailSMTPSetup\"),\n                  description:\n                    \"SMTP settings for sending registration/magic-link emails. Allowed providers: SMTP (host, port, username, password) or AWS SES (region, accessKeyId, secretAccessKey).\",\n                  children: [],\n                },\n                {\n                  type: \"accordion-item\",\n                  title: \"Email Template setup\",\n                  selector: getCommandElemSelector(\"EmailTemplateSetup\"),\n                  description:\n                    \"Email template for registration/magic-link emails\",\n                  children: [],\n                },\n                {\n                  type: \"button\",\n                  title: \"Test and save\",\n                  selector: getCommandElemSelector(\n                    \"EmailSMTPAndTemplateSetup.save\",\n                  ),\n                  description:\n                    \"Test and Save SMTP and email template settings.\",\n                },\n              ],\n            },\n          ],\n        },\n        {\n          type: \"list\",\n          title: \"Third-party login providers\",\n          description: \"Third-party login providers (OAuth2)\",\n          selectorCommand: \"AuthProviders.list\",\n          itemSelector:\n            getCommandElemSelector(\"AuthProviders.list\") + \" > .Section\",\n          itemContent: [],\n        },\n      ],\n    },\n    {\n      type: \"tab\",\n      iconPath: mdiTools,\n      title: \"MCP Servers\",\n      selector: getDataKeyElemSelector(\"mcpServers\"),\n      description:\n        \"Manage MCP servers and tools that can then be used in the Ask AI chat\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      title: \"LLM Providers\",\n      selector: getDataKeyElemSelector(\"llmProviders\"),\n      description:\n        \"Manage LLM providers, credentials and models to be used in the Ask AI chat\",\n      children: [],\n    },\n    {\n      type: \"tab\",\n      title: \"Services\",\n      selector: getDataKeyElemSelector(\"services\"),\n      description: \"Manage services\",\n      children: [],\n    },\n  ],\n} satisfies UIDocContainers;\n"
  },
  {
    "path": "client/src/app/UIDocs.ts",
    "content": "import { filterArrInverse } from \"@common/llmUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { ROUTES } from \"@common/utils\";\nimport type { Route } from \"react-router-dom\";\nimport { isPlaywrightTest } from \"../i18n/i18nUtils\";\nimport type { Command } from \"../Testing\";\nimport { isDefined } from \"../utils/utils\";\nimport { domToThemeAwareSVG } from \"./domToSVG/domToThemeAwareSVG\";\nimport { accountUIDoc } from \"./UIDocs/accountUIDoc\";\nimport { commandPaletteUIDoc } from \"./UIDocs/commandPaletteUIDoc\";\nimport { connectionConfigUIDoc } from \"./UIDocs/connection/connectionConfigUIDoc\";\nimport { dashboardUIDoc } from \"./UIDocs/connection/dashboardUIDoc\";\nimport { connectionsUIDoc } from \"./UIDocs/connectionsUIDoc\";\nimport { desktopInstallationUIDoc } from \"./UIDocs/desktopInstallation\";\nimport { navbarUIDoc } from \"./UIDocs/navbarUIDoc\";\nimport { overviewUIDoc } from \"./UIDocs/overviewUIDoc\";\nimport { serverSettingsUIDoc } from \"./UIDocs/serverSettingsUIDoc\";\nimport { UIInstallation } from \"./UIDocs/UIInstallationUIDoc\";\nimport { getSVGif } from \"./domToSVG/SVGif/getSVGif\";\n\n/**\n * The purpose of UIDocs is to provide structured metadata about the UI elements.\n * This metadata is used for:\n * 1. Command Palette. It requires a list of all UI elements and their relationships to enable quick navigation.\n * 2. Documentation. Generating user guides and documentation with screenshots and descriptions.\n * 3. Automated testing to ensure UI elements are present and functional.\n *\n * Each UIDoc describes a UI element or a group of elements, including their type, selector, description, and hierarchical relationships.\n * This structured approach allows for easy maintenance and scalability of the documentation system.\n */\n\ntype UIDocCommon = {\n  title: string;\n\n  /**\n   * Optional mdi icon path representing the element, enhancing visual identification in the Command Palette.\n   */\n  iconPath?: string;\n\n  /**\n   * Short description of the element's purpose or functionality used in Command Palette.\n   */\n  description: string;\n\n  /**\n   * If defined, this will be used to generate a separate section in the documentation.\n   */\n  docs?: string;\n\n  /**\n   * If defined, this will be used as the title for the children list in the documentation.\n   */\n  childrenTitle?: string;\n\n  docOptions?: /**\n   * If docs is defined, then it will be rendered as a separate header in the documentation with this title.\n   */\n  | { title: string }\n    /**\n     * If \"asSeparateFile\" AND docs is defined, this will be saved as a separate file in the documentation.\n     * By default, a single file is generated for each root UIDoc with child items with docs appended to the bottom.\n     */\n    | \"asSeparateFile\"\n    /**\n     * Hides children from documentation. Meant to be used when the children content is obvious/documented on the parent.\n     */\n    | \"hideChildren\";\n\n  /** If true then this is not available for Prostgles Desktop */\n  uiVersionOnly?: true;\n};\n\n/**\n * UI Documentation system for generating interactive element guides and documentation.\n * Defines structured metadata for UI elements including selectors, types, and hierarchical relationships.\n * Used for automated testing, user guidance, and generating SVG documentation from DOM elements.\n */\ntype UIDocBase<T> = (\n  | {\n      selector: string;\n      selectorCommand?: undefined;\n    }\n  | {\n      selector?: undefined;\n      /**\n       * data-command attribute of the element to be selected.\n       */\n      selectorCommand: Command;\n    }\n) &\n  UIDocCommon &\n  T;\n\ntype Route = (typeof ROUTES)[keyof typeof ROUTES];\n\nexport type UIDocInputElement = UIDocBase<{\n  type: \"input\";\n  inputType: \"text\" | \"number\" | \"checkbox\" | \"select\" | \"file\";\n}>;\n\nexport type UIDocElement =\n  | UIDocBase<{\n      type: \"button\";\n    }>\n  | UIDocBase<{\n      type: \"drag-handle\";\n      direction: \"x\" | \"y\";\n    }>\n  | UIDocBase<{\n      type: \"canvas\";\n    }>\n  | UIDocInputElement\n  | UIDocBase<{\n      type: \"text\";\n    }>\n  | UIDocBase<{\n      type: \"list\";\n      itemSelector: string;\n      itemContent: UIDocElement[];\n    }>\n  | UIDocBase<{\n      type: \"select\";\n    }>\n  | UIDocBase<{\n      type: \"link\";\n      path: Route;\n      /**\n       * If defined this means that the final url is `${pagePath}/pathItemRow.id`\n       */\n      pathItem?: {\n        tableName: keyof DBSSchema;\n      };\n      pageContent?: UIDocElement[];\n    }>\n  | UIDocBase<{\n      type: \"popup\";\n      triggerMode?: \"click\" | \"contextmenu\";\n      /**\n       * Used in detecting if popup is shown\n       */\n      contentSelectorCommand?: Command;\n      children: UIDocElement[];\n    }>\n  | UIDocBase<{\n      type: \"tab\" | \"accordion-item\";\n      children: UIDocElement[];\n    }>\n  | UIDocBase<{\n      type: \"section\";\n      children: UIDocElement[];\n    }>\n  | UIDocBase<{\n      type: \"smartform\" | \"smartform-popup\";\n      tableName: string;\n      fieldNames?: string[];\n    }>\n  | (UIDocCommon & {\n      /**\n       * Documentation-only. Does not appear in Command Palette.\n       */\n      type: \"info\";\n    });\n\nexport type UIDocPage = UIDocCommon & {\n  type: \"page\";\n  path: Route;\n  pathItem?: {\n    tableName: keyof DBSSchema;\n    selectorCommand?: Command;\n    selector?: string;\n    selectorPath?: Route;\n  };\n  children: UIDocElement[];\n};\nexport type UIDocContainers =\n  | UIDocPage\n  | UIDocBase<{\n      type: \"hotkey-popup\";\n      hotkey: [\"Ctrl\" | \"Alt\" | \"Shift\", \"A\" | \"K\"];\n      children: UIDocElement[];\n    }>;\n\nexport type UIDocNavbar = UIDocBase<{\n  type: \"navbar\";\n  docs?: string;\n  children: UIDocElement[];\n  /**\n   * List of paths the navbar appears on.\n   */\n  paths: (Route | { route: Route; exact: true })[];\n}>;\n\nexport type UIDoc = UIDocContainers | UIDocElement | UIDocNavbar;\n\nexport const UIDocs = [\n  overviewUIDoc,\n  UIInstallation,\n  desktopInstallationUIDoc,\n  navbarUIDoc,\n  connectionsUIDoc,\n  dashboardUIDoc,\n  connectionConfigUIDoc,\n  serverSettingsUIDoc,\n  accountUIDoc,\n  commandPaletteUIDoc,\n] satisfies UIDoc[];\n\nconst getFlatDocs = (\n  doc: UIDoc | undefined,\n  parentDocs: UIDoc[] = [],\n):\n  | ({\n      parentTitles: string[];\n      parentDocs: UIDoc[];\n    } & UIDoc)[]\n  | undefined => {\n  if (!doc) return [];\n  const parentTitles = parentDocs.map((d) => d.title);\n  const children =\n    \"children\" in doc ? doc.children\n    : \"itemContent\" in doc ? doc.itemContent\n    : \"pageContent\" in doc ? doc.pageContent\n    : undefined;\n\n  if (!children?.length) {\n    return [\n      {\n        parentTitles,\n        ...doc,\n        parentDocs,\n      },\n    ];\n  }\n\n  const nextParentDocs = [...parentDocs, doc];\n  const flatChildren = children.flatMap(\n    (childDoc: UIDocContainers | UIDocElement) => {\n      const flatChildren = getFlatDocs(childDoc, nextParentDocs) ?? [];\n      return flatChildren;\n    },\n  );\n  return [\n    {\n      parentTitles,\n      ...doc,\n      parentDocs,\n    },\n    ...flatChildren,\n  ];\n};\nexport type UIDocNonInfo = Exclude<UIDoc, { type: \"info\" }>;\nexport type UIDocFlat = UIDocNonInfo & {\n  parentTitles: string[];\n  parentDocs: UIDoc[];\n};\nexport const flatUIDocs = filterArrInverse(UIDocs, { type: \"info\" } as const)\n  .map((doc) => getFlatDocs(doc))\n  .filter(isDefined)\n  .flat() as UIDocFlat[];\nwindow.flatUIDocs = flatUIDocs;\n\nif (isPlaywrightTest) {\n  window.toSVG = domToThemeAwareSVG;\n  window.getSVGif = getSVGif;\n}\n"
  },
  {
    "path": "client/src/app/XRealIpSpoofableAlert.tsx",
    "content": "import { mdiAlertOutline } from \"@mdi/js\";\nimport React from \"react\";\nimport { NavLink } from \"react-router-dom\";\nimport { ROUTES } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { t } from \"../i18n/i18nUtils\";\nimport type { useAppState } from \"../useAppState/useAppState\";\n\ntype P = Pick<ReturnType<typeof useAppState>, \"serverState\" | \"user\">;\nexport const XRealIpSpoofableAlert = ({ serverState, user }: P) => {\n  return (\n    <>\n      {serverState?.xRealIpSpoofable && user?.type === \"admin\" && (\n        <PopupMenu\n          button={\n            <Btn color=\"danger\" iconPath={mdiAlertOutline} variant=\"filled\">\n              {t[\"App\"][\"Security issue\"]}\n            </Btn>\n          }\n          style={{ position: \"fixed\", right: 0, top: 0, zIndex: 999999 }}\n          positioning=\"beneath-left-minfill\"\n          clickCatchStyle={{ opacity: 0.5 }}\n          content={\n            <InfoRow>\n              Failed login rate limiting is based on x-real-ip header which can\n              be spoofed based on your current connection.{\" \"}\n              <NavLink to={ROUTES.SERVER_SETTINGS}>\n                {t[\"App\"][\"Settings\"]}\n              </NavLink>\n            </InfoRow>\n          }\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/addSVGifCaption.ts",
    "content": "import { SVG_NAMESPACE } from \"../domToSVG\";\n\nexport const addSVGifCaption = ({\n  svgDom,\n  appendStyle,\n  height,\n  caption,\n  fromPerc,\n  toPerc,\n  sceneId,\n  totalDuration,\n}: {\n  svgDom: SVGElement;\n  appendStyle: (style: string) => void;\n  width: number;\n  height: number;\n  caption: string;\n  fromPerc: number;\n  toPerc: number;\n  sceneId: string;\n  totalDuration: number;\n}) => {\n  appendStyle(`\n    :root {\n      color-scheme: light dark;\n    }\n    .caption-background {\n      fill: light-dark(#ffffff, #1a1a1a);\n      stroke: light-dark(#e0e0e0, #404040);\n      stroke-width: 2;\n    }\n    .caption-text {\n      fill: light-dark(#333333, #e0e0e0);\n      font-family: system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;\n      font-size: 28px;\n      user-select: none;\n    }\n    .caption-progress-bar {\n      fill: light-dark(#00000066, #ffffff66);\n    }\n  `);\n\n  // Create caption group\n  const captionGroup = document.createElementNS(SVG_NAMESPACE, \"g\");\n  svgDom.appendChild(captionGroup);\n  captionGroup.setAttribute(\"class\", \"caption\");\n\n  // Create text element\n  const text = document.createElementNS(SVG_NAMESPACE, \"text\");\n  text.setAttribute(\"text-anchor\", \"start\");\n  text.setAttribute(\"class\", \"caption-text\");\n  text.textContent = caption;\n\n  captionGroup.appendChild(text);\n\n  const bbox = text.getBBox();\n  const textWidth = bbox.width;\n  const textHeight = bbox.height;\n  if (!textWidth || !textHeight) {\n    throw new Error(\"Failed to measure caption text dimensions\");\n  }\n\n  // Background dimensions with padding\n  const padding = { x: 22, y: 22 };\n  const bgWidth = textWidth + padding.x * 2;\n  const bgHeight = textHeight + padding.y * 2;\n  const bgX = 40;\n  const bgY = height - bgHeight - 10;\n  const borderRadius = 4;\n\n  text.setAttribute(\"x\", bgX + padding.x);\n  text.setAttribute(\"y\", bgY + bgHeight / 2 + textHeight / 3);\n\n  const addProgressBar = () => {\n    const progressBar = document.createElementNS(SVG_NAMESPACE, \"rect\");\n    const progressBarHeight = 4;\n    const barId = \"caption-progress-bar-\" + sceneId;\n    progressBar.setAttribute(\"id\", barId);\n    progressBar.setAttribute(\"class\", \"caption-progress-bar\");\n    progressBar.setAttribute(\"x\", bgX);\n    progressBar.setAttribute(\"y\", bgY + bgHeight - progressBarHeight);\n    progressBar.setAttribute(\"width\", bgWidth);\n    progressBar.setAttribute(\"height\", progressBarHeight);\n    progressBar.setAttribute(\"rx\", \"2\");\n    progressBar.setAttribute(\"ry\", \"2\");\n    captionGroup.prepend(progressBar);\n\n    appendStyle(`\n    @keyframes ${barId}-anim {\n      0% { width: 0; }\n      ${fromPerc + 0.1}% { width: 0; }\n      ${toPerc - 0.1}% { width: ${bgWidth}px; }\n      ${toPerc}% { width: 0; }\n      100% { width: 0; }\n    }\n    #${barId} {\n      animation: ${barId}-anim ${totalDuration}ms ease-in-out infinite; \n    }\n  `);\n  };\n  // addProgressBar();\n\n  const bgRect = document.createElementNS(SVG_NAMESPACE, \"rect\");\n  bgRect.setAttribute(\"x\", bgX);\n  bgRect.setAttribute(\"y\", bgY);\n  bgRect.setAttribute(\"width\", bgWidth);\n  bgRect.setAttribute(\"height\", bgHeight);\n  bgRect.setAttribute(\"rx\", borderRadius);\n  bgRect.setAttribute(\"ry\", borderRadius);\n  bgRect.setAttribute(\"class\", \"caption-background\");\n  captionGroup.prepend(bgRect);\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/addSVGifPointer.ts",
    "content": "import { SVG_NAMESPACE } from \"../domToSVG\";\nimport { getAnimationProperty } from \"./getSVGif\";\n\nexport const addSVGifPointer = ({\n  g,\n  appendStyle,\n  cursorKeyframes,\n  totalDuration,\n}: {\n  g: SVGGElement;\n  appendStyle: (style: string) => void;\n  totalDuration: number;\n  cursorKeyframes: string[];\n}) => {\n  const pointerId = \"pointer\";\n  const pointerCircle = document.createElementNS(SVG_NAMESPACE, \"circle\");\n  pointerCircle.setAttribute(\"r\", \"10\");\n  pointerCircle.setAttribute(\"opacity\", \"0\");\n  pointerCircle.setAttribute(\"id\", pointerId);\n\n  const cursorAnimationName = `cursor-move`;\n  appendStyle(`\n    #${pointerId} { \n      transform-origin: center;\n      fill: #00000036;\n      filter: drop-shadow(0 0 2px #000000aa);\n    }\n\n    @media (prefers-color-scheme: dark) {\n      #${pointerId} {\n        fill: #ffffff36;\n        filter: drop-shadow(0 0 2px #ffffffaa);\n      }\n    }\n\n    @keyframes ${cursorAnimationName} {\n    ${cursorKeyframes.map((v) => `  ${v}`).join(\"\\n\")}\n    } \n    ${getAnimationProperty({\n      totalDuration,\n      elemSelector: `#${pointerId}`,\n      animName: cursorAnimationName,\n      easeFunction: \"ease-out\",\n    })}\n  `);\n  g.appendChild(pointerCircle);\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/addSVGifTimelineControls.ts",
    "content": "import { SVG_NAMESPACE } from \"../domToSVG\";\nimport { getAnimationProperty } from \"./getSVGif\";\nimport type { getSVGifAnimations } from \"./getSVGifAnimations\";\n\nexport const addSVGifTimelineControls = ({\n  g,\n  appendStyle,\n  width,\n  height,\n  totalDuration,\n  sceneAnimations,\n}: {\n  g: SVGGElement;\n  appendStyle: (style: string) => void;\n  width: number;\n  height: number;\n  totalDuration: number;\n  sceneAnimations: ReturnType<typeof getSVGifAnimations>[\"sceneAnimations\"];\n}) => {\n  const progressBarId = \"progress-bar\";\n  const animationProgressBar = document.createElementNS(SVG_NAMESPACE, \"rect\");\n  const animationProgressBarHeight = 2;\n  animationProgressBar.setAttribute(\"id\", progressBarId);\n  animationProgressBar.setAttribute(\"x\", \"0\");\n  animationProgressBar.setAttribute(\"y\", height - animationProgressBarHeight);\n  animationProgressBar.setAttribute(\"height\", animationProgressBarHeight);\n  animationProgressBar.setAttribute(\"width\", `${width}px`);\n  animationProgressBar.setAttribute(\"rx\", \"2.5\");\n  animationProgressBar.setAttribute(\"ry\", \"2.5\");\n\n  const progressBarAnimationName = \"animation-progress-bar\";\n  appendStyle(` \n    @keyframes ${progressBarAnimationName} {\n      0% { transform: translateX(-100%); }\n      100% { transform: translateX(0%); }\n    } \n\n    #${progressBarId} {\n      fill: red;\n      opacity: 0.3;\n      ${getAnimationProperty({ totalDuration, animName: progressBarAnimationName, elemSelector: `#${progressBarId}` }, true)} \n    }\n\n    @media (prefers-color-scheme: dark) {\n      #${progressBarId} {\n        opacity: 0.5;\n      }\n    }\n\n    /* Pause only while SVG is pressed */\n    g.paused * {\n      animation-play-state: paused;\n    }\n  `);\n\n  const setPause = document.createElementNS(SVG_NAMESPACE, \"set\");\n  setPause.setAttribute(\"attributeName\", \"class\");\n  setPause.setAttribute(\"to\", \"paused\");\n  setPause.setAttribute(\"begin\", \"mousedown\");\n  setPause.setAttribute(\"end\", \"mouseup\");\n\n  const sceneSkipWrapper = document.createElementNS(SVG_NAMESPACE, \"g\");\n  sceneAnimations.forEach((scene, index) => {\n    const sceneSkipRect = document.createElementNS(SVG_NAMESPACE, \"rect\");\n    const sceneStartTime = scene.startMs;\n    sceneSkipRect.setAttribute(\n      \"x\",\n      `${(sceneStartTime / totalDuration) * width}`,\n    );\n    sceneSkipRect.setAttribute(\"y\", \"0\");\n    sceneSkipRect.setAttribute(\n      \"width\",\n      `${(scene.duration / totalDuration) * width}`,\n    );\n    sceneSkipRect.setAttribute(\"height\", `${height}`);\n    sceneSkipRect.setAttribute(\"fill\", \"none\");\n    sceneSkipRect.setAttribute(\"cursor\", \"pointer\");\n\n    const seekTime = sceneStartTime / 1000; // in seconds\n    sceneSkipRect.addEventListener(\"click\", () => {\n      const svgElem = g.ownerSVGElement;\n      if (!svgElem) return;\n      const currentTime = svgElem.getCurrentTime();\n      const timeDiff = seekTime - currentTime;\n      svgElem.setCurrentTime(currentTime + timeDiff + 0.01); // add small offset to trigger time update\n    });\n\n    sceneSkipWrapper.appendChild(sceneSkipRect);\n  });\n\n  g.appendChild(animationProgressBar);\n  // g.appendChild(sceneSkipWrapper);\n  g.appendChild(setPause);\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/animations/getSVGifCursorAnimationHandler.ts",
    "content": "import type { SVGif } from \"src/Testing\";\nimport type { SVGifParsedScene } from \"../getSVGifParsedScenes\";\nimport { getSVGifRevealKeyframes } from \"../getSVGifRevealKeyframes\";\nimport { toFixed } from \"../../utils/toFixed\";\nimport type { SceneNodeAnimation } from \"../getSVGifAnimations\";\nimport { fixIndent } from \"@common/utils\";\n\nexport const getSVGifCursorAnimationHandler = ({\n  parsedScenes,\n  getPercent,\n  totalSvgifDuration,\n  width,\n  height,\n}: {\n  parsedScenes: SVGifParsedScene[];\n  getPercent: (ms: number, offset?: 0 | 0.1 | -0.1) => number;\n  totalSvgifDuration: number;\n  width: number;\n  height: number;\n}) => {\n  const cursorMovements: {\n    fromPerc: number;\n    toPerc: number;\n    lingerPerc: number | undefined;\n    target: [number, number];\n  }[] = [];\n  const [x0, y0] = [width / 2, height];\n\n  const addAnimation = ({\n    currentPrevDuration,\n    animationIndex,\n    animations,\n    sceneIndex,\n    animation,\n    bbox,\n    svgFileName,\n    sceneNodeAnimations,\n    sceneId,\n  }: {\n    currentPrevDuration: number;\n    animations: SVGif.Animation[];\n    animationIndex: number;\n    sceneIndex: number;\n    animation: SVGif.CursorAnimation;\n    bbox: DOMRect | undefined;\n    svgFileName: string;\n    sceneNodeAnimations: SceneNodeAnimation[];\n    sceneId: string;\n  }) => {\n    if (animation.type === \"moveTo\") {\n      cursorMovements.push({\n        fromPerc: Number(getPercent(currentPrevDuration)),\n        toPerc: Number(getPercent(currentPrevDuration + animation.duration)),\n        lingerPerc: undefined,\n        target: animation.xy,\n      });\n      return;\n    }\n\n    if (!bbox) {\n      throw new Error(`Unexpected. BBox missing`);\n    }\n\n    const {\n      type,\n      lingerMs = 500,\n      waitBeforeClick = 500,\n      duration,\n      elementSelector,\n      offset,\n    } = animation;\n\n    const xOffset = offset?.x ?? Math.min(60, bbox.width / 2);\n    const yOffset = offset?.y ?? Math.min(30, bbox.height / 2);\n    const cx = bbox.x + xOffset;\n    const cy = bbox.y + yOffset;\n\n    if (duration < waitBeforeClick) {\n      throw new Error(\n        fixIndent(`\n          Duration ${duration}ms for \"${type}\" animation on element ${elementSelector} in SVG file ${svgFileName} is too short. \n          It must be greater than the waitBeforeClick time of ${waitBeforeClick}ms.\n        `),\n      );\n    }\n    const clickEndTime = currentPrevDuration + duration - waitBeforeClick;\n    const nextAnimation =\n      animations[animationIndex + 1] ||\n      parsedScenes[sceneIndex + 1]?.animations[0];\n    const anotherClickFollowing = nextAnimation?.type === \"click\";\n    cursorMovements.push({\n      fromPerc: getPercent(currentPrevDuration),\n      toPerc: getPercent(clickEndTime),\n      lingerPerc:\n        !lingerMs || anotherClickFollowing ? undefined : (\n          Number(\n            getPercent(Math.min(totalSvgifDuration, clickEndTime + lingerMs)),\n          )\n        ),\n      target: [cx, cy],\n    });\n\n    if (type === \"clickAppearOnHover\") {\n      const toPerc = getPercent(clickEndTime);\n      /** Ensure it appears as the cursor enters the bbox */\n      const xDistance = Math.abs(\n        cx - (cursorMovements.at(-2)?.target[0] ?? x0),\n      );\n      const yDistance = Math.abs(\n        cy - (cursorMovements.at(-2)?.target[1] ?? y0),\n      );\n      const distance = Math.sqrt(xDistance ** 2 + yDistance ** 2);\n      const approxMsToEnter = Math.max(300, distance * 4);\n      const appearTime = Math.max(\n        currentPrevDuration,\n        clickEndTime - approxMsToEnter,\n      );\n      const appearPerc = getPercent(appearTime);\n\n      sceneNodeAnimations.push({\n        sceneId,\n        elemSelector: elementSelector,\n        keyframes: getSVGifRevealKeyframes({\n          fromPerc: appearPerc,\n          toPerc,\n          mode: \"opacity\",\n        }),\n      });\n    }\n  };\n\n  const getCursorKeyframes = () => {\n    const firstTranslate = `transform: translate(${toFixed(x0)}px, ${toFixed(y0)}px)`;\n    const cursorKeyframes = [`0% { opacity: 0; ${firstTranslate}; }`];\n    const cursorMovementsFixed = cursorMovements.map((e) => ({\n      ...e,\n      target: e.target.map((v) => toFixed(v)),\n    }));\n    cursorMovementsFixed.forEach(\n      ({ fromPerc, toPerc, lingerPerc, target: [x, y] }, i, arr) => {\n        const translate = `transform: translate(${x}px, ${y}px)`;\n        const prevTarget =\n          arr[i - 1]?.target ?? [x0, y0].map((v) => toFixed(v));\n        const prevTranslate = `transform: translate(${prevTarget[0]}px, ${prevTarget[1]}px)`;\n        cursorKeyframes.push(\n          ...[\n            `${toFixed(fromPerc, 4)}% { opacity: 0; ${prevTranslate}; }`,\n            `${toFixed(fromPerc + 0.0001, 4)}% { opacity: 1; ${prevTranslate}; }`,\n            `${toFixed(toPerc - 0.0001, 4)}% { opacity: 1; ${translate}; }`,\n            `${toFixed(lingerPerc ?? toPerc, 4)}% { opacity: 0; ${translate}; }`,\n          ].filter(Boolean),\n        );\n      },\n    );\n\n    return { cursorKeyframes };\n  };\n\n  return { addAnimation, cursorMovements, getCursorKeyframes };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/animations/getSVGifTypeAnimation.ts",
    "content": "import type { SVGif } from \"src/Testing\";\nimport type { SceneNodeAnimation } from \"../getSVGifAnimations\";\nimport type { SVGifParsedScene } from \"../getSVGifParsedScenes\";\nimport { getSVGifRevealKeyframes } from \"../getSVGifRevealKeyframes\";\nimport type { getSVGifTargetBBox } from \"../getSVGifTargetBBox\";\nimport { getSVGifZoomToAnimation } from \"./getSVGifZoomToAnimation\";\n\n/**\n * Given an SVGifScenes, return the animations\n */\nexport const getSVGifTypeAnimation = (\n  viewport: { width: number; height: number },\n  { element, bbox: rawBBox }: ReturnType<typeof getSVGifTargetBBox>,\n  { svgDom, svgFileName }: SVGifParsedScene,\n  animation: Extract<SVGif.Animation, { type: \"type\" }>,\n  {\n    sceneId,\n    sceneIndex,\n    totalDuration,\n    getPercent,\n    fromTime,\n  }: {\n    sceneIndex: number;\n    sceneId: string;\n    totalDuration: number;\n    getPercent: (time: number, increment?: 0.1 | -0.1) => number;\n    fromTime: number;\n  },\n) => {\n  const { elementSelector, duration } = animation;\n\n  const sceneNodeAnimations: SceneNodeAnimation[] = [];\n  const tSpansOrText = Array.from(\n    element.querySelectorAll<SVGTSpanElement | SVGTextElement>(\"tspan, text\"),\n  );\n  if (!tSpansOrText.length) {\n    throw `No tspan elements found in element: ${elementSelector} in SVG file ${svgFileName}. \"type\" animations require the target element to contain one or more <tspan> elements.`;\n  }\n\n  const rootGId = svgDom.querySelector(\":scope > g\")?.id;\n  if (!rootGId) {\n    throw `No root <g> element with id found in SVG file ${svgFileName}. \"type\" animations require the SVG to have a root <g> element with an id.`;\n  }\n\n  const totalWidth = tSpansOrText.reduce(\n    (acc, tspan) => acc + tspan.getComputedTextLength(),\n    0,\n  );\n\n  const { extraAnimation } = animation;\n  const zoomInDuration = extraAnimation ? 500 : 0;\n  const zoomOutDuration = extraAnimation ? 500 : 0;\n  const waitBeforeZoomOut = extraAnimation ? 300 : 0;\n  const typingDuration =\n    duration - zoomInDuration - zoomOutDuration - waitBeforeZoomOut;\n  if (typingDuration < 500) {\n    throw [\n      `Duration ${duration}ms for \"type\" animation on element ${elementSelector} in SVG file ${svgFileName} is too short.`,\n      `Must be at least ${zoomInDuration + zoomOutDuration + waitBeforeZoomOut + 500}ms`,\n    ].join(\"\\n\");\n  }\n  const zoomInEndTime = fromTime + zoomInDuration;\n  const typingStartTime = zoomInEndTime;\n\n  const msPerPx = typingDuration / totalWidth;\n  let fromTimeLocal = typingStartTime;\n  tSpansOrText.forEach((tspanOrText, i) => {\n    const tspanWidth = tspanOrText.getComputedTextLength();\n    const tspanDuration = tspanWidth * msPerPx;\n    sceneNodeAnimations.push({\n      sceneId,\n      elemSelector: `${elementSelector} ${tspanOrText.nodeName}:nth-of-type(${i + 1})`,\n      keyframes: getSVGifRevealKeyframes({\n        fromPerc: getPercent(fromTimeLocal),\n        toPerc: getPercent(fromTimeLocal + tspanDuration),\n        mode: \"left to right\",\n      }),\n    });\n    fromTimeLocal += tspanDuration;\n  });\n  const zoomToStyle =\n    !animation.extraAnimation ? \"\" : (\n      getSVGifZoomToAnimation(\n        viewport,\n        { bbox: rawBBox },\n        { svgDom, svgFileName },\n        animation.extraAnimation.type === \"zoomToElement\" ?\n          { ...animation, type: \"zoomToElement\" }\n        : {\n            ...animation,\n            type: \"bringToFront\",\n            bringToFrontSelector: animation.extraAnimation.elementSelector,\n          },\n        {\n          sceneId,\n          sceneIndex,\n          totalDuration,\n          getPercent,\n          fromTime,\n        },\n        false,\n      ).style\n    );\n  return { sceneNodeAnimations, style: zoomToStyle };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/animations/getSVGifZoomToAnimation.ts",
    "content": "import { fixIndent } from \"@common/utils\";\nimport type { SVGif } from \"src/Testing\";\nimport { toFixed } from \"../../utils/toFixed\";\nimport { getAnimationProperty } from \"../getSVGif\";\nimport type { SVGifParsedScene } from \"../getSVGifParsedScenes\";\nimport type { getSVGifTargetBBox } from \"../getSVGifTargetBBox\";\n\nexport const getSVGifZoomToAnimation = (\n  viewport: { width: number; height: number },\n  { bbox: rawBBox }: Pick<ReturnType<typeof getSVGifTargetBBox>, \"bbox\">,\n  { svgDom, svgFileName }: Pick<SVGifParsedScene, \"svgDom\" | \"svgFileName\">,\n  animation: Extract<\n    SVGif.Animation,\n    { type: \"zoomToElement\" | \"bringToFront\" }\n  >,\n  {\n    sceneId,\n    sceneIndex,\n    totalDuration,\n    getPercent,\n    fromTime,\n  }: {\n    sceneIndex: number;\n    sceneId: string;\n    totalDuration: number;\n    getPercent: (time: number, increment?: 0.1 | -0.1) => number;\n    fromTime: number;\n  },\n  addToRootSvg: boolean,\n) => {\n  const {\n    elementSelector,\n    duration,\n    maxScale = 3,\n    type,\n    bringToFrontSelector,\n  } = animation;\n\n  const rootGId = svgDom.querySelector(\":scope > g\")?.id;\n  if (!rootGId) {\n    throw `No root <g> element with id found in SVG file ${svgFileName}. \"type\" animations require the SVG to have a root <g> element with an id.`;\n  }\n\n  const zoomInDuration = 500;\n  const zoomOutDuration = 500;\n  const waitBeforeZoomOut = 300;\n  const dwellDuration =\n    duration - zoomInDuration - zoomOutDuration - waitBeforeZoomOut;\n  if (dwellDuration < 500) {\n    throw [\n      `Duration ${duration}ms for \"type\" animation on element ${elementSelector} in SVG file ${svgFileName} is too short.`,\n      `Must be at least ${zoomInDuration + zoomOutDuration + waitBeforeZoomOut + 500}ms`,\n    ].join(\"\\n\");\n  }\n  const zoomInStartTime = fromTime;\n  const zoomInEndTime = fromTime + zoomInDuration;\n  const dwellEndTime = zoomInEndTime + dwellDuration;\n  const zoomOutStartTime = dwellEndTime + waitBeforeZoomOut;\n  const zoomOutEndTime = zoomOutStartTime + zoomOutDuration;\n\n  const adjustedBBox = {\n    x: rawBBox.x,\n    y: rawBBox.y,\n    height: rawBBox.height,\n    width: rawBBox.width,\n  };\n\n  const xPadding = 50;\n  const requiredScale = (svgDom.clientWidth - xPadding) / adjustedBBox.width;\n  const effectiveScale = toFixed(Math.min(requiredScale, maxScale));\n  const toPerc = getPercent(fromTime + duration);\n\n  // Calculate center of the element\n  const elementCenterX = toFixed(adjustedBBox.x + adjustedBBox.width / 2);\n  const elementCenterY = toFixed(adjustedBBox.y + adjustedBBox.height / 2);\n\n  // Calculate viewport center\n  const viewportCenterX = toFixed(viewport.width / 2);\n  const viewportCenterY = toFixed(viewport.height / 2);\n\n  // Calculate translation needed to center the element\n  const translateX = toFixed(viewportCenterX - elementCenterX);\n  const translateY = toFixed(viewportCenterY - elementCenterY);\n\n  const transformOrigin = `${elementCenterX}px ${elementCenterY}px`;\n\n  const rootGSelector = `svg#${sceneId} g#${rootGId}`;\n  const animatedElementSelector =\n    type === \"bringToFront\" ? `${rootGSelector} ${bringToFrontSelector}`\n    : addToRootSvg ? \":scope\"\n    : rootGSelector;\n\n  /** Add root svg zoom in-out animation to the typed */\n  const animProp = getAnimationProperty({\n    animName: `scene-${sceneIndex}-type-zoom`,\n    elemSelector: animatedElementSelector,\n    totalDuration,\n    otherProps: `transform-origin: ${transformOrigin};`,\n  });\n  const style = fixIndent(`\n    @keyframes scene-${sceneIndex}-type-zoom {\n      ${getPercent(zoomInStartTime)}% { transform: translate(0px, 0px) scale(1);}\n      ${getPercent(zoomInEndTime)}% { transform: translate(${translateX}px, ${translateY}px) scale(${effectiveScale}); }\n      ${getPercent(zoomOutStartTime)}% { transform: translate(${translateX}px, ${translateY}px) scale(${effectiveScale}); }\n      ${getPercent(zoomOutEndTime)}% { transform: translate(0px, 0px) scale(1); }\n      ${toFixed(\n        Math.min(100, toPerc + 0.1),\n        4,\n      )}% { transform: translate(0px, 0px) scale(1); }\n    }\n    ${animProp}\n  `);\n  return { style };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/compressSVGif.ts",
    "content": "/**\n * Given an SVG with multiple SVG scenes inside <g id=\"all-scenes\">,\n * identify which g elements have the same data-selector attribute and outerHTML across scenes and\n * compress them by only keeping one instance and replacing the others with <use> elements referencing the first.\n */\n\nimport { isDefined } from \"src/utils/utils\";\nimport { SVG_NAMESPACE } from \"../domToSVG\";\nimport type { SVGifParsedScene } from \"./getSVGifParsedScenes\";\n\nexport const compressSVGif = (\n  svg: SVGSVGElement,\n  parsedScenes: SVGifParsedScene[],\n) => {\n  const defs = document.createElementNS(SVG_NAMESPACE, \"defs\");\n  svg.appendChild(defs); // to ensure the SVG namespace is defined\n\n  const allSelectorNodes = Array.from(\n    svg.querySelectorAll<SVGGElement>(`[data-selector]`),\n  );\n  const nodesMap = new Map<string, SVGGElement>();\n  allSelectorNodes.forEach((n) => {\n    const { selector } = n.dataset;\n    if (selector && n.outerHTML.length > 150) {\n      nodesMap.set(selector, n);\n    }\n  });\n  const nodesToReuse = new Map<string, SVGGElement>();\n  const pushNodeToReuse = (selector: string, node: SVGGElement) => {\n    const id = `c-${selector.replaceAll(\" \", \"_\").replaceAll(\".\", \"_\")}`;\n    if (!nodesToReuse.has(selector)) {\n      nodesToReuse.set(selector, node);\n      const clonedNode = node.cloneNode(true) as SVGGElement;\n      clonedNode.setAttribute(\"id\", id);\n      clonedNode.removeAttribute(\"data-selector\");\n      defs.appendChild(clonedNode);\n    }\n    const useElem = document.createElementNS(SVG_NAMESPACE, \"use\");\n    useElem.setAttribute(\"href\", `#${id}`);\n    node.replaceWith(useElem);\n  };\n  const scenes = Array.from(svg.querySelectorAll(`#all-scenes > svg`));\n\n  nodesMap.forEach((originalNode, selector) => {\n    const matchingScenes = scenes\n      .map((scene) => {\n        const [matchingNode, ...others] = scene.querySelectorAll<SVGGElement>(\n          `[data-selector=${JSON.stringify(selector)}]`,\n        );\n        /** Do not compress nodes that are selected in animations to ensure css selectors still work */\n        const parsedScene = parsedScenes.find((ps) => ps.svgDom === scene);\n        if (!parsedScene) {\n          throw \"Could not find parsedScene\";\n        }\n        if (\n          matchingNode &&\n          !others.length &&\n          matchingNode.outerHTML === originalNode.outerHTML &&\n          !parsedScene.animations.some((anim) => {\n            const node =\n              anim.type !== \"wait\" && anim.type !== \"moveTo\" ?\n                matchingNode.querySelector(anim.elementSelector)\n              : null;\n            return node;\n          })\n        ) {\n          return { scene, matchingNode };\n        }\n      })\n      .filter(isDefined);\n\n    if (matchingScenes.length > 1) {\n      matchingScenes.forEach(({ matchingNode }) => {\n        pushNodeToReuse(selector, matchingNode);\n      });\n    }\n  });\n\n  return svg;\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/getSVGif.ts",
    "content": "import { fixIndent } from \"@common/utils\";\nimport type { SVGif } from \"src/Testing\";\nimport { SVG_NAMESPACE } from \"../domToSVG\";\nimport { addSVGifPointer } from \"./addSVGifPointer\";\nimport { addSVGifTimelineControls } from \"./addSVGifTimelineControls\";\nimport { getSVGifAnimations } from \"./getSVGifAnimations\";\nimport { getSVGifParsedScenes } from \"./getSVGifParsedScenes\";\nimport { compressSVGif } from \"./compressSVGif\";\n\nexport const getSVGif = (\n  scenes: SVGif.Scene[],\n  svgFiles: Map<string, string>,\n) => {\n  const { parsedScenes, firstScene } = getSVGifParsedScenes(scenes, svgFiles);\n  const { viewBox, width, height } = firstScene;\n  const svg = document.createElementNS(SVG_NAMESPACE, \"svg\");\n  svg.setAttribute(\"xmlns\", SVG_NAMESPACE);\n  svg.setAttribute(\"viewBox\", viewBox);\n  const style = document.createElementNS(SVG_NAMESPACE, \"style\");\n  svg.appendChild(style);\n  const appendStyle = (s: string) => {\n    style.textContent += \"\\n\\n\" + fixIndent(s);\n  };\n  const g = document.createElementNS(SVG_NAMESPACE, \"g\");\n  g.setAttribute(\"id\", \"all-scenes\");\n  svg.appendChild(g);\n\n  const { cursorKeyframes, sceneAnimations, totalDuration } =\n    getSVGifAnimations({ width, height }, g, parsedScenes, appendStyle);\n\n  const getThisAnimationProperty = (\n    args: Omit<\n      Parameters<typeof getAnimationProperty>[0],\n      \"totalDuration\" | \"loop\"\n    >,\n    onlyValue?: boolean,\n  ) => getAnimationProperty({ ...args, totalDuration }, onlyValue);\n\n  sceneAnimations.forEach(({ sceneId, keyframes }) => {\n    const animationName = `scene-${sceneId}-anim`;\n    appendStyle(`\n      @keyframes ${animationName} {\n      ${keyframes.map((v) => `  ${v}`).join(\"\\n\")}\n      }\n      ${getThisAnimationProperty({ elemSelector: `#${sceneId}`, animName: animationName })}\n    `);\n  });\n\n  addSVGifTimelineControls({\n    width,\n    height,\n    appendStyle,\n    g,\n    totalDuration,\n    sceneAnimations,\n  });\n  addSVGifPointer({ cursorKeyframes, g, appendStyle, totalDuration });\n\n  /** This appears to break/appear narrower on ios ?!! */\n  compressSVGif(svg, parsedScenes);\n\n  svg\n    .querySelectorAll(\"[data-selector]\")\n    .forEach((el) => el.removeAttribute(\"data-selector\"));\n  // document.body.appendChild(svg); // debugging\n\n  /** Remove defs with empty styles */\n  svg.querySelectorAll(\"defs\").forEach((defs) => {\n    if (!defs.innerHTML || !defs.innerHTML.trim()) {\n      defs.remove();\n    }\n  });\n  svg.setAttribute(\"data-duration\", totalDuration);\n  const xmlSerializer = new XMLSerializer();\n  const svgString = xmlSerializer.serializeToString(svg);\n  return svgString;\n};\n\nexport const getAnimationProperty = (\n  {\n    elemSelector,\n    animName,\n    totalDuration,\n    otherProps = \"\",\n    easeFunction = \"ease-in-out\",\n  }: {\n    elemSelector: string;\n    animName: string;\n    totalDuration: number;\n    otherProps?: string;\n    easeFunction?: \"ease-in-out\" | \"ease-out\";\n  },\n  onlyValue = false,\n) => {\n  const loop = true as boolean;\n  const value = `animation: ${animName} ${totalDuration}ms ${easeFunction} ${\n    loop ? \"infinite\" : \"forwards\"\n  };`;\n  if (onlyValue) return value;\n  return fixIndent(`\n      ${elemSelector} {\n        ${value}\n        ${otherProps}\n      }\n    `);\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/getSVGifAnimations.ts",
    "content": "import { fixIndent } from \"@common/utils\";\nimport { SVG_NAMESPACE } from \"../domToSVG\";\nimport { renderSvg } from \"../text/textToSVG\";\nimport { toFixed } from \"../utils/toFixed\";\nimport { addSVGifCaption } from \"./addSVGifCaption\";\nimport { getSVGifCursorAnimationHandler } from \"./animations/getSVGifCursorAnimationHandler\";\nimport { getSVGifTypeAnimation } from \"./animations/getSVGifTypeAnimation\";\nimport { getSVGifZoomToAnimation } from \"./animations/getSVGifZoomToAnimation\";\nimport { getAnimationProperty } from \"./getSVGif\";\nimport type { SVGifParsedScene } from \"./getSVGifParsedScenes\";\nimport { getSVGifRevealKeyframes } from \"./getSVGifRevealKeyframes\";\nimport { getSVGifTargetBBox } from \"./getSVGifTargetBBox\";\n\n/**\n * Given an SVGifScenes, return the animations\n */\nexport const getSVGifAnimations = (\n  { height, width }: { width: number; height: number },\n  g: SVGGElement,\n  parsedScenes: SVGifParsedScene[],\n  appendRootStyle: (s: string) => void,\n) => {\n  const totalSvgifDuration = parsedScenes.reduce(\n    (acc, { animations }) =>\n      acc + animations.reduce((a, { duration }) => a + duration, 0),\n    0,\n  );\n\n  const getPercent = (ms: number, offset: 0 | 0.1 | -0.1 = 0) => {\n    const perc = (ms / totalSvgifDuration) * 100;\n    const result = Math.max(0, Math.min(100, perc));\n    const resultWithOffset = result + offset;\n\n    return toFixed(resultWithOffset, 4);\n  };\n  const cursorHandler = getSVGifCursorAnimationHandler({\n    getPercent,\n    parsedScenes,\n    totalSvgifDuration,\n    width,\n    height,\n  });\n\n  const sceneAnimations: {\n    sceneId: string;\n    svgName: string;\n    startMs: number;\n    duration: number;\n    keyframes: string[];\n  }[] = [];\n  let currentPrevDuration = 0;\n  const prevSceneAnim = sceneAnimations.at(-1);\n\n  for (const [sceneIndex, parsedScene] of parsedScenes.entries()) {\n    const { svgFileName, animations, svgDom, caption } = parsedScene;\n    if (!animations.length) {\n      throw new Error(\n        `No animations provided for scene ${sceneIndex} (${svgFileName}). Each scene must have at least one animation.`,\n      );\n    }\n    const sceneId = `scene-${sceneIndex}`;\n    const sceneStartMs = currentPrevDuration;\n    const sceneFromPercent = getPercent(currentPrevDuration);\n\n    const renderedSceneSVG = renderSvg(svgDom);\n\n    const sceneKeyframes: string[] = [];\n    if (prevSceneAnim) {\n      if (prevSceneAnim.svgName === svgFileName) {\n        throw new Error(\"SVG file must change between scenes\");\n      }\n    }\n    if (currentPrevDuration) {\n      sceneKeyframes.push(`0% ${hidden}`);\n      sceneKeyframes.push(\n        `${getPercent(currentPrevDuration, -0.1)}% ${hidden}`,\n      );\n    }\n\n    sceneKeyframes.push(`${getPercent(currentPrevDuration)}% ${visible}`);\n\n    const sceneNodeAnimations: SceneNodeAnimation[] = [];\n    let sceneNodeAnimationsStyle = \"\";\n    const getDefs = () => {\n      let defs = svgDom.querySelector(\"defs\");\n      if (!defs) {\n        defs = document.createElementNS(SVG_NAMESPACE, \"defs\");\n        svgDom.insertBefore(defs, svgDom.firstChild);\n      }\n      return defs;\n    };\n    let styleElem = svgDom.querySelector<SVGStyleElement>(\"style\");\n    const appendStyle = (style: string) => {\n      if (!styleElem) {\n        styleElem = document.createElementNS(SVG_NAMESPACE, \"style\");\n        const defs = getDefs();\n        defs.appendChild(styleElem);\n      }\n      console.log(\"Appending style:\", style);\n      styleElem.textContent += style;\n    };\n    for (const [animationIndex, animation] of animations.entries()) {\n      const bboxInfo =\n        animation.type !== \"moveTo\" && animation.type !== \"wait\" ?\n          getSVGifTargetBBox({\n            elementSelector: animation.elementSelector,\n            svgDom,\n            svgFileName,\n            width,\n            height,\n          })\n        : undefined;\n      if (animation.type === \"wait\") {\n      } else if (\n        animation.type === \"click\" ||\n        animation.type === \"clickAppearOnHover\" ||\n        animation.type === \"moveTo\"\n      ) {\n        cursorHandler.addAnimation({\n          currentPrevDuration,\n          animation,\n          animationIndex,\n          animations,\n          bbox: bboxInfo?.bbox,\n          sceneIndex,\n          svgFileName,\n          sceneNodeAnimations,\n          sceneId,\n        });\n      } else {\n        const { duration, elementSelector } = animation;\n        if (!bboxInfo) throw new Error(\"Missing bboxInfo\");\n        const { bbox, element } = bboxInfo;\n        const fromTime = currentPrevDuration;\n        const fromPerc = getPercent(fromTime);\n        if (animation.type === \"fadeIn\" || animation.type === \"growIn\") {\n          const toTime = fromTime + duration;\n          const toPerc = getPercent(toTime);\n          sceneNodeAnimations.push({\n            sceneId,\n            elemSelector: elementSelector,\n            keyframes: getSVGifRevealKeyframes({\n              fromPerc,\n              toPerc,\n              mode: animation.type === \"fadeIn\" ? \"opacity\" : \"growIn\",\n              // mode: \"top to bottom\",\n            }),\n          });\n        } else if (animation.type === \"type\") {\n          const parsedAnimations = getSVGifTypeAnimation(\n            { height, width },\n            { element, bbox },\n            parsedScene,\n            animation,\n            {\n              sceneId,\n              sceneIndex,\n              totalDuration: totalSvgifDuration,\n              getPercent,\n              fromTime,\n            },\n          );\n          sceneNodeAnimations.push(...parsedAnimations.sceneNodeAnimations);\n          appendStyle(parsedAnimations.style);\n\n          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n        } else if (animation.type === \"zoomToElement\") {\n          const parsedAnimations = getSVGifZoomToAnimation(\n            { height, width },\n            { bbox },\n            parsedScene,\n            animation,\n            {\n              sceneId,\n              sceneIndex,\n              totalDuration: totalSvgifDuration,\n              getPercent,\n              fromTime,\n            },\n            true,\n          );\n          appendRootStyle(parsedAnimations.style);\n        }\n      }\n      const isParallelAnimation = animation.type === \"zoomToElement\";\n      currentPrevDuration += isParallelAnimation ? 0 : animation.duration;\n    }\n\n    if (sceneNodeAnimations.length) {\n      sceneNodeAnimationsStyle += \"\\n\";\n      let nodeAnimIndex = 0;\n      sceneNodeAnimations.forEach(({ sceneId, elemSelector, keyframes }) => {\n        nodeAnimIndex++;\n        const animationName = `node-${sceneId}-anim-${nodeAnimIndex}`;\n        sceneNodeAnimationsStyle += \"\\n\";\n        sceneNodeAnimationsStyle += fixIndent(`\n          @keyframes ${animationName} {\n          ${keyframes.map((v) => `  ${v}`).join(\"\\n\")}\n          }\n          ${getAnimationProperty({ elemSelector: `#${sceneId} ${elemSelector}`, animName: animationName, totalDuration: totalSvgifDuration })}\n        `);\n      });\n\n      appendStyle(sceneNodeAnimationsStyle);\n    }\n\n    if (caption) {\n      addSVGifCaption({\n        svgDom,\n        appendStyle,\n        width,\n        height,\n        caption,\n        fromPerc: sceneFromPercent,\n        toPerc: getPercent(currentPrevDuration),\n        totalDuration: totalSvgifDuration,\n        sceneId,\n      });\n    }\n    const serializer = new XMLSerializer();\n    const svgString = serializer.serializeToString(svgDom);\n    appendSvgToSvg({ id: sceneId, svgFile: svgString, svgDom }, g);\n\n    const isLastScene = sceneIndex === parsedScenes.length - 1;\n    sceneKeyframes.push(`${getPercent(currentPrevDuration)}% ${visible}`);\n    if (!isLastScene) {\n      const toPerc = getPercent(currentPrevDuration, 0.1);\n      sceneKeyframes.push(`${getPercent(currentPrevDuration, 0.1)}% ${hidden}`);\n      if (toPerc < 100) {\n        sceneKeyframes.push(`100% ${hidden}`);\n      }\n    }\n\n    sceneAnimations.push({\n      sceneId,\n      startMs: sceneStartMs,\n      duration: currentPrevDuration - sceneStartMs,\n      svgName: svgFileName,\n      keyframes: sceneKeyframes,\n    });\n    renderedSceneSVG.remove();\n  }\n  const { cursorKeyframes } = cursorHandler.getCursorKeyframes();\n  return {\n    cursorKeyframes,\n    sceneAnimations,\n    totalDuration: totalSvgifDuration,\n  };\n};\n\nexport type SceneNodeAnimation = {\n  sceneId: string;\n  elemSelector: string;\n  keyframes: string[];\n};\n\nconst appendSvgToSvg = (\n  { svgFile, id, svgDom }: { svgFile: string; id: string; svgDom: SVGElement },\n  g: SVGGElement,\n) => {\n  svgDom.setAttribute(\"id\", id);\n  g.appendChild(svgDom);\n\n  return {\n    remove: () => {\n      svgDom.remove();\n    },\n  };\n};\n\nconst visible = \"{  visibility: visible; }\";\nconst hidden = \"{  visibility: hidden; }\";\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/getSVGifParsedScenes.ts",
    "content": "import type { SVGif } from \"src/Testing\";\n\nconst parseSVGWithViewBox = (\n  svgFileName: string,\n  svgFiles: Map<string, string>,\n) => {\n  if (!svgFileName) {\n    throw \"SVG file name is empty\";\n  }\n  const svgFile = svgFiles.get(svgFileName + \".svg\");\n  if (!svgFile) {\n    throw `SVG file not found: ${svgFileName} \\nExpecting one of: ${svgFiles.keys().toArray()}`;\n  }\n  const parsedSVG = new DOMParser().parseFromString(svgFile, \"image/svg+xml\");\n  const viewBox = parsedSVG.documentElement.getAttribute(\"viewBox\");\n  if (!viewBox) {\n    throw `SVG file ${svgFileName} does not have a viewBox attribute`;\n  }\n\n  const width = Number(viewBox.split(\" \")[2]);\n  const height = Number(viewBox.split(\" \")[3]);\n  if (!width || !height) {\n    throw `Invalid viewBox dimensions in SVG file ${svgFileName}`;\n  }\n\n  return {\n    svgDom: parsedSVG.documentElement as unknown as SVGElement,\n    width,\n    height,\n    viewBox,\n    svgFile,\n  };\n};\n\nexport type SVGifParsedScene = ReturnType<typeof parseSVGWithViewBox> &\n  SVGif.Scene;\n\nexport const getSVGifParsedScenes = (\n  scenes: SVGif.Scene[],\n  svgFiles: Map<string, string>,\n) => {\n  const parsedScenes = scenes.map((scene) => ({\n    ...scene,\n    ...parseSVGWithViewBox(scene.svgFileName, svgFiles),\n  }));\n  const firstScene = parsedScenes[0];\n  if (!firstScene) {\n    throw \"No scenes provided\";\n  }\n\n  return { parsedScenes, firstScene };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/getSVGifRevealKeyframes.ts",
    "content": "import { toFixed } from \"../utils/toFixed\";\n\nexport const getSVGifRevealKeyframes = ({\n  fromPerc,\n  toPerc,\n  mode,\n}: {\n  fromPerc: number;\n  toPerc: number;\n  mode: \"top to bottom\" | \"left to right\" | \"opacity\" | \"growIn\";\n}) => {\n  if (mode === \"growIn\") {\n    return [\n      !fromPerc ? \"\" : (\n        `0% { opacity: 0; transform: scale(0.2); transform-origin: center; }`\n      ),\n      `${toFixed(fromPerc, 4)}% { opacity: 0; transform: scale(0.2); transform-origin: center; }`,\n      `${toFixed(fromPerc + 0.1, 4)}% { opacity: 0; transform: scale(0.2); transform-origin: center; }`,\n      `${toFixed(toPerc, 4)}% { opacity: 1; transform: scale(1); transform-origin: center; }`,\n      toPerc === 100 ? \"\" : (\n        `100% { opacity: 1; transform: scale(1); transform-origin: center; }`\n      ),\n    ].filter(Boolean);\n  }\n  if (mode === \"opacity\") {\n    return [\n      !fromPerc ? \"\" : `0% { opacity: 0; }`,\n      `${toFixed(fromPerc, 4)}% { opacity: 0; }`,\n      `${toFixed(fromPerc + 0.1, 4)}% { opacity: 0; }`,\n      `${toFixed(toPerc, 4)}% { opacity: 1; }`,\n      toPerc === 100 ? \"\" : `100% { opacity: 1; }`,\n    ].filter(Boolean);\n  }\n  const clippedInset =\n    mode === \"top to bottom\" ? `inset(0 0 100% 0)` : `inset(0 100% 0 0)`;\n  return [\n    !fromPerc ? \"\" : `0% { opacity: 0; clip-path: ${clippedInset} }`,\n    `${toFixed(fromPerc, 4)}% { opacity: 0; clip-path: ${clippedInset} }`,\n    `${toFixed(fromPerc + 0.1, 4)}% { opacity: 1; clip-path: ${clippedInset} }`,\n    `${toFixed(toPerc, 4)}% { opacity: 1;  clip-path: inset(0 0 0 0);  }`,\n    toPerc === 100 ? \"\" : `100% { opacity: 1; clip-path: inset(0 0 0 0); }`,\n  ].filter(Boolean);\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/SVGif/getSVGifTargetBBox.ts",
    "content": "export const getSVGifTargetBBox = ({\n  elementSelector,\n  svgDom,\n  svgFileName,\n  width,\n  height,\n}: {\n  elementSelector: string;\n  svgDom: SVGElement;\n  svgFileName: string;\n  width: number;\n  height: number;\n}) => {\n  const element = svgDom.querySelector<SVGGElement>(elementSelector);\n  if (!element) {\n    throw `Element not found: ${elementSelector} in SVG file ${svgFileName}`;\n  }\n\n  const bbox = element.getBBox();\n\n  /* Clamp width and height to be within visible bounds */\n  bbox.x = Math.max(0, Math.min(bbox.x, width));\n  bbox.y = Math.max(0, Math.min(bbox.y, height));\n  bbox.width = Math.max(0, Math.min(bbox.width, width - bbox.x));\n  bbox.height = Math.max(0, Math.min(bbox.height, height - bbox.y));\n\n  return { bbox, element };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/addFragmentViewBoxes.ts",
    "content": "import { SVG_NAMESPACE } from \"./domToSVG\";\n\nexport const addFragmentViewBoxes = (svg: SVGSVGElement, padding = 10) => {\n  // Ensure the SVG has a viewBox defined (needed for calculations)\n  if (!svg.hasAttribute(\"viewBox\")) {\n    throw new Error(\"SVG must have a viewBox attribute\");\n  }\n  if (!svg.isConnected) {\n    throw new Error(\"SVG must be in the DOM for bbox calculations\");\n  }\n\n  // Iterate over <g> elements with a data-command attribute\n  const groups = svg.querySelectorAll<SVGGElement>(\"g[data-command]\");\n\n  groups.forEach((g) => {\n    const cmd = g.getAttribute(\"data-command\");\n    if (!cmd) return;\n\n    const bbox = g.getBBox();\n\n    const x = bbox.x - padding;\n    const y = bbox.y - padding;\n    const w = bbox.width + 2 * padding;\n    const h = bbox.height + 2 * padding;\n\n    const view = document.createElementNS(SVG_NAMESPACE, \"view\");\n    const id = cmd.replace(/\\./g, \"_\");\n    if (svg.querySelector(`view#${id}`)) {\n      console.warn(`View ID collision for ${id}`);\n      return;\n    }\n    view.setAttribute(\"id\", id);\n    view.setAttribute(\"viewBox\", [x, y, w, h].map(Math.round).join(\" \"));\n\n    svg.appendChild(view);\n  });\n  /**\n   * Ensures the img tag size matches the viewBox size when using fragment identifiers\n   */\n  svg.setAttribute(\"preserveAspectRatio\", \"xMidYMid meet\");\n  svg.setAttribute(\"width\", \"100%\");\n  svg.setAttribute(\"height\", \"100%\");\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/containers/addOverflowClipPath.ts",
    "content": "import { includes } from \"../../../dashboard/W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\nimport type { WhatToRenderOnSVG } from \"../utils/getWhatToRenderOnSVG\";\nimport { isElementNode } from \"../utils/isElementVisible\";\nimport type { SVGContext } from \"./elementToSVG\";\n\nexport const addOverflowClipPath = (\n  element: HTMLElement,\n  style: CSSStyleDeclaration,\n  g: SVGGElement,\n  {\n    x,\n    height,\n    width,\n    y,\n  }: {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  },\n  context: SVGContext,\n  whatToRender: Pick<WhatToRenderOnSVG, \"border\" | \"background\">,\n) => {\n  /**\n   * If overflow is set to hidden, we need to add a clip path to the group\n   */\n  if (!mustAddClipPath(element, style)) return;\n  const borderWidth =\n    whatToRender.border?.type === \"border\" ?\n      whatToRender.border.borderWidth\n    : 0;\n\n  /**\n   * This is to ensure we don't cut out the rounded parent corners\n   * */\n  // const parentWithRoundedCorners = element\n  const inputBorderRadius =\n    element instanceof HTMLInputElement ? \"8px\" : style.borderRadius || \"0\";\n  const borderRadiusValue = parseFloat(inputBorderRadius);\n  const roundProperty =\n    borderRadiusValue ? ` round ${borderRadiusValue}px` : \"\";\n  const translate = g\n    .getAttribute(\"transform\")\n    ?.split(\"translate(\")[1]\n    ?.split(\")\")[0];\n  const [transformX = 0, transformY = 0] =\n    translate ? translate.split(\",\").map((v) => parseFloat(v.trim())) : [0, 0];\n\n  const top = y - borderWidth - transformY;\n  const right = context.width - x - width - borderWidth - transformX;\n  const bottom = context.height - y - height - borderWidth - transformY;\n  const left = x - borderWidth - transformX;\n\n  const insetValues = [top, right, bottom, left]\n    .map((v) => (v < 0 ? 0 : v))\n    .map((v) => `${v}px`)\n    .join(\" \");\n\n  g._overflowClipPath = {\n    x,\n    y,\n    width,\n    height,\n  };\n  g.style.clipPath = `inset(${insetValues} ${roundProperty}) view-box`;\n};\n\nconst mustAddClipPath = (element: HTMLElement, style: CSSStyleDeclaration) => {\n  if (\n    element instanceof HTMLSelectElement ||\n    element instanceof HTMLButtonElement ||\n    element instanceof HTMLLabelElement\n  ) {\n    return false;\n  }\n  if (\n    element instanceof HTMLInputElement ||\n    element instanceof HTMLTextAreaElement\n  ) {\n    return true;\n  }\n\n  if (element instanceof HTMLCanvasElement) {\n    return true;\n  }\n  if (!includes(style.overflow, [\"hidden\", \"auto\", \"scroll\", \"clip\"])) {\n    return false;\n  }\n  if (!element.children.length && element.childNodes.length) {\n    return true;\n  }\n\n  /**\n   * Ensures the sql tooltip is not overflowing out of the sql editor\n  if (element.className.includes(\"monaco-scrollable-element\")) {\n    debugger;\n  }\n   */\n  const isRelativeOrAbsolute = includes(style.position, [\n    \"relative\",\n    \"absolute\",\n  ]);\n\n  const isBoundingChildren = Array.from(element.children).some((child) => {\n    if (!isElementNode(child)) {\n      return false;\n    }\n    const childStyle = getComputedStyle(child);\n    if (childStyle.position === \"absolute\") return isRelativeOrAbsolute;\n    if (childStyle.position !== \"fixed\") return true;\n  });\n  return isBoundingChildren;\n  /* To expensive and not accurate enough */\n  // return Array.from(element.children).some(\n  //   (child) => isElementVisible(child).isVisible,\n  // );\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/containers/bgAndBorderToSVG.ts",
    "content": "import { SVG_NAMESPACE } from \"../domToSVG\";\nimport type { SVGScreenshotNodeType } from \"../domToThemeAwareSVG\";\nimport { toFixed } from \"../utils/toFixed\";\n\nexport function hasBorder(style: CSSStyleDeclaration) {\n  return (\n    style.borderTopWidth !== \"0px\" ||\n    style.borderRightWidth !== \"0px\" ||\n    style.borderBottomWidth !== \"0px\" ||\n    style.borderLeftWidth !== \"0px\"\n  );\n}\n\nexport const addSpecificBorders = (\n  g: SVGGElement,\n  x: number,\n  y: number,\n  width: number,\n  height: number,\n  style: CSSStyleDeclaration,\n) => {\n  const drawBorder = (\n    x1: number,\n    y1: number,\n    x2: number,\n    y2: number,\n    color: string,\n    width: number,\n  ) => {\n    const border = document.createElementNS(SVG_NAMESPACE, \"line\") as Extract<\n      SVGScreenshotNodeType,\n      SVGLineElement\n    >;\n    border._purpose = { border: true };\n    border.setAttribute(\"x1\", x1);\n    border.setAttribute(\"y1\", y1);\n    border.setAttribute(\"x2\", x2);\n    border.setAttribute(\"y2\", y2);\n    border.setAttribute(\"stroke\", color);\n    border.setAttribute(\"stroke-width\", width);\n    g.appendChild(border);\n  };\n\n  const {\n    borderTopWidth,\n    borderRightWidth,\n    borderBottomWidth,\n    borderLeftWidth,\n    borderTopColor,\n    borderRightColor,\n    borderBottomColor,\n    borderLeftColor,\n  } = style;\n\n  // Top border\n  if (borderTopWidth !== \"0px\") {\n    const borderWidth = parseFloat(borderTopWidth);\n    drawBorder(\n      x,\n      /** Given that the x,y,w,h refer to the rectangle edges, we need to subtract half width of the overflowing border to match the dimensions */\n      y + borderWidth / 2,\n      x + width,\n      y + borderWidth / 2,\n      borderTopColor,\n      borderWidth,\n    );\n  }\n\n  // Right border\n  if (borderRightWidth !== \"0px\") {\n    const borderWidth = parseFloat(borderRightWidth);\n    drawBorder(\n      x - borderWidth / 2 + width,\n      y,\n      x - borderWidth / 2 + width,\n      y + height,\n      borderRightColor,\n      borderWidth,\n    );\n  }\n\n  // Bottom border\n  if (borderBottomWidth !== \"0px\") {\n    const borderWidth = parseFloat(borderBottomWidth);\n\n    drawBorder(\n      x,\n      y - borderWidth / 2 + height,\n      x + width,\n      y - borderWidth / 2 + height,\n      borderBottomColor,\n      borderWidth,\n    );\n  }\n\n  // Left border\n  if (borderLeftWidth !== \"0px\") {\n    const borderWidth = parseFloat(borderLeftWidth);\n    drawBorder(\n      x + borderWidth / 2,\n      y,\n      x + borderWidth / 2,\n      y + height,\n      borderLeftColor,\n      borderWidth,\n    );\n  }\n};\n\nexport function getBackgroundColor(style: CSSStyleDeclaration) {\n  const { backgroundColor, backgroundImage } = style;\n  if (backgroundImage.includes(\"gradient\")) {\n    console.warn(\"TODO: handle gradients\");\n    // TODO: handle gradients\n    // return backgroundImage\n    //   .split(\"var(--\")\n    //   .map((cssVar, index) => {\n    //     if (!index) return cssVar;\n    //     const [varName, ...endParts] = cssVar.split(\")\");\n    //     const value = getComputedStyle(document.documentElement)\n    //       .getPropertyValue(\"--\" + varName)\n    //       .trim();\n    //     return value + endParts.join(\")\");\n    //   })\n    //   .join(\"\");\n  }\n  if (backgroundColor.startsWith(\"rgba\") && backgroundColor.endsWith(\"0)\")) {\n    return undefined;\n  }\n  if (backgroundColor === \"transparent\") {\n    return undefined;\n  }\n  return backgroundColor;\n}\n\nexport const getBackdropFilter = (style: CSSStyleDeclaration) => {\n  return style.backdropFilter && style.backdropFilter !== \"none\" ?\n      style.backdropFilter\n    : undefined;\n};\n\n/**\n * Build a <path> \"d\" attribute for a rectangle with arbitrary corner radii.\n * Accounts for border width and ensures crisp edges by using whole numbers\n * and half-pixel offsets when needed.\n */\nexport const roundedRectPath = (\n  x: number,\n  y: number,\n  w: number,\n  h: number,\n  [rtl, rtr, rbr, rbl]: [number, number, number, number],\n) => {\n  /* Clamp radii so that they never overlap (same thing the browser does) */\n  const sumH = rtl + rtr;\n  const sumH2 = rbl + rbr;\n  const sumV = rtl + rbl;\n  const sumV2 = rtr + rbr;\n\n  if (sumH > w) {\n    const scale = w / sumH;\n    rtl *= scale;\n    rtr *= scale;\n  }\n  if (sumH2 > w) {\n    const scale = w / sumH2;\n    rbl *= scale;\n    rbr *= scale;\n  }\n  if (sumV > h) {\n    const scale = h / sumV;\n    rtl *= scale;\n    rbl *= scale;\n  }\n  if (sumV2 > h) {\n    const scale = h / sumV2;\n    rtr *= scale;\n    rbr *= scale;\n  }\n\n  /* Path – clockwise, starting in the top-left corner */\n  return [\n    `M${toFixed(x + rtl)},${toFixed(y)}`, // start\n    `H${toFixed(x + w - rtr)}`, // top\n    rtr ?\n      `A${toFixed(rtr)},${toFixed(rtr)} 0 0 1 ${toFixed(x + w)},${toFixed(y + rtr)}`\n    : \"\",\n    `V${toFixed(y + h - rbr)}`, // right\n    rbr ?\n      `A${toFixed(rbr)},${toFixed(rbr)} 0 0 1 ${toFixed(x + w - rbr)},${toFixed(y + h)}`\n    : \"\",\n    `H${toFixed(x + rbl)}`, // bottom\n    rbl ?\n      `A${toFixed(rbl)},${toFixed(rbl)} 0 0 1 ${toFixed(x)},${toFixed(y + h - rbl)}`\n    : \"\",\n    `V${toFixed(y + rtl)}`, // left\n    rtl ?\n      `A${toFixed(rtl)},${toFixed(rtl)} 0 0 1 ${toFixed(x + rtl)},${toFixed(y)}`\n    : \"\",\n    \"Z\",\n  ]\n    .filter(Boolean)\n    .join(\" \");\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/containers/deduplicateSVGPaths.ts",
    "content": "export const deduplicateSVGPaths = (svgElement: SVGElement) => {\n  // Get or create defs element\n  let defs = svgElement.querySelector(\"defs\");\n  if (!defs) {\n    defs = document.createElementNS(\"http://www.w3.org/2000/svg\", \"defs\");\n    svgElement.insertBefore(defs, svgElement.firstChild);\n  }\n\n  // Map to track identical paths: key = path signature, value = { id, count }\n  const pathMap = new Map();\n\n  // Find all path elements (excluding those already in defs)\n  const paths = Array.from(\n    svgElement.querySelectorAll<SVGPathElement>(\"path:not(defs path)\"),\n  );\n\n  paths.forEach((path) => {\n    // Create a signature from the path's key attributes\n    const signature = createPathSignature(path);\n\n    if (pathMap.has(signature)) {\n      // Duplicate found - replace with <use>\n      const { id } = pathMap.get(signature);\n      replaceWithUse(path, id);\n    } else {\n      // First occurrence - move to defs if beneficial\n      const id = `path-${pathMap.size}`;\n      pathMap.set(signature, { id, element: path });\n    }\n  });\n\n  // Move paths that appear multiple times to defs\n  let deduplicatedCount = 0;\n  pathMap.forEach(({ id, element }, signature) => {\n    // Count how many times this signature appears\n    const occurrences = paths.filter(\n      (p) => createPathSignature(p) === signature,\n    ).length;\n\n    if (occurrences > 1) {\n      moveToDefsAndReplaceAll(element, id, signature, paths, defs);\n      deduplicatedCount++;\n    }\n  });\n\n  return {\n    deduplicatedCount,\n    totalPaths: paths.length,\n  };\n};\n\nconst createPathSignature = (path: SVGPathElement) => {\n  // Create a unique signature based on path data and styling attributes\n  // Exclude transform and position attributes\n  const d = path.getAttribute(\"d\") || \"\";\n  // const fill = path.getAttribute(\"fill\") || \"\";\n  // const stroke = path.getAttribute(\"stroke\") || \"\";\n  // const strokeWidth = path.getAttribute(\"stroke-width\") || \"\";\n  // const fillRule = path.getAttribute(\"fill-rule\") || \"\";\n  // const strokeLinecap = path.getAttribute(\"stroke-linecap\") || \"\";\n  // const strokeLinejoin = path.getAttribute(\"stroke-linejoin\") || \"\";\n  // const opacity = path.getAttribute(\"opacity\") || \"\";\n  // const fillOpacity = path.getAttribute(\"fill-opacity\") || \"\";\n  // const strokeOpacity = path.getAttribute(\"stroke-opacity\") || \"\";\n\n  return JSON.stringify({\n    d,\n    // fill,\n    // stroke,\n    // strokeWidth,\n    // fillRule,\n    // strokeLinecap,\n    // strokeLinejoin,\n    // opacity,\n    // fillOpacity,\n    // strokeOpacity,\n  });\n};\n\nconst moveToDefsAndReplaceAll = (\n  originalPath: SVGPathElement,\n  id,\n  signature: string,\n  allPaths: SVGPathElement[],\n  defs: SVGDefsElement,\n) => {\n  // Clone the path for defs (without transform)\n  const defPath = originalPath.cloneNode(true) as SVGPathElement;\n  defPath.setAttribute(\"id\", id);\n  defPath.removeAttribute(\"transform\");\n  defs.appendChild(defPath);\n\n  // Replace all matching paths with <use> elements\n  allPaths.forEach((path) => {\n    if (createPathSignature(path) === signature) {\n      replaceWithUse(path, id);\n    }\n  });\n};\n\nconst replaceWithUse = (path: SVGPathElement, refId: string) => {\n  const use = document.createElementNS(\"http://www.w3.org/2000/svg\", \"use\");\n  use.setAttributeNS(\"http://www.w3.org/1999/xlink\", \"xlink:href\", `#${refId}`);\n  use.setAttribute(\"href\", `#${refId}`);\n\n  // Preserve transform and position attributes\n  const preserveAttrs = [\"transform\", \"x\", \"y\", \"class\", \"id\", \"data-*\"];\n  preserveAttrs.forEach((attr) => {\n    if (path.hasAttribute(attr)) {\n      use.setAttribute(attr, path.getAttribute(attr));\n    }\n  });\n\n  // Copy data attributes\n  Array.from(path.attributes).forEach((attr) => {\n    if (attr.name.startsWith(\"data-\")) {\n      use.setAttribute(attr.name, attr.value);\n    }\n  });\n\n  const parent = path.parentNode;\n  if (!parent) {\n    return;\n    // throw new Error(\"Path element has no parent node.\");\n  }\n\n  parent.replaceChild(use, path);\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/containers/elementToSVG.ts",
    "content": "import { getEntries } from \"@common/utils\";\nimport { drawShapesOnSVG } from \"../../../dashboard/Charts/drawShapes/drawShapesOnSVG\";\nimport { SVG_NAMESPACE } from \"../domToSVG\";\nimport { getBBoxCode, type SVGScreenshotNodeType } from \"../domToThemeAwareSVG\";\nimport { fontIconToSVG } from \"../graphics/fontIconToSVG\";\nimport { addImageFromDataURL, imgToSVG } from \"../graphics/imgToSVG\";\nimport { textToSVG } from \"../text/textToSVG\";\nimport { canvasToDataURL } from \"../utils/canvasToDataURL\";\nimport { getAnimationsHandler } from \"../utils/copyAnimationStylesToSvg\";\nimport { getWhatToRenderOnSVG } from \"../utils/getWhatToRenderOnSVG\";\nimport { isElementNode } from \"../utils/isElementVisible\";\nimport { toFixed } from \"../utils/toFixed\";\nimport { addOverflowClipPath } from \"./addOverflowClipPath\";\nimport { rectangleToSVG } from \"./rectangleToSVG\";\n\nexport type SVGContext = {\n  docId: string;\n  offsetX: number;\n  offsetY: number;\n  defs: SVGDefsElement;\n  idCounter: number;\n  fontFamilies: string[];\n  cssDeclarations: Map<string, string>;\n  width: number;\n  height: number;\n};\nexport type SVGNodeLayout = {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  style: CSSStyleDeclaration;\n};\n\nexport const elementToSVG = async (\n  element: HTMLElement,\n  parentSvg: SVGElement | SVGGElement,\n  context: SVGContext,\n) => {\n  /** Ensures bbox calculations are stable */\n\n  const copyAnimations = getAnimationsHandler(element);\n\n  const _whatToRender = await getWhatToRenderOnSVG(element, context, parentSvg);\n\n  const { elemInfo, ...whatToRender } = _whatToRender;\n  const { x, y, width, height, style, isVisible } = elemInfo;\n\n  if (!isVisible && !_whatToRender.mightBeHovered) {\n    return whatToRender;\n  }\n\n  const g = document.createElementNS(SVG_NAMESPACE, \"g\");\n  g._domElement = element;\n  g._whatToRender = _whatToRender;\n  g.setAttribute(\n    \"data-selector\",\n    [element.nodeName.toLowerCase(), element.className].join(\".\"),\n  );\n\n  const roundedPosition = {\n    x: Math.round(x),\n    y: Math.round(y),\n    width: Math.round(width),\n    height: Math.round(height),\n  };\n  const bboxCode = getBBoxCode(element, roundedPosition);\n  (g as SVGScreenshotNodeType)._bboxCode = bboxCode;\n\n  getEntries({\n    ...whatToRender.attributeData,\n  }).forEach(([key, value]) => {\n    if (value) {\n      g.setAttribute(key, value);\n    }\n  });\n\n  getEntries({\n    ...whatToRender.childAffectingStyles,\n  }).forEach(([key, value]) => {\n    if (value && key === \"opacity\") {\n      g.style[key] = value;\n    }\n  });\n\n  const rectElem = rectangleToSVG(\n    g,\n    element,\n    style,\n    elemInfo,\n    whatToRender,\n    bboxCode,\n  );\n\n  if (whatToRender.text?.length) {\n    whatToRender.text.forEach((textForSVG) => {\n      textToSVG(element, g, textForSVG, style, bboxCode);\n    });\n  }\n\n  if (element instanceof HTMLCanvasElement) {\n    if (element._drawn?.shapes.length) {\n      const { shapes, scale, translate } = element._drawn;\n      const transformedG = document.createElementNS(SVG_NAMESPACE, \"g\");\n      g.setAttribute(\"transform\", `translate(${x}, ${y})`);\n      g.appendChild(transformedG);\n      drawShapesOnSVG(\n        shapes,\n        context,\n        transformedG,\n        {\n          scale,\n          translate,\n        },\n        {\n          width,\n          height,\n        },\n      );\n    } else {\n      element._deckgl?.redraw(\"screenshot\");\n      const canvas = element._deckgl?.getCanvas() || element;\n      const dataURL = canvasToDataURL(canvas);\n      addImageFromDataURL(g, dataURL, context, elemInfo);\n    }\n  }\n\n  if (whatToRender.image?.type === \"foreignObject\") {\n    parentSvg.appendChild(whatToRender.image.foreignObject);\n\n    (whatToRender.image.foreignObject as SVGScreenshotNodeType)._bboxCode =\n      bboxCode;\n    return;\n  }\n\n  const { image } = whatToRender;\n  if (image?.type === \"svgElement\") {\n    const width =\n      image.element instanceof HTMLImageElement ?\n        image.element.width\n      : element.clientWidth;\n    const height =\n      image.element instanceof HTMLImageElement ?\n        image.element.height\n      : element.clientHeight;\n    const gWrapper = document.createElementNS(SVG_NAMESPACE, \"g\");\n    parentSvg.appendChild(gWrapper);\n    const svgClone = image.element.cloneNode(true) as SVGElement;\n    svgClone.setAttribute(\"width\", `${toFixed(width)}`);\n    svgClone.setAttribute(\"height\", `${toFixed(height)}`);\n    gWrapper.style.transform = `translate(${toFixed(x)}px, ${toFixed(y)}px)`;\n    gWrapper.style.color = style.color;\n    gWrapper._gWrapperFor = element;\n    gWrapper.appendChild(svgClone);\n  } else if (image?.type === \"fontIcon\") {\n    await fontIconToSVG(g, image, context, elemInfo);\n  } else if (image?.type === \"img\") {\n    await imgToSVG(g, image.element, elemInfo, context);\n  } else if (image?.type === \"maskedElement\") {\n    const { width, height, x, y } = element.getBoundingClientRect();\n    const dataUrl = decodeURIComponent(\n      style.maskImage.split(\",\")[1]!.slice(0, -2),\n    );\n    const parser = new DOMParser();\n    const svgDoc = parser.parseFromString(dataUrl, \"image/svg+xml\");\n    const svgElement = svgDoc.documentElement;\n    svgElement.setAttribute(\"width\", `${toFixed(width)}`);\n    svgElement.setAttribute(\"height\", `${toFixed(height)}`);\n    svgElement.setAttribute(\"x\", toFixed(x));\n    svgElement.setAttribute(\"y\", toFixed(y));\n    svgElement.setAttribute(\"fill\", style.color);\n\n    const wrapperG = document.createElementNS(SVG_NAMESPACE, \"g\");\n    wrapperG.appendChild(svgElement);\n    /** wrapperG is required to ensure animations work on safari */\n    if (style.animation) {\n      wrapperG.setAttribute(\n        \"style\",\n        `animation: ${style.animation}; transform-origin: ${toFixed(x + width / 2)}px ${toFixed(y + height / 2)}px;`,\n      );\n    }\n\n    copyAnimations?.(style, wrapperG, context.cssDeclarations, false);\n\n    parentSvg.appendChild(wrapperG);\n  }\n\n  if (image?.type !== \"maskedElement\") {\n    copyAnimations?.(style, rectElem?.path ?? g, context.cssDeclarations, true);\n  }\n\n  for (const child of getChildrenSortedByZIndex(element)) {\n    if (isElementNode(child)) {\n      await elementToSVG(child, g, context);\n    }\n  }\n\n  /** Must ensure we have a bbox for clicking interaction placement */\n  if (!g.childNodes.length && whatToRender.attributeData) {\n    const bboxRect = document.createElementNS(SVG_NAMESPACE, \"rect\");\n    bboxRect.setAttribute(\"x\", toFixed(x));\n    bboxRect.setAttribute(\"y\", toFixed(y));\n    bboxRect.setAttribute(\"width\", toFixed(width));\n    bboxRect.setAttribute(\"height\", toFixed(height));\n    bboxRect.setAttribute(\"fill\", \"transparent\");\n    g.appendChild(bboxRect);\n  }\n  if (g.childNodes.length) {\n    addOverflowClipPath(\n      element,\n      style,\n      g,\n      { x, y, width, height },\n      context,\n      whatToRender,\n    );\n    parentSvg.appendChild(g);\n  }\n\n  return whatToRender;\n};\n\nconst getChildrenSortedByZIndex = (element: HTMLElement): HTMLElement[] => {\n  const children = Array.from(element.children) as HTMLElement[];\n  return children.slice(0).sort((a, b) => {\n    const aZIndex = parseInt(getComputedStyle(a).zIndex) || 0;\n    const bZIndex = parseInt(getComputedStyle(b).zIndex) || 0;\n    return aZIndex - bZIndex;\n  });\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/containers/rectangleToSVG.ts",
    "content": "import { fromEntries, getEntries } from \"@common/utils\";\nimport { SVG_NAMESPACE } from \"../domToSVG\";\nimport type { SVGScreenshotNodeType } from \"../domToThemeAwareSVG\";\nimport type { getWhatToRenderOnSVG } from \"../utils/getWhatToRenderOnSVG\";\nimport { addSpecificBorders, roundedRectPath } from \"./bgAndBorderToSVG\";\nimport type { SVGNodeLayout } from \"./elementToSVG\";\nimport { getBoxShadowAsDropShadow } from \"./shadowToSVG\";\nexport const BORDER_ELEMENT_TYPES = [\"rect\", \"path\", \"line\"] as const;\n\nexport const rectangleToSVG = (\n  g: SVGGElement,\n  element: HTMLElement,\n  style: CSSStyleDeclaration,\n  { x, y, width, height }: Pick<SVGNodeLayout, \"x\" | \"y\" | \"width\" | \"height\">,\n  {\n    border,\n    background,\n    backdropFilter,\n  }: Pick<\n    Awaited<ReturnType<typeof getWhatToRenderOnSVG>>,\n    \"background\" | \"border\" | \"backdropFilter\"\n  >,\n  bboxCode: string,\n) => {\n  const shadow = getBoxShadowAsDropShadow(style);\n  const scrollMask =\n    style.backdropFilter &&\n    style.mask &&\n    style.mask.includes(\"linear-gradient\");\n  if (!border && !background && !shadow && !scrollMask && !backdropFilter) {\n    return;\n  }\n\n  let _path: ReturnType<typeof getRectanglePath> | undefined;\n  const getPath = () => {\n    if (_path) return _path;\n    const entries = getEntries({\n      border,\n      background,\n      shadow,\n      scrollMask,\n      backdropFilter,\n    } as const);\n    const _purpose = fromEntries(entries.map(([k, v]) => [k, v]));\n    const { path, showBorder, rtl, rtr, rbr, rbl, borderWidth } =\n      getRectanglePath(style, { x, y, width, height }, { border });\n    path._domElement = element;\n    path._bboxCode = bboxCode;\n    path._purpose = _purpose;\n\n    /** This is required to make backgroundSameAsRenderedParent work as expected */\n    path.setAttribute(\"fill\", \"none\");\n\n    g.appendChild(path);\n    _path = { path, showBorder, rtl, rtr, rbr, rbl, borderWidth };\n    return _path;\n  };\n\n  const maskLinearGradients = style.maskImage.split(\"linear-gradient(\");\n  const blendModes = style.maskComposite.split(\", \");\n  if (\n    maskLinearGradients.length > 1 &&\n    blendModes.every((b) => b === \"source-in\")\n  ) {\n    const masks = maskLinearGradients.slice(1);\n\n    masks.forEach((grad, index) => {\n      if (index) {\n        // Mask combining does not work\n        return;\n      }\n\n      g.style.maskImage = style.maskImage;\n      g.style.maskSize = `${width}px ${height}px`;\n      g.style.maskPosition = `${x}px ${0}px`;\n      g.style.maskPosition = `0px 0px`;\n    });\n  }\n\n  if (background) {\n    getPath().path.setAttribute(\"fill\", background);\n  }\n\n  if (style.animation) {\n    getPath().path.style.animation = style.animation;\n  }\n\n  // TODO: shadow and border must be drawn outside the overflow clip path\n  if (shadow) {\n    getPath().path.style.filter = shadow.filter;\n  }\n\n  if (border) {\n    const { outline } = border;\n    if (outline) {\n      // TODO: outline  must be drawn outside the overflow clip path\n      const outlineNode = getPath().path.cloneNode(\n        true,\n      ) as SVGScreenshotNodeType;\n      outlineNode.setAttribute(\"fill\", \"none\");\n      outlineNode.setAttribute(\"stroke-width\", outline.borderWidth + \"px\");\n      outlineNode.setAttribute(\"stroke\", outline.borderColor);\n      outlineNode.setAttribute(\"stroke-linejoin\", \"round\");\n      outlineNode.setAttribute(\"stroke-linecap\", \"round\");\n      g.appendChild(outlineNode);\n      // const { path, rtl, rtr, rbr, rbl, showBorder, borderWidth } = getPath();\n      // if (path instanceof SVGRectElement) {\n      //   throw new Error(\"Outline not supported for rect element\");\n      // }\n      // path.setAttribute(\n      //   \"d\",\n      //   roundedRectPath(\n      //     /** This is to ensure the new-connection connection type radio buttons are aligned */\n      //     x - outline.borderWidth / 2 + (!showBorder ? borderWidth : 0),\n      //     y - outline.borderWidth / 2 + (!showBorder ? borderWidth : 0),\n      //     width + outline.borderWidth,\n      //     height + outline.borderWidth,\n      //     [rtl, rtr, rbr, rbl],\n      //   ),\n      // );\n    }\n\n    if (border.type === \"border\") {\n      getPath().path.setAttribute(\"stroke-width\", border.borderWidth + \"px\");\n      getPath().path.setAttribute(\"stroke\", border.borderColor);\n    } else if (border.type === \"borders\") {\n      addSpecificBorders(g, x, y, width, height, style);\n    }\n  }\n\n  return _path;\n};\n\nconst getRectanglePath = (\n  style: CSSStyleDeclaration,\n  { x, y, width, height }: Pick<SVGNodeLayout, \"x\" | \"y\" | \"width\" | \"height\">,\n  { border }: Pick<Awaited<ReturnType<typeof getWhatToRenderOnSVG>>, \"border\">,\n) => {\n  const minDimension = Math.min(width, height);\n  const [rtl = 0, rtr = 0, rbr = 0, rbl = 0] = [\n    style.borderTopLeftRadius,\n    style.borderTopRightRadius,\n    style.borderBottomRightRadius,\n    style.borderBottomLeftRadius,\n  ].map((r) => {\n    const radiusNumber = parseFloat(r);\n    if (r.includes(\"%\")) {\n      const clampedPercentage = Math.min(50, radiusNumber);\n      return (clampedPercentage / 100) * minDimension;\n    }\n    return radiusNumber;\n  });\n\n  const showBorder =\n    border?.type == \"border\" && border.borderColor !== \"rgba(0, 0, 0, 0)\";\n\n  const borderWidth = parseFloat(style.borderWidth);\n  const visibleBorderWidth = showBorder ? borderWidth : 0;\n  const adjusted = {\n    x: x + visibleBorderWidth / 2,\n    y: y + visibleBorderWidth / 2,\n    width: width - visibleBorderWidth,\n    height: height - visibleBorderWidth,\n  };\n\n  /** Use recangle if possible */\n  const hasSingleRadius = new Set([rtl, rtr, rbr, rbl]).size === 1;\n  const hasConstantBorder = !border || border.type === \"border\";\n  if (hasSingleRadius && hasConstantBorder) {\n    const rect = document.createElementNS(SVG_NAMESPACE, \"rect\") as Extract<\n      SVGScreenshotNodeType,\n      SVGRectElement\n    >;\n    rect.setAttribute(\"x\", adjusted.x);\n    rect.setAttribute(\"y\", adjusted.y);\n    rect.setAttribute(\"width\", adjusted.width);\n    rect.setAttribute(\"height\", adjusted.height);\n    rect.setAttribute(\"rx\", rtl);\n    rect.setAttribute(\"ry\", rtl);\n    return { path: rect, showBorder, rtl, rtr, rbr, rbl, borderWidth };\n  }\n\n  const path = document.createElementNS(SVG_NAMESPACE, \"path\") as Extract<\n    SVGScreenshotNodeType,\n    SVGPathElement\n  >;\n  path.setAttribute(\n    \"d\",\n    roundedRectPath(\n      /** This is to ensure the new-connection connection type radio buttons are aligned */\n      adjusted.x,\n      adjusted.y,\n      adjusted.width,\n      adjusted.height,\n      [rtl, rtr, rbr, rbl],\n    ),\n  );\n\n  path satisfies SVGElementTagNameMap[(typeof BORDER_ELEMENT_TYPES)[number]];\n  return { path, showBorder, rtl, rtr, rbr, rbl, borderWidth };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/containers/shadowToSVG.ts",
    "content": "import { isDefined } from \"src/utils/utils\";\n\n/**\n *   'rgba(255, 0, 0, 0.21) 0px 1px 2px 0px'\n *     or\n *   'rgba(255, 0, 0) 0px 1px 2px 0px'\n */\nexport const getBoxShadowAsDropShadow = (style: CSSStyleDeclaration) => {\n  if (!style.boxShadow || style.boxShadow === \"none\") return;\n  const boxShadows = style.boxShadow\n    .split(\"rgb\")\n    .filter((v) => v)\n    .map((v) => \"rgb\" + v.trim())\n    .map((v) => (v.endsWith(\",\") ? v.slice(0, -1) : v));\n\n  const parsedBoxShadows = boxShadows\n    .map((boxShadow) => {\n      const [colorPart, offsetParts = \"\"] = boxShadow.split(\")\");\n      const color = colorPart + \")\";\n      const offsets = offsetParts\n        .split(\" \")\n        .map((v) => v.trim())\n        .filter((v) => v);\n\n      const offsetValues = offsets.map((v) => parseFloat(v));\n      if (offsetValues.slice(0, 3).filter((v) => v === 0).length >= 3) {\n        return;\n      }\n      return {\n        color,\n        offsets,\n        offsetValues,\n      };\n    })\n    .filter(isDefined);\n\n  const filterParts = parsedBoxShadows.slice().map(\n    ({ color, offsets }) =>\n      `drop-shadow(${offsets\n        .slice(0, 3)\n        .map((part, index) => {\n          if (index === 2 && part.endsWith(\"px\")) {\n            // reduce blur radius due to SVG rendering differences\n            return `${(parseFloat(part) * 0.5).toFixed(1)}px`;\n          }\n          return part;\n        })\n        .join(\" \")} ${color})`,\n  );\n  const filter = filterParts.join(\" \");\n  return { filter, filterParts };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/domToSVG.ts",
    "content": "import { includes } from \"prostgles-types\";\nimport { tout } from \"src/utils/utils\";\nimport { elementToSVG, type SVGContext } from \"./containers/elementToSVG\";\nimport type { SVGScreenshotNodeType } from \"./domToThemeAwareSVG\";\nimport { renderSvg, wrapAllSVGText } from \"./text/textToSVG\";\nimport { BORDER_ELEMENT_TYPES } from \"./containers/rectangleToSVG\";\n\nexport const SVG_NAMESPACE = \"http://www.w3.org/2000/svg\";\n\nexport const domToSVG = async (node: HTMLElement) => {\n  const svg = document.createElementNS(SVG_NAMESPACE, \"svg\");\n  const cssDeclarations = new Map<string, string>();\n\n  // Get dimensions and position\n  const nodeBBox = node.getBoundingClientRect();\n\n  svg.setAttribute(\"width\", nodeBBox.width.toString());\n  svg.setAttribute(\"height\", nodeBBox.height.toString());\n  svg.setAttribute(\"viewBox\", `0 0 ${nodeBBox.width} ${nodeBBox.height}`);\n\n  // Create defs section for gradients, patterns, etc.\n  const defs = document.createElementNS(SVG_NAMESPACE, \"defs\");\n  svg.appendChild(defs);\n\n  const rootId = \"id-\" + crypto.randomUUID().split(\"-\")[0];\n  const context: SVGContext = {\n    docId: rootId,\n    offsetX: -nodeBBox.left,\n    offsetY: -nodeBBox.top,\n    width: nodeBBox.width,\n    height: nodeBBox.height,\n    defs: defs,\n    idCounter: 0,\n    cssDeclarations,\n    fontFamilies: [],\n  };\n  await elementToSVG(node, svg, context);\n  const style = document.createElementNS(SVG_NAMESPACE, \"style\");\n  style.setAttribute(\"type\", \"text/css\");\n  defs.appendChild(style);\n  style.textContent = Array.from(cssDeclarations.entries())\n    .map(([_selector, declaration]) => declaration)\n    .join(\"\\n\");\n\n  setBackdropFilters(svg);\n  const { remove } = renderSvg(svg);\n  wrapAllSVGText(svg);\n\n  /** Add textLength to prevent bugs in ios (It uses a different font which is wider and overflows the existing rects and clip paths) */\n  svg.querySelectorAll(\"text,tspan\").forEach((_text) => {\n    const text = _text as SVGTextElement | SVGTSpanElement;\n    const isMultiLine = text.textContent.includes(\"\\n\");\n    if (\n      isMultiLine ||\n      /** Has tspans that we'll handle separately */\n      (text instanceof SVGTextElement && text.children.length)\n    ) {\n      return;\n    }\n    const bbox = text.getBoundingClientRect();\n    const ctm = text.getCTM();\n    const scaleX = !ctm ? 1 : Math.hypot(ctm.a, ctm.c);\n    text.setAttribute(\"textLength\", bbox.width / scaleX);\n    text.setAttribute(\"lengthAdjust\", \"spacingAndGlyphs\");\n  });\n\n  /** Does not really seem effective */\n  // deduplicateSVGPaths(svg);\n  // await addFragmentViewBoxes(svg, 10);\n  repositionAbsoluteFixedAndSticky(svg);\n  moveBordersToTop(svg);\n  removeOverflowedElements(svg);\n  repositionMasks(svg);\n  remove();\n  await tout(100);\n\n  const xmlSerializer = new XMLSerializer();\n  const svgString = xmlSerializer.serializeToString(svg);\n  const [firstG, otherChild] = Array.from(svg.children).filter(\n    (c) => c instanceof SVGGElement,\n  );\n  if (!firstG) {\n    throw new Error(\"No SVG content generated\");\n  }\n  if (otherChild) {\n    throw new Error(\"Unexpected SVG structure - multiple root elements\");\n  }\n  firstG.setAttribute(\"id\", rootId);\n  return { svgString, svg, rootId };\n};\n\n/**\n * In divs the mask positioning is relative to the div, but in SVG it's relative to the SVG canvas\n */\nconst repositionMasks = (svg: SVGGElement) => {\n  const gElements = svg.querySelectorAll(\"g\");\n  gElements.forEach((g) => {\n    const { elemInfo } = g._whatToRender ?? {};\n    const maskImage = g.style.maskImage;\n    if (maskImage && elemInfo) {\n      const { x, y, width, height } = elemInfo;\n      const gBBox = g.getBoundingClientRect();\n      const offsetX = x - gBBox.left;\n      const offsetY = y - gBBox.top;\n      g.style.maskSize = `${width}px ${height}px`;\n      g.style.maskPosition = `${offsetX}px ${offsetY}px`;\n    }\n  });\n};\n\nconst removeOverflowedElements = (svg: SVGGElement) => {\n  svg.querySelectorAll(\"g\").forEach((g) => {\n    if (g._overflowClipPath) {\n      const { x, y, width, height } = g._overflowClipPath;\n      const clipXMin = x;\n      const clipYMin = y;\n      const clipXMax = x + width;\n      const clipYMax = y + height;\n      if (g.querySelector(`[style*=animation]`)) {\n        return;\n      }\n      g.childNodes.forEach((child) => {\n        /** Ignore animated elements */\n        if (child instanceof SVGGElement || child instanceof SVGTextElement) {\n          const elBBox = child.getBoundingClientRect();\n          const cXMin = elBBox.x;\n          const cYMin = elBBox.y;\n          const cXMax = elBBox.x + elBBox.width;\n          const cYMax = elBBox.y + elBBox.height;\n          const bboxesOverlap =\n            clipXMin < cXMax &&\n            clipXMax > cXMin &&\n            clipYMin < cYMax &&\n            clipYMax > cYMin;\n          if (!bboxesOverlap) {\n            child.remove();\n          }\n        }\n      });\n    }\n  });\n};\n\n/**\n * Hacky (because bg+border case is not handled) approach to ensure row card foreign key select fields rounded border corners are visible\n */\nconst moveBordersToTop = (svg: SVGGElement) => {\n  svg\n    .querySelectorAll<SVGScreenshotNodeType>(BORDER_ELEMENT_TYPES.join(\",\"))\n    .forEach((path) => {\n      if (\n        path._purpose?.border &&\n        !path._purpose.background &&\n        path.parentElement instanceof SVGGElement\n      ) {\n        path.parentElement.appendChild(path);\n      }\n    });\n};\n\nconst repositionAbsoluteFixedAndSticky = (svg: SVGGElement) => {\n  const [gBody, ...other] = Array.from(\n    svg.querySelectorAll<SVGGElement>(\":scope > g\"),\n  );\n  if (!gBody || other.length || gBody._domElement !== document.body) {\n    console.error(\"Unexpected SVG structure\", { svg, gBody, other });\n    throw new Error(\"Unexpected SVG structure\");\n  }\n  const gElements = Array.from(svg.querySelectorAll(\"g\"));\n  gElements.forEach((g) => {\n    const style = g._domElement && getComputedStyle(g._domElement);\n    if (style?.position === \"fixed\") {\n      gBody.appendChild(g);\n    }\n    if (style?.position === \"absolute\") {\n      const closestParent = getClosestRelativeOrAbsoluteParent(g) || gBody;\n      const closestParentOrGBody =\n        gBody.contains(closestParent) ? closestParent : gBody;\n      closestParentOrGBody.appendChild(g);\n    }\n\n    /** Move sticky to end */\n    if (\n      style?.position === \"sticky\" &&\n      g.parentElement instanceof SVGGElement\n    ) {\n      g.parentElement.appendChild(g);\n    }\n  });\n};\n\nconst getClosestRelativeOrAbsoluteParent = (g: SVGGElement) => {\n  let parentG = g.parentElement;\n  while (parentG && parentG instanceof SVGGElement && parentG._domElement) {\n    const position = getComputedStyle(parentG._domElement).position;\n    if (\n      includes([\"relative\", \"absolute\"] as const, position) &&\n      parentG._domElement !== g._domElement\n    ) {\n      return parentG;\n    }\n    parentG = parentG.parentElement;\n  }\n};\n\ndeclare global {\n  interface Element {\n    setAttribute(name: string, value: any): void;\n  }\n}\n\nconst setBackdropFilters = (svg: SVGGElement) => {\n  const gElements = svg.querySelectorAll(\"g\");\n  gElements.forEach((g) => {\n    const prevContent = g.parentElement?.parentElement?.previousSibling;\n    if (\n      g._whatToRender?.backdropFilter &&\n      prevContent &&\n      prevContent instanceof SVGGElement &&\n      prevContent._whatToRender?.elemInfo\n    ) {\n      const { width, height } = g._whatToRender.elemInfo;\n      const pBBox = prevContent._whatToRender.elemInfo;\n      // If not fully covering it then ignore it\n      if (width > pBBox.width * 0.8 && height > 0.8 * pBBox.height) {\n        prevContent.style.filter = g._whatToRender.backdropFilter;\n      }\n    }\n  });\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/domToThemeAwareSVG.ts",
    "content": "import { hashCode } from \"src/utils/hashCode\";\nimport { isDefined } from \"src/utils/utils\";\nimport { domToSVG } from \"./domToSVG\";\nimport { getCorrespondingDarkNode } from \"./getCorrespondingDarkNode\";\nimport { setThemeForSVGScreenshot } from \"./setThemeForSVGScreenshot\";\nimport type { TextForSVG } from \"./text/getTextForSVG\";\nimport { renderSvg } from \"./text/textToSVG\";\nimport type { getWhatToRenderOnSVG } from \"./utils/getWhatToRenderOnSVG\";\nexport const displayNoneIfDark = \"--dark-theme-hide\";\nexport const displayNoneIfLight = \"--light-theme-hide\";\nexport const domToThemeAwareSVG = async (\n  node: HTMLElement,\n  themes?: \"current\" | \"both\",\n) => {\n  const { svg: svgLight, rootId: svgLightRootId } = await domToSVG(node);\n  if (themes === \"current\") {\n    renderSvg(svgLight);\n    return;\n  }\n  svgLight.parentElement?.removeChild(svgLight);\n  await setThemeForSVGScreenshot(\"dark\");\n  const { svg: svgDark } = await domToSVG(node);\n  svgDark.parentElement?.removeChild(svgDark);\n  document.body.appendChild(svgDark);\n  document.body.appendChild(svgLight);\n\n  let varId = 0;\n  type CSSProperty = \"color\" | \"shadow\" | \"opacity\" | \"fontFamily\" | \"href\";\n  const getUniqueColorVarName = (\n    property: CSSProperty,\n    value: string,\n    darkValue: string,\n  ): string => {\n    /** This allows us to deduplicate g elements that have the same outerHTML */\n    let varName = `${(value + \"-\" + darkValue).replace(/[^a-zA-Z0-9]/g, \"\")}`;\n\n    // If bigger than a hash then use hash\n    if (varName.length > 40) {\n      varName = `${property}-${hashCode(varName)}`;\n    }\n\n    while (lightToDarkMap.get(value)?.some((c) => c.varName === varName)) {\n      varName = `${property}-${varId++}`;\n    }\n    return varName;\n  };\n  const lightToDarkMap = new Map<\n    string,\n    { darkValue: string; varName: string }[]\n  >();\n  const upsertCssVar = (\n    property: CSSProperty,\n    value: string,\n    darkValue: string,\n  ): string => {\n    const existingGroup = lightToDarkMap.get(value) ?? [];\n\n    const existing = existingGroup.find((c) => c.darkValue === darkValue);\n    if (existing) {\n      return existing.varName;\n    }\n    const varName = getUniqueColorVarName(property, value, darkValue);\n    lightToDarkMap.set(value, [\n      ...existingGroup,\n      { darkValue: darkValue, varName },\n    ]);\n    return varName;\n  };\n\n  const isVisibleColor = (color: string | null) => {\n    const isVisible =\n      color &&\n      color !== \"none\" &&\n      color !== \"currentColor\" &&\n      color !== \"rgba(0, 0, 0, 0)\" &&\n      color !== \"transparent\";\n\n    return isVisible;\n  };\n\n  const selector = \"line, path, text, foreignObject, g, use, rect\";\n  const lightNodes = svgLight.querySelectorAll<SVGScreenshotNodeType>(selector);\n  const darkNodes = svgDark.querySelectorAll<SVGScreenshotNodeType>(selector);\n\n  if (!svgDark.isConnected || !svgLight.isConnected) {\n    throw new Error(\n      \"SVGs must be connected to the DOM to ensure bbox calculations work.\",\n    );\n  }\n\n  const matchesMap: Map<\n    SVGScreenshotNodeType,\n    SVGScreenshotNodeType | undefined\n  > = new Map();\n  const matches = Array.from(lightNodes)\n    .map((lightNode, index) => {\n      // Ignore nested svgs\n      if (lightNode.ownerSVGElement !== svgLight) {\n        return;\n      }\n      const darkNode = getCorrespondingDarkNode(darkNodes, lightNode, index);\n      matchesMap.set(lightNode, darkNode);\n      return { lightNode, darkNode };\n    })\n    .filter(isDefined);\n\n  matches.forEach(({ lightNode, darkNode }) => {\n    if (lightNode instanceof SVGUseElement) {\n      const lightHref = lightNode.getAttribute(\"href\");\n      const darkHref = darkNode?.getAttribute(\"href\");\n      if (lightHref && darkHref) {\n        const darkRefImgSymbol =\n          svgDark.querySelector<SVGImageElement>(darkHref);\n        const lightRefImgSymbol =\n          svgLight.querySelector<SVGImageElement>(lightHref);\n        const darkRefImgData =\n          darkRefImgSymbol?.querySelector(\"image\")?.href.baseVal;\n        const lightRefImgData =\n          lightRefImgSymbol?.querySelector(\"image\")?.href.baseVal;\n        if (\n          darkRefImgData &&\n          lightRefImgData &&\n          darkRefImgData !== lightRefImgData\n        ) {\n          /** Add dark image into light svg */\n          const darkImageSymbolClone = darkRefImgSymbol.cloneNode(\n            true,\n          ) as SVGImageElement;\n          darkImageSymbolClone.id = `${darkImageSymbolClone.id}-dark`;\n          lightNode.ownerSVGElement\n            ?.querySelector(\"defs\")\n            ?.appendChild(darkImageSymbolClone);\n\n          const darkThemeUse = lightNode.cloneNode(true) as SVGUseElement;\n          darkThemeUse.setAttribute(\"href\", `#${darkImageSymbolClone.id}`);\n          darkThemeUse.style.opacity = `var(${displayNoneIfLight})`;\n          lightNode.parentElement?.appendChild(darkThemeUse);\n          lightNode.style.opacity = `var(${displayNoneIfDark})`;\n        }\n      }\n      return;\n    }\n    if (!darkNode) {\n      console.warn(\n        \"No corresponding dark node found for light node \" + lightNode.nodeName,\n        lightNode._bboxCode,\n      );\n      // if (\n      //   lightNode instanceof SVGTextElement\n      //   // &&\n      //   // lightNode.textContent?.includes(\"11:4\")\n      // ) {\n      //   console.log(lightNodes, darkNodes);\n      //   debugger;\n      // }\n      return;\n    }\n\n    /** Add extra elements from dark node (sometimes the background changes from transparent to color on match case button) */\n    // if (lightNode instanceof SVGGElement && darkNode instanceof SVGGElement) {\n    //   addNewChildren(lightNode, darkNode, matchesMap);\n    // }\n\n    const fill = lightNode.getAttribute(\"fill\");\n    const darkFill = darkNode.getAttribute(\"fill\");\n    if (fill !== darkFill) {\n      const varName = upsertCssVar(\"color\", fill || \"\", darkFill || fill || \"\");\n      lightNode.setAttribute(\"fill\", `var(--${varName})`);\n    }\n    const stroke = lightNode.getAttribute(\"stroke\");\n    const darkStroke = darkNode.getAttribute(\"stroke\");\n    if (stroke !== darkStroke) {\n      const varName = upsertCssVar(\n        \"color\",\n        stroke || \"\",\n        darkStroke || stroke || \"\",\n      );\n      lightNode.setAttribute(\"stroke\", `var(--${varName})`);\n    }\n    const color = lightNode.style.color;\n    if (color) {\n      const darkColor = darkNode.style.color;\n      const varName = upsertCssVar(\"color\", color, darkColor || color);\n      lightNode.style.color = `var(--${varName})`;\n    }\n\n    const opacity = lightNode.style.opacity;\n    if (!opacity || opacity !== \"1\") {\n      const darkOpacity = darkNode.style.opacity;\n      if (darkOpacity !== opacity) {\n        const varName = upsertCssVar(\n          \"opacity\",\n          opacity,\n          darkOpacity || opacity,\n        );\n        lightNode.style.opacity = `var(--${varName})`;\n      }\n    }\n\n    /** Just to save space */\n    if (lightNode instanceof SVGTextElement) {\n      const fontFamily = lightNode.getAttribute(\"font-family\");\n      if (fontFamily && fontFamily.length > 12) {\n        const darkFontFamily = darkNode.getAttribute(\"font-family\");\n        const varName = upsertCssVar(\n          \"fontFamily\",\n          fontFamily,\n          darkFontFamily || fontFamily,\n        );\n        lightNode.setAttribute(\"font-family\", `var(--${varName})`);\n      }\n    }\n\n    const filter = lightNode.style.filter;\n    const darkFilter = darkNode.style.filter;\n    if (darkFilter !== filter) {\n      const varName = upsertCssVar(\"shadow\", filter, darkFilter || filter);\n      lightNode.style.filter = `var(--${varName})`;\n    }\n\n    if (lightNode instanceof SVGForeignObjectElement) {\n      const color = lightNode.style.color;\n      if (color && isVisibleColor(color)) {\n        const darkColor = darkNode.style.color;\n        const varName = upsertCssVar(\"color\", color, darkColor || color);\n        lightNode.style.color = `var(--${varName})`;\n      }\n    }\n  });\n  const colorArr = Array.from(lightToDarkMap.entries()).flatMap(\n    ([lightColor, darkItems]) =>\n      darkItems.map(({ darkValue: darkColor, varName }) => ({\n        lightColor,\n        darkColor,\n        sameForBoth: lightColor === darkColor,\n        varName,\n      })),\n  );\n\n  const cssSheet = document.createElement(\"style\");\n  cssSheet.setAttribute(\"type\", \"text/css\");\n  svgLight.appendChild(cssSheet);\n  cssSheet.textContent = [\n    `:root #${svgLightRootId} { `,\n    `  ${displayNoneIfDark}: 1;`,\n    `  ${displayNoneIfLight}: 0;`,\n    ...colorArr.map(\n      ({ varName, lightColor }) => `  --${varName}: ${lightColor}; `,\n    ),\n    `}\\n`,\n  ].join(\"\\n\");\n  cssSheet.textContent += [\n    `@media (prefers-color-scheme: dark) { `,\n    ` :root #${svgLightRootId}  { `,\n    `  ${displayNoneIfDark}: 0;`,\n    `  ${displayNoneIfLight}: 1;`,\n    ...colorArr\n      .filter((c) => !c.sameForBoth)\n      .map(({ varName, darkColor }) => `  --${varName}: ${darkColor}; `),\n    `  }`,\n    `} \\n`,\n  ].join(\"\\n\");\n\n  const xmlSerializer = new XMLSerializer();\n  const svgString = xmlSerializer.serializeToString(svgLight);\n  document.body.removeChild(svgDark);\n  await setThemeForSVGScreenshot(undefined);\n  document.body.removeChild(svgLight);\n\n  if (themes === \"both\") {\n    renderSvg(svgLight);\n    return;\n  }\n  return {\n    light: svgString,\n    dark: xmlSerializer.serializeToString(svgDark),\n  };\n};\n\ndocument.body.addEventListener(\"keydown\", (e) => {\n  if (e.key === \"F2\") {\n    void domToThemeAwareSVG(document.body, \"current\");\n  } else if (e.key === \"F4\") {\n    void domToThemeAwareSVG(document.body, \"both\");\n  } else if (e.key === \"F6\") {\n    // eslint-disable-next-line no-debugger\n    debugger;\n  }\n});\n\n/** Interleave data */\nexport const getBBoxCode = (\n  element: HTMLElement,\n  {\n    x,\n    y,\n    width,\n    height,\n  }: {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  },\n) => {\n  return `${x}-${y}-${width}-${height}__${element.nodeName}${getElementPath(element).join(\"-\")}`;\n};\n\ndeclare global {\n  interface SVGGElement {\n    _gWrapperFor?: HTMLElement;\n    _overflowClipPath?: {\n      x: number;\n      y: number;\n      width: number;\n      height: number;\n    };\n    _domElement?: HTMLElement;\n    _whatToRender?: Awaited<ReturnType<typeof getWhatToRenderOnSVG>>;\n  }\n}\nexport type SVGScreenshotNodeType = (\n  | SVGPathElement\n  | SVGTextElement\n  | SVGRectElement\n  | SVGLineElement\n  | SVGForeignObjectElement\n) & {\n  _bboxCode?: string;\n  _purpose?: Partial<\n    Record<\n      \"border\" | \"background\" | \"shadow\" | \"scrollMask\" | \"backdropFilter\",\n      any\n    >\n  >;\n  _bbox?: DOMRect;\n  _gWrapperFor?: HTMLElement;\n  _domElement?: HTMLElement;\n  _domElementId?: string;\n  _domElementPath?: number[];\n  _domElementPathString?: string;\n  _textInfo?: TextForSVG;\n};\n\nconst getElementPath = (element: HTMLElement) => {\n  const path: number[] = [];\n  let current: HTMLElement | ParentNode | null = element;\n\n  while (current && current !== document.body) {\n    const index = Array.from(current.parentNode?.children ?? []).indexOf(\n      // @ts-ignore\n      current,\n    );\n    path.unshift(index);\n    current = current.parentNode;\n  }\n\n  return path;\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/getCorrespondingDarkNode.ts",
    "content": "import type { SVGScreenshotNodeType } from \"./domToThemeAwareSVG\";\n\nexport const getCorrespondingDarkNode = (\n  darkNodes: NodeListOf<SVGScreenshotNodeType>,\n  lightNode: SVGScreenshotNodeType,\n  index: number,\n): SVGScreenshotNodeType | undefined => {\n  let darkNode = darkNodes[index];\n\n  const darkNodesArr = Array.from(darkNodes);\n  const matchesTypes = darkNodesArr.filter(\n    (n) => n.nodeName === lightNode.nodeName,\n  );\n  darkNode = matchesTypes.find(\n    (n) => lightNode._bboxCode && n._bboxCode === lightNode._bboxCode,\n  );\n\n  if (darkNode) return darkNode;\n\n  const lightBBox = lightNode.getBBox();\n  let matchedTypeAndOverlap = matchesTypes.filter((n) => {\n    if (lightNode._gWrapperFor) {\n      return n._gWrapperFor === lightNode._gWrapperFor;\n    }\n    const nBBox = n.getBBox();\n\n    if (!lightBBox.width || !lightBBox.height) {\n      return (\n        lightBBox.width === nBBox.width &&\n        lightBBox.height === nBBox.height &&\n        lightBBox.x === nBBox.x &&\n        lightBBox.y === nBBox.y\n      );\n    }\n\n    const bboxesOverlap =\n      lightBBox.x < nBBox.x + nBBox.width &&\n      lightBBox.x + lightBBox.width > nBBox.x &&\n      lightBBox.y < nBBox.y + nBBox.height &&\n      lightBBox.y + lightBBox.height > nBBox.y;\n    return bboxesOverlap;\n  });\n  if (matchedTypeAndOverlap.length > 1) {\n    matchedTypeAndOverlap = matchedTypeAndOverlap.filter((n) =>\n      n.nodeName === \"use\" ?\n        n.getAttribute(\"href\") === lightNode.getAttribute(\"href\") ||\n        n.parentElement?.dataset.selector ===\n          lightNode.parentElement?.dataset.selector\n      : n.nodeName === \"path\" ?\n        n.getAttribute(\"d\") === lightNode.getAttribute(\"d\")\n      : n.nodeName === \"rect\" ?\n        n.getAttribute(\"x\") === lightNode.getAttribute(\"x\") &&\n        n.getAttribute(\"y\") === lightNode.getAttribute(\"y\") &&\n        n.getAttribute(\"width\") === lightNode.getAttribute(\"width\") &&\n        n.getAttribute(\"height\") === lightNode.getAttribute(\"height\")\n      : n.nodeName === \"line\" ?\n        n.getAttribute(\"x1\") === lightNode.getAttribute(\"x1\") &&\n        n.getAttribute(\"y1\") === lightNode.getAttribute(\"y1\") &&\n        n.getAttribute(\"x2\") === lightNode.getAttribute(\"x2\") &&\n        n.getAttribute(\"y2\") === lightNode.getAttribute(\"y2\")\n      : n.textContent === lightNode.textContent ||\n        /** Some text content changes between renders */\n        (n.getAttribute(\"x\") === lightNode.getAttribute(\"x\") &&\n          n.getAttribute(\"y\") === lightNode.getAttribute(\"y\")),\n    );\n  }\n  // if (\n  //   lightNode instanceof SVGTextElement &&\n  //   lightNode.textContent?.includes(\"11:5\")\n  //   // &&\n  //   // lightNode.getAttribute(\"fill\") === \"rgb(108, 6, 171)\"\n  // ) {\n  //   debugger;\n  // }\n  if (matchedTypeAndOverlap.length > 1 && lightNode.nodeName === \"path\") {\n    matchedTypeAndOverlap = matchedTypeAndOverlap.filter(\n      (n) => n._bboxCode?.length === lightNode._bboxCode?.length,\n    );\n  }\n  if (lightNode._gWrapperFor && matchedTypeAndOverlap.length > 1) {\n    throw new Error(\"Multiple matching gWrappers found\");\n  }\n  if (matchedTypeAndOverlap.length === 1) {\n    darkNode = matchedTypeAndOverlap[0];\n  }\n  return darkNode;\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/graphics/fontIconToSVG.ts",
    "content": "import { includes } from \"../../../dashboard/W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\nimport type { SVGContext, SVGNodeLayout } from \"../containers/elementToSVG\";\nimport { isElementNode } from \"../utils/isElementVisible\";\nimport { SVG_NAMESPACE } from \"../domToSVG\";\n\nexport const fontIconToSVG = async (\n  g: SVGGElement,\n  iconInfo: NonNullable<ReturnType<typeof getFontIconElement>>,\n  context: SVGContext,\n  layout: SVGNodeLayout,\n) => {\n  // const { x, y, height, width } = layout;\n  const style = getComputedStyle(iconInfo.element);\n  const fontFamily = style.getPropertyValue(\"font-family\");\n  const fontSize = parseInt(style.getPropertyValue(\"font-size\"));\n  const iconColor = style.getPropertyValue(\"color\");\n  await addFontFamily(fontFamily, context).catch((err) => {\n    /** Might be system font. Ignore error */\n    if (fontFamily.includes(\", \")) return;\n    console.error(\n      `Failed to add font ${fontFamily} to SVG:`,\n      iconInfo.element,\n      err,\n    );\n  });\n\n  const rect = iconInfo.element.getBoundingClientRect();\n  const width = rect.width;\n  const height = rect.height;\n  /** TODO: Must calculate the actual position of :after based on main content width + bbox - :after width */\n  const x =\n    iconInfo.iconStyle.type === \"after\" ?\n      layout.width + layout.x - rect.width / 2\n    : layout.x;\n  const y = rect.y;\n\n  // Create a text element with the icon\n  const textEl = document.createElementNS(SVG_NAMESPACE, \"text\");\n  textEl.setAttribute(\"x\", x + width / 2);\n  textEl.setAttribute(\"y\", y + height / 2);\n  textEl.setAttribute(\"font-family\", fontFamily);\n  textEl.setAttribute(\"font-size\", `${fontSize}px`);\n  textEl.setAttribute(\"fill\", iconColor);\n  textEl.setAttribute(\"text-anchor\", \"middle\");\n  textEl.setAttribute(\"dominant-baseline\", \"middle\");\n  textEl.textContent = iconInfo.content;\n\n  g.appendChild(textEl);\n};\n\n/**\n * TODO: extract used icons only using opentype.js\n */\nconst addFontFamily = async (familyName: string, context: SVGContext) => {\n  if (context.fontFamilies.includes(familyName)) {\n    return;\n  }\n\n  const fontURL = findFontURL(familyName);\n  if (!fontURL) return;\n  return fetch(fontURL)\n    .then((response) => response.blob())\n    .then((blob) => {\n      // Convert blob to data URL\n      return new Promise<string>((resolve) => {\n        const reader = new FileReader();\n        reader.onloadend = () => resolve(reader.result as string);\n        reader.readAsDataURL(blob);\n      });\n    })\n    .then((dataURL) => {\n      // Create a style element for the font\n      const styleEl = document.createElementNS(SVG_NAMESPACE, \"style\");\n      styleEl.textContent = `\n            @font-face {\n              font-family: \"${familyName}\";\n              src: url(\"${dataURL}\");\n              font-weight: normal;\n              font-style: normal;\n            }\n          `;\n\n      context.defs.appendChild(styleEl);\n\n      context.fontFamilies.push(familyName);\n    });\n};\n\nexport const getFontIconElement = (node: Node) => {\n  if (!isElementNode(node)) return;\n  const beforeStyle = getComputedStyle(node, \":before\");\n  const afterStyle = getComputedStyle(node, \":after\");\n  const iconStyle =\n    beforeStyle.content && !includes(beforeStyle.content, [\"\", \"none\"]) ?\n      ({ type: \"before\", style: beforeStyle } as const)\n    : afterStyle.content && !includes(afterStyle.content, [\"\", \"none\"]) ?\n      ({ type: \"after\", style: afterStyle } as const)\n    : \"\";\n  if (!iconStyle) {\n    return;\n  }\n  return {\n    element: node,\n    iconStyle,\n    content: iconStyle.style.content.replace(/['\"]/g, \"\"),\n  };\n};\n\nfunction findFontURL(fontFamily: string) {\n  // This is a simplified approach to find the font URL\n\n  for (const sheet of document.styleSheets) {\n    try {\n      const rules = sheet.cssRules;\n\n      for (let j = 0; j < rules.length; j++) {\n        const rule = rules[j];\n        if (rule instanceof CSSFontFaceRule) {\n          const fontFamilyValue = rule.style\n            .getPropertyValue(\"font-family\")\n            .replace(/['\"]/g, \"\");\n\n          if (fontFamilyValue === fontFamily) {\n            // Extract the URL from the src property\n            const src = rule.style.getPropertyValue(\"src\");\n            const urlMatch = src.match(/url\\(['\"]?([^'\"]+)['\"]?\\)/);\n\n            if (urlMatch && urlMatch[1]) {\n              return urlMatch[1];\n            }\n          }\n        }\n      }\n    } catch (e) {\n      // Security error, likely due to cross-origin stylesheet\n      console.warn(\"Could not access stylesheet:\", e);\n    }\n  }\n\n  throw new Error(`Could not find URL for font family: ${fontFamily}`);\n}\n"
  },
  {
    "path": "client/src/app/domToSVG/graphics/getForeignObject.ts",
    "content": "import { isImgNode } from \"../utils/isElementVisible\";\nimport { toFixed } from \"../utils/toFixed\";\n\nexport const isSVGElement = (element: Element): element is SVGElement => {\n  return element instanceof SVGElement;\n};\n\nexport const getForeignObject = async (\n  element: Element,\n  style: CSSStyleDeclaration,\n  x: number,\n  y: number,\n) => {\n  if (isImgNode(element) && element.src.endsWith(\".svg\")) {\n    return new Promise<SVGElement | undefined>((resolve) => {\n      fetch(element.src)\n        .then((response) => {\n          if (!response.ok) {\n            throw new Error(`Failed to fetch SVG: ${response.statusText}`);\n          }\n          return response.text();\n        })\n        .then((svgContent) => {\n          const parser = new DOMParser();\n          const svgDoc = parser.parseFromString(svgContent, \"image/svg+xml\");\n          const svgElement = svgDoc.documentElement;\n\n          const { width, height } = element;\n          const paddingLeft = parseFloat(style.paddingLeft) || 0;\n          const paddingTop = parseFloat(style.paddingTop) || 0;\n          svgElement.setAttribute(\"x\", `${toFixed(x + paddingLeft)}`);\n          svgElement.setAttribute(\"y\", `${toFixed(y + paddingTop)}`);\n          svgElement.setAttribute(\"width\", `${toFixed(width)}`);\n          svgElement.setAttribute(\"height\", `${toFixed(height)}`);\n          resolve(svgElement as unknown as SVGElement);\n        })\n        .catch((error) => {\n          console.error(\"Error fetching SVG:\", error);\n          resolve(undefined);\n        });\n    });\n  }\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/graphics/imgToSVG.ts",
    "content": "import { hashCode } from \"src/utils/hashCode\";\nimport type { SVGContext, SVGNodeLayout } from \"../containers/elementToSVG\";\nimport { SVG_NAMESPACE } from \"../domToSVG\";\nimport { canvasToDataURL } from \"../utils/canvasToDataURL\";\n\nexport const addImageFromDataURL = (\n  g: SVGGElement,\n  dataUrl: string,\n  context: SVGContext,\n  { style, height, width, x, y }: SVGNodeLayout,\n) => {\n  const sameDataUrlSymbol = Array.from(\n    context.defs.querySelectorAll(\"symbol\"),\n  ).find((symbol) => {\n    const imgInSymbol = symbol.querySelector(\"image\");\n    if (!imgInSymbol) {\n      return false;\n    }\n    const href = imgInSymbol.getAttribute(\"href\");\n    return href === dataUrl;\n  })?.id;\n\n  let imageSymbolId = sameDataUrlSymbol;\n  if (!imageSymbolId) {\n    imageSymbolId = \"id\" + hashCode(dataUrl.slice(100));\n    while (context.defs.querySelector(`#${imageSymbolId}`)) {\n      imageSymbolId += Math.floor(Math.random() * 10).toString();\n    }\n    const imageElem = document.createElementNS(SVG_NAMESPACE, \"image\");\n    imageElem.setAttribute(\"href\", dataUrl);\n    imageElem.setAttribute(\"width\", \"100%\");\n    imageElem.setAttribute(\"height\", \"100%\");\n    imageElem.setAttribute(\"preserveAspectRatio\", \"xMidYMid meet\");\n\n    const symbolElem = document.createElementNS(SVG_NAMESPACE, \"symbol\");\n    symbolElem.setAttribute(\"id\", imageSymbolId);\n    symbolElem.appendChild(imageElem);\n    symbolElem.setAttribute(\"viewBox\", `0 0 ${width} ${height}`);\n    context.defs.appendChild(symbolElem);\n  }\n\n  const useElem = document.createElementNS(SVG_NAMESPACE, \"use\");\n  useElem.setAttribute(\"href\", `#${imageSymbolId}`);\n  useElem.setAttribute(\"x\", x);\n  useElem.setAttribute(\"y\", y);\n  useElem.setAttribute(\"width\", width);\n  useElem.setAttribute(\"height\", height);\n  if (style.opacity && style.opacity !== \"1\") {\n    useElem.style.opacity = style.opacity;\n  }\n\n  g.appendChild(useElem);\n};\n\nexport const imgToSVG = async (\n  g: SVGGElement,\n  imgElement: HTMLImageElement,\n  layout: SVGNodeLayout,\n  context,\n) => {\n  const loadedImage = await loadImage(imgElement);\n\n  const dataUrl = convertImageToDataURL(loadedImage);\n  addImageFromDataURL(g, dataUrl, context, layout);\n};\n\nconst loadImage = async (\n  imgSource: HTMLImageElement,\n): Promise<HTMLImageElement> => {\n  if (!imgSource.complete) {\n    return new Promise((resolve, reject) => {\n      imgSource.onload = () => resolve(imgSource);\n      imgSource.onerror = () =>\n        reject(new Error(\"Failed to load the provided image element\"));\n    });\n  }\n  return imgSource;\n};\n\nconst convertImageToDataURL = (img: HTMLImageElement): string => {\n  try {\n    if (img.src.startsWith(\"data:\")) {\n      return img.src;\n    }\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = img.naturalWidth;\n    canvas.height = img.naturalHeight;\n    const ctx = canvas.getContext(\"2d\");\n\n    if (!ctx) {\n      throw new Error(\"Failed to get canvas context\");\n    }\n\n    ctx.drawImage(img, 0, 0);\n    return canvasToDataURL(canvas);\n  } catch (error) {\n    console.error(\"Error converting image to data URL:\", error);\n    // Fallback to original source if conversion fails\n    return img.src;\n  }\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/recordDomChanges.ts",
    "content": "import { isElementNode, isElementVisible } from \"./utils/isElementVisible\";\n\nexport const recordDomChanges = (targetNode: HTMLElement) => {\n  const config = {\n    attributes: true,\n    childList: true,\n    subtree: true,\n    attributeOldValue: true,\n  };\n\n  const observer = new MutationObserver((mutationList, observer) => {\n    for (const mutation of mutationList) {\n      const { target, oldValue } = mutation;\n      if (!isElementNode(target) || !isElementVisible(target).isVisible)\n        continue;\n      if (mutation.type === \"childList\") {\n        console.log(\n          \"A child node has been added or removed.\",\n          mutation.addedNodes,\n          mutation.removedNodes,\n        );\n        changes.push({\n          type: \"childList\",\n          target: target,\n          addedNodes: mutation.addedNodes,\n          removedNodes: mutation.removedNodes,\n          timestamp: Date.now(),\n        });\n      } else if (\n        mutation.type === \"attributes\" &&\n        mutation.attributeName === \"style\"\n      ) {\n        const currentStyle = target.getAttribute(\"style\");\n        const styleDiff = getStyleDiff(oldValue, currentStyle, target);\n        changes.push({\n          type: \"style\",\n          target: target,\n          oldValue,\n          currentStyle,\n          changes: styleDiff,\n          timestamp: Date.now(),\n        });\n      }\n    }\n  });\n\n  observer.observe(targetNode, config);\n};\n\nsetInterval(() => {\n  if (changes.length > 0) {\n    console.log(\"DOM changes detected:\", changes);\n  }\n}, 3000);\n\ntype DOMChange =\n  | {\n      type: \"childList\";\n      target: HTMLElement;\n      addedNodes: NodeList;\n      removedNodes: NodeList;\n      timestamp: number;\n    }\n  | {\n      type: \"style\";\n      target: HTMLElement;\n      oldValue: string | null;\n      currentStyle: string | null;\n      changes: Record<string, any>;\n      timestamp: number;\n    };\nconst changes: DOMChange[] = [];\n\nconst getStyleDiff = (\n  oldStyle: string | null,\n  currentStyle: string | null,\n  target: HTMLElement,\n) => {\n  const oldStyleObj = parseStyle(oldStyle);\n  const currentStyleObj = parseStyle(currentStyle);\n\n  // Determine added/changed properties\n  const added: Record<string, string> = {};\n  const changed: Record<string, { old: string; new: string }> = {};\n  const removed: Record<string, string> = {};\n\n  // Find added or changed properties\n  Object.entries(currentStyleObj).forEach(([prop, value]) => {\n    if (!(prop in oldStyleObj)) {\n      added[prop] = value;\n    } else if (oldStyleObj[prop] !== value) {\n      changed[prop] = { old: oldStyleObj[prop]!, new: value };\n    }\n  });\n\n  // Find removed properties\n  Object.entries(oldStyleObj).forEach(([prop, value]) => {\n    if (!(prop in currentStyleObj)) {\n      removed[prop] = value;\n    }\n  });\n\n  console.log(`Style attribute was modified on`, target);\n\n  if (Object.keys(added).length > 0) {\n    console.log(\"Added styles:\", added);\n  }\n\n  if (Object.keys(changed).length > 0) {\n    console.log(\"Changed styles:\", changed);\n  }\n\n  if (Object.keys(removed).length > 0) {\n    console.log(\"Removed styles:\", removed);\n  }\n  return { added, changed, removed };\n};\n\n// Parse the styles into key-value objects for easier comparison\nconst parseStyle = (styleString: string | null): Record<string, string> => {\n  const result: Record<string, string> = {};\n  if (!styleString) return result;\n\n  // Split the style string and extract property-value pairs\n  styleString.split(\";\").forEach((pair) => {\n    const trimmed = pair.trim();\n    if (!trimmed) return;\n\n    const [property, value] = trimmed.split(\":\").map((s) => s.trim());\n    if (property && value) {\n      result[property] = value;\n    }\n  });\n\n  return result;\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/setThemeForSVGScreenshot.ts",
    "content": "import { localSettings } from \"../../dashboard/localSettings\";\nimport { tout } from \"../../utils/utils\";\n\nexport const setThemeForSVGScreenshot = async (theme: undefined | \"dark\") => {\n  const resetUICallbacks: (() => void)[] = [];\n\n  /** Ensure that any sql suggestion popups are opened back */\n  const sqlEditor = document.querySelector<HTMLDivElement>(`div.ProstglesSQL`);\n  if (sqlEditor?.sqlRef?.editor) {\n    const suggestionsAreShown = //@ts-ignore\n      sqlEditor.sqlRef.editor._contextKeyService?.getContextKeyValue(\n        \"suggestWidgetVisible\",\n      );\n    const position = sqlEditor.sqlRef.editor.getPosition();\n    if (suggestionsAreShown && position) {\n      resetUICallbacks.push(async () => {\n        await tout(500);\n        const editor =\n          document.querySelector<HTMLDivElement>(`div.ProstglesSQL`);\n        editor?.sqlRef?.editor.setPosition(position);\n        await tout(100);\n        editor?.sqlRef?.editor.trigger(\n          \"demo\",\n          \"editor.action.triggerSuggest\",\n          {},\n        );\n      });\n    }\n  }\n\n  /** Re-open any closed popup menus */\n  const openPopupSelectors = Array.from(\n    document.querySelectorAll<HTMLDivElement>(\n      `div.PopupMenu_triggerWrapper.is-open, .select-button.is-open`,\n    ),\n  ).map((el) => getUniqueSelector(el));\n  openPopupSelectors.forEach((selector) => {\n    resetUICallbacks.push(async () => {\n      await tout(500);\n      const triggerBtn = document.querySelector<HTMLDivElement>(selector);\n      if (triggerBtn && !triggerBtn.classList.contains(\"is-open\")) {\n        triggerBtn.click();\n      }\n    });\n  });\n\n  localSettings.get().$set({ themeOverride: theme });\n  await tout(500);\n  for (const cb of resetUICallbacks) {\n    await cb();\n  }\n  await tout(500);\n};\n// function getUniqueSelector(element: HTMLElement) {\n//   // If element has an ID, use it\n//   if (element.id) {\n//     return `#${element.id}`;\n//   }\n\n//   // Build path from element to root\n//   const path: string[] = [];\n//   let current: HTMLElement | null = element;\n\n//   while (current && current !== document.body) {\n//     let selector = current.tagName.toLowerCase();\n\n//     // Add class if available\n//     if (current.className && typeof current.className === \"string\") {\n//       const classes = current.className.trim().split(/\\s+/).join(\".\");\n//       if (classes) {\n//         selector += `.${classes}`;\n//       }\n//     }\n\n//     // Add nth-child if needed to make it unique\n//     const parent: HTMLElement | null = current.parentElement;\n//     if (parent) {\n//       const siblings = Array.from(parent.children);\n//       const sameTagSiblings = siblings.filter(\n//         (s) => s.tagName === current?.tagName,\n//       );\n\n//       if (sameTagSiblings.length > 1) {\n//         const index = siblings.indexOf(current) + 1;\n//         selector += `:nth-child(${index})`;\n//       }\n//     }\n\n//     path.unshift(selector);\n//     current = parent;\n//   }\n\n//   return path.join(\" > \");\n// }\nfunction getUniqueSelector(element: HTMLElement): string {\n  // If element has an ID, use it\n  if (element.id) {\n    return `#${element.id}`;\n  }\n\n  // Build path from element to root\n  const path: string[] = [];\n  let current: HTMLElement | null = element;\n\n  while (current && current !== document.documentElement) {\n    const parent = current.parentElement;\n\n    if (!parent) break;\n\n    // Get index among all siblings\n    const siblings = Array.from(parent.children);\n    const index = siblings.indexOf(current) + 1;\n\n    // Build selector: tagname:nth-child(index)\n    const selector = `${current.tagName.toLowerCase()}:nth-child(${index})`;\n    path.unshift(selector);\n\n    current = parent;\n  }\n\n  return path.join(\" > \");\n}\n"
  },
  {
    "path": "client/src/app/domToSVG/text/getTextForSVG.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\nimport {\n  isElementNode,\n  isInputOrTextAreaNode,\n  isTextNode,\n} from \"../utils/isElementVisible\";\n\nexport type TextForSVG = {\n  style: CSSStyleDeclaration;\n  textContent: string;\n  textIndent: number | undefined;\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  isSingleLine: boolean | undefined;\n  numberOfLines?: number;\n  element: HTMLElement;\n};\n\nexport const getTextForSVG = (\n  element: HTMLElement,\n  style: CSSStyleDeclaration,\n  {\n    x,\n    y,\n    width,\n    height,\n  }: { x: number; y: number; width: number; height: number },\n): TextForSVG[] | undefined => {\n  if (isTextNode(element)) {\n    throw new Error(\"Not expecting this to be honest\");\n  }\n  if (isInputOrTextAreaNode(element)) {\n    const inputRect = element.getBoundingClientRect();\n    let textContent = element.value || element.placeholder;\n    if (\n      (element.type === \"date\" || element.type === \"datetime-local\") &&\n      element.value\n    ) {\n      try {\n        textContent = new Date(element.value).toLocaleString();\n      } catch {}\n    }\n    const isPlaceholder = !element.value;\n    if (!textContent.trim()) return;\n    const paddingLeft = parseFloat(style.paddingLeft) || 0;\n    const paddingTop = parseFloat(style.paddingTop) || 0;\n    const paddingBottom = parseFloat(style.paddingBottom) || 0;\n    const borderTop = parseFloat(style.borderTop) || 0;\n    const borderBottom = parseFloat(style.borderBottom) || 0;\n    const contentHeight =\n      inputRect.height - paddingTop - paddingBottom - borderTop - borderBottom;\n    const actualStyle =\n      isPlaceholder ? getComputedStyle(element, \"::placeholder\") : style;\n    const fontSize = parseFloat(actualStyle.fontSize);\n    const fontYPadding = Math.max(0, contentHeight - fontSize);\n    const borderLeft = parseFloat(style.borderLeftWidth) || 0;\n\n    const yTextOffsetForInput =\n      -paddingBottom - borderBottom - fontYPadding / 2 - 0.5;\n    const isTextArea = element instanceof HTMLTextAreaElement;\n    const y =\n      isTextArea ?\n        inputRect.y + paddingTop + fontSize + borderTop + 3\n      : inputRect.y + inputRect.height + yTextOffsetForInput;\n    const result = [\n      {\n        style: actualStyle,\n        textContent,\n        x: inputRect.x + paddingLeft + borderLeft,\n        y,\n        width: inputRect.width - paddingLeft,\n        height: inputRect.height - paddingTop,\n        textIndent: 0,\n        isSingleLine: undefined,\n        element,\n      },\n    ];\n    return result;\n  }\n\n  return Array.from(element.childNodes)\n    .map((childTextNode, index) => {\n      if (!isTextNode(childTextNode)) return;\n      const textContent = childTextNode.textContent;\n      if (!textContent || !textContent.trim()) return;\n      const range = document.createRange();\n      range.selectNodeContents(childTextNode);\n      const textRect = range.getBoundingClientRect();\n\n      const maxX = x + width;\n      const maxY = y + height;\n      const textMaxX = textRect.x + textRect.width;\n      const textMaxY = textRect.y + textRect.height;\n      const textxMaxWidth = Math.min(textMaxX, maxX) - textRect.x;\n      const textyMaxHeight = Math.min(textMaxY, maxY) - textRect.y;\n      const visibleTextWidth = Math.min(textRect.width, textxMaxWidth);\n      const visibleTextHeight = Math.min(textRect.height, textyMaxHeight);\n      const spanHeight =\n        element instanceof HTMLSpanElement ? element.clientHeight : undefined;\n      const numberOfLines = range.getClientRects().length;\n      if (visibleTextWidth && visibleTextHeight) {\n        const edgeRects = getTextEdgeRects(childTextNode, textContent.length);\n        const textIndent = edgeRects.startCharRect.left - textRect.x;\n        const res: TextForSVG = {\n          style: {\n            // eslint-disable-next-line @typescript-eslint/no-misused-spread\n            ...style,\n            /** This is done to preserve leading spaces between spans of the same text block */\n            whiteSpace: textContent.startsWith(\" \") ? \"pre\" : style.whiteSpace,\n          },\n          textContent,\n          x: textRect.x,\n          y: textRect.y + 1,\n          /** This ensures the actual visible/non overflown size of text is used */\n          width: visibleTextWidth,\n          height: spanHeight ?? visibleTextHeight,\n          textIndent: Math.max(0, textIndent),\n          isSingleLine:\n            edgeRects.startCharRect.top === edgeRects.endCharRect.top,\n          numberOfLines,\n          element,\n        };\n        return res;\n      }\n    })\n    .filter(isDefined);\n};\n\nconst getTextEdgeRects = (textNode: Text, contentLength: number) => {\n  const range = document.createRange();\n  range.setStart(textNode, 0);\n  range.setEnd(textNode, 1);\n\n  const startCharRect = range.getBoundingClientRect();\n\n  range.setStart(textNode, contentLength - 1);\n  range.setEnd(textNode, contentLength);\n\n  const endCharRect = range.getBoundingClientRect();\n\n  return { startCharRect, endCharRect };\n};\n\n// Function to recursively process text nodes\nconst getTextNodes = (\n  element: HTMLElement,\n  parentStyles: Partial<CSSStyleDeclaration>,\n) => {\n  const computedStyles = window.getComputedStyle(element);\n\n  // Merge parent styles with current element styles\n  const currentStyles = {\n    fontSize: computedStyles.fontSize,\n    fontFamily: computedStyles.fontFamily,\n    fontWeight: computedStyles.fontWeight,\n    fontStyle: computedStyles.fontStyle,\n    color: computedStyles.color,\n    // eslint-disable-next-line @typescript-eslint/no-misused-spread\n    ...parentStyles,\n  };\n\n  return Array.from(element.childNodes)\n    .map((node) => {\n      if (isTextNode(node) && node.textContent) {\n        const text = node.textContent.trim();\n        if (text) {\n          // Create a range for this text node\n          const range = document.createRange();\n          range.selectNodeContents(node);\n          const rect = range.getBoundingClientRect();\n\n          return {\n            text: text,\n            x: rect.left,\n            y: rect.top,\n            width: rect.width,\n            height: rect.height,\n            styles: currentStyles,\n            element: element.tagName || \"TEXT\",\n          };\n        }\n      } else if (isElementNode(node)) {\n        // Recursively process child elements\n        getTextNodes(node, currentStyles);\n      }\n    })\n    .filter(isDefined);\n};\n\nconst calculateVerticalPosition = (inputElement: HTMLInputElement) => {\n  const computedStyle = window.getComputedStyle(inputElement);\n\n  // Get dimensions\n  const inputHeight = inputElement.offsetHeight;\n  const fontSize = parseFloat(computedStyle.fontSize);\n  const lineHeight =\n    computedStyle.lineHeight === \"normal\" ?\n      fontSize * 1.2\n    : parseFloat(computedStyle.lineHeight);\n\n  // Get padding\n  const paddingTop = parseFloat(computedStyle.paddingTop);\n  const paddingBottom = parseFloat(computedStyle.paddingBottom);\n  const borderTop = parseFloat(computedStyle.borderTopWidth);\n  const borderBottom = parseFloat(computedStyle.borderBottomWidth);\n\n  // Calculate available content height\n  const contentHeight =\n    inputHeight - paddingTop - paddingBottom - borderTop - borderBottom;\n\n  // Calculate vertical center position\n  const textVerticalCenter =\n    paddingTop + borderTop + (contentHeight - lineHeight) / 2;\n\n  return {\n    textTop: textVerticalCenter,\n    textCenter: textVerticalCenter + lineHeight / 2,\n    textBottom: textVerticalCenter + lineHeight,\n    contentHeight: contentHeight,\n    lineHeight: lineHeight,\n  };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/text/textToSVG.ts",
    "content": "import { includes } from \"prostgles-types\";\nimport { SVG_NAMESPACE } from \"../domToSVG\";\nimport type { SVGScreenshotNodeType } from \"../domToThemeAwareSVG\";\nimport { isInputOrTextAreaNode } from \"../utils/isElementVisible\";\nimport { toFixed } from \"../utils/toFixed\";\nimport type { TextForSVG } from \"./getTextForSVG\";\nconst _singleLineEllipsis = \"_singleLineEllipsis\" as const;\nconst TEXT_WIDTH_ATTR = \"data-text-width\";\nconst TEXT_HEIGHT_ATTR = \"data-text-height\";\n\nconst getLineBreakPartsWithDelimiters = (content: string) =>\n  content.split(/([\\s\\-–—:]+)/);\n\nexport const textToSVG = (\n  element: HTMLElement,\n  g: SVGGElement,\n  textInfo: TextForSVG,\n  elementStyle: CSSStyleDeclaration,\n  bboxCode: string,\n) => {\n  const {\n    height,\n    style: placeholderOrElementStyle,\n    textContent: content,\n    width,\n    x,\n    y,\n    isSingleLine,\n    numberOfLines,\n  } = textInfo;\n  const style = placeholderOrElementStyle;\n  if (!content.trim()) return;\n  const textNode = document.createElementNS(SVG_NAMESPACE, \"text\");\n  (textNode as SVGScreenshotNodeType)._bboxCode = bboxCode;\n  (textNode as SVGScreenshotNodeType)._textInfo = textInfo;\n  textNode.setAttribute(TEXT_WIDTH_ATTR, toFixed(width));\n  textNode.setAttribute(TEXT_HEIGHT_ATTR, toFixed(height));\n  textNode.setAttribute(\"x\", toFixed(x));\n\n  /** In firefox it seems the text nodes don't have font size */\n  const textNodeStyle = {\n    color: style.color || elementStyle.color,\n    fontFamily: style.fontFamily || elementStyle.fontFamily,\n    fontSize: style.fontSize || elementStyle.fontSize,\n    fontWeight: style.fontWeight || elementStyle.fontWeight,\n    letterSpacing: style.letterSpacing || elementStyle.letterSpacing,\n    textDecoration: style.textDecoration || elementStyle.textDecoration,\n    lineHeight: style.lineHeight || elementStyle.lineHeight,\n    whiteSpace: style.whiteSpace || elementStyle.whiteSpace,\n    textOverflow: style.textOverflow || elementStyle.textOverflow,\n    textTransform: style.textTransform || elementStyle.textTransform,\n    fontStyle: style.fontStyle || elementStyle.fontStyle,\n  };\n  const fontSize = parseFloat(textNodeStyle.fontSize);\n  const isInputElement = isInputOrTextAreaNode(element);\n  textNode.setAttribute(\"y\", toFixed((isInputElement ? y : y + fontSize) - 2));\n  textNode.setAttribute(\"fill\", textNodeStyle.color);\n  textNode.setAttribute(\"font-family\", textNodeStyle.fontFamily);\n  textNode.setAttribute(\"font-size\", textNodeStyle.fontSize);\n  textNode.setAttribute(\"font-weight\", textNodeStyle.fontWeight);\n  textNode.setAttribute(\"font-style\", textNodeStyle.fontStyle);\n  textNode.setAttribute(\"letter-spacing\", textNodeStyle.letterSpacing);\n  textNode.setAttribute(\"text-decoration\", textNodeStyle.textDecoration);\n  textNode.style.lineHeight = textNodeStyle.lineHeight;\n  textNode.style.whiteSpace = textNodeStyle.whiteSpace;\n  textNode.style.textTransform = textNodeStyle.textTransform;\n  const nonWrappingWhiteSpaces = [\"nowrap\", \"pre\", \"reverse\", \"reverse-wrap\"];\n  if (\n    textNodeStyle.textOverflow === \"ellipsis\" &&\n    (includes(nonWrappingWhiteSpaces, textNodeStyle.whiteSpace) || isSingleLine)\n  ) {\n    textNode[_singleLineEllipsis] = true;\n  }\n  textNode.setAttribute(\"text-anchor\", \"start\");\n\n  textNode.textContent = content.trimEnd();\n\n  g.appendChild(textNode);\n};\n\nconst tolerance = 2;\nconst wrapTextIfOverflowing = (\n  textNode: Extract<SVGScreenshotNodeType, SVGTextElement>,\n  width: number,\n  height: number,\n  content: string,\n) => {\n  const currTextLength = textNode.getComputedTextLength();\n  const {\n    textIndent = 0,\n    isSingleLine,\n    style,\n    numberOfLines,\n  } = textNode._textInfo ?? {};\n\n  if (currTextLength <= width + tolerance && !textIndent) {\n    return;\n  } else if (textNode[_singleLineEllipsis]) {\n    while (\n      textNode.getComputedTextLength() > width + tolerance &&\n      content.length\n    ) {\n      content = content.slice(0, -1);\n      textNode.textContent = content + \"...\";\n    }\n    return;\n  }\n  textNode.textContent = \"\";\n  let line: string[] = [];\n  const wordsWithDelimiters = getLineBreakPartsWithDelimiters(content).filter(\n    (w) => w !== \"\",\n  );\n  const fontSize = textNode.getAttribute(\"font-size\") || \"16\";\n  const lineHeightPx =\n    parseFloat(textNode.style.lineHeight) || 1.1 * parseFloat(fontSize);\n  const x = parseFloat(textNode.getAttribute(\"x\") || \"0\");\n  const maxLines = numberOfLines ?? Math.floor(height / lineHeightPx);\n  let tspan = document.createElementNS(SVG_NAMESPACE, \"tspan\");\n  tspan.setAttribute(\"x\", toFixed(x + textIndent));\n  tspan.setAttribute(\"dy\", 0);\n  tspan.textContent =\n    style?.whiteSpace === \"pre\" ? content : content.trimStart();\n  textNode.appendChild(tspan);\n\n  const willNotWrap =\n    isSingleLine || textNode._textInfo?.element instanceof HTMLInputElement;\n  if (willNotWrap) {\n    return;\n  }\n  let lineNumber = 1;\n  const hasTextOverflowEllipsis =\n    style?.textOverflow === \"ellipsis\" && style.overflow === \"hidden\";\n  for (let wordIndex = 0; wordIndex < wordsWithDelimiters.length; wordIndex++) {\n    const isFirstLine = lineNumber === 1;\n    const currentLineWidth = isFirstLine ? width - textIndent : width;\n    const word = wordsWithDelimiters[wordIndex]!;\n    line.push(word);\n\n    const setTextContent = () => {\n      tspan.textContent =\n        isFirstLine ? line.join(\"\") : line.join(\"\").trimStart();\n    };\n\n    setTextContent();\n    const textLen = tspan.getComputedTextLength();\n\n    const textIsOverflowing = textLen > currentLineWidth + tolerance;\n    const cannotWrapMoreBecauseItsASingleWord = line.length === 1;\n    if (textIsOverflowing && !cannotWrapMoreBecauseItsASingleWord) {\n      if (\n        numberOfLines &&\n        lineNumber === numberOfLines &&\n        !hasTextOverflowEllipsis\n      ) {\n        return;\n      }\n\n      line.pop(); // Remove the word that caused overflow\n\n      setTextContent();\n\n      // Move to next line if possible\n      lineNumber++;\n\n      if (lineNumber >= maxLines && hasTextOverflowEllipsis) {\n        // Add ellipsis to indicate truncation if there's room\n        if (\n          tspan.textContent &&\n          tspan.getComputedTextLength() < currentLineWidth - 10\n        ) {\n          tspan.textContent += \"...\";\n        }\n        return;\n      }\n\n      // Create new tspan for next line\n      line = [word];\n      tspan = document.createElementNS(SVG_NAMESPACE, \"tspan\");\n      tspan.setAttribute(\"x\", toFixed(x));\n      tspan.setAttribute(\"dy\", toFixed(lineHeightPx) + \"px\");\n      textNode.appendChild(tspan);\n      tspan.textContent = word;\n    }\n  }\n};\n\nconst unnestRedundantGElements = (svg: SVGElement) => {\n  const gElements = svg.querySelectorAll(\"g\");\n\n  gElements.forEach((gElement) => {\n    if (\n      gElement.childElementCount === 1 &&\n      gElement.firstElementChild?.tagName.toLowerCase() === \"g\" &&\n      gElement.attributes.length === 0\n    ) {\n      const childG = gElement.firstElementChild as SVGElement;\n\n      gElement.parentNode?.replaceChild(childG, gElement);\n\n      unnestRedundantGElements(svg);\n    }\n  });\n\n  return svg;\n};\n\nexport const wrapAllSVGText = (svg: SVGElement) => {\n  if (!svg.isConnected) {\n    throw new Error(\"SVG must be in the DOM for bbox calculations\");\n  }\n\n  unnestRedundantGElements(svg);\n  svg\n    .querySelectorAll<SVGTextElement>(`text[${TEXT_WIDTH_ATTR}]`)\n    .forEach((text) => {\n      const textWidth = text.getAttribute(TEXT_WIDTH_ATTR);\n      const textHeight = text.getAttribute(TEXT_HEIGHT_ATTR);\n      if (!text.textContent || !textWidth || !textHeight) return;\n\n      wrapTextIfOverflowing(\n        text,\n        +textWidth,\n        +textHeight,\n        text.textContent || \"\",\n      );\n    });\n  svg\n    .querySelectorAll<SVGTextElement>(`text[${TEXT_WIDTH_ATTR}]`)\n    .forEach((text) => {\n      text.removeAttribute(TEXT_WIDTH_ATTR);\n      text.removeAttribute(TEXT_HEIGHT_ATTR);\n    });\n};\n\n/**\n * Appends svg to document to ensure the bbox/text length calcs work\n * */\nexport const renderSvg = (svg: SVGElement) => {\n  const topStyle = {\n    position: \"absolute\",\n    top: \"0\",\n    left: \"0\",\n    zIndex: \"9999\",\n  } as const;\n\n  const getIsAppended = () => document.body.contains(svg);\n\n  if (!getIsAppended()) {\n    Object.entries(topStyle).forEach(([key, value]) => {\n      svg.style[key] = value;\n    });\n    document.body.appendChild(svg);\n  }\n\n  return {\n    remove: () => {\n      if (getIsAppended()) {\n        svg.removeAttribute(\"style\");\n        svg.remove();\n      }\n    },\n  };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/utils/addNewChildren.ts",
    "content": "import { isDefined } from \"src/utils/utils\";\nimport {\n  displayNoneIfLight,\n  type SVGScreenshotNodeType,\n} from \"../domToThemeAwareSVG\";\n\nexport const addNewChildren = (\n  lightNode: SVGScreenshotNodeType,\n  darkNode: SVGScreenshotNodeType,\n  matchesMap: Map<SVGScreenshotNodeType, SVGScreenshotNodeType | undefined>,\n) => {\n  const matchingChildren = Array.from(lightNode.children)\n    .map((lightChild, index) => {\n      const darkChild = matchesMap.get(lightChild as SVGScreenshotNodeType);\n      if (darkChild) {\n        return {\n          lightChild: lightChild as SVGScreenshotNodeType,\n          darkChild,\n          index,\n        };\n      }\n    })\n    .filter(isDefined);\n\n  const newChildren = Array.from(darkNode.children)\n    .map((darkChild, index) => {\n      if (\n        !(\n          darkChild instanceof SVGGElement ||\n          darkChild instanceof SVGPathElement ||\n          darkChild instanceof SVGRectElement ||\n          darkChild instanceof SVGCircleElement ||\n          darkChild instanceof SVGUseElement\n        )\n      )\n        return;\n      const isMatched = matchingChildren.find(\n        (mc) => mc.darkChild === darkChild,\n      );\n      if (!isMatched) {\n        return {\n          darkChild: darkChild as SVGScreenshotNodeType,\n          index,\n        };\n      }\n    })\n    .filter(isDefined);\n\n  /** Add new nodes to lightNode */\n  newChildren.forEach(({ darkChild, index }) => {\n    const clonedChild = darkChild.cloneNode(true) as SVGScreenshotNodeType;\n    clonedChild.style.opacity = `var(${displayNoneIfLight})`;\n    lightNode.insertBefore(clonedChild, lightNode.children[index] || null);\n  });\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/utils/canvasToDataURL.ts",
    "content": "export const canvasToDataURL = (\n  canvas: HTMLCanvasElement,\n  quality = 0.8,\n): string => {\n  return canvas.toDataURL(\"image/webp\", quality);\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/utils/copyAnimationStylesToSvg.ts",
    "content": "import type { SVGContext } from \"../containers/elementToSVG\";\n\n/**\n * Extracts and clones CSS animations and keyframes from an element\n */\nconst getAnimationKeyframeRules = (element: Element) => {\n  const computedStyle = window.getComputedStyle(element);\n  const animationName = computedStyle.animationName;\n\n  if (!animationName || animationName === \"none\") {\n    return;\n  }\n\n  // const styleElement = targetDocument.createElement(\"style\");\n  const keyframeRules: CSSKeyframesRule[] = [];\n  const animationNames = animationName.split(\",\").map((name) => name.trim());\n\n  // Search through all stylesheets for matching @keyframes rules\n  for (const sheet of Array.from(document.styleSheets)) {\n    try {\n      const rules = sheet.cssRules;\n\n      for (const rule of Array.from(rules)) {\n        if (\n          rule instanceof CSSKeyframesRule &&\n          animationNames.includes(rule.name)\n        ) {\n          keyframeRules.push(rule);\n        }\n      }\n    } catch (e) {\n      // Skip stylesheets that can't be accessed (CORS)\n      console.warn(\"Cannot access stylesheet:\", e);\n    }\n  }\n\n  return keyframeRules;\n};\n\n/**\n * Copies inline animation styles to the cloned element\n */\nconst copyAnimationStylesToSvg = (\n  source: CSSStyleDeclaration,\n  target: HTMLElement | SVGElement,\n) => {\n  const computedStyle = source;\n  const animationProperties = [\n    \"animationName\",\n    \"animationDuration\",\n    \"animationTimingFunction\",\n    \"animationDelay\",\n    \"animationIterationCount\",\n    \"animationDirection\",\n    \"animationFillMode\",\n    \"animationPlayState\",\n  ] as const;\n\n  if (!source.animationName || source.animationName === \"none\") {\n    return false;\n  }\n  animationProperties.forEach((prop) => {\n    const value = computedStyle[prop];\n    if (value && value !== \"none\") {\n      target.style[prop] = value;\n    }\n  });\n  return true;\n};\n\nconst copyAnimationsToSvg = (\n  sourceElement: Element,\n  sourceCss: CSSStyleDeclaration,\n  wrapperG: SVGElement,\n  cssDeclarations: SVGContext[\"cssDeclarations\"],\n): void => {\n  // Extract keyframes from the original element's styles\n  const copiedAnimations = copyAnimationStylesToSvg(sourceCss, wrapperG);\n  if (!copiedAnimations) {\n    console.error(\"Unexpected: could not copy animations\");\n    return;\n  }\n  const animationKeyframeRules = getAnimationKeyframeRules(sourceElement);\n  if (animationKeyframeRules) {\n    // defs.appendChild(animationStyles);\n    // cssDeclarations.push(...animationKeyframeRules);\n  }\n  animationKeyframeRules?.forEach((rule) => {\n    const ruleText = rule.cssText;\n    if (!cssDeclarations.has(ruleText)) {\n      cssDeclarations.set(ruleText, ruleText);\n    }\n  });\n};\n\nexport const getAnimationsHandler = (sourceElement: Element) => {\n  let hasAnimations = false as boolean;\n  /** Ensures bbox calculations are stable */\n  sourceElement.getAnimations().forEach((animation) => {\n    animation.cancel();\n    // animation.pause();\n    // animation.currentTime = 0;\n    hasAnimations = true;\n  });\n\n  if (!hasAnimations) return;\n  return (\n    sourceCss: CSSStyleDeclaration,\n    wrapperG: SVGElement,\n    cssDeclarations: SVGContext[\"cssDeclarations\"],\n    /**\n     * Important for correct animation scaling in SVG. Otherwise percentages are\n     * calculated based on the viewport, not the element's bounding box.\n     */\n    addFillBox: boolean,\n  ) => {\n    if (addFillBox) {\n      wrapperG.style.transformBox = \"fill-box\";\n    }\n    return copyAnimationsToSvg(\n      sourceElement,\n      sourceCss,\n      wrapperG,\n      cssDeclarations,\n    );\n  };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/utils/getWhatToRenderOnSVG.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\nimport {\n  getBackdropFilter,\n  getBackgroundColor,\n} from \"../containers/bgAndBorderToSVG\";\nimport type { SVGContext } from \"../containers/elementToSVG\";\nimport { getFontIconElement } from \"../graphics/fontIconToSVG\";\nimport { getTextForSVG } from \"../text/getTextForSVG\";\nimport { isElementVisible, isImgNode, isSVGNode } from \"./isElementVisible\";\nimport { getForeignObject } from \"../graphics/getForeignObject\";\nimport { includes } from \"src/dashboard/W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\n\nconst attributesToKeep = [\n  \"data-command\",\n  \"data-key\",\n  \"data-label\",\n  \"role\",\n] as const;\nexport type WhatToRenderOnSVG = Awaited<\n  ReturnType<typeof getWhatToRenderOnSVG>\n>;\nexport const getWhatToRenderOnSVG = async (\n  element: HTMLElement,\n  context: SVGContext,\n  parentSvg: SVGElement | SVGGElement,\n) => {\n  const { isVisible, style, bbox } = isElementVisible(element);\n  // Calculate absolute position\n  const x = bbox.left + context.offsetX;\n  const y = bbox.top + context.offsetY;\n  const width = bbox.width;\n  const height = bbox.height;\n  const elemInfo = {\n    x,\n    y,\n    width,\n    height,\n    style,\n    bbox,\n    isVisible,\n  };\n\n  /** Used to highlight so will render as a rectangle */\n  const attributeData = attributesToKeep.reduce(\n    (acc, attr) => {\n      const attrValue = element.getAttribute(attr);\n      if (attrValue) {\n        return { ...acc, [attr]: attrValue };\n      }\n      return acc;\n    },\n    undefined as\n      | Partial<Record<(typeof attributesToKeep)[number], string>>\n      | undefined,\n  );\n\n  let mightBeHovered = false;\n  if (!isVisible) {\n    const hoverClasses = [\n      \"show-on-row-hover\",\n      \"show-on-hover\",\n      \"show-on-parent-hover\",\n      \"show-on-trigger-hover\",\n    ];\n    if (\n      hoverClasses.some(\n        (cls) => element.classList.contains(cls) || element.closest(`.${cls}`),\n      ) &&\n      (attributeData ||\n        element.querySelector(`[data-command], [data-key], [data-label]`))\n    ) {\n      mightBeHovered = true;\n    } else return { elemInfo };\n  }\n\n  const background = getBackgroundColor(style);\n  const parentBackground =\n    background &&\n    element.parentElement &&\n    getBackgroundColor(getComputedStyle(element.parentElement));\n\n  /**\n   * Used to prevent drawing over rounded input border corners\n   */\n  const backgroundSameAsRenderedParent =\n    background &&\n    background === parentBackground &&\n    (parentSvg as SVGGElement)._domElement === element.parentElement;\n\n  const backdropFilter = getBackdropFilter(style);\n  const childAffectingStyles: Partial<\n    Pick<CSSStyleDeclaration, \"opacity\" | \"position\">\n  > = {};\n  if (style.opacity && style.opacity !== \"1\") {\n    childAffectingStyles.opacity = style.opacity;\n  }\n  if (includes(style.position, [\"fixed\", \"absolute\", \"relative\"])) {\n    childAffectingStyles.position = style.position;\n  }\n\n  const foreignObject = await getForeignObject(element, style, x, y);\n  const fontIcon = getFontIconElement(element);\n  const image =\n    isSVGNode(element) ?\n      {\n        type: \"svgElement\" as const,\n        element,\n      }\n    : foreignObject ?\n      {\n        type: \"foreignObject\" as const,\n        foreignObject,\n      }\n    : fontIcon ?\n      {\n        type: \"fontIcon\" as const,\n        ...fontIcon,\n      }\n    : isImgNode(element) ?\n      {\n        type: \"img\" as const,\n        element,\n      }\n    : style.maskImage.startsWith(\"url(\") ?\n      {\n        type: \"maskedElement\" as const,\n        element,\n      }\n    : undefined;\n\n  const text = getTextForSVG(element, style, {\n    x,\n    y,\n    width,\n    height,\n  });\n\n  return {\n    elemInfo,\n    attributeData,\n    mightBeHovered,\n    background:\n      /** TODO: addNewChildren should be fixed. This is a workaround when non transparent bg appears after dark theme switch */\n      element instanceof HTMLBodyElement ? style.backgroundColor\n      : backgroundSameAsRenderedParent || image?.type === \"maskedElement\" ?\n        undefined\n      : background,\n    backdropFilter,\n    border: getBorderForSVG(style),\n    childAffectingStyles,\n    image,\n    text,\n  };\n};\n\nconst getBorderDetails = (value: string) => {\n  const [width, display, ...colorParts] = value.split(\" \").map((v) => v.trim());\n  const color = colorParts.join(\" \");\n  if (display !== \"none\" && width) {\n    const widthNum = parseFloat(width);\n    if (widthNum && color) {\n      return {\n        borderWidth: widthNum,\n        borderColor: color,\n      };\n    }\n  }\n};\nconst getBorderForSVG = (style: CSSStyleDeclaration) => {\n  const border = getBorderDetails(style.border);\n  const outline = getBorderDetails(\n    [style.outlineWidth, style.outlineStyle, style.outlineColor].join(\" \"),\n  );\n\n  if (border) {\n    return {\n      type: \"border\" as const,\n      outline,\n      ...border,\n    };\n  }\n\n  const borders = [\n    style.borderTop,\n    style.borderRight,\n    style.borderBottom,\n    style.borderLeft,\n  ]\n    .map(getBorderDetails)\n    .filter(isDefined);\n\n  if (borders.length) {\n    return {\n      type: \"borders\" as const,\n      outline,\n      borders,\n    };\n  }\n\n  if (!outline) return;\n\n  return {\n    type: \"noBorder\" as const,\n    outline,\n  };\n};\n"
  },
  {
    "path": "client/src/app/domToSVG/utils/isElementVisible.ts",
    "content": "import { includes } from \"../../../dashboard/W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\n\nexport const isElementVisible = (element: Element) => {\n  const style = window.getComputedStyle(element);\n  const bbox = element.getBoundingClientRect();\n\n  if (!isElementNode(element) && !isTextNode(element))\n    return { isVisible: false, style, bbox };\n\n  if (isTextNode(element)) {\n    return {\n      isVisible: !!(element.textContent as string | null)?.trim().length,\n      style,\n      bbox,\n    };\n  }\n  const mightBeVisible = element.checkVisibility({\n    checkOpacity: true,\n    checkVisibilityCSS: true,\n  });\n\n  if (!mightBeVisible) return { isVisible: false, style, bbox };\n  const parent = element.parentElement;\n  if (!parent) return { isVisible: true, style, bbox };\n  const isOnParentScreen = isInParentViewport(element, bbox);\n  return { isVisible: isOnParentScreen, style, bbox };\n};\n\nconst isInViewport = (\n  bbox: DOMRect,\n  vport: Pick<DOMRect, \"x\" | \"y\" | \"right\" | \"bottom\">,\n) => {\n  return (\n    bbox.left <= vport.right &&\n    vport.x <= bbox.right &&\n    bbox.top <= vport.bottom &&\n    vport.y <= bbox.bottom\n  );\n};\n\nexport const isInParentViewport = (\n  element: Element,\n  bbox: DOMRect,\n): boolean => {\n  const parent = element.parentElement;\n  if (!parent) return true;\n  const parentHidesOverflow =\n    includes(getComputedStyle(parent).overflow, [\"hidden\", \"scroll\", \"auto\"]) &&\n    !includes(getComputedStyle(element).position, [\"absolute\", \"fixed\"]);\n  if (!parentHidesOverflow) {\n    const isVisible = isInViewport(bbox, {\n      x: 0,\n      y: 0,\n      right: window.innerWidth || document.documentElement.clientWidth,\n      bottom: window.innerHeight || document.documentElement.clientHeight,\n    });\n    // if (!isVisible) {\n    //   return Array.from(element.children).some((child) =>\n    //     isInParentViewport(child, child.getBoundingClientRect()),\n    //   );\n    // }\n    return isVisible;\n  }\n  const parentBbox = parent.getBoundingClientRect();\n  const isVisible = isInViewport(bbox, parentBbox);\n  if (!isVisible) {\n    return Array.from(element.children).some((child) =>\n      isInParentViewport(child, child.getBoundingClientRect()),\n    );\n  }\n  return true;\n};\n\nconst checkIfObscured = (\n  element: Element,\n  bbox = element.getBoundingClientRect(),\n) => {\n  const topElement = document.elementFromPoint(\n    bbox.x + bbox.width / 2,\n    bbox.y + bbox.height / 2,\n  );\n  if (topElement && isElementNode(topElement)) {\n    const isObscured = !element.contains(topElement);\n    return isObscured ? topElement : undefined;\n  }\n};\n\nexport const isElementNode = (node: Node): node is HTMLElement =>\n  node.nodeType === Node.ELEMENT_NODE;\n\nexport const isTextNode = (node: Node): node is Text =>\n  node.nodeType === Node.TEXT_NODE;\n\nexport const isImgNode = (node: Node): node is HTMLImageElement =>\n  isElementNode(node) && node.nodeName.toLowerCase() === \"img\";\n\nexport const isInputOrTextAreaNode = (node: Node): node is HTMLInputElement =>\n  isElementNode(node) &&\n  (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement);\n\nexport const isSVGNode = (node: Node): node is SVGElement =>\n  isElementNode(node) && node.nodeName.toLowerCase() === \"svg\";\n"
  },
  {
    "path": "client/src/app/domToSVG/utils/toFixed.ts",
    "content": "export const toFixed = (num: number, precision = 2) => {\n  return parseFloat(num.toFixed(precision));\n};\n"
  },
  {
    "path": "client/src/appUtils.ts",
    "content": "import { useEffect, useState } from \"react\";\nimport type { Theme } from \"./App\";\nimport { type DocumentationFile } from \"./app/CommandPalette/getDocumentation\";\nimport type { UIDocFlat } from \"./app/UIDocs\";\nimport type { SQLEditorRef } from \"./dashboard/SQLEditor/W_SQLEditor\";\nimport type { getSVGif } from \"./app/domToSVG/SVGif/getSVGif\";\nimport type { domToThemeAwareSVG } from \"./app/domToSVG/domToThemeAwareSVG\";\n\ntype Unsubscribe = {\n  unsubscribe: () => void;\n};\n\ntype OnStateChange<S> = (newState: S) => void;\ntype Subscribe<S> = (sc: OnStateChange<S>) => Unsubscribe;\n\nexport type ReactiveState<S> = {\n  initialState: S;\n  set: (newState: S) => void;\n  get: () => S;\n  subscribe: Subscribe<S>;\n};\nexport const createReactiveState = <S>(\n  initialState: S,\n  onChange?: (state: S) => void,\n) => {\n  const handler: {\n    listeners: OnStateChange<S>[];\n    currentState: S;\n  } = {\n    listeners: [],\n    currentState: initialState,\n  };\n\n  const rootReference: ReactiveState<S> = {\n    subscribe: (listener) => {\n      handler.listeners.push(listener);\n\n      return {\n        unsubscribe: () => {\n          handler.listeners = handler.listeners.filter((l) => l !== listener);\n        },\n      };\n    },\n    set: (newState: S) => {\n      handler.currentState = newState;\n      handler.listeners.forEach((l) => l(handler.currentState));\n      onChange?.(newState);\n    },\n    initialState: initialState,\n    get: () => handler.currentState,\n  };\n\n  return rootReference;\n};\n\nexport const useReactiveState = <S>(store: ReactiveState<S>) => {\n  const [state, setState] = useState(store.get());\n  useEffect(() => {\n    return store.subscribe((newState) => {\n      setState(newState);\n    }).unsubscribe;\n  }, [store]);\n\n  return {\n    state,\n    setState: (data) => {\n      store.set(data);\n    },\n  };\n};\n\nexport const iOS = () => {\n  return (\n    [\n      \"iPad Simulator\",\n      \"iPhone Simulator\",\n      \"iPod Simulator\",\n      \"iPad\",\n      \"iPhone\",\n      \"iPod\",\n    ].includes(navigator.platform) ||\n    // iPad on iOS 13 detection\n    (navigator.userAgent.includes(\"Mac\") && \"ontouchend\" in document)\n  );\n};\n\ndeclare global {\n  interface Window {\n    __prglIsImporting: any;\n    /**\n     * /Mobi/i.test(window.navigator.userAgent);\n     */\n    isMobileDevice: boolean;\n    /**\n     * window.matchMedia(\"(any-hover: none)\").matches\n     */\n    isTouchOnlyDevice: boolean;\n    /**\n     * window.innerWidth < 700\n     */\n    isLowWidthScreen: boolean;\n    /**\n     * window.innerWidth < 1200\n     */\n    isMediumWidthScreen: boolean;\n    isIOSDevice: boolean;\n    isMobile: boolean;\n    toSVG: typeof domToThemeAwareSVG;\n    getSVGif: typeof getSVGif;\n    documentation: DocumentationFile[];\n    flatUIDocs: UIDocFlat[];\n  }\n\n  interface HTMLDivElement {\n    sqlRef?: SQLEditorRef;\n  }\n}\n\nexport const appTheme = createReactiveState<Theme>(\"light\");\n\nconst checkSize = () => {\n  window.isLowWidthScreen = window.innerWidth < 700;\n  window.isMediumWidthScreen = window.innerWidth < 1200;\n};\nwindow.isIOSDevice = iOS();\nwindow.isMobileDevice = /Mobi/i.test(window.navigator.userAgent);\nwindow.isTouchOnlyDevice = window.matchMedia(\"(any-hover: none)\").matches;\nwindow.isMobile = window.isLowWidthScreen || window.isIOSDevice;\ncheckSize();\nwindow.addEventListener(\"resize\", checkSize);\n"
  },
  {
    "path": "client/src/components/AlertProvider.tsx",
    "content": "import React, {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport Popup, { type PopupProps } from \"./Popup/Popup\";\nimport ErrorComponent from \"./ErrorComponent\";\nimport { useIsMounted } from \"prostgles-client\";\n\ntype AlertDialogProps = Pick<\n  PopupProps,\n  \"children\" | \"footerButtons\" | \"title\" | \"subTitle\" | \"contentClassName\"\n>;\n\nexport type AlertContext = {\n  addAlert: (props: AlertDialogProps | string) => void;\n};\n\nconst AlertContext = createContext<AlertContext | undefined>(undefined);\n\nexport const AlertProvider = ({ children }: { children: React.ReactNode }) => {\n  const [alerts, setAlerts] = useState<AlertDialogProps[]>([]);\n  const removeFirstAlert = useCallback(() => {\n    setAlerts((prevAlerts) => prevAlerts.slice(1));\n  }, []);\n\n  const addAlert: AlertContext[\"addAlert\"] = useCallback((newAlert) => {\n    setAlerts((prevAlerts) => [\n      ...prevAlerts,\n      typeof newAlert === \"string\" ? { children: newAlert } : newAlert,\n    ]);\n  }, []);\n\n  const contextValue = useMemo(\n    () => ({\n      addAlert,\n    }),\n    [addAlert],\n  );\n\n  const alertProps = useMemo(() => alerts.at(0), [alerts]);\n  return (\n    <AlertContext.Provider value={contextValue}>\n      {children}\n      {alertProps && (\n        <Popup\n          data-command=\"Alert\"\n          clickCatchStyle={{ opacity: 1 }}\n          footerButtons={[\n            {\n              label: \"OK\",\n              color: \"action\",\n              variant: \"filled\",\n              className: \"ml-auto\",\n              onClick: removeFirstAlert,\n            },\n          ]}\n          {...alertProps}\n          onClose={removeFirstAlert}\n        />\n      )}\n    </AlertContext.Provider>\n  );\n};\n\nexport const useAlert = () => {\n  const context = useContext(AlertContext);\n  if (!context) {\n    throw new Error(\"useAlert must be used within an AlertProvider\");\n  }\n  return context;\n};\n\nexport const useOnErrorAlert = () => {\n  const alert = useAlert();\n  const getIsMounted = useIsMounted();\n  const onErrorAlert = useCallback(\n    async (promiseFunc: () => Promise<void>) => {\n      await promiseFunc().catch((error) => {\n        if (!getIsMounted()) return;\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n        alert.addAlert({ children: <ErrorComponent error={error} /> });\n      });\n    },\n    [alert, getIsMounted],\n  );\n  return { onErrorAlert };\n};\n"
  },
  {
    "path": "client/src/components/Animations.css",
    "content": ".custom-animations.success-checkmark {\n  width: 80px;\n  height: 115px;\n  margin: 0 auto;\n}\n\n.custom-animations.success-checkmark .check-icon {\n  width: 80px;\n  height: 80px;\n  position: relative;\n  border-radius: 50%;\n  box-sizing: content-box;\n  border: 4px solid #4caf50;\n}\n\n.custom-animations.success-checkmark .check-icon::before {\n  top: 3px;\n  left: -2px;\n  width: 30px;\n  transform-origin: 100% 50%;\n  border-radius: 100px 0 0 100px;\n}\n\n.custom-animations.success-checkmark .check-icon::after {\n  top: 0;\n  left: 30px;\n  width: 60px;\n  transform-origin: 0 50%;\n  border-radius: 0 100px 100px 0;\n  animation: rotate-circle 4.25s ease-in;\n}\n\n.custom-animations.success-checkmark .check-icon::before,\n.custom-animations.success-checkmark .check-icon::after {\n  content: \"\";\n  height: 100px;\n  position: absolute;\n  transform: rotate(-45deg);\n}\n\n.custom-animations.success-checkmark .check-icon .icon-line {\n  height: 5px;\n  background-color: #4caf50;\n  display: block;\n  border-radius: 2px;\n  position: absolute;\n  z-index: 10;\n}\n\n.custom-animations.success-checkmark .check-icon .icon-line.line-tip {\n  top: 46px;\n  left: 14px;\n  width: 25px;\n  transform: rotate(45deg);\n  animation: icon-line-tip 0.75s;\n}\n\n.custom-animations.success-checkmark .check-icon .icon-line.line-long {\n  top: 38px;\n  right: 8px;\n  width: 47px;\n  transform: rotate(-45deg);\n  animation: icon-line-long 0.75s;\n}\n\n.custom-animations.success-checkmark .icon-circle {\n  top: -4px;\n  left: -4px;\n  z-index: 10;\n  width: 80px;\n  height: 80px;\n  border-radius: 50%;\n  position: absolute;\n  box-sizing: content-box;\n  border: 4px solid rgba(76, 175, 80, 0.5);\n}\n\n.custom-animations.success-checkmark .icon-fix {\n  top: 8px;\n  width: 5px;\n  left: 26px;\n  z-index: 1;\n  height: 85px;\n  position: absolute;\n  transform: rotate(-45deg);\n}\n\n@keyframes rotate-circle {\n  0% {\n    transform: rotate(-45deg);\n  }\n  5% {\n    transform: rotate(-45deg);\n  }\n  12% {\n    transform: rotate(-405deg);\n  }\n  100% {\n    transform: rotate(-405deg);\n  }\n}\n\n@keyframes icon-line-tip {\n  0% {\n    width: 0;\n    left: 1px;\n    top: 19px;\n  }\n  54% {\n    width: 0;\n    left: 1px;\n    top: 19px;\n  }\n  70% {\n    width: 50px;\n    left: -8px;\n    top: 37px;\n  }\n  84% {\n    width: 17px;\n    left: 21px;\n    top: 48px;\n  }\n  100% {\n    width: 25px;\n    left: 14px;\n    top: 45px;\n  }\n}\n\n@keyframes icon-line-long {\n  0% {\n    width: 0;\n    right: 46px;\n    top: 54px;\n  }\n  65% {\n    width: 0;\n    right: 46px;\n    top: 54px;\n  }\n  84% {\n    width: 55px;\n    right: 0px;\n    top: 35px;\n  }\n  100% {\n    width: 47px;\n    right: 8px;\n    top: 38px;\n  }\n}\n\n.custom-animations.checkmark {\n  width: 1em;\n  height: 1em;\n  font-size: 36px;\n  border-radius: 50%;\n  display: block;\n  stroke-width: 2;\n  stroke: #fff;\n  stroke-miterlimit: 10;\n  margin: 10% auto;\n  box-shadow: inset 0px 0px 0px #7ac142;\n  animation:\n    fill 0.4s ease-in-out 0.4s forwards,\n    scale 0.3s ease-in-out 0.9s both;\n  stroke: #7ac142;\n}\n\n.custom-animations .checkmark__circle {\n  stroke-dasharray: 166;\n  stroke-dashoffset: 166;\n  stroke-width: 2;\n  stroke-miterlimit: 10;\n  stroke: #7ac142;\n  fill: white;\n  /* animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards; */\n  animation: stroke 0.5s ease-out forwards;\n}\n\n.custom-animations .checkmark__check {\n  transform-origin: 50% 50%;\n  stroke-dasharray: 48;\n  stroke-dashoffset: 48;\n  animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.5s forwards;\n}\n\n@keyframes stroke {\n  100% {\n    stroke-dashoffset: 0;\n  }\n}\n@keyframes scale {\n  0%,\n  100% {\n    transform: none;\n  }\n  50% {\n    transform: scale3d(1.1, 1.1, 1);\n  }\n}\n@keyframes fill {\n  100% {\n    box-shadow: inset 0px 0px 0px 30px #7ac142;\n  }\n}\n"
  },
  {
    "path": "client/src/components/Animations.tsx",
    "content": "import React, { useEffect } from \"react\";\nimport \"./Animations.css\";\nimport type { TestSelectors } from \"../Testing\";\nimport { classOverride } from \"./Flex\";\nimport { useIsMounted } from \"prostgles-client\";\n\nexport type DivProps = React.DetailedHTMLProps<\n  React.HTMLAttributes<HTMLDivElement>,\n  HTMLDivElement\n>;\nexport const Success = (props: DivProps) => {\n  const { className = \"\", ...otherProps } = props;\n\n  return (\n    <div\n      {...otherProps}\n      className={classOverride(\n        \"custom-animations success-checkmark f-0 \",\n        className,\n      )}\n    >\n      <div className=\"check-icon\">\n        <span className=\"icon-line line-tip\"></span>\n        <span className=\"icon-line line-long\"></span>\n        <div className=\"icon-circle\"></div>\n        <div className=\"icon-fix\"></div>\n      </div>\n    </div>\n  );\n};\n\nexport class SuccessSVG extends React.Component<React.SVGProps<SVGSVGElement>> {\n  render() {\n    const { className = \"\" } = this.props;\n\n    return (\n      <svg\n        {...this.props}\n        className={\"custom-animations checkmark \" + className}\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 52 52\"\n      >\n        <circle\n          className=\"checkmark__circle\"\n          cx=\"26\"\n          cy=\"26\"\n          r=\"25\"\n          fill=\"none\"\n        />\n        <path\n          className=\"checkmark__check\"\n          fill=\"none\"\n          d=\"M14.1 27.2l7.1 7.2 16.7-16.8\"\n        />\n      </svg>\n    );\n  }\n}\n\nconst smallStyle = {\n  transform: `scale(0.5) translate(-50%, -50%)`,\n  width: \"40px\",\n  height: \"40px\",\n};\nconst textSizedStyle = {\n  transform: `scale(0.25) translate(-75%, -175%)`,\n  width: \"20px\",\n  height: \"20px\",\n};\n\nexport const SuccessMini = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <div className=\"flex-col\">\n      <Success style={smallStyle} />\n      <div>{children}</div>\n    </div>\n  );\n};\n\ntype FlashMessageProps = {\n  message: string;\n  duration?: {\n    millis: number;\n    onEnd: VoidFunction;\n  };\n  variant?: \"small\" | \"text-sized\";\n} & TestSelectors &\n  Pick<DivProps, \"className\" | \"style\">;\n\nexport const SuccessMessage = ({\n  message,\n  className = \"\",\n  duration,\n  variant,\n  ...divProps\n}: FlashMessageProps) => {\n  const getIsMounted = useIsMounted();\n  useEffect(() => {\n    if (duration) {\n      const timeout = setInterval(() => {\n        if (!getIsMounted()) return;\n\n        duration.onEnd();\n      }, duration.millis);\n\n      return () => clearInterval(timeout);\n    }\n  }, [duration, getIsMounted]);\n\n  return (\n    <div\n      {...divProps}\n      className={classOverride(\n        \"SuccessMessage text-green p-1 jc-center ai-center o-hidden gap-p5 w-fit \" +\n          (variant === \"text-sized\" ? \"flex-row-reverse\" : \"flex-col\"),\n        className,\n      )}\n    >\n      <div className={\"text-green p-1 flex-col \"}>{message}</div>\n      <Success\n        style={\n          variant === \"small\" ? smallStyle\n          : variant === \"text-sized\" ?\n            textSizedStyle\n          : {}\n        }\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Btn.css",
    "content": "button.disabled {\n  cursor: not-allowed !important;\n}\nbutton.disabled:not(.no-fade):not(.hidden) {\n  opacity: 0.5 !important;\n}\n\nbutton {\n  background: transparent;\n}\n\nbutton.fade-in,\n.fade-in-2 {\n  -webkit-animation: fadein 2s; /* Safari, Chrome and Opera > 12.1 */\n  -moz-animation: fadein 2s; /* Firefox < 16 */\n  -ms-animation: fadein 2s; /* Internet Explorer */\n  -o-animation: fadein 2s; /* Opera < 12.1 */\n  animation: fadein 2s;\n}\n\n.fade-in {\n  -webkit-animation: fadein 0.5s; /* Safari, Chrome and Opera > 12.1 */\n  -moz-animation: fadein 0.5s; /* Firefox < 16 */\n  -ms-animation: fadein 0.5s; /* Internet Explorer */\n  -o-animation: fadein 0.5s; /* Opera < 12.1 */\n  animation: fadein 0.5s;\n}\n\n@keyframes fadein {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n/* Firefox < 16 */\n@-moz-keyframes fadein {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n/* Safari, Chrome and Opera > 12.1 */\n@-webkit-keyframes fadein {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n/* Internet Explorer */\n@-ms-keyframes fadein {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n/* Opera < 12.1 */\n@-o-keyframes fadein {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n.btn-color-default {\n  --color: var(--gray-600);\n  --filled-bg: var(--gray-400);\n  --border-color: var(--gray-400);\n  --faded-bg: #7d7d7d38;\n  --faded-bg-hover: #5252525e;\n}\n.dark-theme .btn-color-default {\n  --filled-bg: #5d5d5d;\n  --border-color: var(--gray-400);\n  --color: var(--gray-200);\n  --faded-bg: #7d7d7d61;\n  --faded-bg-hover: #7d7d7dc4;\n}\n\n.btn-color-action {\n  --filled-bg: var(--blue-500);\n  --border-color: var(--blue-500);\n  --color: var(--blue-500);\n  --faded-bg: var(--blue-100);\n  --faded-bg-hover: var(--blue-200);\n}\n\n.dark-theme .btn-color-action {\n  --filled-bg: #3079c5;\n  --border-color: #5292d7;\n  --color: var(--blue-200);\n  --faded-bg: #004fa3cc;\n  --faded-bg-hover: #6195e4ee;\n}\n\n.dark-theme .btn-color-action.btn-faded {\n  --color: white;\n}\n\n.btn-outline.btn-color-action {\n  --color: #479dff;\n}\n.dark-theme .btn-default.btn-color-action {\n  --color: #5ba4ff;\n}\n\n.btn-color-green {\n  --filled-bg: var(--green-500);\n  --border-color: var(--green-500);\n  --color: var(--green-800);\n  --faded-bg: var(--green-100);\n  --faded-bg-hover: var(--green-200);\n}\n.btn-color-danger {\n  --color: var(--text-danger);\n  --filled-bg: #b31010;\n  --border-color: var(--text-danger);\n  --faded-bg: var(--red-100);\n  --faded-bg-hover: #be181833;\n}\n\n.btn-color-warn {\n  --filled-bg: var(--yellow-500);\n  --border-color: var(--yellow-700);\n  --color: var(--yellow-800);\n  --faded-bg: #e4ce0559;\n  --faded-bg-hover: #e6d96c;\n}\n.dark-theme .btn-color-warn {\n  --color: #dabb8c;\n}\n.dark-theme .btn-color-danger {\n  --filled-bg: #890808;\n  --faded-bg: #5900003b;\n  --faded-bg-hover: #7e020288;\n  /* --color: #9c2a2a; */\n}\n\n.btn-color-inherit {\n  --color: inherit;\n  --filled-bg: inherit;\n  --border-color: inherit;\n  --faded-bg: inherit;\n  --faded-bg-hover: inherit;\n}\n.btn-color-transparent {\n  --color: transparent;\n  --filled-bg: transparent;\n  --border-color: transparent;\n  --faded-bg: transparent;\n  --faded-bg-hover: transparent;\n}\n.btn-color-white {\n  --color: white;\n  --filled-bg: white;\n  --border-color: white;\n  --faded-bg: white;\n  --faded-bg-hover: var(--gray-100);\n}\n.btn-color-indigo {\n  --filled-bg: var(--indigo-600);\n  --border-color: var(--indigo-400);\n  --color: var(--indigo-800);\n  --faded-bg: var(--indigo-200);\n  --faded-bg-hover: var(--indigo-300);\n}\n\n.btn-filled {\n  background: var(--filled-bg);\n  color: white;\n}\n.btn.btn-filled:hover {\n  filter: brightness(1.2);\n}\n\n.btn-outline {\n  border: 1px solid var(--border-color);\n  color: var(--color);\n  background: var(--bg-color-0);\n}\n.btn-outline:hover {\n  background: var(--filled-bg);\n  border-color: var(--filled-bg) !important;\n  color: white;\n}\n\n:root {\n  --btn-hover-bg: #dbdbdb;\n}\n:root.dark-theme {\n  --btn-hover-bg: var(--li-hover-bg);\n}\n\n.btn-default,\n.btn-icon,\n.btn-text {\n  color: var(--color);\n}\n.btn-default:hover {\n  filter: brightness(1.5);\n}\n\n.btn-icon:hover {\n  background: var(--btn-hover-bg);\n}\n\n.btn-faded {\n  background: var(--faded-bg);\n  color: var(--color);\n}\n\n.btn.btn-faded:hover {\n  background-color: var(--faded-bg-hover);\n}\n\n.underline-on-hover:hover {\n  text-decoration: underline;\n}\n"
  },
  {
    "path": "client/src/components/Btn.tsx",
    "content": "import { mdiAlert, mdiCheck } from \"@mdi/js\";\nimport { omitKeys, pickKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport { NavLink } from \"react-router-dom\";\nimport RTComp from \"../dashboard/RTComp\";\nimport type { TestSelectors } from \"../Testing\";\nimport { tout } from \"../utils/utils\";\nimport \"./Btn.css\";\nimport { parsedError } from \"./ErrorComponent\";\nimport { classOverride } from \"./Flex\";\nimport type { IconProps } from \"./Icon/Icon\";\nimport { Icon } from \"./Icon/Icon\";\nimport { Label, type LabelProps } from \"./Label\";\nimport Loading from \"./Loader/Loading\";\nimport Popup from \"./Popup/Popup\";\n\ntype ClickMessage = (\n  | { err: any }\n  | { ok: React.ReactNode; replace?: boolean }\n  | { loading: 1 | 0; delay?: number }\n) & { duration?: number };\ntype ClickMessageArgs = (msg: ClickMessage, onEnd?: () => void) => void;\n\ntype BtnCustomProps = (\n  | {\n      iconPath?: never;\n      iconStyle?: never;\n      iconProps?: never;\n      iconClassname?: never;\n      iconNode?: never;\n    }\n  | {\n      iconPath?: string;\n      iconStyle?: React.CSSProperties;\n      iconProps?: IconProps;\n      iconClassname?: string;\n      iconNode?: React.ReactNode;\n    }\n) & {\n  iconPosition?: \"left\" | \"right\";\n  label?: LabelProps;\n\n  /**\n   * If provided then the button is disabled and will display a tooltip with this message\n   */\n  disabledInfo?: string;\n  /**\n   * no-fade - will not fade out the button when disabled/loading\n   */\n  disabledVariant?: \"no-fade\";\n  loading?: boolean;\n  fadeIn?: boolean;\n  _ref?: React.RefObject<HTMLButtonElement>;\n\n  size?: \"large\" | \"default\" | \"small\" | \"micro\" | \"nano\";\n  variant?: \"outline\" | \"filled\" | \"faded\" | \"icon\" | \"text\" | \"default\";\n  color?:\n    | \"danger\"\n    | \"warn\"\n    | \"action\"\n    | \"inherit\"\n    | \"transparent\"\n    | \"white\"\n    | \"green\"\n    | \"indigo\"\n    | \"default\";\n\n  \"data-id\"?: string;\n  /**\n   * If true then title will be used as children\n   */\n  titleAsLabel?: boolean;\n\n  /**\n   * If provided then will display a confirmation dialog before running any onClick function\n   */\n  clickConfirmation?: {\n    color: \"danger\" | \"action\" | \"warn\";\n    message: React.ReactNode;\n    buttonText: string;\n  };\n} & (\n    | {\n        onClickMessage?: (\n          e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n          showMessage: ClickMessageArgs,\n        ) => void;\n        onClickPromise?: undefined;\n        onClickPromiseMode?: undefined;\n      }\n    | {\n        onClickMessage?: undefined;\n        onClickPromise?: (\n          e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n        ) => Promise<void> | void;\n        onClickPromiseMode?: \"noTickIcon\";\n        /**\n         * Will display it instead of the error message\n         */\n        onClickPromiseMessage?: React.ReactNode;\n      }\n  );\ntype KeysOfUnion<T> = T extends T ? keyof T : never;\n\ntype OmmitedKeys =\n  | KeysOfUnion<BtnCustomProps>\n  | \"children\"\n  | \"ref\"\n  | \"onClick\"\n  | \"style\"\n  | \"title\";\nconst CUSTOM_ATTRS: OmmitedKeys[] = [\n  \"iconPath\",\n  \"children\",\n  \"disabledInfo\",\n  \"title\",\n  \"disabledVariant\",\n  \"onClick\",\n  \"loading\",\n  \"color\",\n  \"fadeIn\",\n  \"_ref\",\n  \"ref\",\n  \"style\",\n  \"size\",\n  \"iconProps\",\n  \"iconNode\",\n  \"iconPosition\",\n  \"iconClassname\",\n  \"onClickMessage\",\n  \"onClickPromise\",\n  \"onClickPromiseMessage\",\n  \"onClickPromiseMode\",\n  //@ts-ignore\n  \"asNavLink\",\n  \"iconStyle\",\n  \"titleAsLabel\",\n  \"clickConfirmation\",\n  \"label\",\n];\n\nexport type BtnProps<HREF extends string | void = void> = TestSelectors &\n  BtnCustomProps & {\n    /**\n     * If provided then will render as an anchor\n     */\n    href?: HREF;\n    target?: string;\n    asNavLink?: boolean;\n    download?: boolean;\n  } & React.HTMLAttributes<HTMLButtonElement> & {\n    value?: string;\n    type?: \"button\" | \"submit\";\n  };\n\ntype BtnState = {\n  show: boolean;\n  clickMessage?: {\n    type: \"err\" | \"ok\" | \"loading\";\n    msg: React.ReactNode;\n    replace?: boolean;\n  };\n  showClickConfirmation?: boolean;\n};\n\nexport default class Btn<HREF extends string | void = void> extends RTComp<\n  BtnProps<HREF>,\n  BtnState\n> {\n  state: BtnState = {\n    show: true,\n  };\n\n  timeOut?: NodeJS.Timeout;\n  loadingTimeOut?: NodeJS.Timeout;\n\n  latestMsg?: ClickMessage;\n  clickMessage: ClickMessageArgs = (msg, onEnd) => {\n    if (!this.mounted) return;\n    this.latestMsg = msg;\n    const hasErr = \"err\" in msg;\n    if (this.loadingTimeOut) clearTimeout(this.loadingTimeOut);\n    if (this.timeOut) clearTimeout(this.timeOut);\n\n    if (hasErr) {\n      this.setState({\n        clickMessage: {\n          type: \"err\",\n          msg: parsedError(msg.err, true),\n        },\n      });\n    } else if (\"ok\" in msg) {\n      this.setState({\n        clickMessage: {\n          type: \"ok\",\n          msg: msg.ok,\n          replace: msg.replace,\n        },\n      });\n    } else if (\"loading\" in msg) {\n      if (!msg.loading) {\n        this.setState({ clickMessage: undefined }, onEnd);\n      } else {\n        this.loadingTimeOut = setTimeout(() => {\n          /** Check if msg is stale */\n          if (\n            this.mounted &&\n            JSON.stringify(this.latestMsg) === JSON.stringify(msg)\n          ) {\n            this.setState({\n              clickMessage: { type: \"loading\", msg: \"\" },\n            });\n          }\n        }, msg.delay ?? 750);\n      }\n\n      return;\n    }\n\n    this.timeOut = setTimeout(\n      () => {\n        if (this.mounted) {\n          this.setState({ clickMessage: undefined }, onEnd);\n        }\n      },\n      msg.duration ?? (hasErr ? 5000 : 2000),\n    );\n  };\n\n  setPromise = async (\n    promise: ReturnType<Required<BtnProps>[\"onClickPromise\"]>,\n  ) => {\n    this.clickMessage({ loading: 1, delay: 0 });\n    const minDuration = 500;\n    const startTime = Date.now();\n    try {\n      const res = await promise;\n      const endTime = Date.now();\n      const duration = endTime - startTime;\n      if (!this.mounted) return;\n      if (duration < minDuration) {\n        await tout(Math.max(0, minDuration - duration));\n      }\n      this.clickMessage({ ok: \"\" });\n    } catch (err) {\n      this.clickMessage({\n        err:\n          (\"onClickPromiseMessage\" in this.props ?\n            this.props.onClickPromiseMessage\n          : undefined) ?? err,\n      });\n    }\n  };\n\n  render() {\n    const {\n      iconPath,\n      iconPosition = \"left\",\n      className = \"\",\n      style = {},\n      iconStyle = {},\n      disabledInfo,\n      disabledVariant,\n      title,\n      fadeIn,\n      variant = \"default\",\n      iconProps,\n      iconNode,\n      iconClassname = \"\",\n      titleAsLabel,\n      label,\n      clickConfirmation,\n      onClickPromiseMode,\n      ...otherProps\n    } = this.props;\n    const { clickMessage, showClickConfirmation } = this.state;\n    let extraStyle: React.CSSProperties = {};\n\n    const color =\n      (clickMessage?.type === \"err\" ? \"danger\"\n      : clickMessage?.type === \"ok\" ? \"action\"\n      : this.props.color) ?? \"default\";\n    const loading =\n      clickMessage?.type === \"loading\" ? true : (this.props.loading ?? false);\n    const children =\n      clickMessage?.msg || (titleAsLabel ? title : this.props.children);\n\n    if (clickMessage?.replace) return clickMessage.msg;\n\n    const isDisabled = disabledInfo || loading;\n    let _className = \"\";\n    const { size = window.isLowWidthScreen ? \"small\" : \"default\" } = this.props;\n\n    const hasBgClassname = (className + \"\").includes(\"bg-\");\n    _className =\n      \" f-0 flex-row gap-p5 ai-center  \" +\n      (\"href\" in this.props ? \" button-css \" : \"  \") +\n      (variant === \"outline\" ? \" b \" : \"\");\n\n    if (children && !iconNode && !iconPath && !loading) {\n      if (variant === \"outline\") {\n        if (!hasBgClassname)\n          _className =\n            _className.replace(\"bg-transparent\", \"\") + \" bg-color-0 \";\n        extraStyle = {\n          borderColor: \"currentcolor\",\n        };\n      }\n\n      if (size === \"micro\") {\n        extraStyle.padding = \"5px\";\n      }\n\n      if (size === \"small\") {\n        extraStyle.padding = \"6px 8px\";\n      }\n\n      if (size === \"default\") {\n        extraStyle.padding = \"12px 14px\";\n      }\n      if (size === \"large\") {\n        extraStyle.padding = \"12px\";\n      }\n\n      if (variant === \"text\") {\n        extraStyle.paddingLeft = 0;\n      }\n    } else {\n      const padding =\n        size === \"nano\" ? \"0px\"\n        : size === \"micro\" ? \"4px\"\n        : size === \"small\" ? \"6px\"\n        : size === \"default\" ? \"8px\"\n        : \"10px\";\n      const sidePadding = children ? `calc(${padding} * 1.5)` : padding;\n      /** Must add right padding to icon and text button */\n      extraStyle = {\n        padding: `${padding} ${sidePadding}`,\n      };\n    }\n\n    _className +=\n      (fadeIn ? \" fade-in \" : \"\") +\n      (iconPath && !children ? \"  \" : \"rounded\") + // round\n      (isDisabled ? ` disabled ${disabledVariant ? \"no-fade\" : \"\"} ` : \" \");\n\n    const loadingSize = {\n      large: 22,\n      default: 20,\n      small: 14,\n      micro: 12,\n      nano: 10,\n    }[size];\n    const loadingMargin = {\n      large: 1,\n      default: 1,\n      small: 0,\n      micro: 2,\n      nano: 0,\n    }[size];\n    const childrenContent =\n      children === undefined || children === null || children === \"\" ? null\n      : loading ?\n        <div\n          className=\"min-w-0 ws-nowrap text-ellipsis f-1 o-hidden flex-row\"\n          style={{ opacity: 0.5 }}\n        >\n          {children}\n        </div>\n      : children;\n\n    const content = (\n      <>\n        {iconPosition === \"right\" && childrenContent}\n\n        {!(iconPath || iconProps?.path || iconNode) || loading ? null : (\n          (iconNode ?? (\n            <Icon\n              path={\n                clickMessage?.type === \"err\" ? mdiAlert\n                : (\n                  clickMessage?.type === \"ok\" &&\n                  onClickPromiseMode !== \"noTickIcon\"\n                ) ?\n                  mdiCheck\n                : (iconPath ?? iconProps!.path)\n              }\n              sizeName={size}\n              className={iconClassname + \" f-0 \"}\n              style={iconStyle}\n              {...iconProps}\n            />\n          ))\n        )}\n\n        {loading && (\n          <Loading\n            style={{ margin: `${loadingMargin}px` }}\n            sizePx={loadingSize}\n            delay={0}\n            colorAnimation={false}\n          />\n        )}\n\n        {iconPosition === \"left\" && childrenContent}\n      </>\n    );\n\n    type PropsOf<E> = React.HTMLAttributes<E> & { ref?: React.Ref<E> };\n\n    const needsConfirmation = () => {\n      if (clickConfirmation && !showClickConfirmation) {\n        this.setState({ showClickConfirmation: true });\n        return true;\n      }\n      return false;\n    };\n    let onClick = this.props.onClick;\n    if (this.props.onClickPromise) {\n      const { onClickPromise } = this.props;\n      onClick = (e) => {\n        !needsConfirmation() && this.setPromise(onClickPromise(e));\n      };\n    } else if (this.props.onClickMessage) {\n      const { onClickMessage } = this.props;\n      onClick = (e) => {\n        !needsConfirmation() && onClickMessage(e, this.clickMessage);\n      };\n    } else if (this.props.onClick) {\n      onClick = (e) => {\n        if (needsConfirmation()) return;\n        this.props.onClick?.(e);\n      };\n    }\n\n    if (size === \"small\") {\n      extraStyle.minHeight = \"32px\";\n      extraStyle.minWidth = \"32px\";\n    }\n\n    const FontSizeMap: Record<Required<BtnCustomProps>[\"size\"], string> = {\n      large: \"16px\",\n      default: \"16px\",\n      small: \"14px\",\n      micro: \"12px\",\n      nano: \"10px\",\n    };\n\n    const finalProps: PropsOf<HTMLAnchorElement> | PropsOf<HTMLButtonElement> =\n      {\n        ...omitKeys(this.props, CUSTOM_ATTRS as any),\n        onClick:\n          isDisabled ?\n            !window.isMobileDevice ?\n              undefined\n            : () => alert(disabledInfo)\n          : onClick,\n        title: disabledInfo || title,\n        style: {\n          ...extraStyle,\n          display: \"flex\",\n          lineHeight: \"1em\",\n          width: \"fit-content\",\n          fontSize: FontSizeMap[size],\n          ...style,\n        },\n        onMouseDown: (e) => e.preventDefault(),\n        className: classOverride(\n          `${_className} btn btn-${variant} btn-size-${size} btn-color-${color} ws-nowrap w-fit `,\n          className,\n        ),\n        ref: this.props._ref as any,\n        ...pickKeys(otherProps, [\"data-id\"]),\n      };\n\n    const withLabel = (content: React.ReactNode) => {\n      if (label) {\n        return (\n          <div className=\"flex-col f-0 min-h-fit\">\n            <Label {...label} />\n            {content}\n          </div>\n        );\n      }\n      return content;\n    };\n\n    if (\"href\" in this.props && this.props.href) {\n      if (this.props.asNavLink) {\n        return withLabel(\n          <NavLink\n            {...(finalProps as PropsOf<HTMLAnchorElement>)}\n            onClick={\n              !disabledInfo ? undefined : (\n                (e) => {\n                  e.preventDefault();\n                }\n              )\n            }\n            to={this.props.href}\n            tabIndex={-1}\n          >\n            {content}\n          </NavLink>,\n        );\n      }\n      return withLabel(\n        <a\n          {...(finalProps as PropsOf<HTMLAnchorElement>)}\n          target={this.props.target}\n          onClick={disabledInfo ? undefined : () => false}\n          {...(this.props.download && { download: true })}\n        >\n          {content}\n        </a>,\n      );\n    }\n\n    return withLabel(\n      <>\n        <button\n          {...finalProps}\n          data-color={color}\n          disabled={isDisabled ? true : undefined}\n        >\n          {content}\n        </button>\n        {clickConfirmation && showClickConfirmation && (\n          <Popup\n            data-command=\"Btn.ClickConfirmation\"\n            onClickClose={false}\n            onClose={() => this.setState({ showClickConfirmation: false })}\n            clickCatchStyle={{ opacity: 1 }}\n            footerButtons={[\n              {\n                label: \"Cancel\",\n                onClickClose: true,\n              },\n              {\n                label: clickConfirmation.buttonText,\n                color: clickConfirmation.color,\n                \"data-command\": \"Btn.ClickConfirmation.Confirm\",\n                variant: \"filled\",\n                className: \"ml-auto\",\n                onClick: (e) => {\n                  this.setState({ showClickConfirmation: false });\n                  onClick?.(e);\n                },\n              },\n            ]}\n          >\n            {clickConfirmation.message}\n          </Popup>\n        )}\n      </>,\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/ButtonBar.tsx",
    "content": "import React from \"react\";\nimport type { BtnProps } from \"./Btn\";\nimport Btn from \"./Btn\";\nimport ErrorComponent from \"./ErrorComponent\";\n\ntype P = {\n  error?: any;\n  buttons: BtnProps[];\n  style?: React.CSSProperties;\n  className?: string;\n};\n\nexport const ButtonBar = ({ buttons, className = \"\", error, style }: P) => {\n  if (!buttons.length && !error) return null;\n\n  return (\n    <div\n      className={`ButtonBar flex-col ${window.isMobileDevice ? \"gap-1\" : \"gap-2\"} ${className}`}\n      style={style}\n    >\n      <ErrorComponent error={error} />\n      <div className={\"flex-row-wrap py-1 gap-2 \"}>\n        {buttons.map((btnProps, i) => (\n          <Btn key={i} {...btnProps} />\n        ))}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/ButtonGroup.tsx",
    "content": "import React from \"react\";\nimport { isObject } from \"@common/publishUtils\";\nimport Btn from \"./Btn\";\nimport { FlexCol } from \"./Flex\";\nimport type { LabelProps } from \"./Label\";\nimport { Label } from \"./Label\";\nimport type { FullOption } from \"./Select/Select\";\nimport { Select } from \"./Select/Select\";\nimport { pickKeys } from \"prostgles-types\";\nimport type { TestSelectors } from \"../Testing\";\n\ntype P<Option extends string> = TestSelectors & {\n  onChange: (val: Option, e: any) => void;\n  value?: Option;\n  style?: object;\n  className?: string;\n  id?: string;\n  variant?: \"dense\" | \"select\";\n  size?: \"small\";\n  color?: \"default\" | \"action\" | \"warn\" | \"danger\";\n  label?: string | LabelProps;\n} & (\n    | {\n        options: readonly Option[];\n        fullOptions?: never;\n      }\n    | {\n        options?: never;\n        fullOptions: readonly FullOption<Option>[];\n      }\n  );\n\nexport default class ButtonGroup<Option extends string> extends React.Component<\n  P<Option>,\n  any\n> {\n  render() {\n    const {\n      onChange,\n      className = \"\",\n      value,\n      style = {},\n      size,\n      variant,\n      label,\n      color = \"action\",\n      fullOptions: _fullOptions,\n      options: _options,\n      ...testSelectors\n    } = this.props;\n    const br = \".375rem\";\n\n    const fullOptions: readonly FullOption<Option>[] =\n      _fullOptions ?? _options.map((key) => ({ key, label: key }));\n    const getStyle = (i: number) => {\n      const nb: React.CSSProperties = {\n        borderTopLeftRadius: 0,\n        borderBottomLeftRadius: 0,\n        borderTopRightRadius: 0,\n        borderBottomRightRadius: 0,\n        fontWeight: 500,\n      };\n\n      if (!i)\n        return {\n          ...nb,\n          borderTopLeftRadius: br,\n          borderBottomLeftRadius: br,\n          marginRight: \"-1px\",\n        };\n      if (i < fullOptions.length - 1) {\n        return {\n          ...nb,\n          marginRight: \"-1px\",\n        };\n      }\n      return {\n        ...nb,\n        borderTopRightRadius: br,\n        borderBottomRightRadius: br,\n      };\n    };\n\n    if (variant === \"select\") {\n      return (\n        <Select\n          {...testSelectors}\n          btnProps={{ color: \"default\", variant: \"faded\" }}\n          className=\"shadow\"\n          fullOptions={fullOptions}\n          value={value}\n          //@ts-ignore\n          onChange={(val, e) => onChange(val, e)}\n        />\n      );\n    }\n\n    const buttons = (\n      <div className={\"bbutton-group flex-row o-auto  rounded-md w-fit \"}>\n        {fullOptions.map(({ key, label, disabledInfo, ...otherProps }, i) => (\n          <Btn\n            key={key}\n            data-key={otherProps[\"data-key\"] ?? key}\n            color={color}\n            size={size}\n            style={{ ...getStyle(i) }}\n            variant={value === key ? \"filled\" : \"outline\"}\n            disabledInfo={disabledInfo}\n            onClick={(e) => (key === value ? undefined : onChange(key, e))}\n            value={key.toString()}\n            {...pickKeys(otherProps, [\"data-command\", \"id\"])}\n          >\n            {label ?? key}\n          </Btn>\n        ))}\n      </div>\n    );\n\n    return (\n      <FlexCol\n        {...testSelectors}\n        style={{ gap: \".5em\", ...style }}\n        className={className}\n      >\n        {label !== undefined && (\n          <Label\n            label={typeof label === \"string\" ? label : undefined}\n            {...(isObject(label) ? label : {})}\n          />\n        )}\n        {buttons}\n      </FlexCol>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/Chat/Chat.css",
    "content": ".chat-container.chat-component .active-color {\n  color: rgb(5, 149, 252);\n}\n\n.chat-container.chat-component .bg-active-hover:hover {\n  background: rgba(5, 149, 252, 0.3);\n}\n\n.chat-container.chat-component {\n  flex: 1 1 0%;\n  display: flex;\n  flex-direction: column;\n  /* background: var(--gray-100); */\n  /* border: 1px solid var(--gray-300); */\n}\n\n.chat-container.chat-component .send-wrapper {\n  flex: 0 0 auto;\n  min-width: 0px;\n  min-height: 60px;\n  display: flex;\n  flex-direction: row;\n  justify-content: center;\n}\n\n.chat-container.chat-component .send-wrapper textarea {\n  flex: 1 1 0%;\n  resize: none;\n  color: var(--text-1);\n  border: none;\n  font-size: 16px;\n  field-sizing: content;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen,\n    Ubuntu, Cantarell, \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n}\n\n.chat-container.chat-component .send-wrapper button {\n  flex: 0 0 auto;\n  cursor: pointer;\n}\n\n.chat-container.chat-component .send-wrapper svg {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.chat-container.chat-component .send-wrapper svg path {\n  fill: currentColor;\n}\n\n.chat-container.chat-component .message-list {\n  line-height: 1.75rem;\n  position: relative;\n  flex: 1 1 0%;\n  min-width: 0px;\n  min-height: 200px;\n  display: flex;\n  flex-direction: column;\n  font-size: 16px;\n  overflow: unset;\n  padding: 1em;\n  align-self: center;\n  /* scroll-behavior: smooth; */\n}\n\n.chat-container.chat-component .message {\n  color: var(--text-0);\n  flex: 0 0 auto;\n  opacity: 1;\n}\n.chat-container.chat-component .message .content-wrapper {\n  padding: 8px 12px;\n  border-radius: 12px;\n  min-width: 2em;\n  text-align: start;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  max-width: 100%;\n}\n.chat-container.chat-component .message:not(.incoming) {\n  margin-left: auto;\n}\n.chat-container.chat-component .message:not(.incoming) .content-wrapper {\n  background: var(--bg-color-2);\n  margin-bottom: 0.5em;\n}\n\n.chat-container.chat-component .message.incoming {\n  max-width: 100%;\n  min-width: 2em;\n  margin-left: unset;\n}\n\n.chat-container.chat-component .message.media {\n  padding: 0;\n  background: none;\n  width: fit-content;\n  max-width: unset;\n  height: fit-content;\n}\n\n.chat-container.chat-component .message video,\n.chat-container.chat-component .message audio {\n  outline: none;\n}\n"
  },
  {
    "path": "client/src/components/Chat/Chat.tsx",
    "content": "import React, { useEffect, useRef } from \"react\";\nimport \"./Chat.css\";\n\nimport { classOverride, FlexCol } from \"../Flex\";\nimport { ChatFileAttachments } from \"./ChatFileAttachments/ChatFileAttachments\";\nimport { ChatMessage } from \"./ChatMessage\";\nimport { ChatSendControls } from \"./ChatSendControls\";\nimport { useChatState } from \"./useChatState\";\n\nexport type Message = {\n  id: number | string;\n  messageTopContent?: React.ReactNode;\n  message: React.ReactNode;\n  sender_id: number | string;\n  incoming: boolean;\n  isLoading?: boolean;\n};\n\nexport type ChatProps = {\n  style?: React.CSSProperties;\n  className?: string;\n  onSend: (msg?: string, files?: File[]) => Promise<void>;\n  onStopSending: undefined | (() => void);\n  messages: Message[];\n  disabledInfo?: string;\n  isLoading: boolean;\n  actionBar?: React.ReactNode;\n  /**\n   * Defaults to 800\n   */\n  maxWidth?: number;\n  currentlyTypedMessage: string | null | undefined;\n  onCurrentlyTypedMessageChange: (currentlyTypedMessage: string) => void;\n};\n\nexport const Chat = (props: ChatProps) => {\n  const {\n    className = \"\",\n    style = {},\n    messages,\n    onSend,\n    onStopSending,\n    disabledInfo,\n    actionBar,\n    isLoading,\n    maxWidth = 800,\n    currentlyTypedMessage,\n    onCurrentlyTypedMessageChange,\n  } = props;\n\n  const textAreaRef = useRef<HTMLTextAreaElement>(null);\n  const {\n    files,\n    sendMsg,\n    setFiles,\n    onAddFiles,\n    chatIsLoading,\n    filesAsBase64,\n    sendingMsg,\n    setScrollRef,\n    setCurrentMessage,\n    getCurrentMessage,\n    divHandlers,\n    handleOnPaste,\n    isEngaged,\n    onCurrentlyTypedMessageChangeDebounced,\n  } = useChatState({\n    isLoading,\n    messages,\n    onSend,\n    textAreaRef,\n    currentlyTypedMessage,\n    onCurrentlyTypedMessageChange,\n  });\n\n  useEffect(() => {\n    if (!isLoading && textAreaRef.current) {\n      textAreaRef.current.focus();\n    }\n  }, [isLoading]);\n\n  return (\n    <div\n      className={classOverride(\"chat-container chat-component \", className)}\n      style={style}\n    >\n      <FlexCol\n        className=\"chat-scroll-wrapper w-full o-auto f-1\"\n        ref={setScrollRef}\n      >\n        <div\n          className=\"message-list\"\n          style={{\n            maxWidth: `min(${maxWidth}px, 100%)`,\n            width: \"100%\",\n            margin: \"0 auto\",\n          }}\n          data-command=\"Chat.messageList\"\n        >\n          {messages.map((message) => (\n            <ChatMessage key={message.id} message={message} />\n          ))}\n        </div>\n      </FlexCol>\n\n      <FlexCol className=\"chat-scroll-wrapper w-full p-p5\">\n        <div\n          title={disabledInfo}\n          data-command=\"Chat.sendWrapper\"\n          style={{\n            maxWidth: `min(${maxWidth}px, 100%)`,\n            alignSelf: \"center\",\n            width: \"100%\",\n          }}\n          className={\n            \"send-wrapper relative rounded p-p5 \" +\n            (disabledInfo ? \"no-interaction not-allowed\" : \"\") +\n            (isEngaged ?\n              \"active-shadow bg-action\"\n            : \"bg-colord-2 shadow b b-color-0  \")\n          }\n        >\n          <FlexCol\n            className={\n              \"f-1 gap-p5 \" +\n              (chatIsLoading ? \"no-interaction not-allowed\" : \"\")\n            }\n            {...divHandlers}\n          >\n            <ChatFileAttachments\n              filesAsBase64={filesAsBase64}\n              setFiles={setFiles}\n            />\n            <textarea\n              ref={textAreaRef}\n              name=\"chat-input\"\n              data-command={\"Chat.textarea\"}\n              className=\"no-scroll-bar text-0 bg-transparent\"\n              rows={1}\n              style={{\n                maxHeight: \"50vh\",\n              }}\n              disabled={!!disabledInfo || chatIsLoading}\n              defaultValue={getCurrentMessage() || currentlyTypedMessage || \"\"}\n              onPaste={handleOnPaste}\n              onChange={({ currentTarget }) => {\n                onCurrentlyTypedMessageChangeDebounced(currentTarget.value);\n              }}\n              onKeyDown={(e) => {\n                if (\n                  textAreaRef.current &&\n                  !e.shiftKey &&\n                  e.key.toLocaleLowerCase() === \"enter\"\n                ) {\n                  e.preventDefault();\n                  void sendMsg();\n                }\n              }}\n            />\n            {actionBar}\n          </FlexCol>\n          <ChatSendControls\n            onStopSending={onStopSending}\n            onAddFiles={onAddFiles}\n            disabledInfo={disabledInfo}\n            files={files}\n            onSend={onSend}\n            sendMsg={sendMsg}\n            sendingMsg={sendingMsg}\n            setCurrentMessage={setCurrentMessage}\n            textAreaRef={textAreaRef}\n          />\n        </div>\n      </FlexCol>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/ChatFileAttachments/ChatFileAttachments.tsx",
    "content": "import React from \"react\";\nimport { mdiClose } from \"@mdi/js\";\nimport type { ChatState } from \"../useChatState\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { FlexCol } from \"@components/Flex\";\nimport { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport { t } from \"src/i18n/i18nUtils\";\nimport Btn from \"@components/Btn\";\n\nexport const ChatFileAttachments = ({\n  filesAsBase64,\n  setFiles,\n}: Pick<ChatState, \"filesAsBase64\" | \"setFiles\">) => {\n  return (\n    <>\n      {!!filesAsBase64?.length && (\n        <ScrollFade\n          data-command=\"Chat.attachedFiles\"\n          className=\"flex-row-wrap gap-1 o-auto\"\n          style={{ maxHeight: \"40vh\" }}\n        >\n          {filesAsBase64.map(({ file, base64Data }, index) => (\n            <FlexCol\n              key={file.name + index}\n              data-key={file.name}\n              title={file.name}\n              className=\"relative pt-p5 pr-p5 \"\n            >\n              <MediaViewer\n                url={base64Data}\n                style={{\n                  maxHeight: \"100px\",\n                  borderRadius: \"var(--rounded)\",\n                  boxShadow: \"var(--shadow)\",\n                }}\n              />\n              <Btn\n                title={t.common.Remove}\n                iconPath={mdiClose}\n                style={{\n                  position: \"absolute\",\n                  top: 0,\n                  right: 0,\n                  borderRadius: \"50%\",\n                }}\n                variant=\"filled\"\n                size=\"small\"\n                onClick={() => {\n                  setFiles((prev) =>\n                    prev.filter((f, i) => f.name + i !== file.name + index),\n                  );\n                }}\n              />\n            </FlexCol>\n          ))}\n        </ScrollFade>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/ChatMessage.tsx",
    "content": "import React from \"react\";\nimport { FlexCol } from \"../Flex\";\nimport Loading from \"../Loader/Loading\";\nimport type { Message } from \"./Chat\";\n\ntype ChatMessageProps = {\n  message: Message;\n};\nexport const ChatMessage = ({ message: m }: ChatMessageProps) => {\n  const { id, messageTopContent, isLoading } = m;\n\n  return (\n    <FlexCol\n      className={\n        \"message gap-0 ai-start relative \" + (m.incoming ? \"incoming\" : \"\")\n      }\n      key={id}\n    >\n      {isLoading ?\n        <div\n          className=\"content-wrapper\"\n          style={{ height: \"80px\", width: \"80px\" }}\n        >\n          <Loading className=\"m-1\" sizePx={22} />\n        </div>\n      : <>\n          {messageTopContent}\n          <div className=\"content-wrapper\">{m.message}</div>\n        </>\n      }\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/ChatSendControls.tsx",
    "content": "import React, { useCallback } from \"react\";\n\nimport { useOnErrorAlert } from \"@components/AlertProvider\";\nimport { mdiArrowUp, mdiAttachment, mdiStopCircle } from \"@mdi/js\";\nimport { ChatActionBarBtnStyleProps } from \"src/dashboard/AskLLM/ChatActionBar/AskLLMChatActionBar\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport Btn from \"../Btn\";\nimport { FlexRow } from \"../Flex\";\nimport type { ChatProps } from \"./Chat\";\nimport { ChatSpeech } from \"./ChatSpeech/ChatSpeech\";\nimport type { ChatState } from \"./useChatState\";\n\ntype ChatSendControlsProps = Pick<\n  ChatProps,\n  \"onStopSending\" | \"disabledInfo\" | \"onSend\"\n> &\n  Pick<\n    ChatState,\n    \"onAddFiles\" | \"files\" | \"setCurrentMessage\" | \"sendMsg\" | \"sendingMsg\"\n  > & {\n    textAreaRef: React.RefObject<HTMLTextAreaElement>;\n  };\nexport const ChatSendControls = ({\n  onStopSending,\n  onAddFiles,\n  onSend,\n  files,\n  disabledInfo,\n  setCurrentMessage,\n  sendMsg,\n  sendingMsg,\n  textAreaRef,\n}: ChatSendControlsProps) => {\n  const onSpeech = useCallback(\n    async (audioOrTranscript: Blob | string, autoSend: boolean) => {\n      if (typeof audioOrTranscript === \"string\") {\n        if (autoSend) {\n          await onSend(audioOrTranscript, files);\n        } else {\n          setCurrentMessage(audioOrTranscript);\n        }\n        return;\n      } else {\n        try {\n          const file = new File([audioOrTranscript], \"recording.ogg\", {\n            type: \"audio/webm\",\n            lastModified: Date.now(),\n          });\n          if (autoSend) {\n            await onSend(\"\", [file]);\n          } else {\n            onAddFiles([file]);\n          }\n        } catch (e) {\n          console.error(e);\n        }\n      }\n    },\n    [files, onAddFiles, onSend, setCurrentMessage],\n  );\n  const { onErrorAlert } = useOnErrorAlert();\n  const fileRef = React.useRef<HTMLInputElement>(null);\n  return (\n    <div\n      className={`ChatSendControls ${window.isMobile ? \"flex-col\" : \"flex-row\"} as-end ai-center jc-center gap-p5`}\n    >\n      <FlexRow className=\"gap-0\">\n        <>\n          <Btn\n            data-command=\"Chat.addFiles\"\n            iconPath={mdiAttachment}\n            {...ChatActionBarBtnStyleProps}\n            onClick={() => {\n              fileRef.current?.click();\n            }}\n          />\n          <input\n            ref={fileRef}\n            type=\"file\"\n            multiple\n            style={{ display: \"none\" }}\n            onChange={(e) => {\n              onAddFiles(Array.from(e.target.files || []));\n            }}\n          />\n        </>\n        <ChatSpeech onFinished={onSpeech} isSending={Boolean(onStopSending)} />\n      </FlexRow>\n      {onStopSending ?\n        <Btn\n          data-command=\"Chat.sendStop\"\n          onClick={onStopSending}\n          title={t.common.Stop}\n          iconPath={mdiStopCircle}\n        />\n      : <Btn\n          iconPath={mdiArrowUp}\n          loading={sendingMsg}\n          title={t.common.Send}\n          className=\"b bg-color-3 round\"\n          data-command=\"Chat.send\"\n          disabledInfo={disabledInfo}\n          onClick={() => {\n            if (!textAreaRef.current) return;\n            void onErrorAlert(() => sendMsg());\n          }}\n        />\n      }\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/ChatSpeech/ChatSpeech.tsx",
    "content": "import React, {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { useOnErrorAlert } from \"@components/AlertProvider\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { mdiMicrophone, mdiMicrophoneSettings, mdiStop } from \"@mdi/js\";\nimport { ChatActionBarBtnStyleProps } from \"src/dashboard/AskLLM/ChatActionBar/AskLLMChatActionBar\";\nimport { useDebouncedCallback } from \"src/hooks/useDebouncedCallback\";\nimport { useThrottledCallback } from \"src/hooks/useThrottledCallback\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport Btn from \"../../Btn\";\nimport { ChatSpeechSetup } from \"./ChatSpeechSetup\";\nimport { renderSpeechAudioLevelsIcon } from \"./hooks/renderSpeechAudioLevelsIcon\";\nimport { useSpeechRecorder } from \"./hooks/useSpeechRecorder\";\nimport { useSpeechToTextWeb } from \"./hooks/useSpeechToTextWeb\";\nimport { useChatSpeechSetup } from \"./useChatSpeechSetup\";\n\ntype P = {\n  onFinished: (\n    audioOrTranscript: Blob | string,\n    autoSend: boolean,\n  ) => Promise<void>;\n  isSending: boolean;\n};\nexport const ChatSpeech = ({ onFinished, isSending }: P) => {\n  const [sessionState, setSessionState] = useState<\"recorded\" | \"stopped\">();\n  const { onErrorAlert } = useOnErrorAlert();\n  const chatSpeechSetupState = useChatSpeechSetup();\n  const { speechToTextMode, transcribeAudio, sendMode, speechEnabledErrors } =\n    chatSpeechSetupState;\n\n  const [isTranscribing, setIsTranscribing] = useState(false);\n  const onFinishedWithOptions = useCallback(\n    async (audioOrTranscript: Blob | string) => {\n      await onErrorAlert(async () => {\n        const autoSend = sendMode === \"auto\";\n        if (speechToTextMode === \"stt-local\") {\n          if (!transcribeAudio) {\n            throw new Error(\"Transcription service is not available\");\n          }\n          setIsTranscribing(true);\n          const sttResult = await transcribeAudio(audioOrTranscript as Blob);\n          if (\"success\" in sttResult) {\n            if (sttResult.transcription) {\n              await onFinished(sttResult.transcription, autoSend);\n            }\n          } else throw sttResult.error;\n        } else {\n          await onFinished(audioOrTranscript, autoSend);\n        }\n        setSessionState(\"recorded\");\n      }).finally(() => {\n        setIsTranscribing(false);\n      });\n    },\n    [speechToTextMode, transcribeAudio, onErrorAlert, onFinished, sendMode],\n  );\n\n  const speechToTextWeb = useSpeechToTextWeb(onFinishedWithOptions);\n  const [anchorEl, setAnchorEl] = useState<HTMLElement>();\n  const listeningCanvasRef = useRef<HTMLCanvasElement>(null);\n  const onSoundLevelChange = useThrottledCallback(\n    (recentLevels: number[], max: number, isSpeaking: boolean) => {\n      if (listeningCanvasRef.current) {\n        renderSpeechAudioLevelsIcon(\n          listeningCanvasRef.current,\n          recentLevels,\n          max,\n          isSpeaking,\n        );\n      }\n    },\n    [],\n    50,\n  );\n\n  const opts = useMemo(\n    () => ({\n      onSoundLevel: onSoundLevelChange,\n    }),\n    [onSoundLevelChange],\n  );\n  const speechAudio = useSpeechRecorder(onFinishedWithOptions, opts);\n\n  const isSpeechToTextEnabled =\n    speechToTextMode === \"stt-local\" || speechToTextMode === \"stt-web\";\n  const speechHooks = {\n    \"stt-web\": speechToTextWeb,\n    \"stt-local\": speechAudio,\n    audio: speechAudio,\n    off: undefined,\n  }[speechToTextMode];\n  const { isListening } = speechHooks ?? {};\n\n  const startRecording =\n    speechHooks?.isListening ? undefined : speechHooks?.start;\n  const debouncedStart = useDebouncedCallback(\n    () => {\n      if (!startRecording) return;\n      startRecording();\n    },\n    [startRecording],\n    1500,\n  );\n\n  useEffect(() => {\n    if (sessionState !== \"recorded\" || sendMode !== \"auto\" || isSending) return;\n\n    debouncedStart();\n  }, [debouncedStart, sessionState, isSending, isTranscribing, sendMode]);\n\n  return (\n    <>\n      <Btn\n        className={\n          \"ChatSpeech relative bg-transparent round \" +\n          (isListening ? \"bdb-action\" : \"\")\n        }\n        disabledInfo={isSending ? \"Waiting for response ...\" : undefined}\n        data-command=\"Chat.speech\"\n        loading={isTranscribing}\n        onClick={({ currentTarget }) => {\n          if (!speechHooks || speechEnabledErrors) {\n            setAnchorEl(currentTarget);\n          } else {\n            if (speechHooks.isListening) {\n              speechHooks.stop();\n              setSessionState(\"stopped\");\n            } else {\n              speechHooks.start();\n            }\n          }\n        }}\n        {...ChatActionBarBtnStyleProps}\n        style={{\n          ...ChatActionBarBtnStyleProps.style,\n          borderRadius: \"50%\",\n        }}\n        size={undefined}\n        color={\n          speechEnabledErrors ? \"warn\"\n          : isListening ?\n            \"action\"\n          : undefined\n        }\n        title={\n          speechEnabledErrors ??\n          (isListening ? t.common[\"Stop recording\"]\n          : isSpeechToTextEnabled ? t.common[\"Speech to text\"]\n          : t.common[\"Record audio\"]) + \" (right click for options)\"\n        }\n        iconNode={\n          <>\n            <canvas\n              ref={listeningCanvasRef}\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                borderRadius: \"50%\",\n                opacity: isListening ? 1 : 0,\n              }}\n            />\n            {/** Just to take up space */}\n            <Icon\n              path={\n                isListening ? mdiStop\n                : speechToTextMode === \"off\" ?\n                  mdiMicrophoneSettings\n                : mdiMicrophone\n              }\n              style={{ opacity: isListening ? 0 : 1 }}\n            />\n          </>\n        }\n        onContextMenu={(e) => {\n          e.preventDefault();\n          e.stopPropagation();\n          setAnchorEl(e.currentTarget);\n        }}\n      />\n      {anchorEl && (\n        <ChatSpeechSetup\n          anchorEl={anchorEl}\n          {...chatSpeechSetupState}\n          onClose={() => setAnchorEl(undefined)}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/ChatSpeech/ChatSpeechSetup.tsx",
    "content": "import React from \"react\";\n\nimport { Select } from \"@components/Select/Select\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport { Services } from \"@pages/ServerSettings/Services\";\nimport Popup from \"../../Popup/Popup\";\nimport {\n  SpeechModeOptions,\n  SpeechToTextSendModes,\n  type ChatSpeechSetupState,\n} from \"./useChatSpeechSetup\";\nimport ErrorComponent from \"@components/ErrorComponent\";\n\nexport const ChatSpeechSetup = ({\n  onClose,\n  sendMode,\n  setSendMode,\n  setSpeechToTextMode,\n  speechToTextMode,\n  anchorEl,\n  mustEnableTranscriptionService,\n}: {\n  onClose: VoidFunction;\n  anchorEl: HTMLElement;\n  mustEnableTranscriptionService: boolean;\n} & ChatSpeechSetupState) => {\n  const { dbs, dbsMethods, dbsTables, user } = usePrgl();\n  return (\n    <Popup\n      title={\"Microphone options\"}\n      onClickClose={false}\n      onClose={onClose}\n      positioning=\"above-center\"\n      contentClassName=\"p-1 gap-1\"\n      anchorEl={anchorEl}\n      clickCatchStyle={{ opacity: 1 }}\n    >\n      <Select\n        label={\"Speech mode\"}\n        value={speechToTextMode}\n        onChange={(newMode) => setSpeechToTextMode(newMode)}\n        fullOptions={SpeechModeOptions}\n        variant=\"button-group-vertical\"\n      />\n      {speechToTextMode === \"stt-local\" &&\n        user?.type !== \"admin\" &&\n        mustEnableTranscriptionService && (\n          <ErrorComponent\n            error={\"Transcription service must be enabled by an admin user.\"}\n          />\n        )}\n      {speechToTextMode === \"stt-local\" && user?.type === \"admin\" && (\n        <Services\n          showSpecificService={{\n            color: mustEnableTranscriptionService ? \"red\" : undefined,\n            title:\n              mustEnableTranscriptionService ?\n                \"Must enable Speech to Text Service\"\n              : \"Transcription service\",\n            serviceName: \"speechToText\",\n          }}\n          dbs={dbs}\n          dbsMethods={dbsMethods}\n          dbsTables={dbsTables}\n        />\n      )}\n      {speechToTextMode !== \"off\" && (\n        <Select\n          fullOptions={SpeechToTextSendModes}\n          label={\"Send mode\"}\n          value={sendMode}\n          onChange={setSendMode}\n          variant=\"button-group-vertical\"\n        />\n      )}\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/ChatSpeech/hooks/SpeechRecorder.ts",
    "content": "import { isPlaywrightTest } from \"src/i18n/i18nUtils\";\n\nconst MINIMUM_VARIANCE = 20;\nconst MINIMUM_SPEECH_THRESHOLD = 15;\nlet calibrated:\n  | undefined\n  | {\n      variance: number;\n      threshold: number;\n    };\nlet calibrationLevels: number[] = [];\n\n/** Time to wait before sending */\nconst SILENCE_DURATION = 1500;\nconst SUSTAINED_SPEECH_DURATION = 200;\nconst INITIAL_PAUSE_DURATION = isPlaywrightTest ? 100 : 1000;\n\ntype SpeechRecorderConfig = {\n  silenceDuration?: number;\n  maxDuration?: number;\n  sustainedSpeechDuration?: number;\n};\n\ntype SpeechRecorderCallbacks = {\n  onRecording?: () => void;\n  onFinished: (audio: Blob) => void;\n  onError?: (error: Error) => void;\n  onNoSpeech?: () => void;\n  onSoundLevel?: (\n    recentLevels: number[],\n    max: number,\n    isSpeaking: boolean,\n  ) => void;\n};\n\nconst MIME_TYPE =\n  MediaRecorder.isTypeSupported(\"audio/webm;codecs=opus\") ?\n    \"audio/webm;codecs=opus\"\n  : MediaRecorder.isTypeSupported(\"audio/webm\") ? \"audio/webm\"\n  : \"audio/mp4\";\n\nexport class SpeechRecorder {\n  private config: Required<SpeechRecorderConfig>;\n  private callbacks: SpeechRecorderCallbacks;\n\n  private stream: MediaStream | null = null;\n  private audioContext: AudioContext | null = null;\n  private analyser: AnalyserNode | null = null;\n  private mediaRecorder: MediaRecorder | null = null;\n  private animationFrame: number | null = null;\n\n  private chunks: Blob[] = [];\n  private startTime = 0;\n  private silenceStart: number | null = null;\n  private sustainedSpeechStart: number | null = null;\n  private hasConfirmedSpeech = false;\n  private isStopping = false;\n\n  constructor(\n    callbacks: SpeechRecorderCallbacks,\n    config: SpeechRecorderConfig = {},\n  ) {\n    this.callbacks = callbacks;\n    this.config = {\n      silenceDuration: config.silenceDuration ?? SILENCE_DURATION,\n      maxDuration: config.maxDuration ?? 60000,\n      sustainedSpeechDuration:\n        config.sustainedSpeechDuration ?? SUSTAINED_SPEECH_DURATION,\n    };\n  }\n\n  async start(): Promise<void> {\n    this.reset();\n\n    try {\n      this.stream = await navigator.mediaDevices.getUserMedia({\n        audio: {\n          echoCancellation: true,\n          noiseSuppression: true,\n          autoGainControl: true,\n        },\n      });\n\n      this.audioContext = new AudioContext();\n      this.analyser = this.audioContext.createAnalyser();\n      this.analyser.fftSize = 1024;\n      this.analyser.smoothingTimeConstant = 0.3;\n      this.audioContext\n        .createMediaStreamSource(this.stream)\n        .connect(this.analyser);\n\n      this.mediaRecorder = new MediaRecorder(this.stream, {\n        mimeType: MIME_TYPE,\n      });\n      this.mediaRecorder.ondataavailable = (e) =>\n        e.data.size > 0 && this.chunks.push(e.data);\n      this.mediaRecorder.onstop = () => this.handleStop();\n      this.mediaRecorder.onerror = () =>\n        this.handleError(new Error(\"MediaRecorder error\"));\n\n      this.mediaRecorder.start(100);\n      this.startTime = Date.now();\n      this.callbacks.onRecording?.();\n      this.monitor();\n    } catch (error) {\n      await this.handleError(\n        error instanceof Error ? error : new Error(\"Failed to start recording\"),\n      );\n    }\n  }\n\n  stop(): void {\n    if (this.isStopping) return;\n    this.isStopping = true;\n\n    if (this.animationFrame) cancelAnimationFrame(this.animationFrame);\n    if (this.mediaRecorder?.state === \"recording\") this.mediaRecorder.stop();\n  }\n\n  private reset(): void {\n    this.chunks = [];\n    this.silenceStart = null;\n    this.sustainedSpeechStart = null;\n    this.hasConfirmedSpeech = false;\n    this.isStopping = false;\n    calibrationLevels = [];\n    this.recentLevels = [];\n  }\n\n  private async cleanup() {\n    if (this.animationFrame) cancelAnimationFrame(this.animationFrame);\n    if (this.audioContext?.state !== \"closed\") await this.audioContext?.close();\n    this.stream?.getTracks().forEach((t) => t.stop());\n    this.stream = null;\n    this.audioContext = null;\n    this.analyser = null;\n    this.mediaRecorder = null;\n  }\n\n  private getAudioLevel(): number {\n    if (!this.analyser) return 0;\n    const data = new Uint8Array(this.analyser.frequencyBinCount);\n    this.analyser.getByteFrequencyData(data);\n    // return Math.sqrt(data.reduce((sum, v) => sum + v * v, 0) / data.length);\n\n    // With fftSize=1024 and 48kHz sample rate, each bin ≈ 46.9Hz\n    // Speech fundamentals: bins 2-12 (~85-560Hz)\n    // Speech formants: bins 12-64 (~560-3000Hz)\n    // Skip bin 0 (DC) and very low frequencies where fan noise dominates\n\n    const sampleRate = this.audioContext?.sampleRate ?? 48000;\n    const binWidth = sampleRate / 1024;\n\n    // Focus on 200Hz - 3500Hz (speech range)\n    const startBin = Math.floor(200 / binWidth);\n    const endBin = Math.min(Math.floor(3500 / binWidth), data.length);\n\n    let sum = 0;\n    for (let i = startBin; i < endBin; i++) {\n      const dataItem = data[i];\n      if (dataItem) {\n        sum += dataItem * dataItem;\n      }\n    }\n\n    return Math.sqrt(sum / (endBin - startBin));\n  }\n\n  private getVariance(values: number[]): number {\n    if (values.length < 2) return 0;\n    const mean = values.reduce((a, b) => a + b, 0) / values.length;\n    return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n  }\n\n  get elapsed(): number {\n    return Date.now() - this.startTime;\n  }\n\n  /**\n   * When just started, the sound levels may be unreliable due to\n   * initial microphone adjustments or ambient noise calibration.\n   */\n  get justStarted(): boolean {\n    return this.elapsed < INITIAL_PAUSE_DURATION;\n  }\n\n  private recentLevels: number[] = [];\n  private monitor = (): void => {\n    if (this.isStopping) return;\n    const now = Date.now();\n\n    const elapsed = this.elapsed;\n    const level = this.getAudioLevel();\n    // Track recent levels for variance calculation\n    this.recentLevels.push(level);\n    if (this.recentLevels.length > 1000) this.recentLevels.shift();\n\n    // Calculate variance (speech has high variance, fan noise is steady)\n    const variance = this.getVariance(this.recentLevels.slice(-10));\n\n    const varianceThreshold = calibrated?.variance ?? MINIMUM_VARIANCE;\n    const speechThreshold = calibrated?.threshold ?? MINIMUM_SPEECH_THRESHOLD;\n\n    const isSpeaking =\n      !this.justStarted &&\n      level > speechThreshold &&\n      variance > varianceThreshold;\n\n    if (isPlaywrightTest) {\n      console[isSpeaking ? \"warn\" : \"log\"](\n        [\n          `sustained:${now - (this.sustainedSpeechStart ?? now)}`,\n          `elapsed:${elapsed}`,\n          `isSpeaking:${isSpeaking}`,\n          `level: ${level.toFixed(2)}`,\n          `levelT: ${speechThreshold.toFixed(2)}`,\n          `variance: ${variance}`,\n          `varianceT: ${varianceThreshold.toFixed(2)}`,\n          `varianceC: ${calibrated?.variance}`,\n          `(Threshold: ${speechThreshold.toFixed(2)})`,\n        ].join(\" \"),\n      );\n    }\n\n    const { onSoundLevel } = this.callbacks;\n    if (onSoundLevel && this.recentLevels.length >= 5) {\n      const binSize = Math.min(5, Math.floor(this.recentLevels.length / 5));\n      const binLevels = this.recentLevels.slice(-binSize * 5);\n      const levels: number[] = [];\n      for (let i = 0; i < 5 * binSize; i += binSize) {\n        levels.push(binLevels[i]!);\n      }\n      onSoundLevel(levels, Math.max(...this.recentLevels), isSpeaking);\n    }\n\n    // Calibration during first few seconds\n    if (!calibrated) {\n      if (elapsed > 500 && elapsed < 3000) {\n        calibrationLevels.push(level);\n      } else if (calibrationLevels.length) {\n        const avg =\n          calibrationLevels.reduce((a, b) => a + b, 0) /\n          calibrationLevels.length;\n        const max = Math.max(...calibrationLevels);\n        const threshold = Math.max(15, 0.5 * (avg + (max - avg) * 0.5 + 5));\n        const variance = Math.max(\n          MINIMUM_VARIANCE,\n          0.01 * this.getVariance(calibrationLevels),\n        );\n        calibrated = { variance, threshold };\n      }\n    }\n\n    if (isSpeaking) {\n      this.silenceStart = null;\n      if (!this.sustainedSpeechStart) {\n        this.sustainedSpeechStart = now;\n      } else if (\n        now - this.sustainedSpeechStart >=\n        this.config.sustainedSpeechDuration\n      ) {\n        this.hasConfirmedSpeech = true;\n      }\n    } else {\n      this.sustainedSpeechStart = null;\n      if (this.hasConfirmedSpeech) {\n        this.silenceStart ??= now;\n        if (now - this.silenceStart >= this.config.silenceDuration) {\n          return this.stop();\n        }\n      }\n    }\n\n    if (elapsed >= this.config.maxDuration) return this.stop();\n\n    this.animationFrame = requestAnimationFrame(this.monitor);\n  };\n\n  private async handleStop() {\n    const hadSpeech = this.hasConfirmedSpeech;\n    const blob = new Blob(this.chunks, { type: MIME_TYPE });\n    await this.cleanup();\n\n    if (hadSpeech && blob.size > 0) {\n      this.callbacks.onFinished(blob);\n    } else {\n      this.callbacks.onNoSpeech?.();\n    }\n  }\n\n  private async handleError(error: Error) {\n    await this.cleanup();\n    this.callbacks.onError?.(error);\n  }\n}\n"
  },
  {
    "path": "client/src/components/Chat/ChatSpeech/hooks/renderSpeechAudioLevelsIcon.ts",
    "content": "import { createHiPPICanvas } from \"src/dashboard/Charts/createHiPPICanvas\";\nimport {\n  drawShapes,\n  type ShapeV2,\n} from \"src/dashboard/Charts/drawShapes/drawShapes\";\n\nconst BAR_COUNT = 5;\nconst BAR_SPACING = 4;\nconst X_PADDING = 4;\n\nexport const renderSpeechAudioLevelsIcon = (\n  canvas: HTMLCanvasElement,\n  recentLevels: number[],\n  levelThreshold: number,\n  isSpeaking: boolean,\n) => {\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    return;\n  }\n  const { width: w, height: h } = canvas.parentElement!.getBoundingClientRect();\n  ctx.canvas.width = w;\n  ctx.canvas.height = h;\n  createHiPPICanvas(canvas, w, h);\n  const netWidth = w - X_PADDING * 2 - (BAR_COUNT - 1) * BAR_SPACING;\n  const barWidth = Math.floor(netWidth / BAR_COUNT);\n\n  drawShapes(\n    [\n      {\n        id: \"background\",\n        type: \"rectangle\" as const,\n        coords: [0, 0],\n        w: ctx.canvas.width,\n        h: ctx.canvas.height,\n        fillStyle:\n          !isSpeaking ? \"transparent\" : (\n            getComputedStyle(document.documentElement).getPropertyValue(\n              \"--faded-blue\",\n            )\n          ),\n        lineWidth: 0,\n        strokeStyle: \"transparent\",\n      },\n      ...recentLevels.map((audioLevel, index) => {\n        const max = levelThreshold * 2;\n        const levelHeight = Math.min(h, (audioLevel / max) * h);\n        const edgeDiff = (h - levelHeight) / 2;\n        const x = X_PADDING + index * (barWidth + BAR_SPACING);\n        return {\n          id: `level-${index}`,\n          type: \"rectangle\" as const,\n          coords: [x, edgeDiff],\n          h: levelHeight,\n          w: barWidth,\n          borderRadius: 2,\n          fillStyle: getComputedStyle(\n            document.documentElement,\n          ).getPropertyValue(isSpeaking ? \"--blue\" : \"--gray\"),\n          lineWidth: 0,\n          strokeStyle: \"transparent\",\n        } satisfies ShapeV2;\n      }),\n    ],\n    canvas,\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/ChatSpeech/hooks/useSpeechRecorder.ts",
    "content": "import { useState, useRef, useCallback, useEffect } from \"react\";\nimport { SpeechRecorder } from \"./SpeechRecorder\";\n\ntype SpeechRecorderConfig = {\n  silenceDuration?: number;\n  maxDuration?: number;\n  initialGracePeriod?: number;\n  sustainedSpeechDuration?: number;\n  onSoundLevel?: (\n    recentLevels: number[],\n    max: number,\n    isSpeaking: boolean,\n  ) => void;\n};\n\ntype UseSpeechRecorderState =\n  | { isListening: false; start: () => void }\n  | { isListening: true; stop: () => void };\n\nexport function useSpeechRecorder(\n  onFinished: (audio: Blob, restart: () => void) => void,\n  config: SpeechRecorderConfig = {},\n): UseSpeechRecorderState {\n  const [isListening, setIsListening] = useState(false);\n  const recorderRef = useRef<SpeechRecorder | null>(null);\n\n  const start = useCallback(() => {\n    const recorder = new SpeechRecorder(\n      {\n        onRecording: () => setIsListening(true),\n        onFinished: (blob) => {\n          setIsListening(false);\n          onFinished(blob, start);\n        },\n        onNoSpeech: () => setIsListening(false),\n        onError: () => setIsListening(false),\n        onSoundLevel: config.onSoundLevel,\n      },\n      config,\n    );\n\n    recorderRef.current = recorder;\n    void recorder.start();\n  }, [onFinished, config]);\n\n  const stop = useCallback(() => {\n    recorderRef.current?.stop();\n  }, []);\n\n  useEffect(() => {\n    return () => {\n      recorderRef.current?.stop();\n    };\n  }, []);\n\n  return isListening ?\n      { isListening: true, stop }\n    : { isListening: false, start };\n}\n"
  },
  {
    "path": "client/src/components/Chat/ChatSpeech/hooks/useSpeechToTextWeb.ts",
    "content": "import { useState, useRef, useCallback, useEffect } from \"react\";\n\nexport type SpeechToTextState =\n  | {\n      isListening: false;\n      start: () => void;\n    }\n  | {\n      isListening: true;\n      stop: () => void;\n    };\n\nlet confirmed = false;\n\nexport const useSpeechToTextWeb = (\n  onFinished: (audioOrTranscript: Blob | string, start: VoidFunction) => void,\n): SpeechToTextState => {\n  const [isListening, setIsListening] = useState(false);\n  const [text, setText] = useState(\"\");\n  const recognitionRef = useRef<SpeechRecognition>();\n  const initializeRecognition = useCallback(() => {\n    const SpeechRecognitionAPI =\n      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n      window.SpeechRecognition || window.webkitSpeechRecognition;\n\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    if (!SpeechRecognitionAPI) return;\n\n    const recognition = new SpeechRecognitionAPI();\n    recognition.continuous = true;\n    recognition.interimResults = true;\n    recognition.lang = \"en-US\";\n\n    recognition.onresult = (event: SpeechRecognitionEvent): void => {\n      const finalTranscript = Array.from(event.results)\n        .slice(event.resultIndex)\n        .filter((result) => result.isFinal)\n        .map((result) => result[0]?.transcript)\n        .join(\"\");\n\n      console.log(\n        \"Speech recognition result:\",\n        event.results[0]?.isFinal,\n        finalTranscript,\n      );\n      if (finalTranscript) {\n        setText(finalTranscript);\n      }\n    };\n\n    recognition.onerror = (error): void => {\n      console.error(\"Speech recognition error:\", error);\n      if (error.error === \"network\") {\n        alert(\n          \"Speech recognition is not supported in this browser. Please try a different browser (preferrably Chrome) or check your network connection.\",\n        );\n      }\n      setIsListening(false);\n    };\n\n    recognition.onend = (): void => {\n      setIsListening(false);\n    };\n\n    return recognition;\n  }, []);\n\n  const start = useCallback((): void => {\n    confirmed =\n      confirmed ||\n      window.confirm(\n        \"Speech recognition will access your microphone to convert speech to text. Your browser's privacy settings control how this data is handled. Do you want to continue?\",\n      );\n    if (!confirmed) {\n      return;\n    }\n    if (!recognitionRef.current) {\n      recognitionRef.current = initializeRecognition();\n    }\n\n    if (recognitionRef.current && !isListening) {\n      try {\n        recognitionRef.current.start();\n        setIsListening(true);\n      } catch (error) {\n        // Recognition already started or other error\n        console.error(\"Speech recognition error:\", error);\n        setIsListening(false);\n      }\n    }\n  }, [initializeRecognition, isListening]);\n\n  useEffect(() => {\n    if (text.length) {\n      onFinished(text, start);\n    }\n  }, [isListening, text, onFinished, start]);\n\n  const stop = useCallback((): void => {\n    if (recognitionRef.current && isListening) {\n      recognitionRef.current.stop();\n      setIsListening(false);\n    }\n  }, [isListening]);\n\n  useEffect(() => {\n    return () => {\n      if (recognitionRef.current) {\n        recognitionRef.current.stop();\n      }\n    };\n  }, []);\n\n  if (isListening) {\n    return {\n      isListening: true,\n      stop,\n    };\n  }\n\n  return {\n    isListening: false,\n    start,\n  };\n};\n"
  },
  {
    "path": "client/src/components/Chat/ChatSpeech/useChatSpeechSetup.ts",
    "content": "import { useOnErrorAlert } from \"@components/AlertProvider\";\nimport type { FullOption } from \"@components/Select/Select\";\nimport {\n  mdiGoogleChrome,\n  mdiMicrophoneMessage,\n  mdiMicrophoneOff,\n  mdiWaveform,\n} from \"@mdi/js\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport { useCallback } from \"react\";\n\nexport const SpeechModeOptions = [\n  {\n    iconPath: mdiMicrophoneOff,\n    key: \"off\",\n    label: \"Off\",\n    subLabel: \"Disable speech input\",\n  },\n  {\n    iconPath: mdiMicrophoneMessage,\n    key: \"stt-local\",\n    label: \"Local Speech Recognition\",\n    subLabel: \"Uses a local model for private transcription\",\n  },\n  {\n    iconPath: mdiGoogleChrome,\n    key: \"stt-web\",\n    label: \"Web Speech API\",\n    subLabel: \"Uses your browser's built-in speech recognition\",\n  },\n  {\n    iconPath: mdiWaveform,\n    key: \"audio\",\n    label: \"Audio Recording\",\n    subLabel: \"Records audio and sends it as a file\",\n  },\n] as const satisfies FullOption[];\n\nexport const SpeechToTextSendModes = [\n  {\n    key: \"manual\",\n    label: \"Manual\",\n    subLabel: \"Press send button to send the message.\",\n  },\n  {\n    key: \"auto\",\n    label: \"Auto\",\n    subLabel:\n      \"Automatically sends messages when you stop speaking for a moment.\",\n  },\n] as const satisfies FullOption[];\n\nexport const useChatSpeechSetup = () => {\n  const { dbsMethods, user, dbs } = usePrgl();\n  const { transcribeAudio } = dbsMethods;\n  const speechToTextMode = user?.options?.speech_mode ?? \"off\";\n  const sendMode = user?.options?.speech_send_mode ?? \"manual\";\n  const { onErrorAlert } = useOnErrorAlert();\n  const setSpeechToTextMode = useCallback(\n    (newMode = speechToTextMode, newSendMode = sendMode) => {\n      void onErrorAlert(async () => {\n        if (!user) throw new Error(\"No user logged in\");\n        await dbs.users.update(\n          { id: user.id },\n          {\n            options: {\n              $merge: [{ speech_mode: newMode, speech_send_mode: newSendMode }],\n            },\n          },\n        );\n      });\n    },\n    [speechToTextMode, sendMode, onErrorAlert, user, dbs.users],\n  );\n\n  const setSendMode = useCallback(\n    (newSendMode: typeof sendMode) => {\n      setSpeechToTextMode(undefined, newSendMode);\n    },\n    [setSpeechToTextMode],\n  );\n  const { data: transcriptionService } = dbs.services.useSubscribeOne({\n    name: \"speechToText\",\n  });\n  const mustEnableTranscriptionService = Boolean(\n    speechToTextMode === \"stt-local\" &&\n      transcriptionService &&\n      transcriptionService.status !== \"running\",\n  );\n  const speechEnabledErrors =\n    mustEnableTranscriptionService ?\n      \"Must enable speech to text service\"\n    : undefined;\n\n  return {\n    sendMode,\n    setSendMode,\n    speechToTextMode,\n    setSpeechToTextMode,\n    transcribeAudio,\n    speechEnabledErrors,\n    mustEnableTranscriptionService,\n  };\n};\n\nexport type ChatSpeechSetupState = ReturnType<typeof useChatSpeechSetup>;\n"
  },
  {
    "path": "client/src/components/Chat/Marked.css",
    "content": ".Marked p,\n.Marked ul,\n.Marked ol,\n/* .Marked h1,\n.Marked h2,\n.Marked h3,\n.Marked h4,\n.Marked h5, */\n.Marked blockquote,\n.Marked pre {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n\n/** Used to ensure the li marker does not appear above li item */\n.Marked {\n  white-space: normal;\n  /* Allows long words to break and wrap onto the next line */\n  word-break: break-word;\n}\n\n/* .Marked ul,\n.Marked ol {\n  line-height: 1em;\n} */\n\n.Marked ol {\n  list-style: none;\n  padding-left: 0;\n}\n\n/* Used to ensure the li marker does not appear above li item  \n.Marked ol > li {\n  line-height: 0;\n}\n  */\n"
  },
  {
    "path": "client/src/components/Chat/Marked.tsx",
    "content": "import { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport React, { useCallback } from \"react\";\nimport Markdown from \"react-markdown\";\nimport { classOverride, type DivProps } from \"../Flex\";\nimport {\n  MonacoCodeInMarkdown,\n  type MonacoCodeInMarkdownProps,\n} from \"./MonacoCodeInMarkdown/MonacoCodeInMarkdown\";\nimport \"./Marked.css\";\nimport remarkGfm from \"remark-gfm\";\nimport rehypeRaw from \"rehype-raw\";\n\nexport type MarkedProps = DivProps &\n  Pick<\n    MonacoCodeInMarkdownProps,\n    \"codeHeader\" | \"sqlHandler\" | \"loadedSuggestions\"\n  > & {\n    content: string;\n  };\n\nexport const Marked = (props: MarkedProps) => {\n  const { content, codeHeader, sqlHandler, loadedSuggestions, ...divProps } =\n    props;\n\n  const CodeComponent = useCallback(\n    ({\n      node,\n      className,\n      ...props\n    }: React.DetailedHTMLProps<\n      React.HTMLAttributes<HTMLElement>,\n      HTMLElement\n    > & { node?: any }) => {\n      const match = /language-(\\w+)/.exec(className || \"\");\n      const language = match ? match[1] : \"\";\n      // eslint-disable-next-line @typescript-eslint/no-base-to-string\n      const codeString = props.children?.toString() ?? \"\";\n\n      if (!codeString || !className || !language || language === \"markdown\") {\n        const isSingleWord =\n          !codeString.includes(\"\\n\") && !codeString.includes(\" \");\n\n        if (isSingleWord) {\n          <code {...props} />;\n        }\n        return (\n          <code {...props} style={{ ...props.style, whiteSpace: \"pre-line\" }} />\n        );\n      }\n\n      return (\n        <MonacoCodeInMarkdown\n          className=\"my-1\"\n          key={codeString}\n          codeHeader={codeHeader}\n          language={language}\n          codeString={codeString}\n          sqlHandler={sqlHandler}\n          loadedSuggestions={loadedSuggestions}\n        />\n      );\n    },\n    [codeHeader, sqlHandler, loadedSuggestions],\n  );\n\n  return (\n    <ScrollFade\n      {...divProps}\n      className={classOverride(\n        \"Marked flex-col o-auto min-w-0 max-w-full\",\n        divProps.className,\n      )}\n    >\n      <Markdown\n        remarkPlugins={[remarkGfm]}\n        rehypePlugins={[rehypeRaw]}\n        components={{\n          pre: React.Fragment,\n          code: CodeComponent,\n          a: (props) => (\n            <a\n              {...props}\n              className=\"link\"\n              target={props.href?.startsWith(\"#\") ? undefined : \"_blank\"}\n              rel={props.href?.startsWith(\"#\") ? undefined : \"noreferrer\"}\n            />\n          ),\n        }}\n      >\n        {content}\n      </Markdown>\n    </ScrollFade>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/MonacoCodeInMarkdown/MarkdownMonacoCodeHeader.tsx",
    "content": "import ErrorComponent from \"@components/ErrorComponent\";\nimport Popup from \"@components/Popup/Popup\";\nimport {\n  mdiDownload,\n  mdiFullscreen,\n  mdiOpenInNew,\n  mdiPlay,\n  mdiStop,\n} from \"@mdi/js\";\nimport React, { useMemo, useState } from \"react\";\nimport { download } from \"../../../dashboard/W_SQL/W_SQL\";\nimport Btn from \"../../Btn\";\nimport { CopyToClipboardBtn } from \"../../CopyToClipboardBtn\";\nimport { FlexCol, FlexRow } from \"../../Flex\";\nimport type { MonacoCodeInMarkdownProps } from \"./MonacoCodeInMarkdown\";\nimport type { useOnRunSQL } from \"./useOnRunSQL\";\n\nexport const MarkdownMonacoCodeHeader = (\n  props: MonacoCodeInMarkdownProps & {\n    titleOrLanguage: string;\n    fullscreen: boolean;\n    setFullscreen: (val: boolean) => void;\n  } & ReturnType<typeof useOnRunSQL>,\n) => {\n  const {\n    codeHeader,\n    language,\n    codeString,\n    fullscreen,\n    setFullscreen,\n    sqlHandler,\n    onRunSQL,\n    titleOrLanguage,\n    sqlResult,\n    setSqlResult,\n  } = props;\n  return (\n    <FlexRow className=\"MarkdownMonacoCodeHeader bg-color-2 p-p25\">\n      <div className=\"text-sm text-color-4 f-1 px-1 ta-start\">\n        {titleOrLanguage}\n      </div>\n      {codeHeader && codeHeader({ language, codeString })}\n      {sqlResult && sqlResult.state !== \"loading\" ?\n        <Btn onClick={() => setSqlResult(undefined)}>Close result</Btn>\n      : <>\n          {language === \"sql\" && sqlHandler && (\n            <>\n              <Btn\n                title=\"Execute SQL (Commited)\"\n                iconPath={mdiPlay}\n                variant=\"faded\"\n                size=\"small\"\n                clickConfirmation={{\n                  buttonText: \"Execute\",\n                  color: \"action\",\n                  message:\n                    \"This query will COMMIT (permanently save) changes. Double-check before running\",\n                }}\n                onClick={() => onRunSQL(true)}\n                disabledInfo={\n                  sqlResult?.state === \"loading\" && !sqlResult.withCommit ?\n                    \"Already running query\"\n                  : undefined\n                }\n                loading={sqlResult?.state === \"loading\" && sqlResult.withCommit}\n              />\n              <Btn\n                title=\"Execute SQL (With rollback)\"\n                iconPath={mdiPlay}\n                color=\"action\"\n                variant=\"faded\"\n                size=\"small\"\n                onClick={() => onRunSQL(false)}\n                disabledInfo={\n                  sqlResult?.state === \"loading\" && sqlResult.withCommit ?\n                    \"Already running query\"\n                  : undefined\n                }\n                loading={\n                  sqlResult?.state === \"loading\" && !sqlResult.withCommit\n                }\n              />\n              {sqlResult?.state === \"loading\" && (\n                <Btn\n                  title=\"Stop query (pg_terminate_backend)\"\n                  iconPath={mdiStop}\n                  color=\"action\"\n                  variant=\"faded\"\n                  size=\"small\"\n                  onClickPromise={async () => {\n                    await sqlHandler(\n                      \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE query = $1\",\n                      [sqlResult.query],\n                    );\n                  }}\n                />\n              )}\n            </>\n          )}\n          {language === \"html\" && <OpenHTMLPreviewBtn html={codeString} />}\n          <CopyToClipboardBtn\n            size=\"small\"\n            style={{\n              marginLeft: \"auto\",\n              flex: \"none\",\n            }}\n            content={codeString}\n          />\n          <Btn\n            title=\"Download\"\n            iconPath={mdiDownload}\n            onClick={() => {\n              download(codeString, `code.${language}`, \"text\");\n            }}\n          />\n        </>\n      }\n      <Btn\n        title=\"Toggle Fullscreen\"\n        iconPath={mdiFullscreen}\n        onClick={() => setFullscreen(!fullscreen)}\n      />\n    </FlexRow>\n  );\n};\n\nconst OpenHTMLPreviewBtn = ({ html }: { html: string }) => {\n  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);\n  const [error, setError] = useState<any>();\n\n  const blobURL = useMemo(() => {\n    const blob = new Blob([html], { type: \"text/html\" });\n    return URL.createObjectURL(blob);\n  }, [html]);\n  const iframeSandbox =\n    \"allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation\";\n  return (\n    <>\n      <Btn\n        iconPath={mdiOpenInNew}\n        title=\"Open preview...\"\n        clickConfirmation={{\n          buttonText: \"Open\",\n          color: \"action\",\n          message: (\n            <FlexCol>\n              <p>\n                {\" \"}\n                This will open the generated HTML code preview in an iframe.\n                Proceed with caution!\n              </p>\n              <span>\n                <strong>iframe sandbox</strong>: {iframeSandbox}\n              </span>\n            </FlexCol>\n          ),\n        }}\n        onClick={({ currentTarget }) => {\n          setAnchorEl(currentTarget);\n        }}\n      />\n      {anchorEl && (\n        <Popup\n          title=\"HTML Preview\"\n          anchorEl={anchorEl}\n          onClose={() => setAnchorEl(null)}\n          positioning=\"fullscreen\"\n        >\n          <iframe\n            title=\"HTML Preview\"\n            style={{ width: \"80vw\", height: \"80vh\", border: \"none\" }}\n            sandbox={iframeSandbox}\n            src={blobURL}\n            onError={() => {\n              setError(new Error(\"Failed to load HTML preview\"));\n            }}\n          />\n          {error && <ErrorComponent error={error} />}\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/MonacoCodeInMarkdown/MonacoCodeInMarkdown.tsx",
    "content": "import type { editor } from \"monaco-editor\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport type { LoadedSuggestions } from \"../../../dashboard/Dashboard/dashboardUtils\";\nimport { SuccessMessage } from \"../../Animations\";\nimport ErrorComponent from \"../../ErrorComponent\";\nimport { classOverride, FlexCol } from \"../../Flex\";\nimport {\n  MONACO_READONLY_DEFAULT_OPTIONS,\n  MonacoEditor,\n} from \"../../MonacoEditor/MonacoEditor\";\nimport Popup from \"../../Popup/Popup\";\nimport { Table } from \"../../Table/Table\";\nimport { MarkdownMonacoCodeHeader } from \"./MarkdownMonacoCodeHeader\";\nimport { useOnRunSQL } from \"./useOnRunSQL\";\n\nconst LANGUAGE_FALLBACK = new Map<string, string>([\n  [\"tsx\", \"typescript\"],\n  [\"ts\", \"typescript\"],\n]);\n\nexport type MonacoCodeInMarkdownProps = {\n  title?: string;\n  className?: string;\n  language: string;\n  codeString: string;\n  codeHeader:\n    | undefined\n    | ((opts: { language: string; codeString: string }) => React.ReactNode);\n  sqlHandler: DBHandlerClient[\"sql\"];\n  loadedSuggestions: LoadedSuggestions | undefined;\n};\nexport const MonacoCodeInMarkdown = (props: MonacoCodeInMarkdownProps) => {\n  const { language, codeString, title, loadedSuggestions, className } = props;\n  const [fullscreen, setFullscreen] = useState(false);\n\n  const monacoOptions = useMemo(() => {\n    return {\n      ...MONACO_READONLY_DEFAULT_OPTIONS,\n      lineNumbers: fullscreen ? \"on\" : \"off\",\n    } satisfies editor.IStandaloneEditorConstructionOptions;\n  }, [fullscreen]);\n\n  const onExit = useCallback(() => {\n    setFullscreen(false);\n  }, []);\n\n  const titleOrLanguage = title ?? language;\n\n  const runSQLState = useOnRunSQL(props);\n  const { sqlResult } = runSQLState;\n\n  const onListenToContentHeightChange = useCallback(\n    (editor: editor.IStandaloneCodeEditor) => {\n      const updateScrollHandling = () => {\n        const contentHeight = editor.getContentHeight();\n        const editorHeight = editor.getLayoutInfo().height;\n        const allowScroll = Boolean(\n          editor.getValue() && contentHeight - 20 > editorHeight,\n        );\n        editor.updateOptions({\n          scrollbar: {\n            handleMouseWheel: allowScroll,\n            alwaysConsumeMouseWheel: allowScroll,\n          },\n        });\n      };\n\n      // call after initial layout and on content change\n      updateScrollHandling();\n      const disposable = editor.onDidChangeModelContent(updateScrollHandling);\n\n      return () => {\n        disposable.dispose();\n      };\n    },\n    [],\n  );\n\n  return (\n    <FlexCol\n      className={classOverride(\n        \"MarkdownMonacoCode relative b b-color rounded gap-0 f-0 o-hidden \",\n        className,\n      )}\n      style={{\n        minWidth: \"min(100%,600px, 100vw)\",\n      }}\n      data-command=\"MarkdownMonacoCode\"\n    >\n      <MarkdownMonacoCodeHeader\n        {...props}\n        {...runSQLState}\n        fullscreen={fullscreen}\n        setFullscreen={setFullscreen}\n        titleOrLanguage={titleOrLanguage}\n      />\n      <FullscreenWrapper\n        key={codeString}\n        isFullscreen={fullscreen}\n        title={titleOrLanguage}\n        onExit={onExit}\n      >\n        <MonacoEditor\n          key={codeString}\n          className={fullscreen ? \"f-1\" : \"f-1\"}\n          loadedSuggestions={loadedSuggestions}\n          value={codeString}\n          language={LANGUAGE_FALLBACK.get(language) ?? language}\n          options={monacoOptions}\n          onMount={onListenToContentHeightChange}\n        />\n        {sqlResult?.state === \"ok-command-result\" ?\n          <SuccessMessage message={sqlResult.commandResult} />\n        : sqlResult?.state === \"error\" ?\n          <ErrorComponent error={sqlResult.error} />\n        : sqlResult?.state === \"ok\" ?\n          <Table\n            tableStyle={{\n              border: \"none\",\n              maxHeight: fullscreen ? undefined : \"70vh\",\n            }}\n            rows={sqlResult.rows}\n            cols={sqlResult.columns}\n          />\n        : null}\n      </FullscreenWrapper>\n    </FlexCol>\n  );\n};\n\nconst FullscreenWrapper = (props: {\n  isFullscreen: boolean;\n  onExit: VoidFunction;\n  children: React.ReactNode;\n  title: string;\n}) => {\n  const { children, isFullscreen, onExit, title } = props;\n\n  if (!isFullscreen) {\n    return children;\n  }\n  return (\n    <Popup\n      title={title}\n      positioning=\"fullscreen\"\n      onClickClose={false}\n      contentClassName=\"p-1\"\n      onClose={onExit}\n      contentStyle={{\n        overflow: \"visible\",\n      }}\n    >\n      {children}\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chat/MonacoCodeInMarkdown/useOnRunSQL.ts",
    "content": "import { useCallback, useState } from \"react\";\nimport type { MonacoCodeInMarkdownProps } from \"./MonacoCodeInMarkdown\";\nimport { getFieldsWithActions } from \"src/dashboard/W_SQL/parseSqlResultCols\";\nimport { getSQLResultTableColumns } from \"src/dashboard/W_SQL/getSQLResultTableColumns\";\n\nexport const useOnRunSQL = ({\n  codeString,\n  sqlHandler,\n}: MonacoCodeInMarkdownProps) => {\n  const [sqlResult, setSqlResult] = useState<SQLResult | undefined>(undefined);\n\n  const onRunSQL = useCallback(\n    (withCommit: boolean) => {\n      const queryId = crypto.randomUUID();\n      const queryWithId = `--${queryId} prostgles_ui_query_id\\n${codeString}`;\n      setSqlResult({ state: \"loading\", query: queryWithId, withCommit });\n      sqlHandler!(queryWithId, undefined, {\n        returnType: withCommit ? \"arrayMode\" : \"default-with-rollback\",\n      })\n        .then((data) => {\n          if (!data.fields.length) {\n            setSqlResult({\n              state: \"ok-command-result\",\n              commandResult: `${data.command || \"sql\"} executed successfully`,\n            });\n            setTimeout(() => {\n              setSqlResult(undefined);\n            }, 2000);\n            return;\n          }\n          const cols = getFieldsWithActions(\n            data.fields,\n            data.command?.toLowerCase() === \"select\",\n          );\n          const columns =\n            !withCommit ? cols : (\n              getSQLResultTableColumns({\n                cols,\n                tables: [],\n                maxCharsPerCell: undefined,\n                onResize: () => {},\n              })\n            );\n          setSqlResult({\n            state: \"ok\",\n            rows: data.rows,\n            columns,\n          });\n        })\n        .catch((err) => {\n          setSqlResult({ state: \"error\", error: err });\n        });\n    },\n    [codeString, sqlHandler],\n  );\n\n  return {\n    sqlResult,\n    setSqlResult,\n    onRunSQL,\n  };\n};\n\ntype SQLResult =\n  | { state: \"ok\"; rows: any[]; columns: any[] }\n  | { state: \"ok-command-result\"; commandResult: string }\n  | { state: \"error\"; error: unknown }\n  | { state: \"loading\"; query: string; withCommit: boolean };\n"
  },
  {
    "path": "client/src/components/Chat/useChatOnPaste.ts",
    "content": "import { useCallback } from \"react\";\nimport { tryCatchV2 } from \"prostgles-types\";\nimport { fixIndent } from \"../../demo/scripts/sqlVideoDemo\";\n\nexport const useChatOnPaste = ({\n  onAddFiles,\n  textAreaRef,\n  setCurrentMessage,\n}: {\n  textAreaRef: React.RefObject<HTMLTextAreaElement>;\n  setCurrentMessage: (msg: string) => void;\n  onAddFiles: (files: File[]) => void;\n}) => {\n  const handleOnPaste = useCallback(\n    (e: React.ClipboardEvent<HTMLTextAreaElement>) => {\n      const files = e.clipboardData.files;\n      if (files.length) {\n        e.preventDefault();\n        // onSend(\"\", file, file.name, file.type);\n        onAddFiles(Array.from(files));\n      } else {\n        const types = e.clipboardData.types;\n        const vsCodeTypes = [\n          \"application/vnd.code.copymetadata\",\n          \"vscode-editor-data\",\n        ];\n        if (vsCodeTypes.some((vsType) => types.includes(vsType))) {\n          const text = e.clipboardData.getData(\"text/plain\");\n          const vsData = e.clipboardData.getData(\"vscode-editor-data\");\n          const { data: languageRaw = \"\" } = tryCatchV2(() => {\n            const result = JSON.parse(vsData).mode as string;\n            return result;\n          });\n          /** Ignore single line of text */\n          if (text.trim().split(\"\\n\").length < 2) {\n            return;\n          }\n          e.preventDefault();\n          const language =\n            (\n              {\n                typescriptreact: \"tsx\",\n              } as const\n            )[languageRaw] ??\n            (languageRaw || \"\");\n\n          const codeSnippetText =\n            text.trim().startsWith(\"```\") ?\n              text\n            : [\"```\" + language, fixIndent(text), \"```\"].join(\"\\n\");\n          /** If existing text then place correctly */\n          if (textAreaRef.current) {\n            insertCodeSnippetAtCursor(textAreaRef.current, codeSnippetText);\n          } else {\n            setCurrentMessage(codeSnippetText);\n          }\n        }\n      }\n    },\n    [setCurrentMessage, textAreaRef, onAddFiles],\n  );\n  return {\n    handleOnPaste,\n  };\n};\n\n// Function to insert text at cursor position\nconst insertCodeSnippetAtCursor = (\n  textarea: HTMLTextAreaElement,\n  text: string,\n) => {\n  const startPos = textarea.selectionStart;\n  const endPos = textarea.selectionEnd;\n  let beforeText = textarea.value.substring(0, startPos);\n  let afterText = textarea.value.substring(endPos);\n\n  if (beforeText.length) {\n    beforeText = beforeText + \"\\n\";\n  }\n  if (afterText.length) {\n    afterText = \"\\n\" + afterText;\n  }\n  // Set the new value with the pasted text inserted\n  textarea.value = beforeText + text + afterText;\n\n  // Move the cursor to after the inserted text\n  const newCursorPos = startPos + text.length + 1; // +1 for the added newline\n  textarea.setSelectionRange(newCursorPos, newCursorPos);\n};\n"
  },
  {
    "path": "client/src/components/Chat/useChatState.ts",
    "content": "import { useCallback, useEffect, useState } from \"react\";\n\nimport { usePromise } from \"prostgles-client\";\nimport { useFileDropZone } from \"../FileInput/useFileDropZone\";\nimport type { ChatProps } from \"./Chat\";\nimport { useChatOnPaste } from \"./useChatOnPaste\";\nimport { useDebouncedCallback } from \"src/hooks/useDebouncedCallback\";\n\nexport type ChatState = ReturnType<typeof useChatState>;\nexport const useChatState = (\n  props: Pick<\n    ChatProps,\n    | \"messages\"\n    | \"onSend\"\n    | \"isLoading\"\n    | \"currentlyTypedMessage\"\n    | \"onCurrentlyTypedMessageChange\"\n  > & {\n    textAreaRef: React.RefObject<HTMLTextAreaElement>;\n  },\n) => {\n  const {\n    messages,\n    onSend,\n    isLoading,\n    textAreaRef,\n    currentlyTypedMessage,\n    onCurrentlyTypedMessageChange,\n  } = props;\n\n  const [files, setFiles] = useState<File[]>([]);\n  const onAddFiles = useCallback(\n    (newFiles: File[]) => {\n      setFiles((prev) => [...prev, ...newFiles]);\n    },\n    [setFiles],\n  );\n\n  const [scrollRef, setScrollRef] = useState<HTMLDivElement | null>(null);\n\n  useEffect(() => {\n    if (scrollRef) {\n      setTimeout(() => {\n        scrollRef.scrollTo(0, scrollRef.scrollHeight);\n        /** Wait for base64 images to load and resize */\n      }, 10);\n    }\n  }, [messages, scrollRef]);\n\n  const getCurrentMessage = useCallback(\n    () => textAreaRef.current?.value || currentlyTypedMessage || \"\",\n    [currentlyTypedMessage, textAreaRef],\n  );\n  const setCurrentMessage = useCallback(\n    (msg: string) => {\n      if (!textAreaRef.current) return;\n      textAreaRef.current.value = msg;\n    },\n    [textAreaRef],\n  );\n\n  const [sendingMsg, setSendingMsg] = useState(false);\n\n  const onCurrentlyTypedMessageChangeDebounced = useDebouncedCallback(\n    (value: string) => {\n      if (sendingMsg && value) return;\n      onCurrentlyTypedMessageChange(value);\n    },\n    [onCurrentlyTypedMessageChange, sendingMsg],\n  );\n\n  const sendMsg = useCallback(async () => {\n    const msg = getCurrentMessage();\n\n    if (!msg.trim() && !files.length) {\n      return;\n    }\n    setSendingMsg(true);\n    try {\n      await onSend(msg, files);\n      onCurrentlyTypedMessageChangeDebounced(\"\");\n      setCurrentMessage(\"\");\n      setFiles([]);\n    } catch (e) {\n      console.error(e);\n    }\n    setSendingMsg(false);\n  }, [\n    getCurrentMessage,\n    files,\n    onSend,\n    onCurrentlyTypedMessageChangeDebounced,\n    setCurrentMessage,\n  ]);\n  const chatIsLoading = isLoading || sendingMsg;\n\n  const filesAsBase64 = usePromise(async () => {\n    if (!files.length) return [];\n    return Promise.all(\n      files.map(async (file) => {\n        const base64Data = await blobToBase64(file);\n        return { file, base64Data };\n      }),\n    );\n  }, [files]);\n\n  const { handleOnPaste } = useChatOnPaste({\n    textAreaRef: textAreaRef,\n    onAddFiles,\n    setCurrentMessage,\n  });\n\n  const { isEngaged, ...divHandlers } = useFileDropZone(onAddFiles);\n\n  return {\n    files,\n    filesAsBase64,\n    chatIsLoading,\n    setFiles,\n    sendMsg,\n    sendingMsg,\n    setSendingMsg,\n    setScrollRef,\n    setCurrentMessage,\n    onAddFiles,\n    getCurrentMessage,\n    handleOnPaste,\n    divHandlers,\n    isEngaged,\n    onCurrentlyTypedMessageChangeDebounced,\n  };\n};\n\nfunction blobToBase64(blob: File): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onloadend = () => {\n      // The result includes the data URL prefix (data:audio/ogg;base64,)\n      const { result } = reader;\n      if (result && typeof result !== \"string\") {\n        reject(new Error(\"Failed to convert blob to base64 string\"));\n        return;\n      }\n      const base64String = result?.toString() || \"\";\n      resolve(base64String);\n    };\n    reader.onerror = reject;\n    reader.readAsDataURL(blob);\n  });\n}\n"
  },
  {
    "path": "client/src/components/Checkbox.css",
    "content": ".checkbox input:checked {\n  /* background-image: url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4a1 1 0 00-1.414-1.414L7 8.586 5.707 7.293z'/%3E%3C/svg%3E\"); */\n  border-color: transparent;\n  background-color: currentColor;\n  background-size: 100% 100%;\n  background-position: 50%;\n  background-repeat: no-repeat;\n}\n\n.Checkbox_inner_label {\n  outline-color: var(--focus-color);\n}\n\n.checkbox {\n  align-items: center;\n}\n\n.checkbox * {\n  cursor: pointer;\n}\n\n.checkbox label {\n  margin-left: 0.5em;\n}\n"
  },
  {
    "path": "client/src/components/Checkbox.tsx",
    "content": "import { mdiCheckBold, mdiCheckboxBlankOutline } from \"@mdi/js\";\nimport React from \"react\";\nimport type { TestSelectors } from \"../Testing\";\nimport \"./Checkbox.css\";\nimport { classOverride } from \"./Flex\";\nimport { Icon } from \"./Icon/Icon\";\nimport { generateUniqueID } from \"./FileInput/FileInput\";\n\ntype P = TestSelectors & {\n  id?: string;\n  style?: React.CSSProperties;\n  className?: string;\n  inputClassname?: string;\n  checked?: boolean;\n  label?: React.ReactNode;\n  onChange?: (e: React.ChangeEvent<HTMLInputElement>, checked: boolean) => any;\n  readOnly?: boolean;\n  title?: string;\n  variant?: \"minimal\" | \"micro\" | \"button\" | \"header\";\n  disabledInfo?: string;\n  iconPath?: string;\n};\n\nexport const Checkbox = (props: P) => {\n  const {\n    id,\n    className = \"\",\n    inputClassname = \"\",\n    label,\n    checked,\n    style = {},\n    onChange,\n    readOnly,\n    title,\n    variant,\n    disabledInfo,\n    iconPath,\n    ...testSel\n  } = props;\n\n  const idRef = React.useRef(id ?? generateUniqueID());\n  const inputRef = React.useRef<HTMLInputElement | null>(null);\n\n  const isBtn = variant === \"button\";\n  const isMiniOrMicro = variant === \"minimal\" || variant === \"micro\";\n  const tickColorClass = checked ? \" text-action \" : \"text-2\";\n\n  const defaultInputClass =\n    \" Checkbox_inner_label flex-row-wrap noselect relative checkbox pointer ai-center jc-center w-fit w-fit h-fit formfield-bg-color \" +\n    (!variant ? \" b b-color \" : \"\") +\n    (variant === \"minimal\" ? \" round \" : \" \") +\n    (isBtn ? \"bg-color-2 b-color p-p5 no-outline\"\n    : isMiniOrMicro ? \"bg-transparent b-unset no-outline\"\n    : \"relative \");\n  const checkbox = (\n    <div\n      className={classOverride(defaultInputClass, inputClassname)}\n      tabIndex={0}\n      style={\n        variant ?\n          {}\n        : {\n            padding: \"3px\",\n            borderRadius: \"3px\",\n          }\n      }\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          inputRef.current?.click();\n        }\n      }}\n    >\n      <input\n        id={idRef.current}\n        ref={inputRef}\n        className=\"hidden\"\n        type=\"checkbox\"\n        checked={checked}\n        onChange={\n          !onChange || disabledInfo ? undefined : (\n            (e) => {\n              onChange(e, e.target.checked);\n            }\n          )\n        }\n        readOnly={readOnly || Boolean(disabledInfo) || !onChange}\n      />\n      <Icon\n        path={\n          iconPath ??\n          (variant === \"header\" && !checked ?\n            mdiCheckboxBlankOutline\n          : mdiCheckBold)\n        }\n        size={\n          variant === \"button\" ? 1\n          : variant === \"minimal\" ?\n            1\n          : 0.75\n        }\n        className={\n          isMiniOrMicro || isBtn || variant === \"header\" ?\n            tickColorClass\n          : \"text-action\"\n        }\n        style={\n          isMiniOrMicro || isBtn || variant === \"header\" ?\n            {}\n          : {\n              opacity: checked ? 1 : 0,\n            }\n        }\n      />\n    </div>\n  );\n\n  return (\n    <label\n      style={{\n        ...style,\n        ...(isMiniOrMicro ?\n          {\n            background: \"transparent\",\n            outline: \"none !important\",\n          }\n        : {}),\n      }}\n      htmlFor={id}\n      className={classOverride(\n        \"Checkbox flex-row-wrap ai-center pointer w-fit h-fit \" +\n          (disabledInfo ? \" disabled \" : \"\"),\n        className,\n      )}\n      title={disabledInfo ?? title}\n      {...testSel}\n    >\n      {checkbox}\n      {!label ? null : (\n        <div\n          className={`ml-1 noselect f-1 text-ellipsis ${variant === \"header\" ? \"text-0\" : tickColorClass} ${isMiniOrMicro ? \"ml-p25\" : \"ml-p5\"}`}\n        >\n          {label}\n        </div>\n      )}\n    </label>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Chip.css",
    "content": ".chip {\n  padding-left: 0.625rem;\n  padding-right: 0.625rem;\n  padding-top: 0.125rem;\n  padding-bottom: 0.125rem;\n  line-height: 1rem;\n  font-size: 0.75rem;\n  font-weight: 500;\n\n  align-items: center;\n  border-radius: 1em;\n  white-space: nowrap;\n\n  width: fit-content;\n  height: fit-content;\n  text-align: start;\n}\n\n.chip.lg {\n  font-size: 1em;\n  padding-top: 0.4rem;\n  padding-bottom: 0.4rem;\n}\n\n.chip.green {\n  color: var(--green-500);\n}\n.chip.variant-naked {\n  gap: 4px;\n}\n.chip.variant-default,\n.chip.variant-outline {\n  background-color: var(--bg-color-1);\n}\n.chip.variant-outline {\n  border: 1px solid var(--text-2);\n}\n\n.chip.green.variant-default,\n.chip.green.variant-outline {\n  background-color: var(--green-100);\n}\n.chip.yellow {\n  color: var(--yellow-500);\n}\n.chip.yellow.variant-default,\n.chip.yellow.variant-outline {\n  background-color: var(--yellow-100);\n}\n.chip.red {\n  color: var(--red-500);\n}\n.chip.red.variant-default,\n.chip.red.variant-outline {\n  background-color: var(--red-100);\n}\n.chip.gray.variant-default,\n.chip.gray.variant-outline {\n  background-color: var(--bg-color-1);\n}\n.chip.blue {\n  color: var(--blue-500);\n}\n.chip.blue.variant-default,\n.chip.blue.variant-outline {\n  background-color: var(--blue-100);\n}\n\n.dark-theme .chip.blue.variant-default,\n.chip.blue.variant-outline {\n  background-color: #242d37;\n}\n"
  },
  {
    "path": "client/src/components/Chip.tsx",
    "content": "import { mdiClose } from \"@mdi/js\";\nimport React from \"react\";\nimport \"./Animations.css\";\nimport \"./Chip.css\";\nimport Btn from \"./Btn\";\nimport { FlexCol, classOverride } from \"./Flex\";\nimport { Icon } from \"./Icon/Icon\";\nimport type { TestSelectors } from \"../Testing\";\n\ntype ChipProps = TestSelectors &\n  Omit<\n    React.DetailedHTMLProps<\n      React.HTMLAttributes<HTMLDivElement>,\n      HTMLDivElement\n    >,\n    \"children\" | \"color\"\n  > & {\n    label?: string;\n    subValues?: (string | number)[];\n    variant?: \"naked\" | \"header\" | \"outline\" | \"default\";\n    color?: \"blue\" | \"yellow\" | \"red\" | \"green\" | \"gray\";\n    onDelete?: React.MouseEventHandler<HTMLButtonElement>;\n    leftIcon?: {\n      path: string;\n      size?: 0.75 | 0.5;\n      onClick?: React.MouseEventHandler<HTMLButtonElement>;\n      style?: React.CSSProperties;\n      className?: string;\n    };\n  } & (\n    | { value?: string | number; children?: undefined }\n    | { children: React.ReactNode }\n  );\n\nexport default class Chip extends React.Component<ChipProps> {\n  render() {\n    const {\n      className = \"\",\n      subValues,\n      label,\n      variant = \"default\",\n      onDelete,\n      leftIcon,\n      color = \"gray\",\n      ...divProps\n    } = this.props;\n\n    const asHeader = variant === \"header\";\n    const labelNode =\n      typeof label !== \"string\" ? null : (\n        <span\n          className={\"text-1 \"}\n          style={\n            asHeader ?\n              { fontSize: \"14px\", fontWeight: 400 }\n            : { fontWeight: 400 }\n          }\n        >\n          {label}:{\" \"}\n        </span>\n      );\n    return (\n      <div\n        {...divProps}\n        style={{\n          ...(!asHeader && {\n            padding: \"6px 12px\",\n            ...(onDelete && {\n              paddingLeft: \"12px\",\n            }),\n          }),\n          ...this.props.style,\n        }}\n        className={classOverride(\n          `chip-component flex-row ws-pre-wrap ai-center chip lg ${color} variant-${variant} ${variant === \"default\" ? \"gap-p25\" : \"\"} ${asHeader ? \"text-ellipsis\" : \"\"}`,\n          className,\n        )}\n      >\n        {leftIcon ?\n          leftIcon.onClick ?\n            <Btn\n              iconPath={leftIcon.path}\n              onClick={leftIcon.onClick}\n              className={classOverride(\"mr-p5 round\", leftIcon.className)}\n              size=\"default\"\n              style={{\n                padding: 0,\n                background: \"transparent\",\n                color: \"black\",\n                ...leftIcon.style,\n              }}\n              variant=\"filled\"\n            />\n          : <Icon\n              path={leftIcon.path}\n              className={classOverride(\"mr-p5 round\", leftIcon.className)}\n              size={leftIcon.size ?? 1}\n              style={leftIcon.style}\n            />\n\n        : null}\n\n        {!asHeader && labelNode}\n\n        <FlexCol className={`gap-p25 `}>\n          {asHeader && labelNode}\n          {\"value\" in this.props && this.props.value ?\n            <span\n              className=\"font-medium f-1 text-ellipsis\"\n              style={{ padding: \"2px\" }}\n            >\n              {this.props.value}\n            </span>\n          : asHeader ?\n            <div className=\"text-ellipsis\">{this.props.children}</div>\n          : this.props.children}\n          {!!subValues?.length &&\n            subValues.map((subValue, i) => (\n              <span\n                key={i}\n                className=\" f-1 text-ellipsis\"\n                style={{ opacity: 0.7, padding: \"2px\" }}\n              >\n                {subValue}\n              </span>\n            ))}\n        </FlexCol>\n\n        {onDelete && (\n          <Btn\n            iconPath={mdiClose}\n            onClick={(e) => {\n              e.stopPropagation();\n              e.preventDefault();\n              onDelete(e);\n            }}\n            className=\"Chip_DeleteBtn ml-p5 round as-start text-1\"\n            size=\"default\"\n            style={{\n              padding: 0,\n              borderRadius: \"1000%\",\n              background: \"transparent\",\n            }}\n            color=\"action\"\n          />\n        )}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/ClickCatch.tsx",
    "content": "import React from \"react\";\n\ntype P = Pick<\n  React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>,\n  | \"style\"\n  | \"className\"\n  | \"onClick\"\n  | \"onPointerUp\"\n  | \"onPointerDown\"\n  | \"children\"\n>;\n\nexport default class ClickCatch extends React.Component<P, any> {\n  clickCatch?: HTMLDivElement;\n\n  render() {\n    const { children, style, onClick, ...otherProps } = this.props;\n    return (\n      <div\n        style={{\n          display: \"block\",\n          position: \"fixed\",\n          backgroundColor: \"rgb(0 0 0 / .5)\",\n          zIndex: 0,\n          ...style,\n        }}\n        className={\"clickcatchcomp fixed noselect inset-0 w-full h-full\"}\n        ref={(e) => {\n          if (e) this.clickCatch = e;\n        }}\n        onClick={(e) => {\n          if (e.target === this.clickCatch) {\n            onClick?.(e);\n          }\n        }}\n        {...otherProps}\n        onContextMenu={(e) => {\n          e.preventDefault();\n          if (e.target === this.clickCatch) {\n            onClick?.(e);\n          }\n          return false;\n        }}\n      >\n        {children}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/ClickCatchOverlay.css",
    "content": ".ClickCatchOverlay {\n  background: #2c2c2c6b;\n}\n\n.dark-theme .ClickCatchOverlay {\n  background: #0000002e;\n}\n"
  },
  {
    "path": "client/src/components/ClickCatchOverlay.tsx",
    "content": "import React from \"react\";\nimport \"./ClickCatchOverlay.css\";\nimport { type DivProps, classOverride } from \"./Flex\";\nimport type { Command } from \"../Testing\";\n\nexport const ClickCatchOverlayZIndex = 1;\n\nexport const ClickCatchOverlay = ({\n  style,\n  className,\n  onClick,\n}: Pick<DivProps, \"onClick\" | \"style\" | \"className\">) => {\n  return (\n    <div\n      data-command={\"ClickCatchOverlay\" satisfies Command}\n      className={classOverride(\"ClickCatchOverlay\", className)}\n      style={{\n        position: \"fixed\",\n        inset: 0,\n        zIndex: ClickCatchOverlayZIndex,\n        backdropFilter: \"blur(2px)\",\n        ...style,\n      }}\n      onClick={onClick}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/components/ConfirmationDialog.tsx",
    "content": "import RTComp from \"../dashboard/RTComp\";\nimport React from \"react\";\nimport Btn from \"./Btn\";\nimport type { PopupProps } from \"./Popup/Popup\";\nimport Popup from \"./Popup/Popup\";\nimport { FlexRowWrap } from \"./Flex\";\nimport type { Command } from \"../Testing\";\nimport { Icon } from \"./Icon/Icon\";\n\nexport type ConfirmDialogProps = Pick<\n  PopupProps,\n  \"anchorEl\" | \"positioning\"\n> & {\n  title?: string;\n  message: string;\n  iconPath?: string;\n  acceptBtn: {\n    color: \"warn\" | \"danger\" | \"action\";\n    text: string;\n    dataCommand: Command;\n  };\n  onAccept: VoidFunction;\n  onClose: VoidFunction;\n  asPopup?: boolean;\n  className?: string;\n  style?: React.CSSProperties;\n};\n\ntype S = {};\n\nexport default class ConfirmationDialog extends RTComp<ConfirmDialogProps, S> {\n  state: S = {};\n\n  render() {\n    const {\n      onClose,\n      onAccept,\n      acceptBtn,\n      message,\n      title,\n      asPopup,\n      className = \"\",\n      style,\n      iconPath,\n      ...popupProps\n    } = this.props;\n\n    const content = (\n      <div className={\"flex-col \" + className} style={style}>\n        <div className=\"flex-row  jc-end p-1 gap-1\">\n          {iconPath && <Icon path={iconPath} size={1} className=\"f-0 text-2\" />}\n          <div className=\"\">{message}</div>\n        </div>\n        <FlexRowWrap className=\"p-p5 f-0 bt b-color jc-end \">\n          <Btn className=\"mr-1\" variant=\"faded\" onClick={() => onClose()}>\n            Cancel\n          </Btn>\n          <Btn\n            color={acceptBtn.color}\n            variant=\"filled\"\n            data-command={acceptBtn.dataCommand}\n            onClick={async () => {\n              await onAccept();\n            }}\n          >\n            {acceptBtn.text}\n          </Btn>\n        </FlexRowWrap>\n      </div>\n    );\n\n    if (!asPopup) return content;\n\n    return (\n      <Popup\n        positioning={\"right-panel\"}\n        {...popupProps}\n        title={title}\n        rootStyle={{ padding: 0 }}\n        clickCatchStyle={{ opacity: 0.3 }}\n        contentClassName=\"flex-col ai-center\"\n        onClose={onClose}\n      >\n        {content}\n      </Popup>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/CopyToClipboardBtn.tsx",
    "content": "import { mdiContentCopy } from \"@mdi/js\";\nimport React from \"react\";\nimport { t } from \"../i18n/i18nUtils\";\nimport Btn, { type BtnProps } from \"./Btn\";\n\nexport const CopyToClipboardBtn = ({\n  content,\n  ...btnProps\n}: { content: string } & Pick<\n  BtnProps,\n  \"className\" | \"children\" | \"style\" | \"size\" | \"variant\" | \"color\"\n>) => {\n  return (\n    <Btn\n      {...btnProps}\n      title={t.common[\"Copy to clipboard\"]}\n      iconPath={mdiContentCopy}\n      onClickMessage={async (_, setM) => {\n        await navigator.clipboard.writeText(content);\n        setM({ ok: t.common[\"Copied!\"] });\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/components/DragOverUpload/DragOverUpload.css",
    "content": "/* .DragOverUpload .DragOverUpload_Border {\n  animation: border-dance 4s infinite linear;\n} */\n\n.rotating-border {\n  /* width: max-content; */\n  background: linear-gradient(90deg, purple 50%, transparent 50%),\n    linear-gradient(90deg, purple 50%, transparent 50%),\n    linear-gradient(0deg, purple 50%, transparent 50%),\n    linear-gradient(0deg, purple 50%, transparent 50%);\n  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;\n  background-size:\n    15px 4px,\n    15px 4px,\n    4px 15px,\n    4px 15px;\n  padding: 10px;\n  animation: border-dance 14s infinite linear;\n}\n\n@keyframes border-dance {\n  0% {\n    background-position:\n      0 0,\n      100% 100%,\n      0 100%,\n      100% 0;\n  }\n  100% {\n    background-position:\n      100% 0,\n      0 100%,\n      0 0,\n      100% 100%;\n  }\n}\n"
  },
  {
    "path": "client/src/components/DragOverUpload/DragOverUpload.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { FlexCol } from \"../Flex\";\nimport \"./DragOverUpload.css\";\n\ntype P = {\n  onOpen: (files: FileList | undefined) => void;\n};\nexport const DragOverUpload = ({ onOpen }: P) => {\n  const [show, setShow] = useState(false);\n\n  useEffect(() => {\n    const onDrag = (ev: DragEvent) => {\n      setShow(true);\n    };\n\n    const onDrop = (ev: DragEvent) => {\n      onOpen(ev.dataTransfer?.files);\n    };\n    const onDragEnd = () => {\n      setShow(false);\n    };\n    window.addEventListener(\"dragover\", onDrag);\n    // window.addEventListener(\"drag\", onDrag);\n    window.addEventListener(\"drop\", onDrop);\n    window.addEventListener(\"dragleave\", onDragEnd);\n\n    return () => {\n      window.removeEventListener(\"dragover\", onDrag);\n      // window.removeEventListener(\"drag\", onDrag);\n      window.removeEventListener(\"drop\", onDrop);\n      window.removeEventListener(\"dragleave\", onDragEnd);\n    };\n  }, [setShow, onOpen]);\n\n  return (\n    <FlexCol\n      className=\"DragOverUpload absolute inset-0 bg-action p-2\"\n      style={{ zIndex: 1e3 }}\n    >\n      <FlexCol\n        className=\"DragOverUpload_Border rotating-border rounded h-full w-full\"\n        style={{ borderStyle: \"dashed\" }}\n      ></FlexCol>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/components/DraggableLI.tsx",
    "content": "import type { PropsWithChildren } from \"react\";\nimport React from \"react\";\nimport type { TestSelectors } from \"../Testing\";\n\ntype P<T> = TestSelectors &\n  PropsWithChildren<\n    React.HTMLAttributes<HTMLLIElement> & {\n      idx: number;\n      items: T;\n      onReorder?: (newList: T) => void;\n    }\n  >;\nexport const DraggableLI = <T extends any[]>({\n  children,\n  onReorder,\n  items,\n  idx,\n  ...props\n}: P<T>) => {\n  const shouldStop = (e: React.DragEvent<HTMLLIElement>) => {\n    if (window.getSelection()?.toString().length) {\n      e.stopPropagation();\n      e.preventDefault();\n      return true;\n    }\n  };\n\n  type ListParent =\n    | null\n    | (HTMLElement & {\n        _initialSize?: {\n          width: string;\n          height: string;\n        };\n        _targetIdx?: number;\n      });\n\n  return (\n    <li\n      {...props}\n      draggable={Boolean(onReorder)}\n      onDrag={\n        !onReorder ? undefined : (\n          (e) => {\n            if (shouldStop(e)) {\n              return false;\n            }\n\n            const elem = e.currentTarget;\n            const p = elem.parentElement as ListParent;\n            // Lock parent size to prevent jitter\n\n            if (!p) {\n              throw \"Not possible 54\";\n            }\n\n            p._initialSize ??= {\n              width: p.style.width,\n              height: p.style.height,\n            };\n            p.style.width = `${p.offsetWidth}px`;\n            p.style.height = `${p.offsetHeight}px`;\n\n            const isRow = getComputedStyle(p).flexDirection === \"row\";\n            const getSize = (e: HTMLLIElement) =>\n              isRow ? e.offsetWidth : e.offsetHeight;\n            let size = getSize(elem);\n            elem.style.display = \"none\";\n            // elem.style.opacity = \"0.2\";\n\n            const siblings = Array.from(p.children) as HTMLLIElement[];\n            const targetIndex = siblings.findIndex((s) => {\n              const r = s.getBoundingClientRect();\n              const horizontalOverlap =\n                e.clientX > r.x && e.clientX < r.x + r.width;\n              const verticalOverlap =\n                e.clientY > r.y && e.clientY < r.y + r.height;\n              return horizontalOverlap && verticalOverlap;\n            });\n\n            if (idx === targetIndex || targetIndex < 0) {\n              return;\n            }\n\n            siblings.map((c, siblingIdx) => {\n              // c.style.background = res? \"red\" : \"\";\n              size = size || getSize(c);\n\n              const isTarget = targetIndex === siblingIdx;\n              const offsetPx = isTarget ? `${size}px` : \"0\";\n\n              if (isRow) {\n                // if(newPadding !== c.style.paddingLeft) c.style.paddingLeft = newPadding;\n                if (offsetPx !== c.style.marginLeft)\n                  c.style.marginLeft = offsetPx;\n              } else {\n                // if(newPadding !== c.style.paddingTop) c.style.paddingTop = newPadding;\n                if (offsetPx !== c.style.marginTop)\n                  c.style.marginTop = offsetPx;\n              }\n\n              if (isTarget) {\n                p._targetIdx = siblingIdx;\n              }\n            });\n\n            const pr = p.getBoundingClientRect();\n\n            if (isRow) {\n              if (e.clientX > pr.x + pr.width) {\n                p.scrollLeft += 10;\n              } else if (e.clientX < pr.x) {\n                p.scrollLeft -= 10;\n              }\n            } else {\n              if (e.clientY > pr.y + pr.height) {\n                p.scrollTop += 10;\n              } else if (e.clientY < pr.y) {\n                p.scrollTop -= 10;\n              }\n            }\n\n            // console.log(pr, e.clientY, e.nativeEvent.pageY);\n          }\n        )\n      }\n      onDragEnd={\n        !onReorder ? undefined : (\n          async (e) => {\n            if (shouldStop(e)) {\n              return false;\n            }\n\n            const elem = e.currentTarget;\n            const p = elem.parentElement as ListParent;\n\n            if (!p) {\n              throw \"Not possible 138\";\n            }\n\n            const tIdx = p._targetIdx;\n            if (typeof p._targetIdx !== \"number\") {\n              throw \"Not possible 143\";\n            }\n            const res = items.slice(0) as typeof items;\n\n            const moveItem = (from, to) => {\n              const f = res.splice(from, 1)[0]!;\n              res.splice(to, 0, f);\n            };\n\n            // [res[i], res[tIdx]] = [res[tIdx], res[i]];\n            moveItem(idx, tIdx);\n            await onReorder(res);\n\n            setTimeout(() => {\n              Array.from(p.children as unknown as HTMLElement[]).map(\n                (c: HTMLElement) => {\n                  c.style.paddingTop = \"\";\n                  c.style.paddingLeft = \"\";\n                  c.style.marginLeft = \"\";\n                  c.style.marginTop = \"\";\n                },\n              );\n              elem.style.display = \"flex\";\n              // elem.style.opacity = \"1\";\n\n              if (!p._initialSize) {\n                throw \"Not possible 169\";\n              }\n              p.style.width = p._initialSize.width;\n              p.style.height = p._initialSize.height;\n              p._initialSize = undefined;\n            }, 1);\n          }\n        )\n      }\n    >\n      {children}\n    </li>\n  );\n};\n"
  },
  {
    "path": "client/src/components/ErrorComponent.tsx",
    "content": "import { mdiAlertOutline, mdiClose } from \"@mdi/js\";\nimport type { ReactNode } from \"react\";\nimport React from \"react\";\nimport { isObject } from \"@common/publishUtils\";\nimport type { TestSelectors } from \"../Testing\";\nimport { isEmpty, scrollIntoViewIfNeeded } from \"../utils/utils\";\nimport Btn from \"./Btn\";\nimport { classOverride, FlexCol, FlexRow } from \"./Flex\";\nimport { Icon } from \"./Icon/Icon\";\n\ntype P = TestSelectors & {\n  error: any;\n  className?: string;\n  noScroll?: boolean;\n  style?: React.CSSProperties;\n  pre?: boolean;\n  findMsg?: boolean;\n  withIcon?: boolean;\n  maxTextLength?: number;\n  title?: string;\n  variant?: \"outlined\";\n  color?: \"warning\" | \"action\" | \"info\";\n  onClear?: VoidFunction;\n  children?: ReactNode;\n  /**\n   * Auto scroll into view when error is shown\n   * Default: true\n   */\n  autoScrollIntoView?: boolean;\n};\nexport default class ErrorComponent extends React.Component<P> {\n  ref?: any;\n\n  scrollIntoView = () => {\n    const { error, autoScrollIntoView = true } = this.props;\n    if (error && autoScrollIntoView && this.ref && this.ref.scrollIntoView) {\n      scrollIntoViewIfNeeded(this.ref);\n    }\n  };\n  componentDidMount() {\n    this.scrollIntoView();\n  }\n  componentDidUpdate() {\n    this.scrollIntoView();\n  }\n  render() {\n    const {\n      error,\n      className = \"\",\n      style = {},\n      pre = false,\n      findMsg = false,\n      withIcon = false,\n      maxTextLength = 1000,\n      autoScrollIntoView,\n      title,\n      noScroll = false,\n      variant,\n      color,\n      onClear,\n      children,\n      ...testSelectors\n    } = this.props;\n\n    if ([null, undefined].includes(error)) {\n      return null;\n    }\n    const colorClass = color ? `text-${color}` : \"text-danger\";\n    return (\n      <FlexRow\n        ref={(e) => {\n          if (e) this.ref = e;\n        }}\n        className={classOverride(\n          `ErrorComponent relative p-p5 gap-1 ai-center text-danger p-1 o-auto min-w-0 min-h-0 o-auto variant:${variant} ${colorClass} ${pre ? \" ws-pre \" : \"\"}`,\n          className,\n        )}\n        data-command=\"ErrorComponent\"\n        {...testSelectors}\n        style={{\n          whiteSpace: \"pre-line\",\n          textAlign: \"left\",\n          display: !error ? \"none\" : \"flex\",\n          maxWidth: \"min(600px, 100vw)\",\n          ...(!className.includes(\"p-\") && { padding: \"0 4px\" }),\n          ...style,\n          minWidth: \"150px\", // To ensure it shows on mobile\n          ...(variant === \"outlined\" && {\n            border: `1px solid var(--${colorClass})`,\n            borderRadius: \"var(--rounded)\",\n            padding: \".5em 1em\",\n          }),\n          ...(noScroll ?\n            { overflow: \"hidden\" }\n          : {\n              alignItems: \"unset\",\n            }),\n        }}\n      >\n        {withIcon && <Icon className=\"as-start f-0\" path={mdiAlertOutline} />}\n        <FlexCol\n          className={\n            \"gap-1 as-center-thisbreakslongerrors \" +\n            (noScroll ? \"ws-break\" : \"o-auto\")\n          }\n        >\n          {title && <div className=\"font-18 bold\">{title}</div>}\n          {(parsedError(error, findMsg) + \"\").slice(0, maxTextLength)}\n        </FlexCol>\n        {onClear && (\n          <Btn\n            onClick={onClear}\n            iconPath={mdiClose}\n            variant=\"faded\"\n            color=\"danger\"\n            size=\"small\"\n          />\n        )}\n      </FlexRow>\n    );\n  }\n}\n\nexport class ErrorTrap extends React.Component<\n  { children: ReactNode },\n  { error: any; errorInfo: any }\n> {\n  state = {\n    error: \"\",\n    errorInfo: \"\",\n  };\n\n  componentDidCatch(error, errorInfo) {\n    this.setState({ error, errorInfo });\n  }\n\n  render() {\n    const { error, errorInfo } = this.state;\n    const compName = (this.props.children as any)?.type?.name;\n    let errVal: any = {\n      error,\n      stack: (errorInfo as any)?.componentStack || errorInfo,\n    };\n    if (compName) {\n      errVal = {\n        component: compName,\n        ...errVal,\n      };\n    }\n    if (error)\n      return (\n        <ErrorComponent\n          error={errVal}\n          className=\"bg-color-0 p-2\"\n          style={{ maxHeight: \"400px\" }}\n        />\n      );\n\n    return this.props.children;\n  }\n}\n\nexport const getErrorMessage = (e: any) => {\n  const msgFields = [\n    \"err_msg\",\n    \"message\",\n    \"details\",\n    \"constraint\",\n    \"txt\",\n    \"hint\",\n  ];\n  if (typeof e === \"string\") return e;\n  if (isObject(e)) {\n    const errorMessage = msgFields.find(\n      (f) => typeof (e[f] ?? e.err?.[f]) === \"string\",\n    );\n\n    /**\n     * Postgres error code for unique constraint violation.\n     * Detail is more useful than message in this case.\n     */\n    if (\n      e.code === \"23505\" &&\n      typeof e.message === \"string\" &&\n      e.message.includes(\"duplicate key\") &&\n      typeof e.detail === \"string\"\n    ) {\n      return e.detail;\n    }\n\n    if (errorMessage) {\n      return e[errorMessage] ?? e.err?.[errorMessage];\n    }\n  }\n  return e ? JSON.stringify(e) : \"Error\";\n};\n\n/**\n * Return a more human readable error message if it's an object\n */\nexport const parsedError = (val, findMsg?: boolean): string => {\n  let res = \"\";\n\n  if (typeof val === \"string\") res = val;\n  else if (Array.isArray(val)) {\n    res = val.map((v) => parsedError(v)).join(\"\\n\");\n  } else if (val && !isEmpty(val) && Object.keys(val).length) {\n    if (findMsg) {\n      res = getErrorMessage(val);\n    }\n    if (!res)\n      res = Object.keys(val)\n        .map((k) => `${k}: ${JSON.stringify(val[k], null, 2)}`)\n        .join(\"\\n\");\n  } else if (val?.toString) res = val.toString();\n  else res = JSON.stringify(val);\n\n  if (typeof res === \"string\" && res.length) {\n    res = res.trim();\n    if (res.startsWith('\"') && res.endsWith('\"')) res = res.slice(1, -1);\n    if (res.toLowerCase().startsWith(\"error: \")) res = res.slice(7);\n    // res = res.replace(/['\"]+/g, '');\n    res = res.replace(/\\\\\"/g, '\"');\n  }\n  return res;\n};\n"
  },
  {
    "path": "client/src/components/ExpandSection.tsx",
    "content": "import { mdiChevronDown } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport type { BtnProps } from \"./Btn\";\nimport Btn from \"./Btn\";\n\ntype ExpandSectionProps = {\n  expanded?: boolean;\n  title?: string;\n  children: JSX.Element | React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n  label?: string;\n  iconPath?: string | ((collapsed: boolean) => string);\n  collapsible?: boolean;\n  buttonProps?: Omit<BtnProps<void>, \"onClick\">;\n};\n\nexport const ExpandSection = ({\n  expanded = false,\n  children,\n  className = \"\",\n  style,\n  title = \"Expand\",\n  label,\n  iconPath: piconP,\n  collapsible = false,\n  buttonProps,\n}: ExpandSectionProps): JSX.Element => {\n  const [collapsed, setCollapsed] = useState(!expanded);\n  const iconPath = piconP ?? mdiChevronDown;\n\n  const button = (\n    <Btn\n      iconPosition={piconP ? \"left\" : \"right\"}\n      className={className + \" ExpandSection flex-row \"}\n      style={style}\n      variant=\"text\"\n      title={title}\n      iconPath={typeof iconPath === \"string\" ? iconPath : iconPath(collapsed)}\n      children={label}\n      {...(buttonProps as BtnProps<void>)}\n      onClick={() => {\n        setCollapsed(!collapsed);\n      }}\n    />\n  );\n\n  if (collapsed && !collapsible) return button;\n  return (\n    <>\n      {!collapsed && children}\n      {collapsible && button}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Expander.tsx",
    "content": "import React, { useState } from \"react\";\n\ntype ExpanderProps = {\n  style?: React.CSSProperties;\n  className?: string;\n  getButton: (isOpen: boolean) => React.ReactChild;\n  children: React.ReactNode;\n};\nfunction Expander({ children, getButton }: ExpanderProps) {\n  const [isOpen, setOpen] = useState(false);\n\n  return (\n    <>\n      <span onClick={() => setOpen(!isOpen)}>{getButton(isOpen)}</span>\n      {isOpen && children}\n    </>\n  );\n}\n\nexport default Expander;\n"
  },
  {
    "path": "client/src/components/FileBrowser/FileBrowser.tsx",
    "content": "import { isDefined } from \"@common/filterUtils\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { Label } from \"@components/Label\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport {\n  mdiBash,\n  mdiCodeJson,\n  mdiDocker,\n  mdiFileDocument,\n  mdiFolderOutline,\n  mdiLanguageGo,\n  mdiLanguageHtml5,\n  mdiLanguageJavascript,\n  mdiLanguageMarkdown,\n  mdiLanguagePython,\n  mdiLanguageRuby,\n  mdiLanguageTypescript,\n} from \"@mdi/js\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport { usePromise } from \"prostgles-client\";\nimport React from \"react\";\nimport { bytesToSize } from \"src/dashboard/BackupAndRestore/BackupsControls\";\nimport { FileBrowserCurrentDirectory } from \"./FileBrowserCurrentDirectory\";\n\ntype P = {\n  title?: string;\n  path: string | undefined;\n  onChange: (filePath: string) => void;\n};\n\nexport const FileBrowser = ({ title, path, onChange }: P) => {\n  const {\n    dbsMethods: { glob },\n  } = usePrgl();\n\n  const pathFiles = usePromise(async () => {\n    const result = glob ? await glob(path) : undefined;\n    if (!path && result?.path) {\n      onChange(result.path);\n    }\n    return result;\n  }, [glob, path, onChange]);\n\n  const sortedPathFiles = React.useMemo(() => {\n    if (!pathFiles?.result) return;\n    return [...pathFiles.result].sort((a, b) => {\n      if (a.type === b.type) {\n        return a.name.localeCompare(b.name);\n      }\n      return a.type === \"directory\" ? -1 : 1;\n    });\n  }, [pathFiles?.result]);\n\n  return (\n    <FlexCol className=\"min-h-0 gap-0\">\n      {title && <Label className=\"mb-2\">{title}</Label>}\n      {path !== undefined && (\n        <FileBrowserCurrentDirectory\n          path={path}\n          onChange={onChange}\n          existingFolderNames={\n            pathFiles?.result\n              .map((p) => (p.type === \"directory\" ? p.name : undefined))\n              .filter(isDefined) ?? []\n          }\n        />\n      )}\n      <SearchList\n        key={path}\n        className=\"f-1 min-h-0 mt-p5\"\n        style={{ maxHeight: \"600px\" }}\n        inputProps={{\n          autoFocus: true,\n        }}\n        placeholder=\"Search files and folders\"\n        limit={500}\n        noSearchLimit={0}\n        noResultsContent={<div className=\"p-1 bg-color-1\">Empty folder</div>}\n        items={\n          sortedPathFiles?.map(\n            ({ path, type, created, lastModified, name, size }) => {\n              const extension = name.toLowerCase().split(\".\").at(-1);\n              return {\n                key: path,\n                label: name,\n                rowStyle: { paddingLeft: \"0\" },\n                contentLeft: (\n                  <Icon\n                    path={\n                      type === \"directory\" ? mdiFolderOutline : (\n                        FILE_EXTENSION_TO_ICON_INFO[extension ?? \"\"]\n                          ?.iconPath || mdiFileDocument\n                      )\n                    }\n                    className=\"mr-p5\"\n                  />\n                ),\n                onPress: () => {\n                  onChange(path);\n                },\n                disabledInfo:\n                  type === \"directory\" ? undefined : \"Select directories only\",\n                contentRight: size ? <>{bytesToSize(size)}</> : null,\n                subLabel: `${\n                  lastModified ?\n                    new Date(lastModified).toISOString().split(\"T\")[0] +\n                    \" modified\"\n                  : \"\"\n                } ${created ? new Date(created).toISOString().split(\"T\")[0] + \" created\" : \"\"} `,\n              };\n            },\n          ) ?? []\n        }\n      />\n    </FlexCol>\n  );\n};\n\nexport const FILE_EXTENSION_TO_ICON_INFO: Record<\n  string,\n  { label: string; iconPath?: string }\n> = {\n  ts: { iconPath: mdiLanguageTypescript, label: \"typescript\" },\n  js: { iconPath: mdiLanguageJavascript, label: \"javascript\" },\n  py: { iconPath: mdiLanguagePython, label: \"python\" },\n  rb: { iconPath: mdiLanguageRuby, label: \"ruby\" },\n  go: { iconPath: mdiLanguageGo, label: \"go\" },\n  sh: { iconPath: mdiBash, label: \"shell\" },\n  dockerfile: { iconPath: mdiDocker, label: \"dockerfile\" },\n  txt: { label: \"plaintext\" },\n  json: { iconPath: mdiCodeJson, label: \"json\" },\n  yaml: { label: \"yaml\" },\n  css: { label: \"css\" },\n  html: { label: \"html\", iconPath: mdiLanguageHtml5 },\n  md: { iconPath: mdiLanguageMarkdown, label: \"markdown\" },\n  sql: { label: \"sql\" },\n  Dockerfile: { iconPath: mdiDocker, label: \"dockerfile\" },\n};\n"
  },
  {
    "path": "client/src/components/FileBrowser/FileBrowserCurrentDirectory.tsx",
    "content": "import { useOnErrorAlert } from \"@components/AlertProvider\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { mdiCheck, mdiClose, mdiFolderPlusOutline } from \"@mdi/js\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport React, { useMemo, useState } from \"react\";\n\ntype P = {\n  path: string;\n  existingFolderNames: string[];\n  onChange: (filePath: string) => void;\n};\n\nexport const FileBrowserCurrentDirectory = ({\n  path,\n  onChange,\n  existingFolderNames,\n}: P) => {\n  const {\n    dbsMethods: { mkdir },\n  } = usePrgl();\n\n  const { onErrorAlert } = useOnErrorAlert();\n  const [newFolderName, setNewFolderName] = useState(\"\");\n  const newFolderError = useMemo(() => {\n    if (!newFolderName) return;\n    if (newFolderName.includes(\"/\")) {\n      return \"Folder name cannot contain '/'\";\n    }\n    if (existingFolderNames.includes(newFolderName)) {\n      return \"A folder with this name already exists\";\n    }\n    return;\n  }, [existingFolderNames, newFolderName]);\n\n  return (\n    <FlexRowWrap className=\"gap-0\">\n      {path.split(\"/\").map((pathPart, index, pathParts) => {\n        return (\n          <React.Fragment key={pathPart + index}>\n            {index > 1 && <div>/</div>}\n            <Btn\n              variant=\"text\"\n              size=\"small\"\n              className=\"underline-on-hover\"\n              style={{\n                paddingRight: 0,\n                fontSize: \"18px\",\n                minWidth: 0,\n              }}\n              onClick={() => {\n                const selectedPath =\n                  pathParts.slice(0, index + 1).join(\"/\") || \"/\";\n                onChange(selectedPath);\n              }}\n            >\n              {!index ? \"/\" : pathPart}\n            </Btn>\n          </React.Fragment>\n        );\n      })}\n      {!newFolderName ?\n        <Btn\n          iconPath={mdiFolderPlusOutline}\n          size=\"small\"\n          title=\"New Folder\"\n          color=\"action\"\n          className=\"ml-1\"\n          onClick={() => {\n            if (!newFolderName) {\n              const defaultFolderName = \"New Folder\";\n              setNewFolderName(defaultFolderName);\n            }\n          }}\n        />\n      : <FlexRow>\n          <FormField\n            type=\"text\"\n            value={newFolderName}\n            className=\"ml-1\"\n            inputStyle={{\n              padding: \"2px\",\n              minHeight: \"unset\",\n            }}\n            inputProps={{\n              autoFocus: true,\n            }}\n            onChange={(newValue) => setNewFolderName(newValue)}\n            error={newFolderError}\n            rightContent={\n              <>\n                <Btn\n                  size=\"small\"\n                  title=\"Cancel\"\n                  onClick={() => setNewFolderName(\"\")}\n                  iconPath={mdiClose}\n                />\n                <Btn\n                  size=\"small\"\n                  title=\"Create\"\n                  onClick={() => {}}\n                  iconPath={mdiCheck}\n                  disabledInfo={newFolderError}\n                  onClickPromise={async () => {\n                    await onErrorAlert(async () => {\n                      if (!mkdir) {\n                        throw new Error(\"Not allowed to create folders\");\n                      }\n                      const newFolderPath = await mkdir(path, newFolderName);\n                      onChange(newFolderPath);\n                      setNewFolderName(\"\");\n                    });\n                  }}\n                />\n              </>\n            }\n          />\n        </FlexRow>\n      }\n    </FlexRowWrap>\n  );\n};\n"
  },
  {
    "path": "client/src/components/FileInput/DropZone.tsx",
    "content": "import React from \"react\";\nimport { FlexCol } from \"../Flex\";\nimport { useFileDropZone } from \"./useFileDropZone\";\n\nexport const DropZone = ({\n  onChange,\n}: {\n  onChange: (files: File[]) => void;\n}) => {\n  const { isEngaged, ...divHandlers } = useFileDropZone(onChange);\n  return (\n    <FlexCol\n      {...divHandlers}\n      className={\n        \"DropZone b b-active rounded w-full flex-col jc-center ai-center f-1\" +\n        (isEngaged ? \" active-shadow \" : \"\")\n      }\n      style={{ minHeight: \"30vh\" }}\n    >\n      <div className=\"noselect text-1\">Drop files here...</div>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/components/FileInput/FileInput.tsx",
    "content": "import { mdiChevronLeft, mdiChevronRight, mdiPlus } from \"@mdi/js\";\nimport React from \"react\";\nimport RTComp from \"../../dashboard/RTComp\";\nimport { FlexCol, classOverride } from \"../Flex\";\nimport { Icon } from \"../Icon/Icon\";\nimport Popup from \"../Popup/Popup\";\nimport { DropZone } from \"./DropZone\";\nimport { FileInputMedia } from \"./FileInputMedia\";\n\nexport type SavedMedia = {\n  id: string;\n  name?: string;\n  content_type: string;\n  url: string;\n};\n\nexport type LocalMedia = {\n  name: string;\n  data: File;\n};\n\nexport type Media = SavedMedia | LocalMedia;\n\nexport const generateUniqueID = (prefix = \"generated\") => {\n  let result = \"generated1\";\n  do {\n    result = `${prefix}${Math.round(Math.random() * 1e14)}`;\n  } while (document.querySelector(\"#\" + result));\n  return result;\n};\n\ntype S = {\n  focusedFile?: {\n    index: number;\n    file: Media;\n  };\n  id?: string;\n  isOverflowing?: boolean;\n};\nexport class FileInput extends RTComp<\n  {\n    id?: string;\n    className?: string;\n    style?: React.CSSProperties;\n\n    media?: Media[];\n\n    onAdd?: (media: LocalMedia[]) => void;\n    onDelete?: (media: Media) => void;\n\n    label?: string;\n\n    accept?: string;\n    maxFileCount?: number;\n\n    showDropZone?: boolean;\n  },\n  S\n> {\n  ref?: any;\n  state: S = {\n    focusedFile: undefined,\n    isOverflowing: false,\n  };\n\n  onMount(): void {\n    this.setState({\n      id: generateUniqueID(),\n    });\n  }\n\n  resizeObserver?: ResizeObserver;\n\n  onDelta = () => {\n    /**\n     * If isViewerMode then add left right buttons if media items overlow refWrapper\n     */\n    const isViewerMode = this.isViewerMode();\n    if (isViewerMode) {\n      if (!this.resizeObserver) {\n        this.resizeObserver = new ResizeObserver((entries) => {\n          const isOverflowing =\n            this.refWrapper!.scrollWidth > this.refWrapper!.offsetWidth;\n          if (this.state.isOverflowing !== isOverflowing) {\n            this.setState({ isOverflowing });\n          }\n        });\n\n        this.resizeObserver.observe(this.refWrapper!);\n      }\n    } else if (this.resizeObserver) {\n      this.resizeObserver.unobserve(this.refWrapper!);\n    }\n  };\n\n  isViewerMode = () => {\n    const { onAdd, onDelete } = this.props;\n\n    return Boolean(!onAdd && !onDelete);\n  };\n\n  refWrapper?: HTMLDivElement;\n  render() {\n    const {\n      className = \"\",\n      style = {},\n      label,\n      maxFileCount = 100,\n      accept,\n      id = this.state.id ?? \"empty\",\n      onAdd,\n      media = [],\n      onDelete,\n      showDropZone = true,\n    } = this.props;\n    const { focusedFile, isOverflowing } = this.state;\n    const isViewerMode = this.isViewerMode();\n    let popup;\n    if (focusedFile) {\n      const contentType =\n        \"data\" in focusedFile.file ?\n          focusedFile.file.data.type\n        : focusedFile.file.content_type;\n\n      popup = (\n        <Popup\n          rootStyle={{ padding: 0 }}\n          contentClassName=\"\"\n          onClose={() => {\n            this.setState({ focusedFile: undefined });\n          }}\n          title={focusedFile.file.name}\n          onKeyDown={\n            !isViewerMode ? undefined : (\n              (e) => {\n                if (e.key === \"ArrowRight\") {\n                  const index =\n                    focusedFile.index < media.length - 1 ?\n                      focusedFile.index + 1\n                    : 0;\n                  this.setState({\n                    focusedFile: {\n                      file: media[index]!,\n                      index,\n                    },\n                  });\n                } else if (e.key === \"ArrowLeft\") {\n                  const index =\n                    focusedFile.index ?\n                      focusedFile.index - 1\n                    : media.length - 1;\n                  this.setState({\n                    focusedFile: {\n                      file: media[index]!,\n                      index,\n                    },\n                  });\n                }\n              }\n            )\n          }\n        >\n          <FileInputMedia\n            file={focusedFile.file}\n            i={\"focused\"}\n            // style={{\n            //   width: \"fit-content\",\n            //   height: \"fit-content\",\n            //   border: \"unset\",\n            // }}\n            onClick={\n              !contentType.startsWith(\"image\") ?\n                undefined\n              : () => {\n                  this.setState({ focusedFile: undefined });\n                }\n            }\n            focused={true}\n          />\n        </Popup>\n      );\n    }\n\n    const setFiles = (files: FileList | File[]) => {\n      const newFiles: LocalMedia[] = Array.from(files).map((file) => ({\n        data: file,\n        // url: URL.createObjectURL(file),\n        // content_type: file.type,\n        name: file.name,\n      }));\n      this.props.onAdd?.(newFiles);\n    };\n\n    const inputText =\n      !media.length && showDropZone ?\n        \"Choose or paste a file\"\n      : \"Choose a file\";\n    const AddBtn =\n      maxFileCount <= media.length || !onAdd ?\n        null\n      : <label\n          htmlFor={id}\n          className=\"FileInput_AddBtn f-1 flex-row ws-nowrap ai-center bg-color-0 pointer rounded text-active b b-active py-p25 pl-p5 pr-1 gap-p25\"\n          data-command={\"FileBtn\"}\n        >\n          <Icon\n            path={mdiPlus}\n            sizeName={\"default\"}\n            title=\"Add files\"\n            className=\" \"\n          />\n          <div>{inputText}</div>\n          <input\n            id={id}\n            style={{ width: 0, height: 0, position: \"absolute\" }}\n            type=\"file\"\n            accept={accept}\n            multiple={maxFileCount > 1}\n            onChange={(e) => {\n              setFiles(e.target.files ?? ([] as any));\n            }}\n          />\n        </label>;\n\n    const showChevrons = isOverflowing && isViewerMode;\n    const showLeftChevron = Boolean(\n      showChevrons && this.refWrapper?.scrollLeft,\n    );\n    const showRightChevron = Boolean(\n      showChevrons &&\n        (!this.refWrapper?.scrollLeft ||\n          this.refWrapper.scrollWidth - 20 >\n            this.refWrapper.scrollLeft + this.refWrapper.clientWidth),\n    );\n\n    const scrollItem = (val: 1 | -1) => {\n      if (this.refWrapper) {\n        /** first child will be chevron */\n        const mediaSize =\n          (this.refWrapper.children[1] as HTMLDivElement).offsetWidth || 150;\n        this.refWrapper.scrollLeft += val * mediaSize;\n        this.forceUpdate();\n      }\n    };\n    const scrollNext = () => scrollItem(1);\n    const scrollPrev = () => scrollItem(-1);\n\n    const chevronCommonProps = {\n      className: \"absolute h-full w-fit flex-row ai-center pointer noselect\",\n      style: { zIndex: 2, top: 0, bottom: 0 },\n    };\n\n    return (\n      <div\n        ref={(e) => {\n          if (e) this.ref = e;\n        }}\n        className={classOverride(\n          \"FileInput flex-col  min-w-0 min-h-0 b-active \",\n          className,\n        )}\n        style={{ ...style }}\n        onDragOver={console.warn}\n        onPaste={\n          !AddBtn ? undefined : (\n            (e) => {\n              const files = e.clipboardData.files;\n              if (files.length) {\n                setFiles(files);\n              }\n            }\n          )\n        }\n      >\n        {popup}\n        <div className={\"relative flex-col min-w-0 min-h-0 \"}>\n          {!AddBtn && !label ? null : (\n            <FlexCol className={\"f-0 \"}>\n              {label && (\n                <div\n                  className=\"noselect text-1p5\"\n                  style={{ textAlign: \"start\" }}\n                >\n                  {label}\n                </div>\n              )}\n              {AddBtn}\n              {!media.length && showDropZone && (\n                <DropZone onChange={setFiles} />\n              )}\n            </FlexCol>\n          )}\n          <div\n            className={\n              isViewerMode ?\n                \" f-1 flex-row o-auto min-w-0 min-h-0 o-auto \"\n              : \" flex-row-wrap gap-p25 \"\n            }\n            ref={(e) => {\n              if (e) this.refWrapper = e;\n            }}\n            onScroll={(e) => {\n              /** Re-render each 100ms of scrolling to check chevrons */\n              const w: any = window;\n              if (\n                !w.__lastMediaScroll ||\n                w.__lastMediaScroll < Date.now() - 100\n              ) {\n                this.forceUpdate();\n                w.__lastMediaScroll = Date.now();\n              }\n            }}\n          >\n            {showLeftChevron && (\n              <div\n                className={chevronCommonProps.className}\n                style={{\n                  ...chevronCommonProps.style,\n                  left: 0,\n                  background: \"linear-gradient( -90deg, transparent, white)\",\n                }}\n                onClick={scrollPrev}\n              >\n                <Icon size={1.5} path={mdiChevronLeft} />\n              </div>\n            )}\n\n            {media.map((mediaItem, i) => {\n              return (\n                <FileInputMedia\n                  key={i}\n                  file={mediaItem}\n                  i={i + \"media\"}\n                  onDelete={\n                    !onDelete ? undefined : (\n                      () => {\n                        onDelete(mediaItem);\n                      }\n                    )\n                  }\n                  onClick={() => {\n                    this.setState({\n                      focusedFile: { file: mediaItem, index: i },\n                    });\n                  }}\n                  // style={\n                  //   !isViewerMode ?\n                  //     {}\n                  //   : {\n                  //       width: \"250px\",\n                  //       height: \"250px\",\n                  //       minWidth: `${minSize}px`,\n                  //       minHeight: `${minSize}px`,\n                  //       margin: 0,\n                  //     }\n                  // }\n                />\n              );\n            })}\n\n            {showRightChevron && (\n              <div\n                className={chevronCommonProps.className}\n                style={{\n                  ...chevronCommonProps.style,\n                  right: 0,\n                  background: \"linear-gradient( 90deg, transparent, white)\",\n                }}\n                onClick={scrollNext}\n              >\n                <Icon size={1.5} path={mdiChevronRight} />\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/FileInput/FileInputMedia.tsx",
    "content": "import { mdiClose, mdiFileOutline } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"../Btn\";\nimport Chip from \"../Chip\";\nimport type { Media } from \"./FileInput\";\n\nexport const FileInputMedia = (props: {\n  file: Media;\n  i: string;\n  onDelete?: () => void;\n  onClick?: () => void;\n  style?: React.CSSProperties;\n  focused?: boolean;\n  minSize?: number;\n}) => {\n  const { file: f, i, onDelete, style = {}, onClick, focused } = props;\n\n  const file: {\n    type: string;\n    name?: string;\n    url: string;\n    isLocalFile?: boolean;\n  } =\n    \"url\" in f ?\n      { ...f, type: f.content_type }\n    : {\n        name: f.name,\n        type: f.data.type,\n        url: URL.createObjectURL(f.data),\n        isLocalFile: true,\n      };\n\n  const { type, url, name } = file;\n  let mediaPreview: React.ReactNode = null;\n  const isVideo = type.startsWith(\"video\");\n  const isImageOrVideo = type.startsWith(\"image\") || isVideo;\n  if (url) {\n    const style = {\n      maxWidth: \"100%\",\n      maxHeight: \"100%\",\n    };\n    if (type.startsWith(\"image\")) {\n      mediaPreview = <img loading=\"lazy\" src={url} style={style}></img>;\n    } else if (type.startsWith(\"video\")) {\n      mediaPreview = <video style={style} controls src={url}></video>;\n    } else if (type.startsWith(\"audio\")) {\n      mediaPreview = <audio style={style} controls src={url}></audio>;\n    } else {\n      return (\n        <Chip\n          key={i}\n          value={file.isLocalFile ? file.name : url}\n          leftIcon={{ path: mdiFileOutline }}\n          onDelete={onDelete}\n        />\n      );\n    }\n  }\n\n  return (\n    <div\n      key={i}\n      className={\"FileInputMedia relative flex-col o-hidden md-auto\"}\n      style={{\n        // width: `${minSize}px`,\n        // height: `${minSize}px`,\n        ...style,\n      }}\n    >\n      <div\n        className={\n          (isVideo ? \"bg-black \" : \"bg-color-0\") +\n          \" relative flex-col f-0 w-fit \"\n        }\n        style={{\n          // ...(focused ?\n          //   {}\n          // : {\n          //     maxWidth: `${minSize}px`,\n          //     maxHeight: `${minSize}px`,\n          //   }),\n          minWidth: \"100px\",\n          minHeight: \"100px\",\n          ...style,\n        }}\n      >\n        {!onClick ? null : (\n          <div\n            className={\n              (isImageOrVideo ? \"\" : \" media-onclick-cover  \") +\n              \" absolute w-full h-full pointer\"\n            }\n            style={{ zIndex: 2 }}\n            onClick={(e) => {\n              e.stopPropagation();\n              e.preventDefault();\n              onClick();\n            }}\n          ></div>\n        )}\n\n        {!onDelete ? null : (\n          <Btn\n            className={\"shadow  b b-color\"}\n            style={{\n              position: \"absolute\",\n              top: \"10px\",\n              right: \"10px\",\n              zIndex: 2,\n              color: \"black\",\n              backdropFilter: `blur(5px)`,\n              backgroundColor: \"white\",\n            }}\n            size=\"small\"\n            iconPath={mdiClose}\n            title=\"Remove file\"\n            onClick={() => {\n              onDelete();\n            }}\n          />\n        )}\n        <div className={\"f-1 min-w-0 min-h-0 flex-row ai-center\"}>\n          {mediaPreview}\n        </div>\n\n        {focused ?\n          null\n        : !name ?\n          <a href={url} target=\"_blank\" className=\"p-5 f-0\" rel=\"noreferrer\">\n            {url}\n          </a>\n        : <div\n            className=\"f-0 noselect p-p5 text-1p5 font-14 absolute w-full f-0\"\n            style={{\n              position: isImageOrVideo ? \"absolute\" : \"relative\",\n              zIndex: 1,\n              bottom: 0,\n              color: isImageOrVideo ? \"white\" : \"black\",\n              background:\n                isImageOrVideo ?\n                  \"linear-gradient(to bottom, rgb(255 255 255 / 0%) 0%,#00000075 70%)\"\n                : \"white\",\n            }}\n          >\n            {name}\n          </div>\n        }\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/FileInput/useFileDropZone.ts",
    "content": "import React, { useMemo, useRef } from \"react\";\nimport { type DivProps } from \"../Flex\";\nimport { isDefined } from \"@common/filterUtils\";\n\nexport const useFileDropZone = (onDropped: (files: File[]) => void) => {\n  const [isEngaged, setIsEngaged] = React.useState(false);\n  const isEngagedRef = useRef(isEngaged);\n  isEngagedRef.current = isEngaged;\n\n  const divHandlers: Pick<DivProps, \"onDragLeave\" | \"onDragOver\" | \"onDrop\"> =\n    useMemo(\n      () => ({\n        onDragLeave: () => setIsEngaged(false),\n        onDragOver: (e) => {\n          if (e.dataTransfer.files.length) {\n            e.preventDefault();\n            e.stopPropagation();\n            setIsEngaged(true);\n          }\n        },\n        onDrop: (ev) => {\n          if (isEngagedRef.current) {\n            ev.preventDefault();\n            onDropped(getDataTransferFiles(ev));\n            setIsEngaged(false);\n          }\n        },\n      }),\n      [onDropped],\n    );\n\n  return { isEngaged, ...divHandlers };\n};\n\nexport const getDataTransferFiles = (ev: React.DragEvent<HTMLDivElement>) => {\n  if (ev.dataTransfer.items as any) {\n    const files = [...ev.dataTransfer.items]\n      .map((item, i) => {\n        if (item.kind === \"file\") {\n          return item.getAsFile() ?? undefined;\n        }\n      })\n      .filter(isDefined);\n    return files;\n  } else {\n    const files = [...ev.dataTransfer.files].map((file, i) => {\n      return file;\n    });\n    return files;\n  }\n};\n"
  },
  {
    "path": "client/src/components/FlashMessage.tsx",
    "content": "import React, { useEffect, useRef } from \"react\";\nimport { useLocation } from \"react-router-dom\";\n\ntype P = {\n  text: string;\n  left: number;\n  top: number;\n  onFinished: () => void;\n};\n\n/** Simple message shown for a short time */\nexport const FlashMessage = (message: P) => {\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      message.onFinished();\n    }, 3000);\n\n    return () => clearTimeout(timer);\n  }, [message]);\n\n  /** Clear message on url change */\n  const location = useLocation();\n  const previousLocation = useRef(location);\n\n  useEffect(() => {\n    // Only trigger if location actually changed (not on initial mount)\n    if (\n      previousLocation.current.pathname !== location.pathname ||\n      previousLocation.current.search !== location.search\n    ) {\n      message.onFinished();\n    }\n    previousLocation.current = location;\n  }, [location, message]);\n\n  /** Clear message on any interaction */\n  useEffect(() => {\n    const clearMessage = () => {\n      message.onFinished();\n    };\n\n    window.addEventListener(\"click\", clearMessage);\n    window.addEventListener(\"keydown\", clearMessage);\n\n    return () => {\n      window.removeEventListener(\"click\", clearMessage);\n      window.removeEventListener(\"keydown\", clearMessage);\n    };\n  }, [message]);\n\n  return (\n    <div\n      className=\"text-warning bg-color-0 p-1 rounded b b-warning\"\n      style={{\n        zIndex: 99,\n        position: \"absolute\",\n        top: message.top,\n        left: message.left,\n      }}\n    >\n      {message.text}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Flex.tsx",
    "content": "import React from \"react\";\nimport type { TestSelectors } from \"../Testing\";\n\nexport type DivProps = React.HTMLAttributes<HTMLDivElement> & TestSelectors;\n\nconst classPropsWithSides = [\"p\", \"m\"] as const;\nconst sides = [\"t\", \"b\", \"l\", \"r\", \"x\", \"y\"] as const;\nconst parseClass = (v: string) => (v.includes(\"-\") ? v.split(\"-\")[0] + \"-\" : v);\n\ntype FlexDivProps = DivProps & {\n  disabledInfo?: string | false;\n};\nexport const classOverride = (defaultClass = \"\", userClass = \"\") => {\n  const userClasses = userClass.split(\" \");\n  const defaultParts = defaultClass\n    .split(\" \")\n    .filter((defPart) => {\n      const defClassProp = parseClass(defPart);\n      return !userClasses.some((userClassPart) => {\n        const userClassProp = parseClass(userClassPart);\n        let userClassProps = [userClassProp];\n        const sidedClassProp = classPropsWithSides.find(\n          (c) => userClassProp === `${c}-`,\n        );\n        if (sidedClassProp) {\n          userClassProps = sides.map((s) => `${sidedClassProp}${s}-`);\n        }\n        return userClassProps.some((uc) => uc.startsWith(defClassProp));\n      });\n    })\n    .join(\" \");\n  return userClass + \" \" + defaultParts;\n};\n\nconst FlexDiv = React.forwardRef<\n  HTMLDivElement,\n  FlexDivProps & { flexClass: string }\n>(({ disabledInfo, flexClass, className, ...p }, ref) => {\n  return (\n    <div\n      {...p}\n      ref={ref}\n      title={(disabledInfo || undefined) ?? p.title}\n      className={classOverride(\n        `${flexClass} ${disabledInfo ? \"no-interaction\" : \"\"}`,\n        className,\n      )}\n    ></div>\n  );\n});\n\nexport const FlexRow = React.forwardRef<HTMLDivElement, FlexDivProps>(\n  (p, ref) => {\n    return (\n      <FlexDiv {...p} ref={ref} flexClass=\"FlexRow flex-row gap-1 ai-center\" />\n    );\n  },\n);\n\nexport const FlexRowWrap = React.forwardRef<HTMLDivElement, FlexDivProps>(\n  (p, ref) => {\n    return (\n      <FlexDiv\n        {...p}\n        ref={ref}\n        flexClass=\"FlexRowWrap flex-row-wrap gap-1 ai-center\"\n      />\n    );\n  },\n);\n\nexport const FlexCol = React.forwardRef<HTMLDivElement, FlexDivProps>(\n  (p, ref) => {\n    return <FlexDiv {...p} ref={ref} flexClass=\"FlexCol flex-col gap-1\" />;\n  },\n);\n"
  },
  {
    "path": "client/src/components/FormField/FormField.css",
    "content": ".form-field input:not([type=\"checkbox\"]) {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  /* background-color: #fff; */\n  border: none;\n  padding: 4px 6px 0px;\n  font-size: 1rem;\n  font-size: 16px;\n  line-height: 1;\n  width: 100%;\n  flex: 1;\n  box-sizing: border-box;\n  outline: none;\n\n  min-width: 80px;\n}\n\n.input-wrapper,\n/* .form-field .input-wrapper.focus-border:has(.ContentRight:focus-within) { \n  Used to prevent input border active when the content right also has an active border.\n  Disabled because it is not working in firefox \n*/\n.form-field .input-wrapper.focus-border {\n  /* min-width: 150px; <- not sure what this was for */\n  border-radius: var(--rounded);\n  border: 1px solid var(--gray-300);\n  box-sizing: border-box;\n}\n\n.dark-theme .form-field .input-wrapper.focus-border {\n  border: 1px solid var(--b-default);\n}\n\n.form-field .input-wrapper {\n  /* background-color: #fff; */\n  min-width: 80px;\n  border-radius: var(--rounded);\n  border-style: solid;\n  box-sizing: border-box;\n  overflow: hidden;\n  /* overflow: visible; */\n}\n.form-field\n  .input-wrapper\n  > :first-child:not(textarea, .ChipArrayEditor, .Checkbox, .JSONBSchema) {\n  border-radius: 0 !important;\n  color: var(--text-0);\n}\n\n/* .form-field input:focus, */\n.form-field label.checkbox:focus {\n  outline: none;\n  /* box-shadow: inset 0 0 0 3px rgba(164,202,254,.45); */\n  box-shadow: inset 0 0 5px 1px rgba(59, 153, 252, 0.7);\n  border-color: #a4cafe;\n}\n\n/* .form-field .input-wrapper.error input:focus, <- input-wrapper below already does this */\n.form-field .input-wrapper.error label.checkbox:focus,\n.form-field .input-wrapper.error {\n  border: 1px solid var(--red-700);\n  box-shadow: inset 0 0 5px 1px rgb(253 158 158 / 70%);\n}\n\n/* .form-field .input-wrapper.error input {\n  border: 1px solid var(--red-700);\n} */\n\n.form-field label.main-label {\n  flex: 1;\n  min-width: 80px;\n}\n\n.form-field select {\n  background: white;\n  padding: 0.5rem 0.75rem;\n  border: none;\n  outline: 0;\n  flex: 1;\n\n  cursor: pointer;\n}\n\n.input-wrapper .SELECTCOMPONENT {\n  border-color: transparent !important;\n}\n\nhtml:not(.dark-theme) .FormField_Select {\n  background-color: white !important;\n}\n"
  },
  {
    "path": "client/src/components/FormField/FormField.tsx",
    "content": "import React from \"react\";\nimport \"./FormField.css\";\n\nimport { mdiClose, mdiFullscreen } from \"@mdi/js\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport { includes, isDefined } from \"prostgles-types\";\nimport { scrollIntoViewIfNeeded } from \"src/utils/utils\";\nimport { ChipArrayEditor } from \"../../dashboard/SmartForm/ChipArrayEditor\";\nimport { getInputType } from \"../../dashboard/SmartForm/SmartFormField/fieldUtils\";\nimport { RenderValue } from \"../../dashboard/SmartForm/SmartFormField/RenderValue\";\nimport type { AsJSON } from \"../../dashboard/SmartForm/SmartFormField/useSmartFormFieldAsJSON\";\nimport type { TestSelectors } from \"../../Testing\";\nimport Btn from \"../Btn\";\nimport { Checkbox } from \"../Checkbox\";\nimport { generateUniqueID } from \"../FileInput/FileInput\";\nimport { classOverride } from \"../Flex\";\nimport { Label } from \"../Label\";\nimport List from \"../List\";\nimport Popup, { DATA_HAS_VALUE, DATA_NULLABLE } from \"../Popup/Popup\";\nimport { Select, type FullOption } from \"../Select/Select\";\nimport {\n  FormFieldSkeleton,\n  type FormFieldCommonProps,\n} from \"./FormFieldSkeleton\";\nimport { onFormFieldKeyDown } from \"./onFormFieldKeyDown\";\n\nexport type FormFieldTypes =\n  | \"text\"\n  | \"number\"\n  | \"password\"\n  | \"email\"\n  | \"file\"\n  | \"checkbox\"\n  | \"integer\"\n  | \"username\"\n  | \"url\"\n  | \"color\";\ntype FormFieldNullOpt<Nullable = false, Optional = false> =\n  Nullable extends true ? null\n  : Optional extends true ? undefined\n  : never;\ntype FormFieldValueType<\n  T extends FormFieldTypes,\n  Nullable = false,\n  Optional = false,\n> =\n  | FormFieldNullOpt<Nullable, Optional>\n  | (T extends \"number\" | \"integer\" ? number\n    : T extends \"text\" | \"password\" | \"email\" | \"url\" | \"username\" ? string\n    : T extends \"file\" ? FileList\n    : T extends \"checkbox\" ? boolean\n    : any);\n\nexport type FormFieldProps<\n  T extends FormFieldTypes,\n  Nullable extends boolean = false,\n  Optional extends boolean = false,\n> = TestSelectors &\n  FormFieldCommonProps & {\n    onChange?: (\n      val: FormFieldValueType<T, Nullable, Optional>,\n      // val: string | boolean | FileList | null | undefined,\n      e?: any,\n    ) => void;\n    onInput?: (e: React.FormEvent<HTMLInputElement>) => void;\n    type?: T;\n    id?: string;\n    readOnly?: boolean;\n    /**\n     * Passed to the input\n     */\n    required?: boolean;\n    /**\n     * If true and value is not null will allow setting value to null\n     */\n    nullable?: Nullable;\n    /**\n     * If true and value is not undefined will allow setting value to undefined\n     */\n    optional?: Optional;\n    value?: FormFieldValueType<T, Nullable, Optional>;\n    rawValue?: any;\n    defaultValue?: string | number;\n    options?: readonly (string | number | null)[];\n    fullOptions?: readonly FullOption[];\n    autoComplete?: string;\n    accept?: string;\n    asTextArea?: boolean;\n    inputContent?: React.ReactNode;\n\n    placeholder?: string;\n    autoResize?: boolean;\n    inputClassName?: string;\n    wrapperStyle?: React.CSSProperties;\n    inputStyle?: React.CSSProperties;\n    multiSelect?: boolean;\n    labelAsValue?: boolean;\n    onSuggest?: (term?: string) => Promise<string[]>;\n    name?: string;\n    inputProps?: Omit<\n      React.DetailedHTMLProps<\n        React.InputHTMLAttributes<HTMLInputElement>,\n        HTMLInputElement\n      >,\n      \"children\" | \"onChange\" | \"value\" | \"defaultValue\"\n    >;\n    hideClearButton?: boolean;\n\n    asJSON?: AsJSON[\"component\"];\n    arrayType?: Pick<ValidatedColumnInfo, \"udt_name\" | \"tsDataType\">;\n    leftIcon?: React.ReactNode;\n    showFullScreenToggle?: boolean;\n\n    variant?: \"row\";\n  };\n\ntype FormFieldState = {\n  activeSuggestionIdx?: number;\n  suggestions?: string[];\n  options?: string[];\n  numLockAlert?: boolean;\n  fullScreen?: boolean;\n};\nexport default class FormField<\n  T extends FormFieldTypes,\n  Nullable extends boolean = false,\n  Optional extends boolean = false,\n> extends React.Component<\n  FormFieldProps<T, Nullable, Optional>,\n  FormFieldState\n> {\n  state: FormFieldState = {};\n\n  rootDiv?: HTMLDivElement;\n  refWrapper?: HTMLDivElement;\n\n  textArea: HTMLTextAreaElement | null = null;\n  setResizer = () => {\n    const { asTextArea, autoResize = true } = this.props;\n    if (this.rootDiv && asTextArea && autoResize && !this.textArea) {\n      const textArea = this.rootDiv.querySelector(\"textarea\");\n      if (textArea) {\n        this.textArea = textArea;\n        textArea.addEventListener(\"input\", this.resize, false);\n        setTimeout(() => {\n          this.resize();\n        }, 10);\n      }\n    }\n  };\n\n  /** Must autoresize only if it's an increase */\n  resize = () => {\n    if (this.textArea) {\n      const ta = this.textArea;\n      const newH = Math.min(100, ta.scrollHeight);\n      const newW = ta.scrollWidth; // + 34\n      let w = ta.offsetWidth; // Number(ta.style.width.slice(0, -2));\n      w = Number.isFinite(w) && w >= ta.offsetWidth ? w : ta.offsetWidth;\n      let h = ta.offsetHeight; // Number(ta.style.height.slice(0, -2));\n      h = Number.isFinite(h) && h >= ta.offsetWidth ? h : ta.offsetHeight;\n\n      if (w < newW) {\n        ta.style.width = \"\";\n        ta.style.width = newW + \"px\";\n      }\n      if (h < newH) {\n        ta.style.height = \"\";\n        ta.style.height = newH + \"px\";\n      }\n    }\n  };\n\n  mounted = false;\n  componentDidMount() {\n    this.mounted = true;\n    this.setResizer();\n  }\n\n  componentWillUnmount() {\n    this.mounted = false;\n    if (this.textArea)\n      this.textArea.removeEventListener(\"input\", this.resize, false);\n  }\n  componentDidUpdate(prevProps: FormFieldProps<T, Nullable, Optional>) {\n    const err = this.props.error,\n      preverr = prevProps.error;\n    if (this.rootDiv && (err || preverr) && err !== preverr) {\n      scrollIntoViewIfNeeded(this.rootDiv, {\n        block: \"end\",\n        behavior: \"smooth\",\n      });\n    }\n    this.setResizer();\n    if (prevProps.value !== this.props.value) {\n      this.resize();\n\n      if (\n        this.inputRef &&\n        /text|password|search|tel|url/.test(this.inputRef.type) &&\n        this.cursorPosition !== undefined\n      ) {\n        this.inputRef.selectionStart = this.cursorPosition;\n        this.inputRef.selectionEnd = this.cursorPosition;\n      }\n    }\n  }\n\n  focus = (e) => {};\n\n  blur = (e) => {};\n\n  changing?: {\n    value: any;\n    timeout: any;\n  };\n\n  onChange = (input: HTMLInputElement) => {\n    const { onChange, onSuggest } = this.props;\n\n    if (!onChange) return;\n\n    const value =\n      input.type === \"file\" ? input.files\n      : input.type === \"checkbox\" ? input.checked\n      : input.value;\n    onChange(value as FormFieldValueType<T, Nullable, Optional>);\n\n    if (!onSuggest) return;\n    if (this.changing) clearTimeout(this.changing.timeout);\n    const delay = this.changing ? 100 : 0;\n    this.changing = {\n      value,\n      timeout: setTimeout(async () => {\n        if (this.mounted && this.changing) {\n          const { onSuggest } = this.props;\n          const { value } = this.changing;\n          if (onSuggest) {\n            const suggestions = await onSuggest(this.changing.value);\n            this.setState({\n              suggestions:\n                suggestions.length === 1 && suggestions[0] === value ?\n                  undefined\n                : suggestions,\n            });\n          }\n          this.changing = undefined;\n        }\n      }, delay),\n    };\n  };\n\n  cursorPosition?: number = 0;\n  inputRef?: HTMLInputElement;\n  inputSelStart?: number;\n  id?: string;\n  render(): React.ReactNode {\n    const {\n      onChange,\n      label,\n      value,\n      defaultValue,\n      required = false,\n      type = \"text\", // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input\n      autoComplete = \"on\", // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete\n      error,\n      hint,\n      placeholder,\n      className = \"\",\n      inputClassName = \"\",\n      style = {},\n      inputStyle = {},\n      asTextArea = false,\n      accept,\n      onInput,\n      rightIcons = null,\n      rightContent = null,\n      title,\n      nullable = false,\n      optional = false,\n      multiSelect,\n      labelAsValue,\n      options = this.state.options,\n      fullOptions,\n      name = this.props.type,\n      inputProps: _inputProps = {},\n      hideClearButton = false,\n      maxWidth = \"400px\",\n      rawValue: rval,\n      labelStyle = {},\n      disabledInfo,\n      arrayType,\n      rightContentAlwaysShow,\n      asJSON,\n      variant,\n      showFullScreenToggle,\n      leftIcon,\n    } = this.props;\n\n    this.id ??= this.props.id ?? generateUniqueID();\n    const id = this.id;\n\n    const arrayEditor =\n      arrayType ?\n        <ChipArrayEditor\n          elemTsType={arrayType.tsDataType}\n          elemUdtName={arrayType.udt_name}\n          inputType={getInputType({\n            name: name ?? \"text\",\n            ...arrayType,\n          }).toLowerCase()}\n          values={(value || []) as string[]}\n          onChange={onChange as any}\n        />\n      : null;\n\n    const readOnly = this.props.readOnly ?? false;\n\n    const { suggestions, activeSuggestionIdx = 0, numLockAlert } = this.state;\n    const rawValue = [rval, defaultValue, value].find((v) => v !== undefined);\n\n    let wrapperStyle: React.CSSProperties =\n      options ?\n        {\n          width: \"fit-content\",\n          minWidth: 0,\n        }\n      : {};\n\n    const disablePressStyle: React.CSSProperties = {\n      pointerEvents: \"none\",\n      touchAction: \"none\",\n    };\n\n    if (type === \"checkbox\" && readOnly) {\n      wrapperStyle = {\n        ...wrapperStyle,\n        backgroundColor: \"var(--bg-color-2)\",\n        ...disablePressStyle,\n      };\n    }\n\n    let valProp: {\n      value?: any;\n      defaultValue?: any;\n      checked?: boolean;\n    } = {\n      value: includes([undefined, null], value) ? \"\" : value,\n    };\n\n    if (readOnly) valProp = { value };\n    if (defaultValue) valProp = { defaultValue };\n\n    let inptClass = \" font-semibold formfield-bg-color \";\n    if (type !== \"checkbox\") {\n      wrapperStyle = {\n        ...wrapperStyle,\n      };\n    }\n\n    if (type.startsWith(\"file\")) {\n      inptClass += \" pointer \";\n    }\n\n    let extraInptStyle = {};\n    if (type === \"color\" && value) {\n      wrapperStyle = {\n        ...wrapperStyle,\n        backgroundColor: (value as string) || \"white\",\n        cursor: \"pointer\",\n      };\n      extraInptStyle = { opacity: 0 };\n    }\n    if (type === \"checkbox\") {\n      valProp = {\n        checked: Boolean(value || defaultValue),\n      };\n      wrapperStyle = {\n        ...wrapperStyle,\n        minWidth: 0,\n        width: \"fit-content\",\n        border: \"none\",\n        overflow: \"visible\",\n      };\n    }\n    if (asTextArea || arrayEditor) {\n      wrapperStyle = { ...wrapperStyle, border: \"none\", overflow: \"visible\" };\n    }\n\n    inptClass += \" \" + inputClassName;\n\n    const onDragStop = (e: React.DragEvent<HTMLInputElement>) => {\n      e.currentTarget.classList.toggle(\"active-drop-target\", false);\n    };\n\n    const ref = (e: HTMLInputElement | null) => {\n      if (e) {\n        this.inputRef = e;\n        //@ts-ignore\n        e.forceDemoValue = (val: any) => {\n          this.props.onChange?.(val);\n        };\n      }\n    };\n\n    const inputProps: React.DetailedHTMLProps<\n      React.InputHTMLAttributes<HTMLInputElement>,\n      HTMLInputElement\n    > = {\n      id,\n      key: id,\n      required,\n      readOnly,\n      className: inptClass,\n      type,\n      accept,\n      ...valProp,\n      // ...(type !== \"file\" ? valProp : undefined),\n      autoComplete,\n      onInput,\n      placeholder,\n      name,\n      /** Why doesn't this happen by default ?! */\n      ...{ autoCorrect: \"off\", autoCapitalize: \"off\" },\n      ..._inputProps,\n      style: { ...extraInptStyle, ...inputStyle, ..._inputProps.style },\n      onChange: ({ currentTarget }) => {\n        this.cursorPosition = currentTarget.selectionStart!;\n        return this.onChange(currentTarget);\n      },\n      onKeyDown:\n        type === \"number\" ?\n          (e) => {\n            const numLockAlert =\n              !e.getModifierState(\"NumLock\") &&\n              e.nativeEvent.code.startsWith(\"Numpad\") &&\n              e.key !== e.nativeEvent.code.slice(0, \"Numpad\".length); //(NUMLOCK_KEYS.includes(e.key) || !e.currentTarget.value && [\"ArrowRight\", \"ArrowLeft\"].includes(e.key) )\n            if (numLockAlert !== this.state.numLockAlert) {\n              this.setState({ numLockAlert });\n            }\n            _inputProps.onKeyDown?.(e);\n            const node = e.target as HTMLInputElement;\n            if (!node.placeholder && !/^-?\\d+$/.test(value as string)) {\n              node.placeholder = \"Numbers only\";\n            }\n          }\n        : undefined,\n      onFocus:\n        !_inputProps.onFocus ? undefined : (\n          (e) => {\n            _inputProps.onFocus?.(e);\n          }\n        ),\n      // ...(!type.startsWith(\"file\") ?\n      //   {}\n      // :\n      ...{\n        onDragOver: (e) => {\n          e.currentTarget.classList.toggle(\"active-drop-target\", true);\n        },\n        onDrop: onDragStop,\n        onDragEnd: onDragStop,\n        onDragExit: onDragStop,\n        onDragLeave: onDragStop,\n      },\n    };\n\n    const selectSuggestion = (key) => {\n      onChange?.(key);\n      this.setState({ suggestions: undefined });\n    };\n\n    const textareaStyle: React.CSSProperties = {\n      width: \"100%\",\n      minWidth: \"5em\",\n      minHeight: \"2em\",\n      whiteSpace: \"pre-line\",\n      borderRadius: \".5em\",\n      resize: \"vertical\",\n      ...inputProps.style,\n    };\n\n    const inputFinalStyle = {\n      ...(inputProps.type !== \"file\" ?\n        {\n          padding: \".4em .5em 4px .5em\",\n        }\n      : {\n          paddingBottom: \"4px\",\n        }),\n      minHeight: window.isLowWidthScreen ? \"36px\" : \"42px\",\n      ...inputProps.style,\n      ...(rval === null && { fontStyle: \"italic\" }),\n    };\n\n    const inputContent = arrayEditor || this.props.inputContent;\n\n    const inputNode =\n      inputContent ? inputContent\n        // : type === \"file\" ? <InputFile {...(inputProps as any)} />\n      : type === \"checkbox\" ? <Checkbox {...(inputProps as any)} />\n      : readOnly ?\n        <div\n          className=\"pr-p5 py-p5 font-16 ta-left o-auto\"\n          style={{ fontWeight: 500, maxHeight: \"30vh\" }}\n        >\n          <RenderValue column={undefined} value={rawValue} />\n        </div>\n      : asTextArea ?\n        <textarea\n          ref={ref}\n          {...(inputProps as any)}\n          className={classOverride(inputProps.className ?? \"\", \" text-0\")}\n          style={textareaStyle}\n        />\n      : <input\n          ref={ref}\n          {...inputProps}\n          {...(rval === null && { placeholder: \"NULL\" })}\n          style={inputFinalStyle}\n        />;\n\n    let clearButton: React.ReactNode = null;\n    if (\n      !readOnly &&\n      !disabledInfo &&\n      onChange &&\n      (nullable || optional) &&\n      !hideClearButton\n    ) {\n      if (nullable && optional) {\n        clearButton = (\n          <Select\n            btnProps={{\n              iconPath: mdiClose,\n              children: \"\",\n              style: { background: \"transparent\" },\n            }}\n            options={[\n              rawValue !== null ? \"NULL\" : undefined,\n              rawValue !== undefined ? \"undefined\" : undefined,\n            ].filter(isDefined)}\n            onChange={(v) => {\n              onChange(\n                (v === \"NULL\" ? null : undefined) as FormFieldValueType<\n                  T,\n                  Nullable,\n                  Optional\n                >,\n              );\n            }}\n          />\n        );\n      } else if (\n        (rawValue !== undefined && optional) ||\n        (rawValue !== null && nullable)\n      ) {\n        clearButton = (\n          <Btn\n            data-command=\"FormField.clear\"\n            title={`Set to ${optional ? \"undefined\" : \"null\"}`}\n            style={{\n              /** To ensure it's centered with the rest of the content */\n              height: \"100%\",\n              ...(type === \"checkbox\" ? { padding: \"0\" } : {}), // paddingLeft: 0\n            }}\n            iconPath={mdiClose}\n            onClick={(e) => {\n              //@ts-ignore\n              onChange(optional ? undefined : null, e);\n            }}\n            size=\"small\"\n            className=\"rounded-r\"\n          />\n        );\n      }\n    }\n\n    const isEditableSelect = !readOnly && Array.isArray(options ?? fullOptions);\n\n    if (this.state.fullScreen && showFullScreenToggle) {\n      return (\n        <Popup\n          title={\n            typeof label === \"string\" ? label : (\n              <Label variant=\"normal\" {...label} />\n            )\n          }\n          positioning=\"fullscreen\"\n          onClose={() => {\n            this.setState({ fullScreen: false });\n          }}\n        >\n          {inputNode}\n        </Popup>\n      );\n    }\n\n    return (\n      <FormFieldSkeleton\n        id={id}\n        title={title}\n        ref={(e) => {\n          if (e) this.rootDiv = e;\n        }}\n        leftIcon={leftIcon}\n        data-command={isEditableSelect ? undefined : this.props[\"data-command\"]}\n        data-key={this.props[\"data-key\"]}\n        className={`${className} ${nullable ? DATA_NULLABLE : \"\"} ${value !== undefined && value !== null && value !== \"\" ? DATA_HAS_VALUE : \"\"}`}\n        disabledInfo={disabledInfo}\n        style={style}\n        onBlur={() => {\n          if (suggestions) {\n            this.setState({ suggestions: undefined });\n          }\n        }}\n        //@ts-ignore\n        onKeyDown={(e) => onFormFieldKeyDown.bind(this)(e, selectSuggestion)}\n        hintWrapperStyle={{\n          flex: 1,\n          ...(variant === \"row\" && { flexDirection: \"row\" }),\n          ...(asJSON && { minWidth: \"min(400px, 90vw)\" }),\n        }}\n        label={label}\n        labelStyle={labelStyle}\n        labelRightContent={\n          showFullScreenToggle && (\n            <Btn\n              title=\"Click to toggle full screen\"\n              className=\"show-on-trigger-hover\"\n              iconPath={mdiFullscreen}\n              size=\"micro\"\n              style={{ padding: \"0\" }}\n              onClick={() => {\n                this.setState({ fullScreen: !this.state.fullScreen });\n              }}\n            />\n          )\n        }\n        errorWrapperClassname={`${type !== \"checkbox\" ? \"flex-col\" : \"flex-row\"} gap-p5 min-w-0 ${isEditableSelect || (inputContent && asJSON !== \"codeEditor\" && type !== \"checkbox\") ? \"\" : \"f-1\"}`}\n        inputWrapperClassname={\n          (type === \"checkbox\" ? \" ai-center \" : \"\") +\n          (type === \"checkbox\" || asJSON === \"JSONBSchema\" || arrayEditor ?\n            \" focus-border-unset \"\n          : \" \") +\n          ((\n            options ||\n            fullOptions ||\n            asJSON === \"JSONBSchema\" ||\n            type === \"checkbox\"\n          ) ?\n            \"w-fit\"\n          : \"w-full\")\n        }\n        inputWrapperStyle={{\n          ...wrapperStyle,\n          maxWidth: asTextArea ? \"100%\" : maxWidth,\n          ...(asJSON === \"codeEditor\" && { minHeight: \"42px\" }),\n          ...(((readOnly && asJSON !== \"codeEditor\") ||\n            asJSON === \"JSONBSchema\") && {\n            border: \"unset\",\n            boxShadow: \"unset\",\n            /**\n             * To ensure focus-border on select controls is visible\n             */\n            overflow: asJSON === \"JSONBSchema\" ? \"visible\" : undefined,\n          }),\n          ...this.props.wrapperStyle,\n        }}\n        rightIconsShowBorder={Boolean(\n          type !== \"checkbox\" && !asTextArea && !inputContent,\n        )}\n        error={error}\n        warning={numLockAlert ? \"NumLock is off\" : undefined}\n        rightIcons={Boolean(rightIcons) && <>{rightIcons}</>}\n        hint={hint}\n        rightContent={\n          Boolean(rightContent || clearButton) && (\n            <>\n              {clearButton}\n              {rightContent}\n            </>\n          )\n        }\n        rightContentAlwaysShow={rightContentAlwaysShow}\n      >\n        {isEditableSelect ?\n          <Select\n            className=\"FormField_Select noselect f-1 formfield-bg-color\"\n            style={{\n              fontSize: \"16px\",\n              fontWeight: 500,\n              paddingLeft: \"6px\",\n            }}\n            data-command={this.props[\"data-command\"]}\n            variant=\"div\"\n            fullOptions={options?.map((key) => ({ key })) ?? fullOptions ?? []}\n            onChange={\n              !onChange ? undefined : (\n                (val) => {\n                  //@ts-ignore\n                  onChange(val);\n                }\n              )\n            }\n            asRow={variant === \"row\"}\n            value={rawValue}\n            required={required}\n            multiSelect={multiSelect}\n            labelAsValue={labelAsValue}\n            btnProps={{\n              id,\n            }}\n          />\n        : inputNode}\n        {!this.props.onSuggest || !suggestions ? null : (\n          <List\n            anchorRef={this.refWrapper}\n            selectedValue={suggestions[activeSuggestionIdx]}\n            items={suggestions.map((key) => ({\n              key,\n              node:\n                (key as any) === null ? <i>NULL</i>\n                : key.trim() === \"\" ? <i>Empty</i>\n                : null,\n              onPress: (e) => {\n                selectSuggestion(key);\n              },\n            }))}\n            onClose={() => {\n              this.setState({ suggestions: undefined });\n            }}\n          />\n        )}\n      </FormFieldSkeleton>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/FormField/FormFieldCodeEditor.tsx",
    "content": "import { useMemoDeep } from \"prostgles-client\";\nimport React, { useCallback, useMemo } from \"react\";\nimport { isObject } from \"@common/publishUtils\";\nimport {\n  CodeEditor,\n  type CodeEditorProps,\n} from \"../../dashboard/CodeEditor/CodeEditor\";\nimport type { AsJSON } from \"../../dashboard/SmartForm/SmartFormField/useSmartFormFieldAsJSON\";\nimport type { FormFieldProps } from \"./FormField\";\n\ntype P = Pick<FormFieldProps<\"text\">, \"value\" | \"onChange\" | \"readOnly\"> & {\n  className?: string;\n  style?: React.CSSProperties;\n  asJSON: AsJSON;\n};\n\nexport const FormFieldCodeEditor = ({\n  asJSON,\n  value: valueAsStringOrObjectOrNull,\n  onChange,\n  className,\n  style,\n  readOnly,\n}: P) => {\n  const validatedSchemaId =\n    asJSON.schemas?.length ? asJSON.schemas[0]!.id : undefined;\n\n  const valueAsString = useMemo(() => {\n    if (\n      valueAsStringOrObjectOrNull &&\n      (isObject(valueAsStringOrObjectOrNull) ||\n        Array.isArray(valueAsStringOrObjectOrNull))\n    ) {\n      return JSON.stringify(valueAsStringOrObjectOrNull, null, 2);\n    }\n    if (typeof valueAsStringOrObjectOrNull === \"string\") {\n      return valueAsStringOrObjectOrNull;\n    }\n    return \"\";\n  }, [valueAsStringOrObjectOrNull]);\n\n  const localOnChange = useCallback(\n    (v: string) => {\n      try {\n        if (!onChange) return;\n        const jsonValue = JSON.parse(v);\n        onChange(jsonValue);\n      } catch (e) {}\n    },\n    [onChange],\n  );\n\n  const hasValue = !!valueAsString.toString().length;\n  const editorProps = useMemoDeep(() => {\n    return {\n      style: {\n        minHeight: hasValue ? \"100px\" : \"26px\",\n        minWidth: \"200px\",\n        borderRadius: \".5em\",\n        flex: 1,\n        resize: \"vertical\",\n        overflow: \"auto\",\n        border: \"unset\",\n        borderRight: `1px solid var(--text-4)`,\n        ...style,\n      },\n      options: {\n        ...asJSON.options,\n        tabSize: 2,\n        minimap: {\n          enabled: false,\n        },\n        lineNumbers: \"off\",\n        automaticLayout: true,\n      },\n      language: {\n        lang: \"json\",\n        jsonSchemas: asJSON.schemas,\n      },\n    } satisfies Pick<CodeEditorProps, \"options\" | \"language\" | \"style\">;\n  }, [\n    //hasValue,\n    asJSON.schemas,\n    asJSON.options,\n    style,\n  ]);\n\n  return (\n    <CodeEditor\n      key={validatedSchemaId}\n      className={className}\n      {...editorProps}\n      value={valueAsString}\n      onChange={readOnly || !onChange ? undefined : localOnChange}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/components/FormField/FormFieldDebounced.tsx",
    "content": "import React from \"react\";\nimport RTComp from \"../../dashboard/RTComp\";\nimport type { FormFieldProps, FormFieldTypes } from \"./FormField\";\nimport FormField from \"./FormField\";\n\ntype S = {\n  value: any;\n  debouncing?: boolean;\n};\n\nexport class FormFieldDebounced<\n  T extends FormFieldTypes,\n  Nullable extends boolean = false,\n  Optional extends boolean = false,\n> extends RTComp<FormFieldProps<T, Nullable, Optional>, S> {\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      value: this.props.value,\n    };\n  }\n\n  inDebounce?: {\n    timer: NodeJS.Timeout;\n    value: any;\n  };\n  onChangeDebounced = (value: any) => {\n    this.setState({ value, debouncing: true });\n    if (this.inDebounce) {\n      clearTimeout(this.inDebounce.timer);\n    }\n    this.inDebounce = {\n      value,\n      timer: setTimeout(async () => {\n        if (!this.mounted) return;\n        await this.props.onChange?.(value);\n        setTimeout(() => {\n          this.setState({ value: undefined, debouncing: false });\n        }, 100);\n        this.inDebounce = undefined;\n      }, 500),\n    };\n  };\n\n  render(): React.ReactNode {\n    const { debouncing, value } = this.state;\n    return (\n      <FormField\n        {...this.props}\n        style={{\n          ...this.props.style,\n          ...(debouncing && { opacity: \".5\" }),\n        }}\n        value={debouncing ? (value ?? this.props.value) : this.props.value}\n        onChange={this.onChangeDebounced}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/FormField/FormFieldSkeleton.tsx",
    "content": "import React, { forwardRef, type Ref } from \"react\";\nimport \"./FormField.css\";\n\nimport { isObject } from \"prostgles-types\";\nimport type { TestSelectors } from \"../../Testing\";\nimport ErrorComponent from \"../ErrorComponent\";\nimport { classOverride, FlexRow, type DivProps } from \"../Flex\";\nimport { InfoRow } from \"../InfoRow\";\nimport { Label, type LabelPropsNormal } from \"../Label\";\n\nconst INPUT_WRAPPER_CLASS = \"input-wrapper\";\n\nexport type FormFieldCommonProps = {\n  className?: string;\n  label?: string | Omit<LabelPropsNormal, \"variant\">;\n  hint?: string;\n  title?: string;\n  style?: React.CSSProperties;\n  rightContentAlwaysShow?: boolean;\n  rightIcons?: React.ReactNode;\n  rightContent?: React.ReactNode;\n  labelStyle?: React.CSSProperties;\n  disabledInfo?: string;\n  error?: unknown;\n  maxWidth?: React.CSSProperties[\"maxWidth\"];\n};\n\ntype FormFieldSkeletonProps = TestSelectors &\n  FormFieldCommonProps &\n  Pick<DivProps, \"onBlur\" | \"onKeyDown\"> & {\n    /**\n     * Used by the label to identify the input\n     */\n    id: string;\n    warning?: string;\n    errorWrapperClassname: string;\n    inputWrapperClassname: string;\n    children: React.ReactNode;\n    rightIconsShowBorder: boolean;\n    hintWrapperStyle: React.CSSProperties;\n    labelRightContent: React.ReactNode;\n    inputWrapperStyle: React.CSSProperties;\n    leftIcon: React.ReactNode;\n  };\n\nexport const FormFieldSkeleton = forwardRef(\n  (props: FormFieldSkeletonProps, ref: Ref<HTMLDivElement>) => {\n    const {\n      id,\n      label,\n      inputWrapperClassname,\n      error,\n      hint,\n      children,\n      className = \"\",\n      style = {},\n      inputWrapperStyle,\n      warning,\n      errorWrapperClassname,\n      rightIcons = null,\n      rightIconsShowBorder,\n      rightContent = null,\n      title,\n      maxWidth = \"400px\",\n      labelStyle = {},\n      labelRightContent,\n      disabledInfo,\n      rightContentAlwaysShow,\n      onBlur,\n      onKeyDown,\n      hintWrapperStyle,\n      leftIcon,\n    } = props;\n\n    const refWrapper = React.useRef<HTMLDivElement>(null);\n\n    const labelString = isObject(label) ? label.label : label;\n\n    return (\n      <div\n        className={`form-field trigger-hover flex-row min-w-0 ${className} ${disabledInfo ? \"disabled\" : \"\"}`}\n        data-command={props[\"data-command\"]}\n        data-label={labelString}\n        data-key={props[\"data-key\"]}\n        style={style}\n        ref={ref}\n        onBlur={onBlur}\n        title={disabledInfo}\n        onKeyDown={onKeyDown}\n      >\n        {leftIcon}\n        <div\n          className={`form-field__hint-wrapper trigger-hover flex-col gap-p5 min-w-0`}\n          title={title}\n          style={{\n            ...hintWrapperStyle,\n            ...(disabledInfo && { pointerEvents: \"none\" }),\n          }}\n        >\n          {isObject(label) && !labelRightContent ?\n            <Label\n              className=\"mb-p25\"\n              {...label}\n              htmlFor={id}\n              variant=\"normal\"\n              style={{ zIndex: 1 }}\n            />\n          : !labelString ?\n            undefined\n          : <label\n              htmlFor={id}\n              className={\n                \"main-label ta-left noselect text-1 flex-row \" +\n                (id ? \" pointer \" : \" \")\n              }\n              style={{\n                flex: 0.5,\n                justifyContent: \"space-between\",\n                ...labelStyle,\n              }}\n            >\n              {labelString}\n              {labelRightContent}\n            </label>\n          }\n\n          <div\n            className={\n              \"form-field__right-content-wrapper flex-row f-0 ai-center min-w-0 gap-p25 \"\n            }\n          >\n            <div\n              className={`form-field__error-wrapper ${errorWrapperClassname}`}\n              style={{\n                maxWidth: \"100%\",\n              }}\n            >\n              <div\n                className={classOverride(\n                  `${INPUT_WRAPPER_CLASS} h-fit flex-row relative f-0 focus-border w-full ${error ? \" error \" : \"\"}`,\n                  inputWrapperClassname,\n                )}\n                ref={refWrapper}\n                style={{\n                  maxWidth:\n                    inputWrapperStyle.maxWidth ??\n                    (children ? \"w-fit\" : undefined) ??\n                    maxWidth,\n                  ...inputWrapperStyle,\n                }}\n              >\n                {children}\n\n                {Boolean(rightIcons) && (\n                  <FlexRow\n                    className={\n                      `RightIcons ${rightContentAlwaysShow ? \"\" : \"show-on-trigger-hover\"} h-fit as-start gap-0 ai-start jc-center ` +\n                      (rightIconsShowBorder ? \"  bl b-color \" : \" \")\n                    }\n                  >\n                    {rightIcons}\n                  </FlexRow>\n                )}\n              </div>\n\n              {(error || warning) && (\n                <div className={\"flex-col jc-center \"}>\n                  {warning && (\n                    <InfoRow\n                      variant=\"naked\"\n                      className=\"font-10\"\n                      iconSize={0.75}\n                    >\n                      {warning}\n                    </InfoRow>\n                  )}\n                  {Boolean(error) && (\n                    <ErrorComponent\n                      error={error}\n                      style={{ padding: 0 }}\n                      findMsg={true}\n                    />\n                  )}\n                </div>\n              )}\n            </div>\n            {Boolean(rightContent) && (\n              <FlexRow\n                className={`RightContent  ${rightContentAlwaysShow ? \"\" : \"show-on-trigger-hover\"} f-0 gap-0`}\n                // style={{ alignSelf: \"center\" }} // So it looks better for asJONB=JSONBSchema\n                style={{ alignSelf: \"start\" }}\n              >\n                {rightContent}\n              </FlexRow>\n            )}\n          </div>\n          {Boolean(hint) && (\n            <p className=\"FormFieldHint ta-left text-2 m-0 text-sm noselect ws-pre-line\">\n              {hint}\n            </p>\n          )}\n        </div>\n      </div>\n    );\n  },\n);\n"
  },
  {
    "path": "client/src/components/FormField/onFormFieldKeyDown.ts",
    "content": "import { getStringFormat } from \"../../utils/utils\";\nimport type FormField from \"./FormField\";\n\nexport function onFormFieldKeyDown(\n  this: FormField<\"text\">,\n  e: React.KeyboardEvent<HTMLDivElement>,\n  selectSuggestion: (key: any) => void,\n) {\n  const { suggestions, activeSuggestionIdx = 0 } = this.state;\n  const { value, onSuggest, type = \"text\" } = this.props;\n\n  const setSuggestions = async () => {\n    const { value, onSuggest } = this.props;\n    if (onSuggest) {\n      const suggestions = await onSuggest(value);\n      this.setState({ suggestions });\n    }\n  };\n\n  if (\n    [\"text\", \"search\"].includes(type) &&\n    document.activeElement?.nodeName === \"INPUT\" &&\n    this.refWrapper?.contains(document.activeElement)\n  ) {\n    if (e.key === \"Enter\") {\n      if (suggestions?.length && activeSuggestionIdx > -1) {\n        selectSuggestion(suggestions[activeSuggestionIdx]);\n      } else if (suggestions) {\n        this.setState({ suggestions: undefined });\n      }\n    } else if ([\"ArrowUp\", \"ArrowDown\"].includes(e.key)) {\n      if (!suggestions?.length) {\n        if ([\"ArrowDown\"].includes(e.key) && !value && onSuggest) {\n          setSuggestions();\n        } else {\n          const inpt = document.activeElement as HTMLInputElement;\n          if (typeof inpt.selectionStart !== \"number\") return;\n          this.cursorPosition = inpt.selectionStart;\n          const value = inpt.value;\n          const f = getStringFormat(value);\n          const selStart = inpt.selectionStart;\n          const selF = f.find(\n            (f) =>\n              f.type === \"n\" && f.idx <= selStart && f.idx + f.len >= selStart,\n          );\n\n          if (selF) {\n            this.inputSelStart = selStart;\n            let removeMinusAtIndex = -1;\n            const up = e.key === \"ArrowUp\";\n            let newValue = f\n              .map((f) => {\n                if (f.idx === selF.idx) {\n                  const step =\n                    e.altKey ? 0.1\n                    : e.ctrlKey ? 10\n                    : 1;\n                  const incrDecimals = e.altKey ? 1 : 0;\n                  const isNegative = value[selF.idx - 1] === \"-\";\n                  const increment =\n                    (up ?\n                      isNegative ? -1\n                      : 1\n                    : isNegative ? 1\n                    : -1) * step;\n                  const val = +(selF.val as string)\n                    .split(\".\")\n                    .map((v, i, arr) => {\n                      const newVal = i < arr.length - 1 ? v : +v + increment;\n                      return newVal;\n                    })\n                    .join(\".\");\n\n                  let res = val\n                    .toFixed(selF.decimalPlaces || incrDecimals)\n                    .padStart(selF.len, \"0\");\n                  if (res.startsWith(\"-\") && isNegative) {\n                    res = res.slice(1);\n                  }\n                  if (isNegative && val === 0) {\n                    removeMinusAtIndex = selF.idx - 1;\n                  }\n                  if (\n                    selF.decimalPlaces &&\n                    res.endsWith(\n                      \".\" + new Array(selF.decimalPlaces).fill(\"0\").join(\"\"),\n                    )\n                  ) {\n                    res = res.slice(0, -selF.decimalPlaces - 1);\n                  }\n                  return res;\n                }\n                return f.val;\n              })\n              .join(\"\");\n            if (removeMinusAtIndex > -1) {\n              newValue =\n                newValue.substring(0, removeMinusAtIndex) +\n                newValue.substring(removeMinusAtIndex + 1);\n            }\n            inpt.value = newValue;\n            this.onChange(inpt);\n          } else {\n            this.inputSelStart = undefined;\n          }\n        }\n      } else {\n        let increment = -1;\n        if ([\"ArrowDown\"].includes(e.key)) {\n          increment = 1;\n        }\n\n        let newSugIdx =\n          Number.isFinite(activeSuggestionIdx) ?\n            Math.max(0, activeSuggestionIdx + increment)\n          : 0;\n        if (newSugIdx > suggestions.length - 1) newSugIdx = 0;\n        this.setState({ activeSuggestionIdx: newSugIdx });\n      }\n      e.preventDefault();\n      e.stopPropagation();\n    }\n  }\n}\n"
  },
  {
    "path": "client/src/components/Hotkey.css",
    "content": ".hotkey {\n  border-radius: 6px;\n  background-color: var(--gray-100);\n  border: 1px solid var(--gray-300);\n  padding: 4px 5px;\n  min-width: 1.5em;\n  text-align: center;\n}\n\n.dark-theme .hotkey {\n  background-color: var(--gray-700);\n}\n"
  },
  {
    "path": "client/src/components/Hotkey.tsx",
    "content": "import React from \"react\";\nimport \"./Hotkey.css\";\n\ntype P = {\n  keyStyle?: React.CSSProperties;\n  style?: React.CSSProperties;\n  label: string;\n  keys: string[];\n};\nexport const Hotkey = ({ label, keys, keyStyle, style }: P) => {\n  return (\n    <div className=\"flex-row-wrap ai-center gap-p5 noselect \" style={style}>\n      {label && <div className=\"mr-p25\">{label}:</div>}\n      <div className=\"flex-row ai-center gap-p25\">\n        {keys.map((key, i) => (\n          <React.Fragment key={i}>\n            {!!i && <span>+</span>}\n            <div className=\"hotkey shadow\" style={keyStyle}>\n              {key}\n            </div>\n          </React.Fragment>\n        ))}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Icon/Icon.tsx",
    "content": "import type { CSSProperties } from \"react\";\nimport * as React from \"react\";\n\nexport type IconProps = {\n  id?: string;\n  path: string;\n  ref?: React.RefObject<SVGSVGElement>;\n  className?: string;\n  title?: string | null;\n  description?: string | null;\n  /**\n   * @deprecated Use sizeName instead\n   */\n  size?: number;\n  sizePx?: number;\n  sizeName?: \"nano\" | \"micro\" | \"small\" | \"default\" | \"large\";\n  color?: string;\n  rotate?: number;\n  spin?: boolean | number;\n  style?: CSSProperties;\n} & React.AriaAttributes;\n\nlet idCounter = 0;\n\nexport const Icon = React.forwardRef<SVGSVGElement, IconProps>(\n  (\n    {\n      path,\n      id = ++idCounter,\n      title = null,\n      description = null,\n      size = 1,\n      color = \"currentColor\",\n      sizeName,\n      rotate = 0,\n      spin = false,\n      style: _style = {},\n      sizePx,\n      ...rest\n    },\n    ref,\n  ) => {\n    const style = { ..._style };\n\n    const pathStyle: any = {};\n    const transform: string[] = [];\n    if (sizePx) {\n      style.width = `${sizePx}px`;\n      style.height = style.width;\n    }\n    if (size) {\n      style.width = `${size * 1.5}rem`;\n      style.height = style.width;\n    }\n    if (sizeName) {\n      const sizePx = {\n        large: 24,\n        default: 22,\n        small: 18,\n        micro: 16,\n        nano: 14,\n      }[sizeName];\n      style.width = `${sizePx}px`;\n      style.height = style.width;\n      style.transform = `scale(1.1)`;\n    }\n    if (rotate !== 0) {\n      transform.push(`rotate(${rotate}deg)`);\n    }\n    if (color) {\n      pathStyle.fill = color;\n    }\n    const pathElement = <path d={path} style={pathStyle} />;\n    const transformElement = pathElement;\n    if (transform.length > 0) {\n      style.transform = transform.join(\" \");\n      style.transformOrigin = \"center\";\n    }\n    let spinElement = transformElement;\n    const spinSec = spin === true || typeof spin !== \"number\" ? 2 : spin;\n    let inverse = true;\n    if (spinSec < 0) {\n      inverse = !inverse;\n    }\n    if (spin) {\n      spinElement = (\n        <g\n          style={{\n            animation: `spin${inverse ? \"-inverse\" : \"\"} linear ${Math.abs(spinSec)}s infinite`,\n            transformOrigin: \"center\",\n          }}\n        >\n          {transformElement}\n          {!(rotate !== 0) && (\n            <rect width=\"24\" height=\"24\" fill=\"transparent\" />\n          )}\n        </g>\n      );\n    }\n    let ariaLabelledby;\n    const labelledById = `icon_labelledby_${id}`;\n    const describedById = `icon_describedby_${id}`;\n    let role;\n    if (title) {\n      ariaLabelledby =\n        description ? `${labelledById} ${describedById}` : labelledById;\n    } else {\n      role = \"presentation\";\n      if (description) {\n        throw new Error(\"title attribute required when description is set\");\n      }\n    }\n    return (\n      <svg\n        ref={ref}\n        viewBox=\"0 0 24 24\"\n        style={style}\n        role={role}\n        aria-labelledby={ariaLabelledby}\n        {...rest}\n      >\n        {title && <title id={labelledById}>{title}</title>}\n        {description && <desc id={describedById}>{description}</desc>}\n        {spin &&\n          (inverse ?\n            <style>\n              {\n                \"@keyframes spin-inverse { from { transform: rotate(0deg) } to { transform: rotate(-360deg) } }\"\n              }\n            </style>\n          : <style>\n              {\n                \"@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }\"\n              }\n            </style>)}\n        {spinElement}\n      </svg>\n    );\n  },\n);\n"
  },
  {
    "path": "client/src/components/IconPalette/IconPalette.tsx",
    "content": "import { mdiChevronDown, mdiClose } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { isDefined } from \"../../utils/utils\";\nimport Btn from \"../Btn\";\nimport type { BtnProps } from \"../Btn\";\nimport { FlexCol, FlexRow } from \"../Flex\";\nimport { FormFieldDebounced } from \"../FormField/FormFieldDebounced\";\nimport Popup from \"../Popup/Popup\";\nimport { ScrollFade } from \"../ScrollFade/ScrollFade\";\nimport { Pagination } from \"../Table/Pagination\";\nimport { SvgIcon } from \"../SvgIcon\";\n\ntype P = {\n  iconName: string | null | undefined;\n  onChange: (newIcon: string | undefined | null) => void;\n  label?: BtnProps[\"label\"];\n};\nexport const IconPalette = ({ iconName, onChange, label }: P) => {\n  const iconList = usePromise(async () => {\n    const iconsNames: string[] = await fetch(\"/icons/_meta.json\").then((r) =>\n      r.json(),\n    );\n    return iconsNames;\n  }, []);\n  const [searchTerm, setSearchTerm] = useState(\"\");\n  const iconSize = 55;\n  const iconStyle = {\n    width: `${iconSize}px`,\n    height: `${iconSize}px`,\n  };\n  const displayedItemsFull = useMemo(() => {\n    if (!iconList) return [];\n    return iconList\n      .map((name) => {\n        //** Camel case to spaced */\n        const label = name\n          .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n          .replace(/_/g, \" \");\n        const rank = label.toLowerCase().indexOf(searchTerm.toLowerCase());\n        if (rank === -1) return;\n        return {\n          name,\n          label,\n          rank,\n          node: (\n            <span>\n              {label.slice(0, rank)}\n              <strong>{label.slice(rank, rank + searchTerm.length)}</strong>\n              {label.slice(rank + searchTerm.length)}\n            </span>\n          ),\n        };\n      })\n      .filter(isDefined)\n      .sort((a, b) => a.rank - b.rank);\n  }, [iconList, searchTerm]);\n  const [open, setOpen] = useState(false);\n  const [page, setPage] = useState(0);\n  useEffect(() => {\n    setPage(0);\n  }, [searchTerm]);\n  const displayedItems = useMemo(\n    () => displayedItemsFull.slice(page * 50, (page + 1) * 50),\n    [page, displayedItemsFull],\n  );\n\n  return (\n    <>\n      <FlexRow className=\"gap-p25\">\n        <Btn\n          label={label}\n          variant=\"faded\"\n          children={!iconName ? \"Set icon...\" : undefined}\n          iconPath={!iconName ? mdiChevronDown : undefined}\n          iconPosition={!iconName ? \"right\" : undefined}\n          iconNode={!iconName ? undefined : <SvgIcon icon={iconName} />}\n          onClick={() => setOpen(true)}\n        />\n        {![undefined, null].includes(iconName as any) && (\n          <Btn\n            className=\"as-end\"\n            iconPath={mdiClose}\n            onClick={() => onChange(null)}\n          />\n        )}\n      </FlexRow>\n      {open && (\n        <Popup\n          footerButtons={[\n            {\n              label: \"Close\",\n              onClick: () => setOpen(false),\n            },\n          ]}\n          rootStyle={{\n            flex: 1,\n          }}\n          clickCatchStyle={{ opacity: 1 }}\n          rootChildClassname=\"f-1\"\n          contentClassName=\"p-0\"\n          positioning=\"center\"\n          persistInitialSize={true}\n          title=\"Chose icon\"\n          onClose={() => setOpen(false)}\n        >\n          <FlexCol\n            className=\"f-1 min-s-0 o-auto p-1 ai-center\"\n            style={{\n              maxWidth: \"min(99vw, 1200px)\",\n            }}\n          >\n            <FormFieldDebounced\n              label={\"Search icons\"}\n              value={searchTerm}\n              onChange={(newTerm) => {\n                setSearchTerm(newTerm);\n                setPage(0);\n              }}\n            />\n            <div\n              style={{\n                height: \"1px\",\n                width: \"100%\",\n                background: \"var(--text-2)\",\n              }}\n            ></div>\n            <ScrollFade className=\"text-1 min-s-0 o-auto flex-row-wrap gap-1 f-1\">\n              {displayedItems.map(({ name, node }) => {\n                return (\n                  <FlexCol\n                    style={{\n                      maxWidth: `${iconSize + 45}px`,\n                    }}\n                    key={name}\n                    className=\"pointer ai-center h-fit\"\n                    onClick={() => {\n                      onChange(name);\n                      setOpen(false);\n                    }}\n                  >\n                    <SvgIcon icon={name} size={iconSize} />\n                    <div className=\" text-ellipsis\">{node}</div>\n                  </FlexCol>\n                );\n              })}\n            </ScrollFade>\n            <Pagination\n              className=\"mt-p25\"\n              totalRows={displayedItemsFull.length}\n              pageSize={50}\n              page={page}\n              onPageSizeChange={() => {\n                console.error(\"onPageSizeChange disabled for performance\");\n              }}\n              onPageChange={(newPage) => {\n                setPage(newPage);\n              }}\n            />\n          </FlexCol>\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/components/InfoRow.tsx",
    "content": "import { mdiInformationOutline } from \"@mdi/js\";\nimport React from \"react\";\nimport { Icon } from \"./Icon/Icon\";\nimport { classOverride } from \"./Flex\";\n\ntype InfoRowProps = {\n  variant?: \"filled\" | \"naked\";\n  color?: \"warning\" | \"danger\" | \"info\" | \"action\";\n  iconPath?: string;\n  iconSize?: number;\n  style?: React.CSSProperties;\n  className?: string;\n  contentClassname?: string;\n  children?: React.ReactNode;\n};\nexport function InfoRow(props: InfoRowProps) {\n  const {\n    className,\n    iconPath = mdiInformationOutline,\n    style = {},\n    variant,\n    children,\n    color = \"warning\",\n    contentClassname = \"\",\n    iconSize = 1.25,\n  } = props;\n\n  let rootClass = \"\";\n  const bgColor = color === \"info\" ? \"default\" : color;\n  if (variant === \"filled\") {\n    rootClass = `b b-${bgColor} text-${bgColor} bg-${bgColor} px-1 py-p75`;\n  } else if (variant === \"naked\") {\n    rootClass = `text-${bgColor} `;\n  } else {\n    rootClass = `b b-${bgColor} text-${bgColor} px-1 py-p75`;\n  }\n\n  return (\n    <div\n      className={classOverride(\n        ` rounded flex-row ai-start ta-left ${rootClass} `,\n        className,\n      )}\n      style={style}\n    >\n      {iconPath && iconPath.length > 0 && (\n        <Icon\n          path={iconPath}\n          size={iconSize}\n          className=\"f-0\"\n          style={{ marginRight: \"10px\" }}\n        />\n      )}\n      <div\n        className={classOverride(\"min-s-0 f-1 as-center\", contentClassname)}\n        style={{ whiteSpace: \"pre-line\" }}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Input.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\n\ntype P = React.HTMLProps<HTMLInputElement> & {\n  onNumLockAlert?: (show: boolean) => void;\n};\n\nexport const Input = React.forwardRef<HTMLInputElement, P>((props, ref) => {\n  const { className = \"\", style = {}, onNumLockAlert, ...otherProps } = props;\n\n  const [showNumLock, setShowNumLock] = useState(false);\n  useEffect(() => {\n    onNumLockAlert?.(showNumLock);\n  }, [showNumLock, onNumLockAlert]);\n\n  return (\n    <input\n      {...otherProps}\n      style={style}\n      className={\"custom-input rounded \" + className}\n      ref={(e) => {\n        if (ref) {\n          typeof ref === \"function\" ? ref(e) : (ref.current = e);\n        }\n        if (e) {\n          setTimeout(() => {\n            if (props.autoFocus && document.activeElement !== e) {\n              e.focus();\n              e.select();\n            }\n          }, 40);\n\n          // @ts-ignore\n          e.forceDemoValue = (val: any) => {\n            // @ts-ignore\n            props.onChange?.({ currentTarget: { value: val } });\n          };\n        }\n      }}\n      onKeyDown={\n        props.type === \"number\" ?\n          (e) => {\n            const numLockAlert =\n              !e.getModifierState(\"NumLock\") &&\n              e.nativeEvent.code.startsWith(\"Numpad\") &&\n              e.key !== e.nativeEvent.code.slice(0, \"Numpad\".length); //(NUMLOCK_KEYS.includes(e.key) || !e.currentTarget.value && [\"ArrowRight\", \"ArrowLeft\"].includes(e.key) )\n            if (numLockAlert !== showNumLock) {\n              setShowNumLock(numLockAlert);\n            }\n            props.onKeyDown?.(e);\n            const node = e.target as HTMLInputElement;\n            if (!/^-?\\d+$/.test(props.value as any)) {\n              node.placeholder = \"Numbers only\";\n              setTimeout(() => {\n                if (!node.isConnected) return;\n                node.placeholder = otherProps.placeholder ?? \"\";\n              }, 1000);\n            }\n          }\n        : otherProps.onKeyDown\n      }\n    />\n  );\n});\n"
  },
  {
    "path": "client/src/components/JSONBSchema/JSONBSchema.tsx",
    "content": "import type { JSONB } from \"prostgles-types\";\nimport { isEqual, isObject } from \"prostgles-types\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport { isCompleteJSONB } from \"./isCompleteJSONB\";\nimport {\n  JSONBSchemaAllowedOptions,\n  JSONBSchemaAllowedOptionsMatch,\n} from \"./JSONBSchemaAllowedOptions\";\nimport { JSONBSchemaArray, JSONBSchemaArrayMatch } from \"./JSONBSchemaArray\";\nimport { JSONBSchemaLookup, JSONBSchemaLookupMatch } from \"./JSONBSchemaLookup\";\nimport { JSONBSchemaObject, JSONBSchemaObjectMatch } from \"./JSONBSchemaObject\";\nimport {\n  JSONBSchemaOneOfType,\n  JSONBSchemaOneOfTypeMatch,\n} from \"./JSONBSchemaOneOfType\";\nimport {\n  JSONBSchemaPrimitive,\n  JSONBSchemaPrimitiveMatch,\n} from \"./JSONBSchemaPrimitive\";\nimport { JSONBSchemaRecord, JSONBSchemaRecordMatch } from \"./JSONBSchemaRecord\";\n\ntype Schema = JSONB.JSONBSchema & { optional?: boolean };\nexport type JSONBSchemaCommonProps = Pick<Prgl, \"db\" | \"tables\"> & {\n  className?: string;\n  style?: React.CSSProperties;\n  value: unknown;\n  setHasErrors?: (hasErrors: boolean) => void;\n  showErrors?: boolean;\n  nestingPath?: (string | number)[];\n  allowIncomplete?: boolean;\n  noLabels?: boolean;\n  schemaStyles?: {\n    path: string[];\n    style?: React.CSSProperties;\n    className?: string;\n  }[];\n};\n\ntype P<S extends Schema> = JSONBSchemaCommonProps & {\n  schema: S;\n  onChange: (newValue: JSONB.GetType<S>) => void;\n};\n\nexport const JSONBSchema = <S extends Schema>(props: P<S>) => {\n  const {\n    style,\n    className = \"\",\n    value,\n    schema,\n    onChange,\n    setHasErrors,\n    ...otherProps\n  } = props;\n  const { allowIncomplete, nestingPath: isNested } = otherProps;\n  const [_localValueRaw, setlocalValue] = useState<any>();\n  const localValueRaw = _localValueRaw ?? value;\n  const localValue = isNested ? value : localValueRaw;\n  const setLocalValue = useCallback(\n    (newlocalValue) => {\n      if (isNested) {\n        onChange(newlocalValue);\n      } else {\n        setlocalValue(newlocalValue);\n      }\n    },\n    [onChange, isNested, setlocalValue],\n  );\n\n  const hasError = !isCompleteJSONB(value, schema);\n  useEffect(() => {\n    setHasErrors?.(hasError);\n  }, [hasError, setHasErrors]);\n\n  useEffect(() => {\n    if (isNested) return;\n\n    /** Fire onchange if data is complete */\n    const shouldFireOnChange =\n      allowIncomplete || isCompleteJSONB(localValue, schema);\n    const valueHasChanged = !isEqual(localValue, value);\n    // console.log({ shouldFireOnChange, valueHasChanged, localValue, value });\n    if (shouldFireOnChange && valueHasChanged) {\n      onChange(localValue);\n      setlocalValue(undefined);\n    }\n  }, [localValue, value, allowIncomplete, isNested, onChange, schema]);\n\n  if (JSONBSchemaLookupMatch(schema)) {\n    return (\n      <JSONBSchemaLookup\n        value={localValue}\n        schema={schema}\n        onChange={setLocalValue}\n        {...otherProps}\n      />\n    );\n  }\n\n  let node: React.ReactNode = null;\n  if (JSONBSchemaAllowedOptionsMatch(schema)) {\n    node = (\n      <JSONBSchemaAllowedOptions\n        value={localValue}\n        schema={schema}\n        onChange={setLocalValue}\n        {...otherProps}\n      />\n    );\n  } else if (JSONBSchemaPrimitiveMatch(schema)) {\n    node = (\n      <JSONBSchemaPrimitive\n        value={localValue}\n        schema={schema}\n        onChange={setLocalValue}\n        {...otherProps}\n      />\n    );\n  } else if (JSONBSchemaOneOfTypeMatch(schema)) {\n    node = (\n      //@ts-ignore\n      <JSONBSchemaOneOfType\n        value={localValue}\n        schema={schema}\n        onChange={setLocalValue}\n        {...otherProps}\n      />\n    );\n  } else if (JSONBSchemaObjectMatch(schema)) {\n    node = (\n      <JSONBSchemaObject\n        value={localValue}\n        schema={schema}\n        onChange={setLocalValue}\n        {...otherProps}\n      />\n    );\n  } else if (JSONBSchemaRecordMatch(schema)) {\n    node = (\n      <JSONBSchemaRecord\n        value={localValue}\n        schema={schema}\n        onChange={setLocalValue}\n        {...otherProps}\n      />\n    );\n  } else if (JSONBSchemaArrayMatch(schema)) {\n    node = (\n      <JSONBSchemaArray\n        value={localValue}\n        schema={schema}\n        onChange={setLocalValue}\n        {...otherProps}\n      />\n    );\n  }\n\n  if (node) {\n    const isDisabled =\n      isObject(schema) && schema.optional && localValue === undefined;\n    const styleDisabled =\n      !isDisabled || otherProps.nestingPath ? {} : { opacity: 0.5 };\n    return (\n      <div\n        style={{ ...style, ...styleDisabled }}\n        className={`JSONBSchema h-fit flex-row ${className}`}\n      >\n        {node}\n      </div>\n    );\n  }\n\n  return (\n    <>\n      Schema not suitable for JSONBSchema.tsx: {JSON.stringify(schema, null, 2)}\n    </>\n  );\n};\n\n// @ts-ignore\nexport const JSONBSchemaA = JSONBSchema as (\n  props: JSONBSchemaCommonProps & {\n    schema: any;\n    onChange: (newValue: any) => void;\n  },\n) => React.JSX.Element;\n"
  },
  {
    "path": "client/src/components/JSONBSchema/JSONBSchemaAllowedOptions.tsx",
    "content": "import type { JSONB } from \"prostgles-types\";\nimport { isObject } from \"prostgles-types\";\nimport React from \"react\";\nimport FormField from \"../FormField/FormField\";\nimport type { FullOption } from \"../Select/Select\";\nimport { isCompleteJSONB } from \"./isCompleteJSONB\";\nimport type { JSONBSchemaCommonProps } from \"./JSONBSchema\";\n\ntype Schema = JSONB.BasicType;\ntype P = JSONBSchemaCommonProps & {\n  schema: Schema;\n  onChange: (newValue: JSONB.GetType<Schema>) => void;\n};\n\nexport const JSONBSchemaAllowedOptionsMatch = (\n  s: JSONB.JSONBSchema,\n): s is JSONB.BasicType => !!getFullOptions(s);\n\nexport const JSONBSchemaAllowedOptions = ({\n  value,\n  schema,\n  onChange,\n  showErrors,\n  noLabels,\n}: P) => {\n  const o = getFullOptions(schema);\n  if (!o) {\n    return (\n      <>\n        Could not render JSONBSchemaAllowedOptions schema:{\" \"}\n        {JSON.stringify(schema)}\n      </>\n    );\n  }\n\n  const error =\n    showErrors && !isCompleteJSONB(value, schema) ? \"Required\" : undefined;\n\n  return (\n    <FormField\n      name={schema.title}\n      label={\n        noLabels ? undefined : (\n          { children: schema.title, info: schema.description }\n        )\n      }\n      className={\"JSONBSchemaAllowedOptions\"}\n      value={value}\n      optional={schema.optional}\n      nullable={schema.nullable}\n      fullOptions={o.fullOptions}\n      multiSelect={o.isMulti}\n      onChange={(newVal) => {\n        onChange(newVal);\n      }}\n      error={error}\n    />\n  );\n};\n\n/**\n * To construct a fullOptions select use this schema:\n * {\n *    oneOf: [\n *      { enum: [\"key\"], description: \"subLabel\" }\n *    ]\n * }\n */\nconst getFullOptions = (\n  s: JSONB.JSONBSchema,\n): { fullOptions: FullOption[]; isMulti: boolean } | undefined => {\n  if (s.allowedValues) {\n    const firstValue = s.allowedValues[0];\n    const fullOptions =\n      isObject(firstValue) ?\n        (\n          s.allowedValues as { label: string; value: any; subLabel?: string }[]\n        ).map(({ value, label, subLabel }) => ({\n          key: value,\n          label,\n          subLabel,\n        }))\n      : s.allowedValues.map((key) => ({ key }));\n    return {\n      fullOptions,\n      isMulti: typeof s.type === \"string\" && s.type.endsWith(\"[]\"),\n    };\n  } else if (s.oneOf?.every((ss) => isObject(ss) && ss.enum)) {\n    const fullOptions = s.oneOf.flatMap((_ss) => {\n      const ss = _ss as JSONB.EnumType;\n      return ss.enum.map((key) => {\n        return {\n          key,\n          subLabel: ss.description,\n        };\n      });\n    });\n\n    return {\n      fullOptions,\n      isMulti: false,\n    };\n  }\n\n  return undefined;\n};\n"
  },
  {
    "path": "client/src/components/JSONBSchema/JSONBSchemaArray.tsx",
    "content": "import { mdiClose, mdiPlus } from \"@mdi/js\";\nimport type { JSONB } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport Btn from \"../Btn\";\nimport { DraggableLI } from \"../DraggableLI\";\nimport Popup from \"../Popup/Popup\";\nimport { Section } from \"../Section\";\nimport type { JSONBSchemaCommonProps } from \"./JSONBSchema\";\nimport { JSONBSchema } from \"./JSONBSchema\";\nimport { classOverride } from \"../Flex\";\n\ntype Schema = JSONB.ArrayOf;\ntype P = JSONBSchemaCommonProps & {\n  schema: Schema;\n  onChange: (newValue: JSONB.GetType<Schema>) => void;\n};\n\nexport const JSONBSchemaArrayMatch = (s: JSONB.JSONBSchema): s is Schema =>\n  !!(s.arrayOf || s.arrayOfType);\n\nexport const JSONBSchemaArray = ({\n  value,\n  schema,\n  onChange,\n  nestingPath,\n  ...oProps\n}: P) => {\n  const [newItem, setNewItem] = useState<{\n    val: any;\n    isComplete: boolean;\n    anchorEl: HTMLElement;\n  }>();\n  const itemSchema =\n    typeof schema.arrayOf === \"string\" ?\n      { type: schema.arrayOf }\n    : (schema.arrayOf ?? { type: schema.arrayOfType });\n\n  const addNewItem = (nItem = newItem) => {\n    if (!nItem) return;\n\n    onChange([...(Array.isArray(value) ? value : []), nItem.val] as any);\n    setNewItem(undefined);\n  };\n\n  const [orderAge, setOrderAge] = useState(0);\n\n  return (\n    <Section\n      className=\"JSONBSchemaArray flex-col gap-p5 f-1\"\n      contentClassName=\"flex-col gap-p5 max-h-500 pl-1 o-auto\"\n      title={schema.title ?? \"Items\"}\n      titleRightContent={\n        <Btn\n          title=\"Add new item\"\n          iconPath={mdiPlus}\n          color=\"action\"\n          variant=\"faded\"\n          size=\"small\"\n          onClick={({ currentTarget }) => {\n            setNewItem({\n              isComplete: false,\n              val: null,\n              anchorEl: currentTarget,\n            });\n          }}\n        />\n      }\n      open={true}\n    >\n      {/* <Label info={schema.description}>{schema.title}</Label> */}\n      <div className=\"JSONBSchemaArray_ItemsList flex-col gap-1\">\n        {Array.isArray(value) &&\n          value.map((item, itemIdx) => {\n            const itemNestingPath = [...(nestingPath ?? []), itemIdx];\n            const itemStyle = oProps.schemaStyles?.find(\n              (ss) => ss.path.join() === itemNestingPath.join(),\n            );\n            return (\n              <DraggableLI\n                key={itemIdx + orderAge}\n                className={classOverride(\n                  \"no-decor flex-row gap-1 trigger-hover hover-bg \" +\n                    (itemIdx ? \"bt b-color \" : \"\"),\n                  itemStyle?.className,\n                )}\n                style={{\n                  padding: \".5em\",\n                  ...itemStyle?.style,\n                }}\n                items={value}\n                idx={itemIdx}\n                onReorder={(newValue) => {\n                  onChange(newValue as any);\n                  setOrderAge(Date.now());\n                }}\n              >\n                <div className=\"flex-row-wrap f-1 \">\n                  <JSONBSchema\n                    schema={{ ...itemSchema } as any}\n                    value={item}\n                    onChange={\n                      ((newValue) => {\n                        onChange(\n                          newValue === undefined ?\n                            (value as any).filter((_, i) => i !== itemIdx)\n                          : (value.map((v, i) =>\n                              i === itemIdx ? newValue : v,\n                            ) as any),\n                        );\n                      }) as any\n                    }\n                    nestingPath={itemNestingPath}\n                    {...oProps}\n                  />\n                </div>\n                <Btn\n                  color=\"danger\"\n                  variant=\"faded\"\n                  className=\"show-on-trigger-hover ml-auto as-start\"\n                  style={{\n                    marginTop: \"24px\",\n                  }}\n                  title=\"Remove element\"\n                  iconPath={mdiClose}\n                  onClick={() => {\n                    onChange((value as any).filter((_, i) => i !== itemIdx));\n                  }}\n                />\n              </DraggableLI>\n            );\n          })}\n      </div>\n      {newItem && (\n        <Popup\n          title={\"Add new item\"}\n          anchorEl={newItem.anchorEl}\n          positioning=\"beneath-left\"\n          clickCatchStyle={{ opacity: 1 }}\n          onClose={() => setNewItem(undefined)}\n          footerButtons={[\n            {\n              label: \"Cancel\",\n              onClickClose: true,\n            },\n            {\n              label: \"Add item\",\n              color: \"action\",\n              variant: \"filled\",\n              disabledInfo:\n                !newItem.isComplete ?\n                  \"Must fill all required data first\"\n                : undefined,\n              onClick: () => addNewItem(),\n            },\n          ]}\n        >\n          <JSONBSchema\n            schema={itemSchema as any}\n            value={newItem.val}\n            onChange={\n              ((newValue) => {\n                setNewItem({ ...newItem, val: newValue, isComplete: true });\n                addNewItem({ ...newItem, val: newValue, isComplete: true });\n              }) as any\n            }\n            {...oProps}\n          />\n        </Popup>\n      )}\n    </Section>\n  );\n};\n"
  },
  {
    "path": "client/src/components/JSONBSchema/JSONBSchemaLookup.tsx",
    "content": "import type { JSONB } from \"prostgles-types\";\nimport { getKeys, isEmpty, isObject, pickKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport { SmartSearch } from \"../../dashboard/SmartFilter/SmartSearch/SmartSearch\";\nimport { areEqual } from \"../../utils/utils\";\nimport ErrorComponent from \"../ErrorComponent\";\nimport { Select } from \"../Select/Select\";\nimport { isCompleteJSONB } from \"./isCompleteJSONB\";\nimport type { JSONBSchemaCommonProps } from \"./JSONBSchema\";\nimport { JSONBSchema } from \"./JSONBSchema\";\nimport { getFinalFilter } from \"@common/filterUtils\";\n\ntype Schema = JSONB.Lookup;\ntype P = JSONBSchemaCommonProps & {\n  schema: Schema;\n  onChange: (newValue: JSONB.GetType<Schema>) => void;\n};\n\nexport const JSONBSchemaLookupMatch = (s: JSONB.JSONBSchema): s is Schema =>\n  isObject(s.lookup);\n\nexport const JSONBSchemaLookup = ({\n  value: rawValue,\n  schema,\n  onChange,\n  db,\n  tables,\n  ...oProps\n}: P) => {\n  let defaultValue: string | undefined = undefined;\n  const { lookup } = schema;\n\n  if (lookup.type === \"schema\" || lookup.type === \"data-def\") {\n    const { filter } = lookup;\n    const tableFilter = filter?.table;\n    const colFilter =\n      (\n        isObject(filter) &&\n        !isEmpty(filter) &&\n        (filter.udt_name || filter.tsDataType)\n      ) ?\n        pickKeys(filter, [\"tsDataType\", \"udt_name\"], true)\n      : undefined;\n\n    if (!tables as any) {\n      return <ErrorComponent error={\"Lookup tables missing\"} />;\n    }\n    const matchingTables = tables.filter(\n      (t) => !tableFilter || t.name === tableFilter,\n    );\n    const delimiter = `||_prgl$_||?!#$@#@$@$#\"4$`;\n    const needsCol =\n      lookup.type === \"data-def\" ||\n      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n      (lookup.type === \"schema\" && lookup.object === \"column\");\n    const fullOptions =\n      needsCol ?\n        matchingTables.flatMap((t) => {\n          return t.columns\n            .filter(\n              (c) => !colFilter || areEqual(colFilter, c, getKeys(colFilter)),\n            )\n            .map((c) => ({\n              key: [t.name, c.name].join(delimiter),\n              label: [t.name, c.name].join(\".\"),\n              subLabel: c.udt_name,\n            }));\n        })\n      : matchingTables.map((t) => ({\n          key: t.name,\n          subLabel: t.columns.map((c) => c.name).join(\", \"),\n        }));\n\n    const setLookupMerged = (l: Partial<typeof lookup>) => {\n      const newLookup = { ...(isObject(rawValue) ? rawValue : {}), ...l };\n\n      onChange(newLookup);\n    };\n\n    const selectedValue =\n      isObject(rawValue) ?\n        [rawValue.table, rawValue.column].join(delimiter)\n      : rawValue;\n\n    // TODO: this must be fixed through LookupTable, LookupTable[], LookupTableColumn etc\n    if (schema.type === \"Lookup[]\" && !lookup.isArray) {\n      return (\n        <ErrorComponent\n          error={\"Schema type is Lookup[] but lookup.isArray is not true\"}\n        />\n      );\n    }\n    const multiSelect = lookup.isArray;\n    const selector = (\n      <Select\n        label={oProps.noLabels ? undefined : schema.title}\n        value={selectedValue}\n        optional={schema.optional}\n        fullOptions={fullOptions}\n        multiSelect={multiSelect}\n        onChange={(opts) => {\n          if (needsCol) {\n            const [table, column] =\n              typeof opts === \"string\" ? opts.split(delimiter) : [];\n            setLookupMerged({ table, column });\n          } else {\n            onChange(opts);\n          }\n        }}\n      />\n    );\n\n    if (lookup.type === \"data-def\") {\n      const value = isObject(rawValue) ? rawValue : undefined;\n      const tableCols =\n        value?.table ?\n          tables.find((t) => t.name === value.table)?.columns.map((c) => c.name)\n        : undefined;\n\n      return (\n        <div className=\"flex-row gap-1\">\n          {selector}\n          {tableCols && (\n            <>\n              <JSONBSchema\n                value={value}\n                schema={\n                  {\n                    title: \"Lookup options...\",\n                    type: {\n                      isFullRow: {\n                        title: \"Full row\",\n                        optional: true,\n                        description:\n                          \"If true then the full row object will be passed to the function\",\n                        type: {\n                          displayColumns: {\n                            type: \"string[]\",\n                            optional: true,\n                            allowedValues: tableCols,\n                          },\n                        },\n                      },\n                      searchColumns: {\n                        title: \"Search columns\",\n                        type: \"string[]\",\n                        description:\n                          \"Columns used for searching the value. By default all columns will be used\",\n                        optional: true,\n                        allowedValues: tableCols,\n                      },\n                      showInRowCard: {\n                        title: \"Show in row card\",\n                        optional: true,\n                        description:\n                          \" If true then a button will be shown in the row edit card to display this action\",\n                        type: {\n                          actionLabel: {\n                            type: \"string\",\n                            optional: true,\n                            title: \"Button label\",\n                          },\n                        },\n                      },\n                    },\n                  } as any\n                }\n                onChange={\n                  ((newLookupOpts) => {\n                    setLookupMerged({\n                      ...newLookupOpts,\n                      type: \"data\",\n                    });\n                  }) as any\n                }\n                db={db}\n                tables={tables}\n                {...oProps}\n              />\n            </>\n          )}\n        </div>\n      );\n    }\n\n    return selector;\n  }\n\n  if (lookup.isFullRow) {\n    if (!isObject(rawValue)) {\n    } else if (lookup.isFullRow.displayColumns?.length) {\n      defaultValue = Object.values(\n        pickKeys(rawValue, lookup.isFullRow.displayColumns),\n      ).join(\", \");\n    } else if (lookup.column) {\n      defaultValue = rawValue[lookup.column];\n    } else {\n      defaultValue = Object.values(rawValue)[0];\n    }\n  } else if (typeof rawValue === \"string\" || typeof rawValue === \"number\") {\n    defaultValue = rawValue.toString();\n  }\n\n  const error =\n    oProps.showErrors && !isCompleteJSONB(defaultValue || undefined, schema) ?\n      \"Required\"\n    : undefined;\n\n  return (\n    <SmartSearch\n      label={oProps.noLabels ? undefined : schema.title}\n      // error={paramError?.[argName]}\n      // disabledInfo={} // Must disallow changing the fixedRowArgument\n      variant=\"search-no-shadow\"\n      defaultValue={defaultValue ?? \"\"}\n      inputStyle={{\n        minHeight: \"42px\",\n      }}\n      db={db}\n      columns={lookup.searchColumns}\n      tableName={lookup.table}\n      tables={tables}\n      error={error}\n      searchOptions={{ includeColumnNames: false, hideMatchCase: true }}\n      onChange={async (searchArgs) => {\n        if (!searchArgs && searchArgs !== defaultValue) {\n          onChange(searchArgs);\n          return;\n        }\n\n        const refCol = lookup.column;\n        const { colName, columnValue, filter } = searchArgs ?? {};\n        const filterItem = filter?.[0];\n        if (!colName || !columnValue || !refCol || !filterItem) return;\n\n        const finalFilter = getFinalFilter(filterItem);\n        const firstMatchingRow = await db[lookup.table]?.findOne!(finalFilter);\n        if (firstMatchingRow) {\n          onChange(\n            lookup.isFullRow ? firstMatchingRow : firstMatchingRow[refCol],\n          );\n          // if(isDisabled){\n          //   otherProps.setState({ disabledArgs: disabledArgs.filter(d => d !== argName) })\n          // }\n        }\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/components/JSONBSchema/JSONBSchemaObject.tsx",
    "content": "import { mdiClose, mdiDotsHorizontal } from \"@mdi/js\";\nimport type { JSONB } from \"prostgles-types\";\nimport { getKeys, isObject, omitKeys } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport Btn from \"../Btn\";\nimport { Label } from \"../Label\";\nimport type { JSONBSchemaCommonProps } from \"./JSONBSchema\";\nimport { JSONBSchema } from \"./JSONBSchema\";\nimport { getSchemaFromField } from \"./isCompleteJSONB\";\nimport { classOverride } from \"../Flex\";\n\ntype Schema = JSONB.ObjectType;\ntype P = JSONBSchemaCommonProps & {\n  schema: Schema;\n  onChange: (newValue: JSONB.GetType<Schema>) => void;\n  showOptionalUndefinedProps?: boolean;\n};\n\nexport const JSONBSchemaObjectMatch = (\n  s: JSONB.JSONBSchema,\n): s is JSONB.ObjectType => isObject(s.type);\n\nexport const JSONBSchemaObject = ({\n  value: rawValue,\n  schema,\n  onChange,\n  nestingPath,\n  className,\n  style,\n  ...oProps\n}: P) => {\n  const value = isObject(rawValue) ? rawValue : undefined;\n  const optionalProps = Object.entries(schema.type)\n    .filter(([k, v]) => isObject(v) && v.optional && value?.[k] === undefined)\n    .map(([k]) => k);\n  const [showOptional, setShowOptional] = useState(false);\n\n  if (schema.optional && value === undefined && !showOptional && nestingPath) {\n    return (\n      <div\n        className={\n          \"JSONBSchemaObject flex-col gap-p5 as-end \" + (className ?? \"\")\n        }\n        style={style}\n      >\n        {!oProps.noLabels && <Label variant=\"normal\">{schema.title}</Label>}\n        <Btn\n          iconPath={mdiDotsHorizontal}\n          title={schema.title}\n          variant=\"faded\"\n          onClick={() => {\n            setShowOptional(true);\n            onChange({} as any);\n          }}\n        />\n      </div>\n    );\n  }\n\n  const objectSchema = schema.type;\n  const requiredPropNames = getKeys(objectSchema).filter(\n    (k) => !getSchemaFromField(objectSchema[k]!).optional,\n  );\n  const optionalPropNames = getKeys(objectSchema).filter(\n    (k) => getSchemaFromField(objectSchema[k]!).optional,\n  );\n\n  return (\n    <div\n      className={classOverride(\n        \"JSONBSchemaObject flex-row-wrap gap-1 \",\n        className,\n      )}\n      style={style}\n    >\n      {[...requiredPropNames, ...optionalPropNames]\n        .filter((k) => showOptional || !optionalProps.includes(k))\n        .map((propName) => {\n          const ps = schema.type[propName]!;\n          const propSchema = {\n            title: propName,\n            ...(typeof ps === \"string\" ? { type: ps } : ps),\n          };\n\n          const itemNestingPath = [...(nestingPath ?? []), propName];\n\n          const itemStyle = oProps.schemaStyles?.find(\n            (ss) => ss.path.join() === itemNestingPath.join(),\n          );\n          return (\n            <JSONBSchema\n              key={propName}\n              value={(value as any)?.[propName]}\n              schema={propSchema as any}\n              nestingPath={itemNestingPath}\n              style={itemStyle?.style}\n              className={itemStyle?.className}\n              onChange={\n                ((newVal) => {\n                  onChange(\n                    (newVal === undefined ?\n                      omitKeys(value ?? {}, [propName])\n                    : { ...value, [propName]: newVal }) as any,\n                  );\n                }) as any\n              }\n              {...oProps}\n            />\n          );\n        })}\n      {!showOptional && !!optionalProps.length && (\n        <Btn\n          title=\"Show more\"\n          iconPath={mdiDotsHorizontal}\n          onClick={() => setShowOptional(true)}\n          className=\"as-end fit\"\n        />\n      )}\n      {value !== undefined && schema.optional && (\n        <Btn\n          title=\"Remove\"\n          iconPath={mdiClose}\n          onClick={() => onChange(undefined as any)}\n          className=\"as-end fit\"\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/JSONBSchema/JSONBSchemaOneOfType.tsx",
    "content": "import type { JSONB } from \"prostgles-types\";\nimport { getKeys, isObject, omitKeys, pickKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport type { JSONBSchemaCommonProps } from \"./JSONBSchema\";\nimport { JSONBSchemaObject } from \"./JSONBSchemaObject\";\nimport { classOverride } from \"../Flex\";\n\ntype Schema = Extract<\n  { oneOfType: Required<JSONB.OneOf>[\"oneOfType\"] },\n  JSONB.OneOf\n>;\ntype P = JSONBSchemaCommonProps & {\n  schema: Schema;\n  onChange: (newValue: JSONB.GetType<Schema>) => void;\n};\n\n/**\n * Can render oneOfType only if there are common properties across objects\n */\nexport const JSONBSchemaOneOfTypeMatch = (s: JSONB.JSONBSchema): s is Schema =>\n  Array.isArray(s.oneOfType);\n\nexport const JSONBSchemaOneOfType = ({\n  value,\n  schema,\n  onChange,\n  nestingPath,\n  style,\n  className,\n  ...oProps\n}: P) => {\n  const s = schema;\n\n  const firstSchema = s.oneOfType[0];\n  const firstSchemaKeys = getKeys(firstSchema ?? {});\n  const commonRequiredPropertyNames = firstSchemaKeys.filter((propName) => {\n    const fPropS = getFieldObj(firstSchema![propName]!);\n    return (\n      !fPropS.optional &&\n      s.oneOfType.every((subSchema) => {\n        const objType = subSchema[propName];\n        const propS = typeof objType === \"string\" ? { type: objType } : objType;\n        return (\n          propS && (propS.type === fPropS.type || (propS.enum && fPropS.enum))\n        );\n      })\n    );\n  });\n\n  /**\n   * oneOfType: [\n   *  { toggle: { enum: [\"opt1\"] } },\n   *  ...otherProps\n   */\n  const toggleProps = commonRequiredPropertyNames.filter((key) => {\n    const s = firstSchema?.[key];\n    return isObject(s) && s.enum?.length;\n  });\n\n  if (!firstSchema) {\n    return <>Cannot render: schema.oneOfType is empty</>;\n  }\n  if (!toggleProps.length) {\n    return <>Cannot render: schema.oneOfType has no common properties</>;\n  }\n\n  const getOneOfSchemaIndex = (value: any): number => {\n    return s.oneOfType.findIndex(\n      (ss) =>\n        isObject(value) &&\n        toggleProps.every((propName) =>\n          (ss[propName] as JSONB.EnumType).enum.includes(value[propName]),\n        ),\n    );\n  };\n\n  const matchingOneOfSchemaIdx = getOneOfSchemaIndex(value);\n\n  const matchingOneOfSchema = structuredClone(\n    s.oneOfType[matchingOneOfSchemaIdx] ||\n      pickKeys(firstSchema, commonRequiredPropertyNames),\n  );\n\n  toggleProps.forEach((tKey) => {\n    /**\n     * Create full options schema\n     */\n    matchingOneOfSchema[tKey] = {\n      ...omitKeys(matchingOneOfSchema[tKey]! as any, [\"enum\"]),\n      oneOf: s.oneOfType.flatMap((ss) => {\n        return {\n          title: tKey,\n          ...(ss[tKey] as JSONB.EnumType),\n        };\n      }),\n    };\n  });\n\n  const itemNestingPath = [...(nestingPath ?? []), matchingOneOfSchemaIdx];\n  const itemStyle = oProps.schemaStyles?.find(\n    (ss) => ss.path.join() === itemNestingPath.join(),\n  );\n  return (\n    <div className=\"JSONBSchemaOneOf flex-row-wrap gap-1\">\n      <JSONBSchemaObject\n        schema={{\n          ...omitKeys(s, [\"oneOfType\"]),\n          type: matchingOneOfSchema,\n        }}\n        value={value as any}\n        onChange={(newValue) => {\n          /**\n           * If matching a different schema then keep only common properties\n           */\n          const newSchemaIdx = getOneOfSchemaIndex(newValue);\n          //@ts-ignore\n          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n          if (newValue && newSchemaIdx !== matchingOneOfSchemaIdx) {\n            const currentSchema = matchingOneOfSchema;\n            const newSchema = s.oneOfType[newSchemaIdx]!;\n            /** Must remove any properties that do not exist in the new schema */\n            const newValueWithCommonProps = pickKeys(\n              newValue,\n              getKeys(newSchema).filter((key) => {\n                if (toggleProps.includes(key)) return true;\n                const oldPropSchema =\n                  currentSchema[key] && getFieldObj(currentSchema[key]);\n                const newPropSchema =\n                  newSchema[key] && getFieldObj(newSchema[key]);\n                return (\n                  oldPropSchema &&\n                  newPropSchema &&\n                  typeof oldPropSchema.type === \"string\" &&\n                  oldPropSchema.type === newPropSchema.type\n                );\n              }),\n            );\n            //@ts-ignore\n            onChange(newValueWithCommonProps);\n          } else {\n            onChange(newValue);\n          }\n        }}\n        nestingPath={itemNestingPath}\n        className={classOverride(className, itemStyle?.className)}\n        style={{ ...style, ...itemStyle?.style }}\n        {...oProps}\n      />\n    </div>\n  );\n};\n\nexport const getFieldObj = (f: JSONB.FieldType): JSONB.FieldTypeObj => {\n  return typeof f === \"string\" ? { type: f } : f;\n};\n"
  },
  {
    "path": "client/src/components/JSONBSchema/JSONBSchemaPrimitive.tsx",
    "content": "import { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport type { JSONB, ValidatedColumnInfo } from \"prostgles-types\";\nimport React from \"react\";\nimport { getInputType } from \"../../dashboard/SmartForm/SmartFormField/fieldUtils\";\nimport type { FormFieldProps } from \"../FormField/FormField\";\nimport type { FullOption } from \"../Select/Select\";\nimport type { JSONBSchemaCommonProps } from \"./JSONBSchema\";\nimport { isCompleteJSONB } from \"./isCompleteJSONB\";\n\ntype Schema = JSONB.BasicType | JSONB.EnumType;\ntype P = JSONBSchemaCommonProps & {\n  className?: string;\n  style?: React.CSSProperties;\n  schema: Schema;\n  value: JSONB.GetType<Schema>;\n  onChange: (newValue: JSONB.GetType<Schema>) => void;\n};\nexport const JSONBSchemaPrimitiveMatch = (\n  s: JSONB.JSONBSchema,\n): s is JSONB.BasicType | JSONB.EnumType =>\n  !s.lookup &&\n  (!!s.enum?.length ||\n    (!s.allowedValues?.length && typeof s.type === \"string\"));\n\nexport const JSONBSchemaPrimitive = ({\n  value,\n  schema,\n  onChange,\n  showErrors,\n  noLabels,\n}: P) => {\n  let fullOptions: FullOption[] | undefined = undefined;\n  if (schema.enum?.length || schema.allowedValues?.length) {\n    fullOptions = (schema.enum || schema.allowedValues!).map((key) => ({\n      key,\n    }));\n  }\n\n  const transformedType = {\n    ...(schemaTypeToColType[schema.type as any] ?? {\n      tsDataType: \"string\",\n      udt_name: \"text\",\n    }),\n  };\n\n  let arrayType: FormFieldProps<\"text\">[\"arrayType\"];\n  if (schema.type?.endsWith(\"[]\")) {\n    const tsDataType = schema.type.slice(0, -2) as any;\n    arrayType = {\n      ...(schemaTypeToColType[tsDataType!] ?? {\n        tsDataType: \"string\",\n        udt_name: \"text\",\n      }),\n    };\n  }\n\n  const inputType = getInputType({\n    ...transformedType,\n    name: schema.title ?? \"text\",\n  });\n\n  const error =\n    showErrors && !isCompleteJSONB(value, schema) ? \"Required\" : undefined;\n\n  return (\n    <FormFieldDebounced\n      name={schema.title}\n      label={\n        /**\n         * Hacky. TODO: find a better approach showing JSONB controls within a form with existing top label and bottom hint.\n         * Should the main column label be removed?!\n         */\n        noLabels && schema.type !== \"boolean\" ?\n          undefined\n        : { children: schema.title, info: schema.description }\n      }\n      value={value}\n      placeholder={noLabels ? schema.title : undefined}\n      className={\"JSONBSchemaPrimitive f-0\"}\n      //@ts-ignore\n      type={inputType}\n      nullable={schema.nullable}\n      optional={schema.optional}\n      arrayType={arrayType}\n      inputProps={schema.type === \"integer\" ? { step: 1 } : undefined}\n      fullOptions={fullOptions}\n      multiSelect={!!schema.allowedValues?.length && schema.type.endsWith(\"[]\")}\n      onChange={(newVal) => {\n        if (schema.type === \"number[]\" || schema.type === \"integer[]\") {\n          if (Array.isArray(newVal) && newVal.every(parseNumber)) {\n            onChange(newVal.map(parseNumber));\n            return;\n          }\n        }\n        if (\n          (schema.type === \"number\" || schema.type === \"integer\") &&\n          parseNumber(newVal)\n        ) {\n          onChange(+newVal);\n          return;\n        }\n        onChange(newVal);\n      }}\n      error={error}\n    />\n  );\n};\n\nconst schemaTypeToColType: Record<\n  Required<Schema>[\"type\"],\n  Pick<ValidatedColumnInfo, \"udt_name\" | \"tsDataType\">\n> = {\n  Date: {\n    tsDataType: \"string\",\n    udt_name: \"date\",\n  },\n  \"Date[]\": {\n    tsDataType: \"string\",\n    udt_name: \"date\",\n  },\n  boolean: {\n    tsDataType: \"boolean\",\n    udt_name: \"bool\",\n  },\n  \"boolean[]\": {\n    tsDataType: \"boolean[]\",\n    udt_name: \"bool\",\n  },\n  integer: {\n    tsDataType: \"number\",\n    udt_name: \"int4\",\n  },\n  \"integer[]\": {\n    tsDataType: \"number[]\",\n    udt_name: \"int4\",\n  },\n  time: {\n    tsDataType: \"string\",\n    udt_name: \"time\",\n  },\n  \"time[]\": {\n    tsDataType: \"string[]\",\n    udt_name: \"time\",\n  },\n  timestamp: {\n    tsDataType: \"string\",\n    udt_name: \"timestamp\",\n  },\n  \"timestamp[]\": {\n    tsDataType: \"string[]\",\n    udt_name: \"timestamp\",\n  },\n  string: {\n    tsDataType: \"string\",\n    udt_name: \"text\",\n  },\n  \"string[]\": {\n    tsDataType: \"string[]\",\n    udt_name: \"text\",\n  },\n  number: {\n    tsDataType: \"number\",\n    udt_name: \"numeric\",\n  },\n  \"number[]\": {\n    tsDataType: \"number[]\",\n    udt_name: \"numeric\",\n  },\n  any: {\n    tsDataType: \"any\",\n    udt_name: \"text\",\n  },\n  \"any[]\": {\n    tsDataType: \"any\",\n    udt_name: \"text\",\n  },\n};\n\nconst parseNumber = (str: string) =>\n  str && Number.isFinite(+str) && (+str.trim()).toString() === str.trim();\n"
  },
  {
    "path": "client/src/components/JSONBSchema/JSONBSchemaRecord.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport type { JSONB } from \"prostgles-types\";\nimport { getKeys, isObject, omitKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport Btn from \"../Btn\";\nimport { FormFieldDebounced } from \"../FormField/FormFieldDebounced\";\nimport type { JSONBSchemaCommonProps } from \"./JSONBSchema\";\nimport { JSONBSchema } from \"./JSONBSchema\";\n\ntype Schema = JSONB.RecordType;\ntype P = JSONBSchemaCommonProps & {\n  schema: Schema;\n  onChange: (newValue: JSONB.GetType<Schema>) => void;\n};\n\nexport const JSONBSchemaRecordMatch = (s: JSONB.JSONBSchema): s is Schema =>\n  isObject(s.record);\nexport const JSONBSchemaRecord = ({\n  value,\n  schema,\n  onChange,\n  ...oProps\n}: P) => {\n  return (\n    <div className=\"JSONBSchemaRecord flex-col gap-1 jc-end\">\n      {isObject(value) &&\n        getKeys(value).map((propName) => {\n          const ps = schema.record.values ?? { type: \"any\" };\n          const propSchema = {\n            title: \"Value\",\n            ...(typeof ps === \"string\" ? { type: ps } : ps),\n          };\n          return (\n            <div key={propName} className=\"flex-row gap-1\">\n              <FormFieldDebounced\n                label={\"Property name\"}\n                type=\"text\"\n                value={propName}\n                onChange={(newPropName) => {\n                  onChange({\n                    ...omitKeys(value, [propName]),\n                    [newPropName]: value[propName],\n                  });\n                }}\n              />\n              <JSONBSchema\n                value={value[propName]}\n                schema={propSchema}\n                //@ts-ignore\n                onChange={(newVal) => {\n                  onChange(\n                    newVal === undefined ?\n                      omitKeys(value, [propName])\n                    : { ...value, [propName]: newVal },\n                  );\n                }}\n                {...oProps}\n              />\n            </div>\n          );\n        })}\n\n      <Btn\n        iconPath={mdiPlus}\n        variant=\"filled\"\n        color=\"action\"\n        onClick={() => {\n          onChange({\n            ...(value ?? {}),\n            new_property: undefined,\n          });\n        }}\n      >\n        Add property\n      </Btn>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/JSONBSchema/isCompleteJSONB.ts",
    "content": "import type { JSONB } from \"prostgles-types\";\nimport { isObject } from \"prostgles-types\";\nimport { getEntries } from \"@common/utils\";\n\nexport const getSchemaFromField = (s: JSONB.FieldType): JSONB.FieldTypeObj =>\n  typeof s === \"string\" ? { type: s } : s;\n\nexport const getJSONBError = (\n  schema: JSONB.JSONBSchema & { optional?: boolean },\n  value: any,\n) => {\n  if (\n    (!schema.nullable && value === null) ||\n    (!schema.optional && value === undefined)\n  ) {\n    return \"Required\";\n  }\n\n  return undefined;\n};\n\nexport const isCompleteJSONB = (\n  v: any,\n  ss: JSONB.JSONBSchema | JSONB.OneOf | JSONB.FieldType,\n) => {\n  const s = typeof ss === \"string\" ? getSchemaFromField(ss) : ss;\n  if ((s as JSONB.BasicType).optional && v === undefined) {\n    return true;\n  }\n  if (s.nullable && v === null) {\n    return true;\n  }\n  if (\"lookup\" in s && s.lookup) {\n    return v !== undefined;\n  } else if (typeof s.type === \"string\" && v !== undefined) {\n    return true;\n  } else if (s.enum?.includes(v)) {\n    return true;\n  } else if (isObject(s.type) && isObject(v)) {\n    return getEntries(s.type).every(([propName, propSchema]) =>\n      isCompleteJSONB(v[propName], propSchema),\n    );\n  } else if (s.arrayOf || s.arrayOfType) {\n    const arrSchema = s.arrayOf ? s.arrayOf : { type: s.arrayOfType };\n    return (\n      Array.isArray(v) && v.every((elem) => isCompleteJSONB(elem, arrSchema))\n    );\n  } else if (s.oneOf || s.oneOfType) {\n    return (s.oneOf || s.oneOfType?.map((type) => ({ type })))?.some((oneS) =>\n      isCompleteJSONB(v, oneS),\n    );\n  }\n  return false;\n};\n"
  },
  {
    "path": "client/src/components/JsonRenderer.tsx",
    "content": "import React from \"react\";\n\ntype P = {\n  value: any;\n};\n\nexport const JsonRenderer: React.FC<P> = ({ value }) => {\n  const renderObject = (obj: any, idx = 1) => {\n    if (Array.isArray(obj)) {\n      return obj.map(renderObject);\n    }\n\n    if (Object(obj) !== obj) {\n      return <span>{obj}</span>;\n    }\n\n    return (\n      <div key={idx} className=\"json-object flex-col gap-p25\">\n        {Object.entries(obj).map(([key, value]) => (\n          <div key={key} className=\"flex-row-wrap\">\n            <div className=\"bold\">{key}:</div>\n            <div>{renderObject(value)}</div>\n          </div>\n        ))}\n      </div>\n    );\n  };\n\n  return <div className=\"flex-col\">{renderObject(value)}</div>;\n};\n"
  },
  {
    "path": "client/src/components/Label.css",
    "content": ".Label_QuestionButton {\n  position: relative;\n}\n\n.Label_QuestionButton:hover::after {\n  content: \"?\";\n  position: absolute;\n  top: 0;\n  right: 0;\n}\n"
  },
  {
    "path": "client/src/components/Label.tsx",
    "content": "import { mdiHelp, mdiInformationOutline } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"./Btn\";\nimport { Checkbox } from \"./Checkbox\";\nimport { classOverride } from \"./Flex\";\nimport { Icon } from \"./Icon/Icon\";\nimport \"./Label.css\";\nimport PopupMenu from \"./PopupMenu\";\n\nexport type NormalLabelProps = {\n  variant: \"normal\";\n  iconPath?: undefined;\n  leftIcon?: React.ReactNode;\n  toggle?: {\n    checked?: boolean;\n    onChange: (checked: boolean) => void;\n  };\n};\n\nexport type HeaderLabelProps = {\n  variant?: \"header\";\n  iconPath?: string;\n  leftIcon?: undefined;\n  toggle?: undefined;\n};\n\ntype LabelPropsCommon = React.DetailedHTMLProps<\n  React.LabelHTMLAttributes<HTMLLabelElement>,\n  HTMLLabelElement\n> & {\n  label?: string;\n  info?: React.ReactNode;\n  popupTitle?: React.ReactNode;\n  size?: \"small\";\n};\n\nexport type LabelPropsNormal = LabelPropsCommon & NormalLabelProps;\nexport type LabelPropsHeader = LabelPropsCommon & HeaderLabelProps;\n\nexport type LabelProps = LabelPropsNormal | LabelPropsHeader;\n\nexport const Label = ({\n  info = null,\n  variant = \"header\",\n  iconPath = mdiHelp,\n  label,\n  popupTitle,\n  className = \"\",\n  toggle,\n  children,\n  size,\n  leftIcon,\n  ...otherProps\n}: LabelProps) => {\n  const isHeader = variant === \"header\";\n\n  const ensureInfoBtnKeepsCardLayoutCellsConsistent = Boolean(\n    !isHeader && info,\n  );\n\n  const IconBtn = info && (\n    <PopupMenu\n      title={popupTitle ?? label ?? \"Information\"}\n      positioning=\"beneath-center\"\n      clickCatchStyle={{ opacity: 0.3 }}\n      rootStyle={{\n        maxWidth: \"500px\",\n      }}\n      className={isHeader ? undefined : \"show-on-parent-hover\"}\n      contentClassName=\"p-1\"\n      style={\n        ensureInfoBtnKeepsCardLayoutCellsConsistent ?\n          {\n            position: \"absolute\",\n            top: \"-.5em\",\n            right: 0,\n            overflow: \"visible\",\n          }\n        : {}\n      }\n      button={\n        !isHeader ?\n          <Btn iconPath={mdiHelp} size=\"micro\" />\n        : <Btn\n            iconPath={iconPath}\n            className=\"Label_QuestionButton text-2  relative ai-center\"\n            title=\"Click for more information\"\n          />\n      }\n    >\n      <div className=\"flex-row ta-left\">\n        <Icon\n          path={mdiInformationOutline}\n          size={1}\n          className=\"f-0 text-2 mr-1\"\n        />\n        {info}\n      </div>\n    </PopupMenu>\n  );\n\n  return (\n    <label\n      {...otherProps}\n      className={classOverride(\n        `Label variant:${variant} relative noselect flex-row ai-center ` +\n          (toggle ? \" pointer gap-p5 \" : \" gap-p25 \") +\n          (otherProps.htmlFor ? \" pointer \" : \" \") +\n          \"w-fit\",\n        className,\n      )}\n      style={{\n        fontSize:\n          size === \"small\" ? \"12px\"\n          : isHeader ? \"18px\"\n          : \"16px\",\n        fontWeight:\n          size === \"small\" ? \"normal\"\n          : isHeader ? 500\n          : 400,\n        color: size === \"small\" ? \"var(--text-1)\" : \"var(--text-1)\",\n        ...otherProps.style,\n        ...(ensureInfoBtnKeepsCardLayoutCellsConsistent &&\n          IconBtn && {\n            paddingRight: \"2em\",\n          }),\n      }}\n    >\n      {isHeader && IconBtn}\n      {leftIcon}\n      {label ?? children}\n      {toggle && (\n        <Checkbox\n          checked={toggle.checked}\n          onChange={(v, c) => toggle.onChange(c)}\n          variant=\"micro\"\n        />\n      )}\n      {!isHeader && IconBtn}\n    </label>\n  );\n};\n"
  },
  {
    "path": "client/src/components/LabeledRow.tsx",
    "content": "import React from \"react\";\nimport { Icon } from \"./Icon/Icon\";\nimport { FlexRowWrap } from \"./Flex\";\n\ntype P = {\n  icon?: string;\n  label?: React.ReactNode;\n  title?: string;\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n  onClick?: React.DOMAttributes<HTMLDivElement>[\"onClick\"];\n\n  labelClassName?: string;\n  labelStyle?: React.CSSProperties;\n  contentClassName?: string;\n  contentStyle?: React.CSSProperties;\n  noContentWrapper?: boolean;\n};\n\nexport const LabeledRow = (p: P) => {\n  return (\n    <div\n      className={\n        \"LabeledRow \" + (p.className || \"\") + \" flex-row-wrap ai-center \"\n      }\n      style={p.style}\n      title={p.title}\n      onClick={p.onClick}\n    >\n      <div\n        className={\n          \"flex-row gap-p5 text-1 ai-center \" + (p.labelClassName ?? \"\")\n        }\n        style={p.labelStyle}\n      >\n        {p.icon && <Icon path={p.icon} className=\"text-2\" />}\n        {p.label}\n      </div>\n      {p.noContentWrapper ?\n        p.children\n      : <FlexRowWrap\n          className={\"px-p5 font-medium gap-p5 \" + (p.contentClassName ?? \"\")}\n          style={p.contentStyle}\n        >\n          {p.children}\n        </FlexRowWrap>\n      }\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/List.css",
    "content": "input.custom-input {\n  -webkit-appearance: none !important;\n  padding: 0.5em;\n  padding-bottom: 4px;\n  font-size: 16px;\n  outline: none;\n  font-weight: 600;\n  /* border: 1px solid rgb(223, 223, 223); */\n  border: unset;\n}\n\n.input-focus.error:focus,\n/* input.custom-input:focus, */\n.focus-border.error:focus-within {\n  -webkit-appearance: none !important;\n  border: 1px solid rgb(255, 0, 0);\n  outline: none;\n  -webkit-box-shadow: 0 0 0 3px rgba(255, 0, 0, 0.151);\n  box-shadow: 0 0 0 3px rgba(255, 0, 0, 0.151);\n}\n\n.list-comp ul, ul.no-decor {\n  padding: 0;\n  margin: 0;\n  text-decoration: none;\n  list-style: none;\n}\n\n.list-comp label {\n  text-align: start;\n}\n\n.hover-bg:hover {\n  background-color: var(--li-hover-bg);\n}\n\n.list-comp li:not(.no-hover, :active, :focus):hover ,\n.list-comp li:not(.no-hover, :active, :focus).hover , \nul > li[role=option]:not(.no-hover, :active, :focus):hover {\n  background-color: var(--li-hover-bg);\n  border: none;\n  outline: none;\n}\n\nul > li:focus {\n  background-color: var(--li-focus-bg);\n  border: none;\n  outline: none;\n}"
  },
  {
    "path": "client/src/components/List.tsx",
    "content": "import React from \"react\";\nimport { Checkbox } from \"./Checkbox\";\nimport { DraggableLI } from \"./DraggableLI\";\nimport Popup from \"./Popup/Popup\";\nimport type { OptionKey } from \"./Select/Select\";\n\ntype Items = {\n  key?: OptionKey;\n  label?: string;\n  node?: React.ReactNode;\n  content?: React.ReactNode;\n  contentLeft?: React.ReactNode;\n  contentRight?: React.ReactNode;\n  onPress: (\n    e:\n      | React.MouseEvent<HTMLLIElement, globalThis.MouseEvent>\n      | React.KeyboardEvent<HTMLLIElement>,\n  ) => void;\n  style?: React.CSSProperties;\n}[];\n\nexport type ListProps = {\n  id?: string;\n  items: Items;\n  onReorder?: (newItems: Items) => void;\n  style?: React.CSSProperties;\n  className?: string;\n\n  onClose: VoidFunction;\n  selectedValue?: string;\n  checkedValues?: OptionKey[];\n  anchorRef?: HTMLElement;\n  anchorContent?: React.ReactChild;\n};\n\nexport default class List extends React.Component<ListProps, any> {\n  refList?: HTMLUListElement;\n  popupAnchor?: HTMLElement;\n  render() {\n    const {\n      className = \"\",\n      style = {},\n      items,\n      onReorder,\n      onClose,\n\n      selectedValue,\n      checkedValues,\n\n      anchorRef,\n      anchorContent,\n    } = this.props;\n\n    const list = (\n      <ul\n        className={\n          \"list-comp f-1 o-auto min-h-0 min-w-0 no-scroll-bar\" + className\n        }\n        role=\"list\"\n        ref={(r) => {\n          if (r) this.refList = r;\n        }}\n        style={{\n          // padding: \"0.5em\",\n          padding: 0,\n          ...style,\n        }}\n      >\n        {!items.length ?\n          <div className=\"p-p5\">No results</div>\n        : items.map((d, i) => (\n            <DraggableLI\n              key={i}\n              role=\"listitem\"\n              idx={i}\n              onReorder={onReorder}\n              items={items.slice(0)}\n              className={\n                \"flex-row bg-li p-p5 pointer \" +\n                (d.key === selectedValue ? \" selected \" : \"\")\n              }\n              onClick={(e) => d.onPress(e)}\n              onKeyUp={(e) => {\n                if (e.key === \"Enter\") d.onPress(e);\n              }}\n            >\n              {d.content || (\n                <>\n                  {d.contentLeft || null}\n                  <label\n                    className=\"mr-p5 f-1 flex-row pointer noselect\"\n                    style={d.style}\n                  >\n                    {d.node || (d.label ?? d.key)?.toString()}\n                  </label>\n                  {d.contentRight || null}\n                  {!checkedValues ? null : (\n                    <Checkbox\n                      className=\"f-0\"\n                      checked={checkedValues.includes(d.key)}\n                      style={{ marginRight: \"12px\" }}\n                    />\n                  )}\n                </>\n              )}\n            </DraggableLI>\n          ))\n        }\n      </ul>\n    );\n\n    let popupAnchor, popupContent;\n    if (anchorRef) {\n      popupAnchor = anchorRef;\n      popupContent = list;\n    }\n\n    if (anchorContent) {\n      popupAnchor = this.popupAnchor;\n      popupContent = (\n        <div\n          className=\"list-comp w-fit flex-col bg-color-0\"\n          style={{\n            padding: 0,\n            margin: 0,\n            textDecoration: \"none\",\n            listStyle: \"none\",\n          }}\n        >\n          <div\n            ref={(e) => {\n              if (e) this.popupAnchor = e;\n            }}\n            className=\"f-0 min-h-0 min-w-0 m-p5 flex-row jc-start \"\n          >\n            {popupContent}\n          </div>\n          {list}\n        </div>\n      );\n    }\n\n    if (popupAnchor && popupContent) {\n      return (\n        <Popup\n          rootStyle={{ padding: 0 }}\n          anchorEl={popupAnchor}\n          positioning=\"beneath-left\"\n          clickCatchStyle={{ opacity: 0 }}\n          onClose={onClose}\n          contentClassName=\"rounded\"\n          focusTrap={false}\n        >\n          {popupContent}\n        </Popup>\n      );\n    }\n\n    if (popupAnchor || popupContent) return null;\n\n    return list;\n  }\n}\n\n{\n  /* <input id={id} type=\"checkbox\" checked={value} onChange={!onChange? undefined : e => {\n  onChange(e.target.checked, e);\n}}/>\n<label htmlFor={id} className=\"noselect f-1\">{label}</label> */\n}\n"
  },
  {
    "path": "client/src/components/Loader/Loading.css",
    "content": ".loader {\n  font-size: 10px;\n  margin: 50px auto;\n  text-indent: -9999em;\n  width: 11em;\n  height: 11em;\n  border-radius: 50%;\n  background: #ffffff;\n  background: -moz-linear-gradient(\n    left,\n    #ffffff 10%,\n    rgba(255, 255, 255, 0) 42%\n  );\n  background: -webkit-linear-gradient(\n    left,\n    #ffffff 10%,\n    rgba(255, 255, 255, 0) 42%\n  );\n  background: -o-linear-gradient(left, #ffffff 10%, rgba(255, 255, 255, 0) 42%);\n  background: -ms-linear-gradient(\n    left,\n    #ffffff 10%,\n    rgba(255, 255, 255, 0) 42%\n  );\n  background: linear-gradient(\n    to right,\n    #ffffff 10%,\n    rgba(255, 255, 255, 0) 42%\n  );\n  position: relative;\n  -webkit-animation: load3 1.4s infinite linear;\n  animation: load3 1.4s infinite linear;\n  -webkit-transform: translateZ(0);\n  -ms-transform: translateZ(0);\n  transform: translateZ(0);\n  will-change: transform;\n}\n.loader:before {\n  width: 50%;\n  height: 50%;\n  background: #ffffff;\n  border-radius: 100% 0 0 0;\n  position: absolute;\n  top: 0;\n  left: 0;\n  content: \"\";\n}\n.loader:after {\n  background: #0dc5c1;\n  width: 75%;\n  height: 75%;\n  border-radius: 50%;\n  content: \"\";\n  margin: auto;\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  right: 0;\n}\n@-webkit-keyframes load3 {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: translateZ(0) rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(360deg);\n    transform: translateZ(0) rotate(360deg);\n  }\n}\n@keyframes load3 {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: translateZ(0) rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(360deg);\n    transform: translateZ(0) rotate(360deg);\n  }\n}\n\n/* $offset: 187;\n$duration: 1.4s; */\n\n.Spinner path {\n  transform-box: fill-box;\n  transform-origin: center;\n  animation: rotator 0.75s infinite linear;\n  will-change: transform;\n}\n@keyframes rotator {\n  100% {\n    transform: translateZ(0) rotate(360deg);\n  }\n}\n\n@keyframes colors {\n  0% {\n    stroke: #4285f4;\n  }\n  25% {\n    stroke: #de3e35;\n  }\n  50% {\n    stroke: #f7c223;\n  }\n  75% {\n    stroke: #1b9a59;\n  }\n  100% {\n    stroke: #4285f4;\n  }\n}\n\n@keyframes dash {\n  0% {\n    stroke-dashoffset: 187;\n  }\n  50% {\n    stroke-dashoffset: 47.65;\n    transform: translateZ(0) rotate(135deg);\n  }\n  100% {\n    stroke-dashoffset: 187;\n    transform: translateZ(0) rotate(450deg);\n  }\n}\n\n.top-bar-loader {\n  background: #eee;\n  background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%);\n  border-radius: 5px;\n  background-size: 200% 100%;\n  animation: 1.5s shine linear infinite;\n}\n\n@keyframes shine {\n  to {\n    background-position-x: -200%;\n  }\n}\n"
  },
  {
    "path": "client/src/components/Loader/Loading.tsx",
    "content": "import React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport \"./Loading.css\";\nimport RTComp from \"../../dashboard/RTComp\";\nimport { classOverride, FlexRow } from \"../Flex\";\nimport { tout } from \"../../utils/utils\";\nimport { SpinnerV2 } from \"./SpinnerV2\";\nimport { SpinnerV4 } from \"./SpinnerV4\";\nimport { SpinnerV3 } from \"./SpinnerV3\";\nexport const pageReload = async (reason: string) => {\n  console.log(\"pageReload due to: \", reason);\n  await tout(200);\n  window.location.reload();\n};\n\ntype P = {\n  /* If provided will ensure continuity to the applied delays */\n  id?: string;\n  style?: React.CSSProperties;\n  className?: string;\n  variant?: \"top-bar\" | \"cover\";\n  sizePx?: number;\n  message?: string;\n  colorAnimation?: boolean;\n} & (\n  | { show?: boolean; delay?: undefined }\n  | { delay?: number; show?: undefined }\n) &\n  (\n    | {\n        /**\n         * Used to call something if stuck on the same loading message\n         */\n        onTimeout?: {\n          message?: string;\n          run: VoidFunction;\n          timeout: number;\n        };\n        /**\n         * Used to refresh page if stuck on the same loading message\n         */\n        refreshPageTimeout?: undefined;\n      }\n    | {\n        onTimeout?: undefined;\n        refreshPageTimeout?: number;\n      }\n  );\n\nconst loaderIdLastShown: Record<string, number> = {};\n\ntype S = {\n  ready: boolean;\n  timeoutMessage?: string;\n};\nexport default class Loading extends RTComp<P, S> {\n  state: S = {\n    ready: false,\n  };\n  mounted = false;\n\n  get show() {\n    const { delay = 500, id } = this.props;\n    const wasShownRecently =\n      id !== undefined &&\n      loaderIdLastShown[id] &&\n      Date.now() - loaderIdLastShown[id] < delay;\n    return !delay || wasShownRecently || this.state.ready;\n  }\n\n  onMount() {\n    this.mounted = true;\n    if (\"show\" in this.props) return;\n\n    const { delay = 500, id } = this.props;\n    if (delay && !this.show) {\n      setTimeout(() => {\n        if (this.mounted) {\n          if (id) loaderIdLastShown[id] = Date.now();\n          this.setState({ ready: true });\n        }\n      }, delay);\n    } else {\n      this.setState({ ready: true });\n    }\n  }\n\n  onTimeout?: NodeJS.Timeout;\n  onDelta(\n    deltaP: Partial<P> | undefined,\n    deltaS: Partial<{ ready: boolean }> | undefined,\n    deltaD: { [x: string]: any } | undefined,\n  ): void {\n    if (deltaP?.onTimeout ?? deltaP?.refreshPageTimeout) {\n      if (this.onTimeout) clearTimeout(this.onTimeout);\n\n      const { onTimeout, message, refreshPageTimeout } = this.props;\n      if (onTimeout || refreshPageTimeout) {\n        this.onTimeout = setTimeout(() => {\n          if (!this.mounted || this.props.message !== message) {\n            return;\n          }\n          if (this.props.onTimeout) {\n            console.log(\"onTimeout\", message, this.props.message);\n            this.props.onTimeout.run();\n            if (this.props.onTimeout.message) {\n              this.setState({ timeoutMessage: this.props.onTimeout.message });\n            }\n          } else {\n            pageReload(\"Loader refreshPageTimeout\");\n          }\n        }, onTimeout?.timeout ?? refreshPageTimeout!);\n      }\n    }\n  }\n\n  render() {\n    const {\n      style = {},\n      className = \"\",\n      variant,\n      sizePx = 30,\n      colorAnimation = false,\n    } = this.props;\n    const { show = this.show } = this.props;\n    const size = sizePx + \"px\";\n\n    const msg = this.state.timeoutMessage ?? this.props.message;\n    const message = msg ? <div>{msg}</div> : null;\n\n    // const delayStyle = { opacity: (!show) ? 0.0001 : 1 };\n    const commonStyle = {\n      display: !show ? \"none\" : undefined,\n      cursor: \"wait\",\n    };\n\n    if (variant === \"top-bar\") {\n      return (\n        <div\n          style={{\n            position: \"absolute\",\n            zIndex: 2,\n            top: 0,\n            left: 0,\n            right: 0,\n            width: \"100%\",\n            height: \"6px\",\n            ...style,\n            ...commonStyle,\n          }}\n          className={\"top-bar-loader \" + className}\n        ></div>\n      );\n    }\n    if (variant === \"cover\") {\n      return (\n        <>\n          <div\n            style={{\n              position: \"absolute\",\n              zIndex: 2,\n              inset: 0,\n              width: \"100%\",\n              height: \"100%\",\n              ...commonStyle,\n              ...(!className.includes(\"bg-\") && {\n                background: \"rgba(255, 255, 255, .5)\",\n              }),\n              ...style,\n            }}\n            className={classOverride(\n              \"cover-loader flex-row ai-center jc-center gap-1\",\n              className,\n            )}\n          >\n            <FlexRow className=\"bg-color-0 rounded p-1 shadow\">\n              <Spinner size={size} colorAnimation={colorAnimation} />\n              {message}\n            </FlexRow>\n          </div>\n        </>\n      );\n    }\n\n    const rootStyle: React.CSSProperties =\n      !message ? { width: size, height: size } : {};\n\n    return (\n      <div\n        style={{ ...style, ...rootStyle, ...commonStyle }}\n        className={classOverride(\n          \"Loading spinner-loader ws-nowrap flex-row gap-1 ai-center \",\n          className,\n        )}\n      >\n        <Spinner size={size} colorAnimation={colorAnimation} />\n        {message}\n      </div>\n    );\n  }\n}\n\n/* \n  The spinner should keep laptop fans quiet. \n  Test pages: \n    http://localhost:3004/component-list#loader-test\n\n\n  Mask image (SpinnerV4): \n    10% cpu 20% gpu\n  SVG as image (SpinnerV3): \n    60% cpu 20% gpu\n  Canvas (SpinnerV2): \n    30% cpu 50% gpu\n*/\n\nconst Spinner = ({ size }: { size: string; colorAnimation: boolean }) => {\n  // return <SpinnerV3 size={size} />;\n  return <SpinnerV4 size={size} />;\n  // return <SpinnerV2 size={size} />;\n\n  /** ONLY USE IN DEVELOPMENT. Causes high cpu usage on high node count page */\n  // return (\n  //   <svg\n  //     xmlns=\"http://www.w3.org/2000/svg\"\n  //     className=\"Spinner f-0\"\n  //     width={size}\n  //     height={size}\n  //     viewBox=\"0 0 24 24\"\n  //   >\n  //     <path\n  //       xmlns=\"http://www.w3.org/2000/svg\"\n  //       fill=\"inherit\"\n  //       d=\"M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z\"\n  //     />\n  //   </svg>\n  // );\n};\n"
  },
  {
    "path": "client/src/components/Loader/SpinnerV2.tsx",
    "content": "import React, { useCallback, useEffect, useRef } from \"react\";\nimport { useResizeObserver } from \"../ScrollFade/useResizeObserver\";\n\n/**\n * @deprecated Use SpinnerV4 instead. Deprecated due to performance issues when page has too many nodes\n */\nexport const SpinnerV2 = ({ size }: { size: string }) => {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const animationFrameIdRef = useRef<number | null>(null);\n\n  const startAnimation = useCallback(() => {\n    if (animationFrameIdRef.current) {\n      cancelAnimationFrame(animationFrameIdRef.current);\n      animationFrameIdRef.current = null; // Clear the stored ID\n    }\n\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n\n    if (!canvas || !ctx) {\n      console.error(\"SpinnerV2: Failed to get canvas element or 2D context.\");\n      return;\n    }\n\n    const width = canvas.clientWidth;\n    const height = canvas.clientHeight;\n\n    canvas.width = width;\n    canvas.height = height;\n\n    const centerX = width / 2;\n    const centerY = height / 2;\n\n    // Spinner properties\n    const radius = Math.max(1, (Math.min(width, height) / 2) * 0.8); // Ensure radius is at least 1\n    const lineWidth = 2;\n    const color = getComputedStyle(canvas).color; // getCssVariableValue(\"--text-2\") || \"#888\";\n    const rotationSpeed = 0.08;\n    const arcLength = Math.PI * 1.5; // 270 degrees\n\n    let currentAngle = 0;\n\n    const drawFrame = () => {\n      if (canvasRef.current) {\n        ctx.clearRect(0, 0, width, height);\n        ctx.beginPath();\n        ctx.arc(\n          centerX,\n          centerY,\n          radius,\n          currentAngle,\n          currentAngle + arcLength,\n        );\n        ctx.lineWidth = lineWidth;\n        ctx.strokeStyle = color;\n        ctx.lineCap = \"round\";\n        ctx.stroke();\n      } else {\n        if (animationFrameIdRef.current) {\n          cancelAnimationFrame(animationFrameIdRef.current);\n          animationFrameIdRef.current = null;\n        }\n      }\n    };\n\n    const animate = () => {\n      // Only proceed if the canvas is still mounted and context exists\n      if (!canvasRef.current) {\n        if (animationFrameIdRef.current) {\n          cancelAnimationFrame(animationFrameIdRef.current);\n          animationFrameIdRef.current = null;\n        }\n        return;\n      }\n\n      currentAngle += rotationSpeed;\n      drawFrame();\n\n      // Store the ID of the *next* animation frame request\n      animationFrameIdRef.current = requestAnimationFrame(animate);\n    };\n\n    animate();\n  }, []);\n\n  useEffect(() => {\n    startAnimation();\n\n    return () => {\n      if (animationFrameIdRef.current) {\n        cancelAnimationFrame(animationFrameIdRef.current);\n        animationFrameIdRef.current = null;\n      }\n    };\n  }, [startAnimation]);\n\n  useResizeObserver({\n    elem: canvasRef.current,\n    onResize: startAnimation,\n  });\n\n  useEffect(() => startAnimation(), [startAnimation]);\n\n  return (\n    <div\n      className=\"SpinnerV2 f-0\"\n      style={{\n        width: size,\n        height: size,\n      }}\n    >\n      <canvas\n        style={{\n          width: size,\n          height: size,\n        }}\n        ref={canvasRef}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Loader/SpinnerV3.tsx",
    "content": "import React, { useEffect, useMemo, useRef, useState } from \"react\";\n\n/**\n * @deprecated Use SpinnerV4 instead. Deprecated due to performance issues and currentColor issues\n */\nexport const SpinnerV3 = ({ size }: { size: string }) => {\n  const imgRef = useRef<HTMLImageElement>(null);\n  const [color, setColor] = useState(\"currentColor\");\n\n  useEffect(() => {\n    if (imgRef.current?.parentElement) {\n      const computedColor = getComputedStyle(\n        imgRef.current.parentElement,\n      ).color;\n      setColor(computedColor);\n    }\n  }, []);\n\n  const dataUrl = useMemo(() => {\n    const sizeStringified = JSON.stringify(size);\n    const svgString = `\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      class=\"Spinner f-0\"\n      width=${sizeStringified}\n      height=${sizeStringified}\n      style=\"color: ${color};\"\n      viewBox=\"0 0 24 24\"\n    >\n      <defs>\n        <style>\n          .Spinner path {\n            transform-box: fill-box;\n            transform-origin: center;\n            animation: rotator 0.75s infinite linear;\n            will-change: transform;\n          }\n          @keyframes rotator {\n            100% {\n              transform: translateZ(0) rotate(360deg);\n            }\n          }\n        </style>\n      </defs>\n      <path\n        xmlns=\"http://www.w3.org/2000/svg\"\n        fill=\"currentColor\"\n        d=\"M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z\"\n      />\n    </svg>\n  `;\n    const dataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgString)}`;\n    return dataUrl;\n  }, [size, color]);\n  return (\n    <img ref={imgRef} src={dataUrl} style={{ width: size, height: size }} />\n  );\n};\n"
  },
  {
    "path": "client/src/components/Loader/SpinnerV4.tsx",
    "content": "import React from \"react\";\n\nexport const SpinnerV4 = ({ size }: { size: string }) => {\n  return (\n    <div\n      className=\"SpinnerV3\"\n      style={{\n        width: size,\n        height: size,\n\n        backgroundColor: \"currentColor\",\n        maskImage: spinnerDataUrl,\n        WebkitMaskImage: spinnerDataUrl,\n        maskSize: \"contain\",\n        WebkitMaskSize: \"contain\",\n        maskRepeat: \"no-repeat\",\n        WebkitMaskRepeat: \"no-repeat\",\n\n        // borderWidth: \"2px\",\n        // borderStyle: \"solid\",\n        // borderColor: \"currentColor\",\n        // borderBottomColor: \"transparent\",\n        // borderRadius: \"50%\",\n\n        // animation: \"rotator 0.75s infinite linear\",\n\n        animation: \"rotator 0.75s steps(36) infinite \", // reduce cpu by using steps\n        willChange: \"transform\",\n        backfaceVisibility: \"hidden\",\n      }}\n    />\n  );\n};\nconst svgString = `\n      <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n        <path d=\"M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z\"/>\n      </svg>\n    `;\nconst spinnerDataUrl = `url(\"data:image/svg+xml;utf8,${encodeURIComponent(svgString)}\")`;\n"
  },
  {
    "path": "client/src/components/MediaViewer/MediaViewer.tsx",
    "content": "import { sliceText } from \"@common/utils\";\nimport { mdiChevronLeft } from \"@mdi/js\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { Icon } from \"../Icon/Icon\";\nimport Popup from \"../Popup/Popup\";\nimport {\n  ContentTypes,\n  RenderMedia,\n  type UrlInfo,\n  type ValidContentType,\n} from \"./RenderMedia\";\n\ntype P = {\n  url: string;\n\n  /**\n   * Request prev or next media\n   */\n  onPrevOrNext?: (increment: -1 | 1) => { url: string | undefined };\n\n  style?: React.CSSProperties;\n\n  /**\n   * If present then check URL hostname before requesting\n   */\n  allowedHostnames?: string[];\n  /**\n   * If present then use this\n   */\n  content_type?: ValidContentType;\n};\n\nexport const MediaViewer = (props: P) => {\n  const { onPrevOrNext, style, content_type, url, allowedHostnames } = props;\n  const [isFocused, setIsFocused] = useState(false);\n  const [urlInfo, setUrlInfo] = useState<UrlInfo | undefined>(\n    content_type && url ?\n      {\n        raw: url,\n        validated: url,\n        forDisplay: sliceText(url, 100),\n        type: content_type,\n        content_type,\n      }\n    : undefined,\n  );\n\n  const setURL = useCallback(\n    async (url: string) => {\n      if (!url) return;\n\n      let contentType: string | undefined = content_type;\n      if (!content_type) {\n        const mimeFromData =\n          url.startsWith(\"data:\") ?\n            url.split(\":\")[1]?.split(\";\")[0]\n          : undefined;\n        const mime = mimeFromData ?? (await fetchMimeFromURLHead(url));\n        contentType = mimeFromData ?? mime?.split(\";\")[0]?.trim();\n      }\n\n      setUrlInfo({\n        raw: url,\n        validated: url,\n        forDisplay: sliceText(url, 100),\n        type: ContentTypes.find((ct) => contentType?.startsWith(ct)),\n        content_type: contentType,\n      });\n    },\n    [content_type],\n  );\n\n  useEffect(() => {\n    if (!url) return;\n    if (allowedHostnames) {\n      try {\n        const u = new URL(url);\n        if (!allowedHostnames.includes(u.hostname)) {\n          throw `Hostname ${u.hostname} is not allowed. Allowed hostnames: ${allowedHostnames}`;\n        }\n        void setURL(url);\n      } catch (e) {\n        console.error(\"Could check media URL\", e);\n      }\n    } else {\n      void setURL(url);\n    }\n  }, [allowedHostnames, setURL, url]);\n\n  const onKeyDown = useCallback(\n    (e: KeyboardEvent) => {\n      if (e.key === \"ArrowRight\") {\n        e.preventDefault();\n        onPrevOrNext?.(1);\n      } else if (e.key === \"ArrowLeft\") {\n        e.preventDefault();\n        onPrevOrNext?.(-1);\n      }\n    },\n    [onPrevOrNext],\n  );\n\n  const toggleClick =\n    !onPrevOrNext ? undefined : (\n      (increment: 1 | -1) => {\n        const { url } = onPrevOrNext(increment);\n        if (url) {\n          void setURL(url);\n        }\n      }\n    );\n  return (\n    <>\n      <RenderMedia\n        isFocused={isFocused}\n        setIsFocused={setIsFocused}\n        style={style}\n        urlInfo={urlInfo}\n        contentOnly={false}\n      />\n      {isFocused && (\n        <Popup\n          rootStyle={{ padding: 0, borderRadius: 0 }}\n          clickCatchStyle={{ opacity: 0.2 }}\n          contentClassName=\"o-hidden\"\n          onClose={() => {\n            setIsFocused(false);\n          }}\n          positioning=\"fullscreen\"\n          autoFocusFirst={\"content\"}\n          focusTrap={true}\n          title={\n            !urlInfo ? \"\" : (\n              <a\n                href={urlInfo.validated}\n                target=\"_blank\"\n                className=\"p-1 f-0 text-1p5\"\n                style={{ fontWeight: 400 }}\n                rel=\"noreferrer\"\n              >\n                {urlInfo.forDisplay}\n              </a>\n            )\n          }\n          onKeyDown={!onPrevOrNext ? undefined : onKeyDown}\n        >\n          <div\n            className={\n              (\n                \"MEDIAVIEWER relative flex-col f-1 o-auto noselect ai-center \" +\n                  urlInfo?.type ===\n                \"image\"\n              ) ?\n                \"\"\n              : \" p-1 f-1 w-full h-full flex-col\"\n            }\n          >\n            {toggleClick && ToggleBtn(true, () => toggleClick(-1))}\n            <RenderMedia\n              isFocused={isFocused}\n              setIsFocused={setIsFocused}\n              style={style}\n              urlInfo={urlInfo}\n              contentOnly={true}\n            />\n            {toggleClick && ToggleBtn(false, () => toggleClick(1))}\n          </div>\n        </Popup>\n      )}\n    </>\n  );\n};\n\nexport const fetchMimeFromURLHead = async (\n  url: string,\n): Promise<string | null> => {\n  try {\n    const resp = await fetch(\n      url,\n      // { method: \"HEAD\" } this approach is not suitable due to CORS issues on some servers\n      {\n        headers: {\n          Range: \"bytes=0-0\",\n        },\n      },\n    );\n    if (resp.status >= 400) return null;\n    return resp.headers.get(\"Content-Type\");\n  } catch (e) {\n    console.error(e);\n    return null;\n  }\n};\n\nconst ToggleBtn = (isLeft: boolean, onClick: VoidFunction) => {\n  return (\n    <div\n      className=\"h-full w-fit absolute text-white flex-row ai-center px-1 pointer\"\n      onClick={onClick}\n      style={{\n        top: 0,\n        bottom: 0,\n        ...(isLeft ? { left: 0 } : { right: 0 }),\n        zIndex: 2,\n        color: \"white\",\n        background: `linear-gradient(to ${isLeft ? \"right\" : \"left\"}, black, transparent)`,\n      }}\n    >\n      <Icon\n        path={mdiChevronLeft}\n        style={{\n          color: \"white\",\n          transform: isLeft ? undefined : \"rotate(180deg)\",\n        }}\n        sizePx={34}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/MediaViewer/RenderMedia.tsx",
    "content": "import { mdiFileDocumentOutline } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport Chip from \"../Chip\";\nimport { FlexCol } from \"../Flex\";\n\nexport const ContentTypes = [\"image\", \"video\", \"audio\"] as const;\nexport type ValidContentType = (typeof ContentTypes)[number];\nexport type UrlInfo = {\n  raw: string;\n  validated: string;\n  forDisplay: string;\n  content_type?: string; // If undefined then show as URL\n  type?: ValidContentType;\n};\n\nexport const RenderMedia = ({\n  contentOnly = false,\n  isFocused,\n  setIsFocused,\n  urlInfo,\n  style,\n}: {\n  contentOnly: boolean;\n  urlInfo: UrlInfo | undefined;\n  isFocused: boolean;\n  style: React.CSSProperties | undefined;\n  setIsFocused: (isFocused: boolean) => void;\n}) => {\n  if (!urlInfo) return null;\n\n  const { validated: url, type = \"\", content_type } = urlInfo;\n  let mediaContent: React.ReactNode = null;\n  if (url) {\n    const commonProps = {\n      style: {\n        minHeight: 0,\n        flex:\n          type === \"audio\" ? \"none\"\n          : type === \"image\" ? undefined\n          : 1,\n        maxWidth: \"100%\",\n        maxHeight: \"100%\",\n        objectFit: \"contain\",\n        ...(isFocused && contentOnly ? {} : style),\n        ...(type === \"audio\" &&\n          isFocused && {\n            margin: \"2em\",\n            border: \"unset\",\n          }),\n        ...(type === \"audio\" && {\n          display: \"block\",\n          objectFit: undefined,\n          minHeight: undefined,\n          maxWidth: \"99vw\",\n          minWidth: \"399px\",\n          height: \"60px\",\n          width: \"400px\",\n          border: \"unset\",\n          flex: 1,\n        }),\n      } satisfies React.CSSProperties,\n    } as const;\n    if (type === \"image\") {\n      mediaContent = (\n        <img\n          onClick={(e) => {\n            e.stopPropagation();\n            e.preventDefault();\n            setIsFocused(!isFocused);\n          }}\n          className=\"pointer\"\n          loading=\"lazy\"\n          src={url}\n          {...commonProps}\n        />\n      );\n    } else if (type === \"video\") {\n      mediaContent = (\n        <video {...commonProps} controls src={url} preload=\"metadata\"></video>\n      );\n    } else if (type === \"audio\") {\n      mediaContent = <audio {...commonProps} controls src={url} />;\n    } else if (!isFocused && url) {\n      mediaContent = (\n        <FlexCol className=\"f-0 gap-p25\">\n          {content_type && renderableContentTypes.includes(content_type) ?\n            <iframe\n              src={url}\n              style={{\n                minHeight: 0,\n              }}\n            ></iframe>\n          : <Chip\n              leftIcon={{ path: mdiFileDocumentOutline }}\n              value={content_type ?? \"Not found\"}\n            />\n          }\n        </FlexCol>\n      );\n    }\n  }\n\n  if (!contentOnly) {\n    const fullscreenTypes = [\"video\"];\n    return (\n      <div\n        className={`MediaViewer relative f-1 noselect flex-row min-h-0`}\n        style={style}\n      >\n        {mediaContent}\n        {type !== \"image\" && fullscreenTypes.includes(type) && (\n          <div\n            className={\"absolute w-full h-full pointer\"}\n            style={{ zIndex: 1, inset: 0 }}\n            onClick={(e) => {\n              e.stopPropagation();\n              e.preventDefault();\n              setIsFocused(true);\n            }}\n          />\n        )}\n      </div>\n    );\n  }\n  return mediaContent;\n};\n\nconst renderableContentTypes = [\n  // PDF Documents\n  \"application/pdf\",\n\n  \"text/plain\",\n  \"application/json\",\n  \"text/xml\",\n  \"application/xml\",\n  \"application/xhtml+xml\",\n\n  // Office Documents (with plugins or modern browsers)\n  \"application/vnd.ms-excel\",\n  \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n  \"application/vnd.ms-powerpoint\",\n  \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n  \"application/msword\",\n  \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n\n  // Rich Text\n  \"application/rtf\",\n  \"text/rtf\",\n\n  // 3D Models (modern browsers)\n  \"model/gltf+json\",\n  \"model/gltf-binary\",\n\n  // Markdown (some contexts)\n  \"text/markdown\",\n  \"text/x-markdown\",\n];\n"
  },
  {
    "path": "client/src/components/MenuList.css",
    "content": ".MenuList__button:hover {\n  background-color: #a4cafe;\n}\n"
  },
  {
    "path": "client/src/components/MenuList.tsx",
    "content": "import { mdiMenu } from \"@mdi/js\";\nimport React, { useMemo, useRef } from \"react\";\nimport type { TestSelectors } from \"../Testing\";\nimport { classOverride } from \"./Flex\";\nimport { Icon } from \"./Icon/Icon\";\nimport \"./List.css\";\nimport { MenuListItem, type MenuListitem } from \"./MenuListItem\";\nimport PopupMenu from \"./PopupMenu\";\nimport { useScrollFade } from \"./ScrollFade/ScrollFade\";\nimport { Select } from \"./Select/Select\";\n\nexport type MenuListProps = TestSelectors & {\n  items: MenuListitem[];\n  style?: React.CSSProperties;\n  className?: string;\n  id?: string;\n  variant?: \"horizontal-tabs\" | \"horizontal\" | \"vertical\" | \"dropdown\";\n  compactMode?: boolean;\n  activeKey?: string;\n};\n\nexport const MenuList = (props: MenuListProps) => {\n  const {\n    className = \"\",\n    style = {},\n    items = [],\n    compactMode,\n    variant,\n  } = props;\n\n  const visibleItems = useMemo(() => {\n    return items.filter((d) => !d.hide);\n  }, [items]);\n\n  const refList = useRef<HTMLUListElement>(null);\n  const { localVariant } = useMemo(() => {\n    const isDropDown = variant === \"dropdown\";\n    const localVariant = variant && !isDropDown ? variant : \"vertical\";\n    return { localVariant };\n  }, [variant]);\n\n  const { isCompactMode, rootStyle } = useMemo(() => {\n    const isCompactMode = Boolean(\n      compactMode && window.isMediumWidthScreen && localVariant === \"vertical\",\n    );\n\n    const variantStyle: React.CSSProperties =\n      localVariant === \"horizontal-tabs\" ?\n        {\n          borderRadius: 0,\n          fontSize: \"20px\",\n          fontWeight: \"bold\",\n          cursor: \"pointer\",\n        }\n      : {};\n    const rootStyle: React.CSSProperties = {\n      maxHeight: \"99vh\",\n      padding: 0,\n      ...variantStyle,\n      ...style,\n    };\n    return { isCompactMode, rootStyle };\n  }, [compactMode, localVariant, style]);\n\n  const { onKeyDownFocusSiblings } = useMemo(() => {\n    const onKeyDownFocusSiblings: React.KeyboardEventHandler<HTMLDivElement> = (\n      e,\n    ) => {\n      const { key } = e;\n      if (!refList.current) return;\n      const lastChild = refList.current.lastChild as HTMLLIElement,\n        firstChild = refList.current.firstChild as HTMLLIElement,\n        previousElementSibling = document.activeElement\n          ?.previousElementSibling as HTMLElement,\n        nextElementSibling = document.activeElement\n          ?.nextElementSibling as HTMLElement;\n\n      if (key === \"ArrowUp\") {\n        if (document.activeElement === firstChild) {\n          lastChild.focus();\n        } else if (refList.current.childElementCount) {\n          previousElementSibling.focus();\n        }\n        e.preventDefault();\n      } else if (key === \"ArrowDown\") {\n        if (document.activeElement === lastChild) {\n          firstChild.focus();\n        } else if (refList.current.childElementCount) {\n          nextElementSibling.focus();\n        }\n        e.preventDefault();\n      }\n    };\n    return { onKeyDownFocusSiblings };\n  }, []);\n\n  const isVertical = localVariant === \"vertical\";\n  const overflows = useScrollFade(refList.current);\n  const showSelect = !isVertical && overflows.x;\n\n  return (\n    <MenuListPopupWrapper\n      {...props}\n      isCompactMode={isCompactMode}\n      visibleItems={visibleItems}\n    >\n      <div\n        className={classOverride(\n          \"MenuList relative list-comp rounded \" +\n            (isVertical ? \" f-1 max-w-fit min-w-fit \" : \"flex-row\"),\n          className,\n        )}\n        data-command={props[\"data-command\"] ?? \"MenuList\"}\n        style={rootStyle}\n        onKeyDown={onKeyDownFocusSiblings}\n      >\n        <ul\n          className={`no-decor relative f-1 o-auto min-h-0 min-w-0 ${isCompactMode ? \"trigger-hover\" : \"\"} ${isVertical ? \" flex-col ws-nowrap \" : \"flex-row no-scroll-bar oy-hidden \"}`}\n          role=\"list\"\n          ref={refList}\n          style={{ padding: 0 }}\n          onWheel={\n            isVertical ? undefined : (\n              (e) => {\n                if (e.shiftKey) return;\n                // e.currentTarget.scrollLeft += e.deltaY;\n                e.currentTarget.scrollBy({\n                  left: e.deltaY,\n                  behavior: \"smooth\",\n                });\n              }\n            )\n          }\n        >\n          {visibleItems.map((d, i) => {\n            return (\n              <MenuListItem\n                key={d.key ?? i}\n                item={d}\n                {...props}\n                isCompactMode={isCompactMode}\n                noClick={false}\n              />\n            );\n          })}\n        </ul>\n        {showSelect && (\n          <Select\n            fullOptions={visibleItems.map(({ key, label }, i) => ({\n              key: key ?? i,\n              label: textContent(label),\n            }))}\n            value={props.activeKey}\n            btnProps={{\n              color: \"white\",\n              children: \"\",\n              variant: \"icon\",\n              className: \"h-full\",\n              size: \"default\",\n            }}\n            onChange={(key, e) => {\n              const item = visibleItems.find((d, i) => (d.key ?? i) === key);\n              refList.current\n                ?.querySelector(`[data-key=\"${key}\"]`)\n                ?.scrollIntoView({ behavior: \"smooth\" });\n              item?.onPress?.(e as any);\n            }}\n          />\n        )}\n      </div>\n    </MenuListPopupWrapper>\n  );\n};\n\nconst MenuListPopupWrapper = ({\n  children,\n  isCompactMode,\n  visibleItems,\n  ...props\n}: MenuListProps & {\n  children: React.ReactNode;\n  isCompactMode: boolean;\n  visibleItems: MenuListitem[];\n}) => {\n  const { activeKey, variant } = props;\n  const activeItem = useMemo(() => {\n    const activeItem =\n      visibleItems.find((d) => activeKey === (d.key ?? d.label)) ??\n      visibleItems[0]!;\n    return activeItem;\n  }, [visibleItems, activeKey]);\n\n  if (variant !== \"dropdown\") {\n    return children;\n  }\n\n  return (\n    <PopupMenu\n      style={{ width: \"100%\" }}\n      positioning=\"beneath-left\"\n      button={\n        <button\n          className=\"MenuList__button\"\n          style={{ width: \"100%\", borderRadius: 0 }}\n        >\n          <div className=\"flex-row ai-center\">\n            <MenuListItem\n              item={activeItem}\n              {...props}\n              isCompactMode={isCompactMode}\n              noClick={true}\n            />\n            <Icon path={mdiMenu} className=\"f-0 ml-auto\" size={1} />\n          </div>\n        </button>\n      }\n      onClickClose={true}\n      contentStyle={{ padding: 0 }}\n    >\n      {children}\n    </PopupMenu>\n  );\n};\n\nfunction textContent(elem: React.ReactElement | string): string {\n  if (!elem) {\n    return \"\";\n  }\n  if (typeof elem === \"string\") {\n    return elem;\n  }\n  const children = elem.props && elem.props.children;\n  if (children instanceof Array) {\n    return children.map(textContent).join(\" \");\n  }\n  return textContent(children);\n}\n"
  },
  {
    "path": "client/src/components/MenuListItem.tsx",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { Icon } from \"./Icon/Icon\";\nimport \"./List.css\";\nimport type { MenuListProps } from \"./MenuList\";\n\nexport type MenuListitem = {\n  key?: string;\n  label: React.ReactElement | string;\n  contentRight?: React.ReactNode;\n  leftIconPath?: string;\n  disabledText?: string;\n  title?: string;\n  onPress?: (e: React.MouseEvent | React.KeyboardEvent) => void;\n  style?: React.CSSProperties;\n  iconStyle?: React.CSSProperties;\n  labelStyle?: React.CSSProperties;\n  hide?: boolean;\n  listProps?:\n    | React.DetailedHTMLProps<\n        React.LiHTMLAttributes<HTMLLIElement>,\n        HTMLLIElement\n      >\n    | AnyObject;\n};\n\ntype Props = Pick<MenuListProps, \"activeKey\" | \"variant\"> & {\n  item: MenuListitem;\n  noClick: boolean;\n  isCompactMode: boolean;\n};\nexport const MenuListItem = ({\n  activeKey,\n  variant,\n  item,\n  noClick,\n  isCompactMode,\n}: Props) => {\n  const canPress = !!(item.onPress && !item.disabledText && !noClick);\n\n  const isActive = (item.key ?? item.label) === activeKey;\n  const { labelVariantStyle, ...itemProps } = useMemo(() => {\n    const variantStyle: React.CSSProperties =\n      variant === \"horizontal-tabs\" ?\n        {\n          borderRadius: 0,\n          fontSize: \"16px\",\n          fontWeight: \"bold\",\n          cursor: \"pointer\",\n          borderColor: \"var(--b-default)\",\n          borderBottomStyle: \"solid\",\n          borderBottomWidth: activeKey ? \"4px\" : \"1px\",\n          flex: 1,\n          color: \"var(--text-0)\",\n          ...(isActive && {\n            borderColor: \"var(--active)\",\n            backgroundColor: \"var(--bg-color-0)\",\n          }),\n        }\n      : {};\n    const labelVariantStyle: React.CSSProperties =\n      variant === \"horizontal-tabs\" ?\n        {\n          justifyContent: \"center\",\n        }\n      : {};\n    const style = {\n      ...variantStyle,\n      ...(item.style || {}),\n      ...(item.disabledText ? { cursor: \"not-allowed\", opacity: 0.5 } : {}),\n    };\n    const onClick: React.MouseEventHandler<HTMLLIElement> | undefined =\n      canPress ?\n        (e) => {\n          item.onPress?.(e);\n        }\n      : undefined;\n    const onKeyUp: React.KeyboardEventHandler<HTMLLIElement> | undefined =\n      canPress ?\n        (e) => {\n          if (e.key === \"Enter\") item.onPress?.(e);\n        }\n      : undefined;\n    return {\n      style,\n      onClick,\n      onKeyUp,\n      labelVariantStyle,\n    };\n  }, [activeKey, canPress, isActive, item, variant]);\n\n  return (\n    <li\n      {...item.listProps}\n      data-key={item.key}\n      role=\"option\"\n      tabIndex={canPress ? 0 : undefined}\n      title={item.disabledText || item.title}\n      className={`flex-row  p-p5  bg-li ${!item.disabledText && item.onPress ? \" pointer \" : \" \"} ${isActive ? \" selected \" : \"\"}`}\n      {...itemProps}\n      aria-current={isActive ? \"true\" : undefined}\n    >\n      <label\n        className=\"mr-p5 f-1 flex-row ai-center noselect\"\n        style={{ ...labelVariantStyle, cursor: \"inherit\" }}\n      >\n        {!!item.leftIconPath && (\n          <Icon\n            className=\"mr-p5 f-0\"\n            path={item.leftIconPath}\n            style={item.iconStyle}\n          />\n        )}\n        {item.label ?\n          <div\n            className={isCompactMode ? \"display-on-trigger-hover\" : \"\"}\n            style={item.labelStyle}\n          >\n            {item.label}\n          </div>\n        : item.label}{\" \"}\n        {item.contentRight || null}\n      </label>\n    </li>\n  );\n};\n"
  },
  {
    "path": "client/src/components/MonacoEditor/MonacoEditor.tsx",
    "content": "import { useEffectDeep, useMemoDeep } from \"prostgles-client\";\nimport { getKeys, isEqual, isObject, pickKeys } from \"prostgles-types\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { appTheme, useReactiveState } from \"../../appUtils\";\nimport type { LoadedSuggestions } from \"../../dashboard/Dashboard/dashboardUtils\";\n\nimport type { TestSelectors } from \"src/Testing\";\nimport {\n  CUSTOM_MONACO_SQL_THEMES,\n  defineCustomMonacoSQLTheme,\n} from \"../../dashboard/SQLEditor/defineCustomMonacoSQLTheme\";\nimport { getMonaco } from \"../../dashboard/SQLEditor/W_SQLEditor\";\nimport type { editor, Monaco } from \"../../dashboard/W_SQL/monacoEditorTypes\";\nimport { loadPSQLLanguage } from \"../../dashboard/W_SQL/MonacoLanguageRegister\";\nimport { isPlaywrightTest } from \"../../i18n/i18nUtils\";\nimport { useMonacoEditorAddActions } from \"./useMonacoEditorAddActions\";\n\nexport type MonacoEditorProps = Pick<TestSelectors, \"data-command\"> & {\n  language: string;\n  value: string;\n  onChange?: (\n    value: string,\n    editor: editor.IStandaloneCodeEditor,\n    monaco: undefined | Monaco,\n  ) => void;\n  className?: string;\n  /**\n   * @default true\n   */\n  expandSuggestionDocs?: boolean;\n  options?: editor.IStandaloneEditorConstructionOptions;\n  onMount?: (editor: editor.IStandaloneCodeEditor) => void;\n  style?: React.CSSProperties;\n  loadedSuggestions: LoadedSuggestions | undefined;\n  /**\n   * @default 200\n   */\n  minHeight?: number;\n};\n\nlet monacoPromise: Promise<Monaco> | undefined;\nlet monacoResolved: Monaco | undefined;\nconst useMonacoSingleton = () => {\n  const [monaco, setMonaco] = useState(monacoResolved);\n  useEffect(() => {\n    if (!monacoResolved) {\n      void (async () => {\n        monacoPromise ??= getMonaco();\n        monacoResolved = await monacoPromise;\n        await defineCustomMonacoSQLTheme();\n        setMonaco(monacoResolved);\n      })();\n    }\n  }, []);\n\n  return { monaco };\n};\n\n/** This wrapping check necessary to ensure:\n * - getTokens returns correct data\n * - opening json schema formfield does not cause cancelled promise errors (/server-settings)\n * */\nexport const MonacoEditor = (props: MonacoEditorProps) => {\n  const { loadedSuggestions } = props;\n\n  const [loadedLanguage, setLoadedLanguage] = useState(false);\n  useEffect(() => {\n    void loadPSQLLanguage(loadedSuggestions).then(() => {\n      setLoadedLanguage(true);\n    });\n  }, [loadedSuggestions]);\n\n  if (!loadedLanguage) {\n    return <div> </div>;\n  }\n  return <MonacoEditorWithoutLanguage {...props} />;\n};\n\nconst MonacoEditorWithoutLanguage = (props: MonacoEditorProps) => {\n  const [editor, setEditor] = React.useState<editor.IStandaloneCodeEditor>();\n  const container = React.useRef<HTMLDivElement>(null);\n  const { state: _appTheme } = useReactiveState(appTheme);\n\n  const {\n    language,\n    value,\n    options,\n    onMount,\n    onChange,\n    expandSuggestionDocs = true,\n    minHeight = 200,\n  } = props;\n\n  const valueRef = React.useRef(value);\n\n  const monacoRef = useRef<Monaco>();\n\n  const fullOptions = useMemoDeep(() => {\n    const themeFromOptions = options?.theme;\n    const theme =\n      themeFromOptions && themeFromOptions !== \"vs\" ?\n        themeFromOptions\n      : CUSTOM_MONACO_SQL_THEMES[_appTheme];\n    return {\n      ...options,\n      theme,\n    };\n  }, [_appTheme, options]);\n\n  const { monaco } = useMonacoSingleton();\n\n  useEffectDeep(() => {\n    if (!container.current || !monaco) return;\n\n    monacoRef.current = monaco;\n    const editorOptions: editor.IStandaloneEditorConstructionOptions = {\n      value: valueRef.current,\n      language,\n      ...fullOptions,\n      matchOnWordStartOnly: false,\n    };\n\n    const newEditor = monaco.editor.create(container.current, editorOptions);\n    hackyFixOptionmatchOnWordStartOnly(newEditor);\n    hackyShowDocumentationBecauseStorageServiceIsBrokenSinceV42(\n      newEditor,\n      expandSuggestionDocs,\n    );\n    /** Remove these keybindings from monaco */\n    monaco.editor.addKeybindingRules([\n      {\n        keybinding:\n          monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyR,\n        command: null,\n      },\n      {\n        keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK,\n        command: null,\n      },\n    ]);\n    setEditor(newEditor);\n    return () => {\n      newEditor.dispose();\n    };\n  }, [\n    monaco,\n    language,\n    /* we deal with fullOptions updates later on to ensure the theme switch doesn't reset monaco text */\n    container,\n    expandSuggestionDocs,\n  ]);\n\n  useEffect(() => {\n    if (!editor) return;\n    if (isPlaywrightTest && container.current) {\n      //@ts-ignore\n      container.current._getValue = () => {\n        return editor.getValue();\n      };\n      //@ts-ignore\n      container.current.editorRef = editor;\n    }\n    onMount?.(editor);\n  }, [editor, onMount]);\n\n  useMonacoEditorAddActions(editor, language);\n\n  useEffect(() => {\n    if (!editor) return;\n\n    if (onChange) {\n      editor.onDidChangeModelContent(() => {\n        const newValue = editor.getValue();\n        if (valueRef.current === newValue) return;\n        onChange(newValue, editor, monacoRef.current);\n      });\n    }\n  }, [editor, onChange]);\n\n  useEffect(() => {\n    if (!editor) return;\n    /** For some reason getRawOptions() returns stale theme from time to time */\n    const theme =\n      //@ts-ignore\n      editor._themeService?.getColorTheme().themeName ?? fullOptions.theme;\n    const currentEditorOptions = pickKeys(\n      { ...editor.getRawOptions(), theme },\n      getKeys(fullOptions as editor.IEditorOptions),\n    );\n    if (isEqual(currentEditorOptions, fullOptions)) return;\n    editor.updateOptions(fullOptions);\n  }, [editor, fullOptions]);\n\n  useEffect(() => {\n    if (editor && valueIsDifferentFromEditor(value, editor)) {\n      editor.setValue(value);\n    }\n  }, [value, editor]);\n\n  const { className, style } = props;\n\n  const monacoStyle: React.CSSProperties = useMemo(() => {\n    if (style) {\n      return {\n        ...style,\n        textAlign: \"initial\",\n      };\n    }\n    /**\n     * automaticLayout does not appear to work so we use this\n     */\n    return {\n      textAlign: \"initial\",\n      minHeight:\n        Math.min(minHeight, (2 + value.trim().split(\"\\n\").length) * 20) + \"px\",\n      flex: \"f-1\",\n    };\n  }, [value, style, minHeight]);\n\n  return (\n    <div\n      key={`${!!language.length}`}\n      ref={container}\n      style={monacoStyle}\n      data-command={props[\"data-command\"] ?? \"MonacoEditor\"}\n      className={`MonacoEditor  ${className}`}\n    />\n  );\n};\n\nconst valueIsDifferentFromEditor = (\n  v1: string,\n  editor: editor.IStandaloneCodeEditor,\n) => {\n  const v2 = editor.getValue();\n  const valuesDiffer = v1 !== v2;\n  try {\n    const v2 = editor.getValue();\n    if (valuesDiffer) {\n      const lang = editor.getModel()?.getLanguageId();\n      if (lang !== \"json\") return valuesDiffer;\n      const o1 = JSON.parse(v1);\n      const o2 = JSON.parse(v2);\n      const valuesMatch = isEqual(o1, o2);\n      return !valuesMatch;\n    }\n  } catch (error) {}\n  return valuesDiffer;\n};\n\nconst hackyShowDocumentationBecauseStorageServiceIsBrokenSinceV42 = (\n  editor: editor.IStandaloneCodeEditor,\n  expandSuggestionDocs = true,\n) => {\n  const sc = editor.getContribution(\"editor.contrib.suggestController\");\n  //@ts-ignore\n  if (sc?.widget) {\n    //@ts-ignore\n    const suggestWidget = sc.widget.value;\n    if (suggestWidget && suggestWidget._setDetailsVisible) {\n      // This will default to visible details. But when user switches it off\n      // they will remain switched off:\n      suggestWidget._setDetailsVisible(expandSuggestionDocs);\n    }\n    // I also wanted my widget to be shorter by default:\n    if (suggestWidget && suggestWidget._persistedSize) {\n      // suggestWidget._persistedSize.store({width: 200, height: 256});\n    }\n  }\n};\n\nconst hackyFixOptionmatchOnWordStartOnly = (\n  editor: editor.IStandaloneCodeEditor,\n) => {\n  try {\n    /* \n      118 for 0.50 \n      119 for 0.52.0\n      133 for 0.53 \n    */\n    const indexOfConfig = 133;\n    // ensure typing name matches relname\n    // suggestModel.js:420\n    //@ts-ignore\n    const confObj = editor._configuration?.options?._values?.[indexOfConfig];\n    if (!isObject(confObj)) {\n      console.error(\n        \"new monaco version might have broken hackyFixOptionmatchOnWordStartOnly again\",\n      );\n    }\n    if (confObj && \"matchOnWordStartOnly\" in confObj) {\n      //@ts-ignore\n      editor._configuration.options._values[\n        indexOfConfig\n      ].matchOnWordStartOnly = false;\n    }\n  } catch (e) {}\n};\n\nexport const MONACO_READONLY_DEFAULT_OPTIONS = {\n  minimap: { enabled: false },\n  lineNumbers: \"off\",\n  tabSize: 2,\n  padding: { top: 10 },\n  scrollBeyondLastLine: false,\n  automaticLayout: true,\n  lineHeight: 19,\n  readOnly: true,\n} satisfies editor.IStandaloneEditorConstructionOptions;\n"
  },
  {
    "path": "client/src/components/MonacoEditor/useMonacoEditorAddActions.ts",
    "content": "import type { editor } from \"monaco-editor\";\nimport { useEffect } from \"react\";\nimport { LANG } from \"src/dashboard/SQLEditor/W_SQLEditor\";\n\nexport const useMonacoEditorAddActions = (\n  editor: editor.IStandaloneCodeEditor | undefined,\n  language: string,\n) => {\n  useEffect(() => {\n    if (!editor) return;\n\n    editor.addAction({\n      id: \"googleSearch\",\n      label: \"Search with Google\",\n      // keybindings: [m.KeyMod.CtrlCmd | m.KeyCode.KEY_V],\n      contextMenuGroupId: \"navigation\",\n      run: (editor) => {\n        window.open(\n          \"https://www.google.com/search?q=\" + getSelectedText(editor),\n        );\n      },\n    });\n    // editor.addAction({\n    //   id: \"savedwa\",\n    //   label: \"Save (Ctrl + S)\",\n    //   keybindings: [KeyMod.CtrlCmd | KeyCode.KeyS],\n    //   contextMenuGroupId: \"navigation\",\n    //   run: (editor) => {\n    //     alert(\"Save action triggered\");\n    //   },\n    // });\n\n    if (language !== LANG) return;\n    editor.addAction({\n      id: \"googleSearchPG\",\n      label: \"Search with Google Postgres\",\n      // keybindings: [m.KeyMod.CtrlCmd | m.KeyCode.KEY_V],\n      contextMenuGroupId: \"navigation\",\n      run: (editor) => {\n        window.open(\n          \"https://www.google.com/search?q=postgres+\" + getSelectedText(editor),\n        );\n      },\n    });\n  }, [editor, language]);\n};\n\nexport const getSelectedText = (\n  editor: editor.ICodeEditor | editor.IStandaloneCodeEditor | undefined,\n): string => {\n  if (!editor) return \"\";\n  const model = editor.getModel();\n  const selection = editor.getSelection();\n  if (!model || !selection) return \"\";\n  return model.getValueInRange(selection);\n};\n"
  },
  {
    "path": "client/src/components/MonacoEditor/useWhyDidYouUpdate.ts",
    "content": "import React, { useEffect, useRef } from \"react\";\n\nexport function useWhyDidYouUpdate(props) {\n  // Store previous props in a ref\n  const prevProps = useRef<any>();\n  const parentComponents = useParentComponents(6);\n  const parentComponentNames = parentComponents.names.join(\" > \");\n  const parentsInfo = [parentComponentNames, parentComponents.parents];\n  useEffect(() => {\n    if (prevProps.current) {\n      // Get all keys from current and previous props\n      const allKeys = Object.keys({ ...prevProps.current, ...props });\n      const changesObj = {};\n\n      // Check which props have changed\n      allKeys.forEach((key) => {\n        if (prevProps.current[key] !== props[key]) {\n          changesObj[key] = {\n            from: prevProps.current[key],\n            to: props[key],\n          };\n        }\n      });\n\n      // If there are changes, log them\n      if (Object.keys(changesObj).length) {\n        console.log(\"RENDERED\", changesObj, parentsInfo);\n      }\n    }\n\n    // Update prev props ref\n    prevProps.current = props;\n  });\n\n  useEffect(() => {\n    console.log(\"MOUNTED\", props, parentsInfo);\n\n    return () => {\n      console.log(\"UNMOUNTED\", props, parentsInfo);\n    };\n  }, []);\n}\n\nexport const getReactInternals = (): ReactInternals =>\n  React[\"__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED\"];\n\nconst useParentComponents = (maxDepth = 1) => {\n  const internals = getReactInternals();\n  // const stack = internals.ReactDebugCurrentFrame.getCurrentStack();\n  // console.log(stack);\n  return getParents(internals.ReactCurrentOwner.current, maxDepth);\n};\n\nconst getParents = (current: RenderedComponentInfo, maxDepth = 1) => {\n  const parents: RenderedComponentInfo[] = [];\n  const names: string[] = [];\n  let currentComponent: RenderedComponentInfo | null = current;\n  while (currentComponent && maxDepth--) {\n    names.push(currentComponent.elementType.name);\n    parents.push(currentComponent);\n    currentComponent = currentComponent._debugOwner;\n  }\n  return { names, parents };\n};\n\ntype ReactInternals = {\n  ReactDebugCurrentFrame: {\n    getCurrentStack: () => string;\n  };\n  ReactCurrentOwner: {\n    current: RenderedComponentInfo;\n  };\n};\n\ntype RenderedComponentInfo = {\n  elementType: {\n    name: string;\n  };\n  _debugOwner: RenderedComponentInfo | null;\n};\n"
  },
  {
    "path": "client/src/components/MonacoLogRenderer/MonacoLogRenderer.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport { mdiFullscreen } from \"@mdi/js\";\nimport type { editor } from \"monaco-editor\";\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { CodeEditor } from \"src/dashboard/CodeEditor/CodeEditor\";\nimport stripAnsi from \"strip-ansi\";\n\nexport const MonacoLogRenderer = ({\n  logs,\n  label,\n}: {\n  logs: string;\n  label: string;\n}) => {\n  const onMount = useCallback((editor: editor.IStandaloneCodeEditor) => {\n    const scrollToLastLine = () => {\n      const lineCount = editor.getModel()?.getLineCount();\n      editor.revealLineInCenter(lineCount ?? 1);\n    };\n    const disposable = editor.onDidChangeModelContent(scrollToLastLine);\n    scrollToLastLine();\n    return () => {\n      disposable.dispose();\n    };\n  }, []);\n\n  const [fullscreen, setFullscreen] = useState(false);\n\n  const logsWithoutAnsi = useMemo(() => stripAnsi(logs), [logs]);\n\n  /** Close on escape */\n  useEffect(() => {\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\" && fullscreen) {\n        setFullscreen(false);\n      }\n    };\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      window.removeEventListener(\"keydown\", onKeyDown);\n    };\n  }, [fullscreen]);\n\n  return (\n    <FlexCol\n      className=\"bg-color-0 gap-p25\"\n      style={\n        fullscreen ?\n          {\n            position: \"fixed\",\n            top: 0,\n            left: 0,\n            width: \"100vw\",\n            height: \"100vh\",\n            zIndex: 1000,\n          }\n        : undefined\n      }\n    >\n      <FlexRow>\n        <Label variant=\"normal\" className={\"f-1\" + (fullscreen ? \" px-1\" : \"\")}>\n          {label}\n        </Label>\n        <Btn\n          iconPath={mdiFullscreen}\n          onClick={() => setFullscreen(!fullscreen)}\n        />\n      </FlexRow>\n      <CodeEditor\n        style={{\n          minWidth: \"400px\",\n          width: \"100%\",\n          maxHeight: fullscreen ? undefined : \"100px\",\n          height: fullscreen ? \"100%\" : undefined,\n          overflow: \"hidden\",\n          flex: 1,\n        }}\n        minHeight={100}\n        value={logsWithoutAnsi}\n        onMount={onMount}\n        options={options}\n        language=\"bash\"\n      />\n    </FlexCol>\n  );\n};\n\nconst options = {\n  minimap: { enabled: false },\n  lineNumbers: \"off\",\n  scrollBeyondLastLine: false,\n  automaticLayout: true,\n} satisfies editor.IStandaloneEditorConstructionOptions;\n"
  },
  {
    "path": "client/src/components/NavBar/NavBar.css",
    "content": ":root {\n  --nav-height: 50px;\n}\n.bg-nav-color {\n  background-color: black;\n}\n\nnav.main {\n  /* box-shadow: #656565 0px 2px 4px; */\n  z-index: 2;\n  position: fixed;\n  width: 100%;\n}\n\n.navwrapper {\n  align-items: center;\n}\n\n.navwrapper a {\n  padding: 12px 12px;\n  border-top: 4px solid transparent;\n  border-bottom-width: 4px;\n  border-bottom-style: solid;\n  border-bottom-color: transparent;\n}\n\nnav a {\n  text-decoration: none;\n}\n\nnav a:focus {\n  outline: none;\n}\n\n.navwrapper a.active {\n  color: var(--active);\n  border-bottom-color: var(--active);\n}\n.navwrapper a:hover:not(.active),\n.navwrapper a:not(.active):focus {\n  color: var(--active-hover);\n  border-bottom-color: var(--active-hover);\n}\n\n@media only screen and (max-width: 600px) {\n  nav {\n    flex-direction: column;\n  }\n  nav.mobile-expanded {\n    position: fixed;\n  }\n  nav .navwrapper {\n    margin: 0;\n    flex-direction: column;\n    align-items: unset;\n    justify-content: center;\n  }\n\n  nav .navwrapper > a {\n    padding: 10px 0px;\n    margin-left: 0;\n    border-bottom: none;\n    border-left-width: 4px;\n    border-left-style: solid;\n    border-left-color: transparent;\n    padding-left: 2em;\n  }\n\n  .prgl-brand-icon {\n    justify-content: start;\n    margin-right: 0;\n    height: fit-content;\n  }\n\n  .prgl-brand-icon img {\n    margin-right: 0;\n  }\n\n  nav.mobile-collapsed .navwrapper > * {\n    display: none !important;\n  }\n  nav.mobile-expanded .prgl-brand-icon {\n    display: none !important;\n  }\n\n  nav button.hamburger {\n    display: flex;\n    background: transparent;\n  }\n}\n\n/* \n.navwrapper > a:hover {\n  border-bottom-color: #a7dfff;\n} */\n\nbutton.hamburger,\nbutton.hamburger:focus {\n  margin: 0.5em;\n  padding: 0;\n  border: none;\n  outline: 0;\n  width: fit-content;\n  height: fit-content;\n  border-radius: unset;\n}\nbutton.hamburger > * {\n  width: 32px;\n  height: 32px;\n}\n"
  },
  {
    "path": "client/src/components/NavBar/NavBar.tsx",
    "content": "import React, { useState } from \"react\";\nimport { NavLink } from \"react-router-dom\";\nimport \"./NavBar.css\";\n\nimport { mdiArrowLeft, mdiClose, mdiMenu } from \"@mdi/js\";\nimport { useNavigate } from \"react-router-dom\";\nimport type { ProstglesState } from \"@common/electronInitTypes\";\nimport type { ClientUser, Prgl } from \"../../App\";\nimport { AccountMenu } from \"../../pages/AccountMenu\";\nimport ClickCatch from \"../ClickCatch\";\nimport { Icon } from \"../Icon/Icon\";\n\ntype P = {\n  title?: string;\n  options?: {\n    label: string;\n    href?: string;\n    to?: string;\n    exact?: boolean;\n    iconPath?: string;\n  }[];\n  user: ClientUser | undefined;\n  serverState: ProstglesState | undefined;\n  endContent?: React.ReactNode;\n} & Pick<Partial<Prgl>, \"dbsMethods\" | \"dbs\">;\n\nexport const NavBar = (props: P) => {\n  const [navCollapsed, setNavCollapsed] = useState(true);\n  const navigate = useNavigate();\n  const { options = [], title, user, serverState, endContent } = props;\n  const endContentWrapped =\n    endContent ?\n      <div\n        className=\"NavBar_endContent\"\n        onClick={(e) => {\n          e.stopPropagation();\n          e.preventDefault();\n        }}\n      >\n        {endContent}\n      </div>\n    : null;\n\n  const MenuButton = !title && (\n    <button\n      data-command=\"NavBar.mobileMenuToggle\"\n      className=\"hamburger hidden ml-auto text-0\"\n      style={{\n        alignSelf: \"flex-end\",\n      }}\n      onClick={() => {\n        setNavCollapsed(!navCollapsed);\n      }}\n    >\n      <Icon path={navCollapsed ? mdiMenu : mdiClose} size={1.5} />\n    </button>\n  );\n\n  return (\n    <>\n      {!navCollapsed && (\n        <ClickCatch\n          style={{ zIndex: 1 }}\n          onClick={() => setNavCollapsed(true)}\n        />\n      )}\n      <nav\n        data-command=\"NavBar\"\n        className={\n          \"flex-row jc-center noselect w-full text-1p5 shadow-l bg-color-0 \" +\n          (navCollapsed ? \" mobile-collapsed \" : \" mobile-expanded pb-1 \")\n        }\n        style={{\n          zIndex: 2,\n        }}\n      >\n        <div className=\"flex-row f-1\" style={{ maxWidth: \"970px\" }}>\n          {(navCollapsed || !window.isMobileDevice) && (\n            <NavLink\n              title={document.head.dataset.version}\n              to={\"/\"}\n              className=\"prgl-brand-icon flex-row ai-center jc-center\"\n            >\n              <img className=\"p-p5 px-1 mr-2 \" src=\"/prostgles-logo.svg\" />\n            </NavLink>\n          )}\n\n          <div className={\"flex-col f-1\"}>\n            <div\n              className=\"navwrapper flex-row gap-p5 ai-center\"\n              style={{ order: 2 }}\n              onClick={() => {\n                setNavCollapsed(true);\n              }}\n            >\n              {title ?\n                <div\n                  className=\"flex-row ai-center \"\n                  style={{ minHeight: \"60px\" }}\n                >\n                  <button\n                    className=\"f-0 ml-1\"\n                    onClick={() => {\n                      navigate(-1);\n                    }}\n                  >\n                    <Icon path={mdiArrowLeft} size={1} />\n                  </button>\n                  <div className=\"f-1 ml-1\">{title}</div>\n                </div>\n              : options.map((o, i) =>\n                  o.href ?\n                    <a\n                      key={i}\n                      href={o.href}\n                      data-key={o.href}\n                      className={\n                        \"text-0 hover ai-center px-1 pt-1 bb font-16  \"\n                      }\n                    >\n                      {\" \"}\n                      {o.label}{\" \"}\n                    </a>\n                  : <NavLink\n                      key={i}\n                      to={o.to as any}\n                      data-key={o.to}\n                      className={\n                        \"text-0 hover gap-p5 flex-row ai-center fs-1 px-1 pt-1 bb font-16\"\n                      }\n                    >\n                      {o.iconPath && <Icon className=\"f-0\" path={o.iconPath} />}\n                      <div className=\"fs-1 ws-no-wrap text-ellipsis ws-nowrap\">\n                        {o.label}\n                      </div>\n                    </NavLink>,\n                )\n              }\n              {/* spacer */}\n              <div className=\"f-1\"></div>\n              {!serverState?.isElectron && (\n                <AccountMenu user={user} forNavBar={true} />\n              )}\n              {endContentWrapped}\n            </div>\n            {MenuButton}\n          </div>\n        </div>\n      </nav>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/components/NavBar/NavBarWrapper.tsx",
    "content": "import React from \"react\";\nimport { NavLink } from \"react-router-dom\";\nimport type { PrglState } from \"../../App\";\nimport \"../../App.css\";\nimport { t } from \"../../i18n/i18nUtils\";\n\nimport { LanguageSelector } from \"../../i18n/LanguageSelector\";\nimport { ThemeSelector, type ThemeOption } from \"../../theme/ThemeSelector\";\nimport { FlexRow } from \"../Flex\";\nimport { NavBar } from \"./NavBar\";\nimport { useNavBarItems } from \"./useNavBarItems\";\n\ntype NavBarWrapperProps = {\n  children: React.ReactNode;\n  needsUser: boolean;\n  extraProps: PrglState;\n  userThemeOption: ThemeOption;\n};\nexport const NavBarWrapper = (props: NavBarWrapperProps) => {\n  const { children, needsUser, extraProps, userThemeOption } = props;\n  const { dbs, dbsMethods, serverState, auth, user } = extraProps;\n  // const withNavBar = (content: React.ReactNode, ) => {\n  const showLoginRegister =\n    needsUser && !extraProps.user && !extraProps.auth.user;\n\n  const options = useNavBarItems(extraProps);\n  return (\n    <div className=\"flex-col ai-center w-full f-1 min-h-0\">\n      <NavBar\n        dbs={dbs}\n        dbsMethods={dbsMethods}\n        serverState={serverState}\n        user={auth.user}\n        options={options}\n        endContent={\n          <FlexRow className={window.isLowWidthScreen ? \"ml-2\" : \"\"}>\n            <ThemeSelector\n              userId={user?.id}\n              dbs={dbs}\n              serverState={serverState}\n              userThemeOption={userThemeOption}\n            />\n            <LanguageSelector isElectron={!!serverState.isElectron} />\n          </FlexRow>\n        }\n      />\n      {showLoginRegister ?\n        <div className=\"flex-col jc-center ai-center h-full gap-2 m-2\">\n          <NavLink to=\"login\">{t.common.Login}</NavLink>\n          <NavLink to=\"register\">{t.common.Register}</NavLink>\n        </div>\n      : children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/NavBar/useNavBarItems.ts",
    "content": "import { ROUTES } from \"@common/utils\";\nimport {\n  mdiAccountMultiple,\n  mdiServerNetwork,\n  mdiServerSecurity,\n} from \"@mdi/js\";\nimport { useMemo } from \"react\";\nimport type { PrglState } from \"src/App\";\nimport { t } from \"src/i18n/i18nUtils\";\nimport { isDefined } from \"src/utils/utils\";\n\nexport const useNavBarItems = ({ user, serverState }: PrglState) => {\n  return useMemo(() => {\n    return [\n      {\n        label: t[\"App\"][\"Connections\"],\n        to: ROUTES.CONNECTIONS,\n        iconPath: mdiServerNetwork,\n      },\n      serverState.isElectron ? undefined : (\n        {\n          label: t[\"App\"][\"Users\"],\n          to: ROUTES.USERS,\n          forAdmin: true,\n          iconPath: mdiAccountMultiple,\n        }\n      ),\n      {\n        label: t[\"App\"][\"Server settings\"],\n        to: ROUTES.SERVER_SETTINGS,\n        forAdmin: true,\n        iconPath: mdiServerSecurity,\n      },\n\n      // { label: \"Permissions\", to: \"/access-management\", forAdmin: true },\n    ]\n      .filter(isDefined)\n      .filter((o) => !o.forAdmin || user?.type === \"admin\");\n  }, [user, serverState]);\n};\n"
  },
  {
    "path": "client/src/components/Pan.tsx",
    "content": "import React, { useEffect } from \"react\";\nimport type { TestSelectors } from \"../Testing\";\nimport { setPan, type PanListeners } from \"../dashboard/setPan\";\n\ntype PanProps = TestSelectors &\n  PanListeners & {\n    /** Setting zIndex AND position absolute allows \"clicking\" through any popups that obscure this element */\n    style?: Omit<React.CSSProperties, \"zIndex\">;\n    className?: string;\n    threshold?: number;\n    children?: React.ReactNode;\n  };\n\nexport const Pan = (props: PanProps) => {\n  const {\n    style,\n    className,\n    children,\n    id,\n    onPanStart,\n    onPan,\n    onDoubleTap,\n    threshold,\n    onPanEnd,\n    onRelease,\n    onPress,\n  } = props;\n  const ref = React.useRef<HTMLDivElement>(null);\n  if ((style as React.CSSProperties | undefined)?.zIndex !== undefined) {\n    throw new Error(\n      \"Setting zIndex AND position absolute allows clicking through any popups that obscure this element.\",\n    );\n  }\n  useEffect(() => {\n    if (!ref.current) {\n      return;\n    }\n    return setPan(ref.current, {\n      onPanStart,\n      onPan,\n      onPanEnd,\n      onRelease,\n      onPress,\n      threshold,\n      onDoubleTap,\n    });\n  }, [onDoubleTap, onPan, onPanEnd, onPanStart, onPress, onRelease, threshold]);\n\n  return (\n    <div\n      ref={ref}\n      id={id}\n      data-command={props[\"data-command\"]}\n      data-key={props[\"data-key\"]}\n      style={style}\n      className={className}\n    >\n      {children}\n    </div>\n  );\n  // }\n};\n"
  },
  {
    "path": "client/src/components/Popup/Footer.tsx",
    "content": "import ErrorComponent, { ErrorTrap } from \"@components/ErrorComponent\";\nimport { classOverride, FlexRow } from \"@components/Flex\";\nimport React from \"react\";\nimport type { TestSelectors } from \"src/Testing\";\n\ntype FooterProps = TestSelectors & {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n  error?: any;\n};\nexport const Footer = ({\n  children,\n  className,\n  style,\n  error,\n  ...testSelectors\n}: FooterProps) => {\n  return (\n    <ErrorTrap>\n      <footer\n        {...testSelectors}\n        style={style}\n        className={classOverride(\n          \"Footer bt b-color flex-row-wrap px-1 pt-1 jc-end \" +\n            (window.isMobileDevice ? \" gap-p5 \" : \" gap-1 \"),\n          className,\n        )}\n      >\n        <ErrorComponent\n          className=\"f-1\"\n          withIcon={true}\n          variant=\"outlined\"\n          error={error}\n          style={{ maxHeight: \"150px\", minHeight: 0, overflow: \"auto\" }}\n        />\n        <FlexRow className=\"f-1\">{children}</FlexRow>\n      </footer>\n    </ErrorTrap>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Popup/FooterButtons.tsx",
    "content": "import React from \"react\";\nimport type { PopupProps } from \"./Popup\";\nimport { Footer } from \"./Footer\";\nimport { isDefined, omitKeys } from \"prostgles-types\";\nimport Btn, { type BtnProps } from \"../Btn\";\nimport type { TestSelectors } from \"../../Testing\";\n\nexport type FooterButton =\n  | (\n      | { node: React.ReactNode }\n      | ({\n          label: string;\n          onClickClose?: boolean;\n        } & Omit<BtnProps<void>, \"label\">)\n    )\n  | undefined;\n\nexport type FooterButtonsProps = TestSelectors &\n  Pick<PopupProps, \"footerButtons\" | \"footer\" | \"onClose\"> & {\n    className?: string;\n    style?: React.CSSProperties;\n    error?: any;\n  };\nexport const FooterButtons = ({\n  footerButtons = [],\n  footer,\n  onClose,\n  ...divProps\n}: FooterButtonsProps) => {\n  const bottomBtns = (\n    typeof footerButtons === \"function\" ?\n      footerButtons(onClose)\n    : footerButtons).filter(isDefined);\n  if (!bottomBtns.length && !footer) {\n    return null;\n  }\n  return (\n    <Footer {...divProps} style={{ padding: \"1em\" }}>\n      {footer}\n      {bottomBtns.map((b, i: any) => {\n        if (\"node\" in b)\n          return <React.Fragment key={i}>{b.node}</React.Fragment>;\n        return (\n          <Btn\n            key={i}\n            {...(omitKeys(b, [\"label\", \"onClickClose\", \"onClick\"]) as any)}\n            onClick={(e) => {\n              if (b.onClickClose && onClose) onClose(e);\n              else if (b.onClick) b.onClick(e);\n              else console.error(\"Button is missing click handler\");\n            }}\n          >\n            {b.label}\n          </Btn>\n        );\n      })}\n    </Footer>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Popup/Popup.css",
    "content": ".box {\n  clip-path: circle(15% at 50% 10%);\n  /* transition: clip-path 1s; */\n}\n\n.box:hover {\n  clip-path: circle(25%);\n}\n\n\n.popup-component-root.positioning_tooltip {\n  pointer-events: none ;\n  touch-action: none;\n}"
  },
  {
    "path": "client/src/components/Popup/Popup.tsx",
    "content": "import { isObject, pickKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport type { Command, TestSelectors } from \"../../Testing\";\nimport RTComp, { type DeltaOf } from \"../../dashboard/RTComp\";\nimport { ClickCatchOverlay } from \"../ClickCatchOverlay\";\nimport { ErrorTrap } from \"../ErrorComponent\";\nimport { classOverride } from \"../Flex\";\nimport {\n  FooterButtons,\n  type FooterButton,\n  type FooterButtonsProps,\n} from \"./FooterButtons\";\nimport \"./Popup.css\";\nimport { PopupHeader } from \"./PopupHeader\";\nimport { getPopupStyle } from \"./getPopupStyle\";\nimport { popupCheckPosition } from \"./popupCheckPosition\";\n\nlet modalRoot: HTMLElement | null = null;\nexport const getModalRoot = (forPointer = false) => {\n  const id = forPointer ? \"pointer-root\" : \"modal-root\";\n  let node = document.getElementById(id);\n  if (!node) {\n    node = document.createElement(\"div\");\n    node.setAttribute(\"id\", id);\n    document.body.appendChild(node);\n  }\n  if (!forPointer) {\n    modalRoot = node;\n  }\n  return node;\n};\ngetModalRoot();\n\nexport const POPUP_CLASSES = {\n  root: \"popup-component-root\",\n  rootChild: \"popup-component-root-child\",\n  title: \"POPUP-TITLE\",\n  content: \"POPUP-CONTENT\",\n};\n\n/**\n * Must be above monaco editor minimap (z-index: 5)\n */\nexport const POPUP_ZINDEX = 5;\nexport type PopupProps = TestSelectors & {\n  /**\n   * On click away (click catch)\n   */\n  onClose?: (\n    e:\n      | KeyboardEvent\n      | React.MouseEvent<HTMLDivElement, MouseEvent>\n      | React.MouseEvent<HTMLButtonElement, MouseEvent>,\n  ) => void;\n  title?: React.ReactNode | ((rootDiv: HTMLDivElement) => React.ReactNode);\n  subTitle?: string;\n  headerRightContent?: React.ReactNode;\n  content?: React.ReactNode;\n  footer?: React.ReactNode;\n  contentClassName?: string;\n  rootChildClassname?: string;\n  contentStyle?: React.CSSProperties;\n\n  children?: React.ReactNode;\n  clickCatchStyle?: React.CSSProperties;\n  rootStyle?: React.CSSProperties;\n  rootChildStyle?: React.CSSProperties;\n  /**\n   * Close popup on clicking content. Used for menus\n   * */\n  onClickClose?: boolean;\n  footerButtons?:\n    | FooterButton[]\n    | ((popupClose: PopupProps[\"onClose\"]) => FooterButton[]);\n  anchorEl?: HTMLElement | Element;\n  /**\n   * Number of pixels to offset from anchor\n   */\n  anchorPadding?: number;\n  anchorXY?: { x: number; y: number };\n  positioning?:\n    | \"fullscreen\"\n    | \"center\"\n    | \"top-center\"\n    | \"beneath-center\"\n    | \"beneath-right\"\n    | \"beneath-left\"\n    | \"beneath-left-minfill\"\n    | \"inside\"\n    | \"tooltip\"\n    | \"inside-top-center\"\n    | \"above-center\"\n    | \"as-is\"\n    | \"left\"\n    | \"right-panel\";\n  focusTrap?: boolean;\n  /**\n   * If true then top left corner position will change only for bigger content\n   * Used to prevent content jumping\n   */\n  fixedTopLeft?: boolean;\n  autoFocusFirst?: \"header\" | \"content\" | { selector: string };\n  onKeyDown?: (e: KeyboardEvent, section: \"header\" | \"content\") => void;\n  collapsible?:\n    | boolean\n    | {\n        defaultValue: boolean;\n      };\n  showFullscreenToggle?: {\n    defaultValue?: boolean;\n    getStyle?: (fullscreen: boolean) => React.CSSProperties;\n    getContentStyle?: (fullscreen: boolean) => React.CSSProperties;\n  };\n  persistInitialSize?: boolean;\n  /**\n   * Callback for when the popup content finished resizing\n   */\n  onContentFinishedResizing?: VoidFunction;\n};\n\nconst FOCUSABLE_ELEMS_SELECTOR =\n  \"a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]\";\n\nexport type PopupState = {\n  prevStateStyle?: React.CSSProperties;\n  stateStyle: React.CSSProperties;\n  collapsed?: boolean;\n  fullScreen?: boolean;\n};\n\nexport default class Popup extends RTComp<PopupProps, PopupState> {\n  el: HTMLElement = document.createElement(\"div\");\n  ref?: HTMLDivElement;\n\n  state: PopupState = {\n    stateStyle: {\n      opacity: 0,\n    },\n  };\n\n  checkFocus = (e: KeyboardEvent) => {\n    const { onClose, focusTrap = true, onKeyDown } = this.props;\n    const isTheTopFocusedPopup = !this.ref?.parentElement?.nextSibling;\n    if (!isTheTopFocusedPopup) {\n      return;\n    }\n\n    if (onKeyDown) {\n      onKeyDown(\n        e,\n        document.activeElement?.closest(\"header.\" + POPUP_CLASSES.title) ?\n          \"header\"\n        : \"content\",\n      );\n    }\n    if (focusTrap && e.key === \"Tab\") {\n      const fcsbl =\n        this.el\n          .querySelector(\".\" + POPUP_CLASSES.root)\n          ?.querySelectorAll<HTMLDivElement>(FOCUSABLE_ELEMS_SELECTOR) ?? [];\n      if (e.shiftKey && document.activeElement === (fcsbl[0] as Node)) {\n        e.preventDefault();\n        if (fcsbl.length) fcsbl[fcsbl.length - 1]?.focus();\n      }\n      if (\n        !e.shiftKey &&\n        (document.activeElement === fcsbl[fcsbl.length - 1] ||\n          !this.el.contains(document.activeElement))\n      ) {\n        e.preventDefault();\n        if (fcsbl.length) {\n          fcsbl[0]?.focus();\n        }\n      }\n    }\n\n    if (e.key === \"Escape\") {\n      onClose?.(e);\n    }\n  };\n\n  rObserver?: ResizeObserver;\n  onMount() {\n    this.el.classList.add(\"w-fit\", \"h-fit\");\n    if (modalRoot) modalRoot.appendChild(this.el);\n    setTimeout(() => {\n      this.checkPosition();\n      const { autoFocusFirst } = this.props;\n      if (!this.ref) return;\n      if (autoFocusFirst) {\n        const selector =\n          typeof autoFocusFirst === \"object\" ?\n            autoFocusFirst.selector\n          : \".\" +\n            (autoFocusFirst === \"header\" ?\n              POPUP_CLASSES.title\n            : POPUP_CLASSES.content);\n        const container =\n          this.ref.querySelector<HTMLDivElement>(selector) ?? this.ref;\n        const firstInputLike = Array.from(\n          container.querySelectorAll<\n            HTMLButtonElement | HTMLSelectElement | HTMLTextAreaElement\n          >('input:not([type=\"hidden\"]), textarea, button'),\n        );\n\n        const firstFocusable = [...firstInputLike].find((e) => {\n          return (\n            getComputedStyle(e).display !== \"none\" &&\n            getComputedStyle(e).opacity !== \"0\" &&\n            !e.closest(\".\" + DATA_NULLABLE) &&\n            !e.closest(\".\" + DATA_HAS_VALUE)\n          );\n        });\n        (firstFocusable ?? container).focus();\n      }\n    }, 200);\n    window.document.body.addEventListener(\"keydown\", this.checkFocus);\n\n    if (!this.ref) return;\n    this.rObserver = new ResizeObserver((a) => {\n      const justToggledFullScreen = Date.now() - this.toggledFullScreen < 100;\n      if (this.props.persistInitialSize || justToggledFullScreen) return;\n      this.checkPosition();\n    });\n\n    this.rObserver.observe(this.ref);\n  }\n\n  /**\n   * Added to improve performance when rendering a monaco editor with a lot of content fullscreen\n   */\n  setMainNodeVisibility = (visible: boolean) => {\n    const mainNode = document.querySelector(\"main\")!;\n    const isVisible = mainNode.style.visibility !== \"hidden\";\n    if (isVisible !== visible) {\n      mainNode.style.visibility = visible ? \"visible\" : \"hidden\";\n    }\n  };\n\n  toggledFullScreen = 0;\n  onDelta(deltaP: DeltaOf<PopupProps>, deltaS: DeltaOf<PopupState>): void {\n    if (deltaS && \"fullScreen\" in deltaS) {\n      this.toggledFullScreen = Date.now();\n    }\n    this.setMainNodeVisibility(this.state.fullScreen !== true);\n  }\n\n  onUnmount() {\n    this.setMainNodeVisibility(true);\n    if (modalRoot) {\n      try {\n        modalRoot.removeChild(this.el);\n      } catch (e) {\n        console.error(e);\n      }\n    }\n    window.document.body.removeEventListener(\"keydown\", this.checkFocus);\n    if (this.ref) {\n      this.rObserver?.unobserve(this.ref);\n    }\n  }\n\n  position?: {\n    x: number;\n    y: number;\n    /** Used for \"center-fixed-top-left\" positioning */\n    xMin: number;\n    yMin: number;\n    opacity: number;\n  };\n  /**\n   * Used to prevent size change jiggle and things moving\n   */\n  prevSize?: {\n    width: number;\n    height: number;\n  };\n\n  /**\n   * Allow the content to grow within the first 250ms before setting opacity to 1\n   */\n  mountedAt = Date.now();\n  checkPositionOpacity = {\n    started: Date.now(),\n    done: false,\n    timeout: undefined as ReturnType<typeof setTimeout> | undefined,\n  };\n  checkPosition = popupCheckPosition.bind(this);\n  prevStateStyles: PopupState[\"stateStyle\"][] = [];\n  render() {\n    const defaultContentClassName =\n      this.props.title && !window.isLowWidthScreen ? \"p-1 pl-2\" : \"p-1\";\n    const {\n      onClose,\n      positioning,\n      content,\n      children,\n      clickCatchStyle = {},\n      rootStyle = {},\n      rootChildStyle = {},\n      rootChildClassname,\n      onClickClose,\n      contentClassName = defaultContentClassName,\n      contentStyle = {},\n      showFullscreenToggle,\n      collapsible,\n    } = this.props;\n\n    const collapsedDefaultValue =\n      isObject(collapsible) ? collapsible.defaultValue : false;\n\n    const {\n      stateStyle,\n      collapsed = collapsedDefaultValue,\n      fullScreen = showFullscreenToggle?.defaultValue,\n    } = this.state;\n    const toggleContent = () => {\n      this.setState({ collapsed: !collapsed });\n    };\n    const style = getPopupStyle({\n      positioning,\n      collapsed,\n      fullScreen,\n      stateStyle,\n      rootStyle,\n    });\n    const fullHeightPositions: PopupProps[\"positioning\"][] = [\n      \"right-panel\",\n      \"fullscreen\",\n      undefined,\n    ];\n    const contentFullScreenStyle = showFullscreenToggle?.getContentStyle?.(\n      Boolean(fullScreen),\n    );\n\n    const result = (\n      <>\n        {/* Used to improve UX for onWaitForContentFinish */}\n        {onClose && style.opacity !== 0 && (\n          <ClickCatchOverlay\n            style={{\n              position: \"fixed\",\n              opacity: 0,\n              zIndex: POPUP_ZINDEX,\n              ...clickCatchStyle,\n            }}\n            className=\"flex-col\"\n            onClick={onClose}\n          />\n        )}\n\n        <div\n          className={`${POPUP_CLASSES.root} positioning_${positioning} card m-auto bg-popup${positioning === \"right-panel\" ? \"-content\" : \"\"} flex-col shadow-xl  o-hidden`}\n          data-command={this.props[\"data-command\"]}\n          ref={(r) => {\n            if (r) {\n              this.ref = r;\n            }\n          }}\n          onClick={\n            !(onClickClose && onClose) ? undefined : (\n              (e) => {\n                if (window.getSelection()?.toString()) {\n                  return;\n                }\n                onClose(e);\n              }\n            )\n          }\n          style={{\n            boxSizing: \"content-box\",\n            ...style,\n          }}\n          role=\"dialog\"\n          aria-modal=\"true\"\n          aria-labelledby=\"modal-headline\"\n        >\n          <div\n            className={classOverride(\n              `${POPUP_CLASSES.rootChild} w-full min-h-0 text-center flex-col bg-inherit ${fullHeightPositions.includes(positioning) ? \"f-1\" : \"\"}`,\n              rootChildClassname,\n            )}\n            style={{\n              ...rootChildStyle,\n              ...showFullscreenToggle?.getStyle?.(Boolean(fullScreen)),\n            }}\n          >\n            <PopupHeader\n              {...this.props}\n              rootDiv={this.ref}\n              onToggleFullscreen={() => {\n                const newFullScreen = !fullScreen;\n                if (!newFullScreen) {\n                  this.position = undefined;\n                }\n                this.setState({ fullScreen: newFullScreen });\n              }}\n              toggleContent={toggleContent}\n              collapsed={collapsed}\n              fullScreen={fullScreen}\n            />\n\n            {!collapsed && (\n              <div\n                className={classOverride(\n                  POPUP_CLASSES.content +\n                    \" bg-inherit flex-col f-1 min-h-0 o-auto \",\n                  contentClassName,\n                )}\n                style={{\n                  ...contentStyle,\n                  ...contentFullScreenStyle,\n                }}\n                data-command={\"Popup.content\" satisfies Command}\n              >\n                <ErrorTrap>{content || children}</ErrorTrap>\n              </div>\n            )}\n          </div>\n          <FooterButtons\n            {...pickKeys(this.props, [\n              \"footerButtons\",\n              \"onClose\",\n              \"footer\",\n              \"onClose\",\n            ] satisfies (keyof FooterButtonsProps)[])}\n            data-command=\"Popup.footer\"\n          />\n        </div>\n      </>\n    );\n\n    return ReactDOM.createPortal(result, this.el);\n  }\n}\n\nexport const DATA_NULLABLE = \"data-nullable\";\nexport const DATA_HAS_VALUE = \"data-has-value\";\n"
  },
  {
    "path": "client/src/components/Popup/PopupHeader.tsx",
    "content": "import {\n  mdiClose,\n  mdiFullscreen,\n  mdiUnfoldLessHorizontal,\n  mdiUnfoldMoreHorizontal,\n} from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"../Btn\";\nimport { FlexCol, FlexRow } from \"../Flex\";\nimport { POPUP_CLASSES, type PopupProps } from \"./Popup\";\nimport { t } from \"../../i18n/i18nUtils\";\n\ntype PopupHeaderProps = PopupProps & {\n  collapsed: boolean;\n  fullScreen: boolean | undefined;\n  onToggleFullscreen: VoidFunction;\n  toggleContent: VoidFunction;\n  rootDiv: HTMLDivElement | undefined;\n};\nexport const PopupHeader = ({\n  subTitle,\n  title,\n  collapsible,\n  showFullscreenToggle,\n  onToggleFullscreen,\n  headerRightContent,\n  positioning,\n  onClose,\n  collapsed,\n  fullScreen,\n  toggleContent,\n  rootDiv,\n}: PopupHeaderProps) => {\n  const showTitle = !!title;\n  if (title && !onClose) {\n    console.warn(\n      \"Popup title will not be shown because onClose is not defined\",\n    );\n  }\n  if (!showTitle) return null;\n  const titleContent =\n    typeof title === \"function\" ? rootDiv && title(rootDiv) : title;\n  return (\n    <header\n      className={`${POPUP_CLASSES.title} ${positioning === \"right-panel\" ? \"pl-2\" : \"pl-1\"} py-p5 pr-p5 flex-row ai-center bb b-color gap-1`}\n      data-command=\"Popup.header\"\n    >\n      {collapsible && (\n        <Btn\n          className=\"f-0\"\n          onClick={toggleContent}\n          iconPath={\n            !collapsed ? mdiUnfoldLessHorizontal : mdiUnfoldMoreHorizontal\n          }\n          title=\"Collapse/Expand content\"\n          variant=\"icon\"\n        />\n      )}\n      <FlexCol\n        id=\"modal-headline\"\n        className={\n          \"ai-none jc-none f-1 font-20 noselect font-medium text-0 o-hidden text-ellipsis ta-left m-0 ws-nowrap \" +\n          (collapsible ? \" pointer \" : \" \")\n        }\n        onClick={collapsible ? toggleContent : undefined}\n      >\n        <h4\n          className=\"m-0\"\n          style={{\n            ...(collapsible ? { paddingLeft: 0 } : {}),\n          }}\n          title={typeof title === \"string\" ? title : undefined}\n        >\n          {titleContent}\n        </h4>\n        {subTitle && (\n          <h6\n            title={subTitle}\n            className=\"font-14 m-0 text-ellipsis text-1\"\n            style={{ opacity: 0.7, maxWidth: \"200px\" }}\n          >\n            {subTitle}\n          </h6>\n        )}\n      </FlexCol>\n      <FlexRow className=\"Popup-header-actions gap-0\">\n        {headerRightContent}\n\n        {showFullscreenToggle && (\n          <Btn\n            className=\"f-0\"\n            variant=\"icon\"\n            iconPath={mdiFullscreen}\n            color={fullScreen ? \"action\" : undefined}\n            onClick={onToggleFullscreen}\n            title={t.common[\"Toggle fullscreen\"]}\n            data-command=\"Popup.toggleFullscreen\"\n          />\n        )}\n        <Btn\n          data-command=\"Popup.close\"\n          variant=\"icon\"\n          data-close-popup={true}\n          className=\"f-0\"\n          style={{ margin: \"1px\" }}\n          iconPath={mdiClose}\n          onClick={onClose}\n          title={t.common.Close}\n        />\n      </FlexRow>\n    </header>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Popup/getPopupPosition.ts",
    "content": "import { isDefined } from \"../../utils/utils\";\nimport type Popup from \"./Popup\";\nimport { POPUP_CLASSES } from \"./Popup\";\n\ntype Args = {\n  anchorX: number;\n  anchorY: number;\n  anchorHeight: number;\n  anchorWidth: number;\n  popup: Popup;\n  opacity: number;\n};\n\nexport const getPopupSize = (popup: Popup) => {\n  if (!popup.ref) return undefined;\n  const rootChild =\n    popup.ref.querySelector<HTMLElement>(\n      `:scope > .${POPUP_CLASSES.rootChild}`,\n    ) ?? undefined;\n  const contentRef =\n    rootChild?.querySelector<HTMLElement>(\n      `:scope > .${POPUP_CLASSES.content}`,\n    ) ?? undefined;\n  const titleRef =\n    rootChild?.querySelector<HTMLElement>(\n      `:scope > header.${POPUP_CLASSES.title}`,\n    ) ?? undefined;\n  const footerRef =\n    popup.ref.querySelector<HTMLElement>(`:scope > footer`) ?? undefined;\n  if (!rootChild) return undefined;\n\n  /** Account for border to ensure there isn't a 2px overflow */\n  const style = window.getComputedStyle(popup.ref);\n  const bTop = Number(style.getPropertyValue(`border-top-width`).slice(0, -2));\n  const bRight = Number(\n    style.getPropertyValue(`border-right-width`).slice(0, -2),\n  );\n  const bBottom = Number(\n    style.getPropertyValue(`border-bottom-width`).slice(0, -2),\n  );\n  const bLeft = Number(\n    style.getPropertyValue(`border-left-width`).slice(0, -2),\n  );\n\n  const { width: widthWithoutB, height: heightWithoutB } = [\n    titleRef,\n    contentRef,\n    footerRef,\n  ]\n    .filter(isDefined)\n    .reduce(\n      (a, { offsetHeight, offsetWidth, scrollHeight, scrollWidth }) => ({\n        ...a,\n        width: Math.max(a.width, offsetWidth, scrollWidth),\n        height: a.height + Math.max(offsetHeight, scrollHeight),\n      }),\n      { width: 0, height: 0 },\n    );\n  const width = widthWithoutB + bLeft + bRight;\n  const height = heightWithoutB + bTop + bBottom;\n  const size = {\n    width: Math.max(width, popup.prevSize?.width ?? 0),\n    height: Math.max(height, popup.prevSize?.height ?? 0),\n  };\n  popup.prevSize = size;\n  return size;\n};\n\nexport const getPopupPosition = ({\n  opacity,\n  anchorX,\n  anchorY,\n  anchorHeight,\n  popup,\n}: Args) => {\n  let x = Math.round(anchorX);\n  let y = Math.round(anchorY + anchorHeight);\n  const contentSize = getPopupSize(popup);\n  if (!contentSize) return;\n  const { width, height } = contentSize;\n\n  const contentHeight = Math.round(height);\n  const contentWidth = Math.round(width);\n  const widthOverflow = x + contentWidth - window.innerWidth;\n  const heightOverflow = y + contentHeight - window.innerHeight;\n  if (widthOverflow > 0) {\n    x -= widthOverflow;\n  }\n  if (heightOverflow > 0) {\n    y -= heightOverflow;\n  }\n\n  x = Math.max(0, x);\n  y = Math.max(0, y);\n\n  /**\n   * Ensure content window does not decrease in size for better UX (fixedTopLeft effectively)\n   */\n  const justToggledFullScreen = Date.now() - popup.toggledFullScreen < 100;\n  if (popup.position && !popup.state.fullScreen && !justToggledFullScreen) {\n    x = Math.min(x, popup.position.x);\n    y = Math.min(y, popup.position.y);\n    popup.position.x = x;\n    popup.position.y = y;\n  }\n\n  if (!popup.state.fullScreen && !justToggledFullScreen) {\n    popup.position ??= {\n      opacity,\n      x,\n      y,\n      xMin: x,\n      yMin: y,\n    };\n  }\n\n  const top = `${y}px`;\n  const left = `${x}px`;\n  const stateStyle = {\n    top,\n    left,\n    maxHeight: `calc(100vh - ${top})`,\n    maxWidth: `calc(100vw - ${left})`,\n    /** This prevents a weird slow height growth for position beneath */\n    ...(heightOverflow > 0 && {\n      bottom: 0,\n    }),\n    ...(widthOverflow > 0 && {\n      right: 0,\n    }),\n  };\n\n  popup.setState({\n    prevStateStyle: { ...popup.state.stateStyle },\n    stateStyle,\n  });\n};\n"
  },
  {
    "path": "client/src/components/Popup/getPopupStyle.ts",
    "content": "import { omitKeys } from \"prostgles-types\";\nimport { POPUP_ZINDEX, type PopupProps } from \"./Popup\";\n\ntype Args = {\n  fullScreen: boolean | undefined;\n  collapsed: boolean;\n  stateStyle: React.CSSProperties;\n  rootStyle: React.CSSProperties;\n  positioning: PopupProps[\"positioning\"];\n};\nexport const getPopupStyle = ({\n  positioning,\n  stateStyle,\n  rootStyle,\n  collapsed,\n  fullScreen,\n}: Args) => {\n  let style: React.CSSProperties = {\n    maxWidth: \"100vw\",\n    maxHeight: \"100vh\",\n    outline: \"none\",\n    position: \"fixed\",\n    // clipPath: \"circle(1% at 50% 10%)\",\n    // transition: \"clip-path .5s\",\n    zIndex: POPUP_ZINDEX,\n    padding: 0,\n    borderRadius: \".5em\",\n    ...stateStyle,\n    ...rootStyle,\n  };\n\n  if (fullScreen) {\n    style = {\n      ...omitKeys(style, [\n        \"transform\",\n        \"top\",\n        \"left\",\n        \"right\",\n        \"bottom\",\n        \"position\",\n        \"inset\",\n        \"width\",\n        \"height\",\n      ]),\n      position: \"fixed\",\n      inset: 0,\n      width: \"100vw\",\n      height: \"100vh\",\n    };\n  }\n\n  if (collapsed) {\n    style = omitKeys(style, [\"bottom\", \"height\"]);\n  }\n\n  return style;\n};\n"
  },
  {
    "path": "client/src/components/Popup/popupCheckPosition.ts",
    "content": "import type Popup from \"./Popup\";\nimport type { PopupState } from \"./Popup\";\nimport { POPUP_ZINDEX } from \"./Popup\";\nimport { getPopupPosition, getPopupSize } from \"./getPopupPosition\";\n\nconst rightPanelStyle: React.CSSProperties = {\n  padding: 0,\n  top: 0,\n  right: 0,\n  bottom: 0,\n  width: \"fit-content\",\n  maxHeight: \"100%\", // window.isIOSDevice? `calc(100vh - 100px)` : `calc(100% - 3em)`,\n  borderRadius: 0,\n  maxWidth: \"100vw\",\n  // maxHeight: `100%`\n};\n\nexport const popupCheckPosition = function (this: Popup) {\n  if (!this.mounted || !this.el.isConnected) return;\n\n  const contentCanShow = this.checkPositionOpacity.done;\n  let canShow = contentCanShow;\n  if (!contentCanShow) {\n    const shouldStop = Date.now() - this.checkPositionOpacity.started >= 400;\n    clearTimeout(this.checkPositionOpacity.timeout);\n    if (shouldStop) {\n      this.checkPositionOpacity.done = true;\n      canShow = true;\n      this.props.onContentFinishedResizing?.();\n    } else {\n      this.checkPositionOpacity.timeout = setTimeout(() => {\n        this.checkPositionOpacity.done = true;\n        this.checkPosition();\n      }, 50);\n    }\n  }\n  const {\n    anchorEl,\n    anchorPadding = 0,\n    anchorXY,\n    positioning,\n    persistInitialSize,\n    fixedTopLeft = true,\n  } = this.props;\n  const persistInitialSizeInvalid = () => {\n    if (!persistInitialSize) return;\n    console.warn(\n      \"Popup: persistInitialSize=true only works with a content-dependant positioning\",\n    );\n  };\n\n  const commonState = {\n    opacity: canShow ? 1 : 0,\n    zIndex: POPUP_ZINDEX,\n    position: \"fixed\",\n  } satisfies React.CSSProperties;\n\n  /**\n   * Center positioning if nothing specified\n   */\n  if (!positioning && !anchorXY && this.ref) {\n    const { offsetHeight, offsetWidth } = this.ref;\n    /**\n     * https://stackoverflow.com/questions/6411361/webkit-based-blurry-distorted-text-post-animation-via-translate3d\n     */\n    const offsetToReduceChromeBlur = 0.5;\n    const x = Math.round(offsetWidth / 2);\n    const y = Math.round(offsetHeight / 2) + offsetToReduceChromeBlur;\n    this.setState({\n      stateStyle: {\n        ...commonState,\n        transform: `translate(-${x}px, -${y}px)`,\n        left: \"50%\",\n        top: \"50%\",\n      },\n    });\n    persistInitialSizeInvalid();\n    return;\n  }\n\n  /** Non content-dependant positioning */\n  if (positioning === \"fullscreen\") {\n    this.setState({ stateStyle: { inset: 0, position: \"absolute\" } });\n    persistInitialSizeInvalid();\n    return;\n  } else if (positioning === \"as-is\" || positioning === \"right-panel\") {\n    this.setState({\n      stateStyle: {\n        ...commonState,\n        ...(positioning === \"right-panel\" ? rightPanelStyle : {}),\n      },\n    });\n    persistInitialSizeInvalid();\n    return;\n  }\n\n  const size = getPopupSize(this);\n  if (!this.ref || !size) return;\n\n  const { height, width } = size;\n  // const firstChild = this.ref.firstChild as HTMLElement;\n  // if(!firstChild.classList.contains(POPUP_CLASSES.rootChild)) {\n  //   throw new Error(\"Popup: unexpected rootChild\");\n  // }\n  // const { scrollWidth, scrollHeight } = firstChild;\n  // let { scrollWidth: width, scrollHeight: height } = this.ref;\n  // const fitContentPositioning = !persistInitialSize;\n  // if(fitContentPositioning){\n  //   width = scrollWidth;\n  //   height = scrollHeight;\n  // }\n\n  // IS THIS STILL THE CASE?\n  /** Prevent jitter loop */\n  // if (width > 100) width += 22;\n  // if (height > 100) height += 22;\n\n  const minPadding = window.innerWidth > 900 ? 10 : 0;\n\n  /** If popup content overflows remove margins */\n  const PADDING_X = window.innerWidth < width ? 0 : minPadding;\n  const PADDING_Y = window.innerHeight < height ? 0 : minPadding;\n\n  let anchorX = 0,\n    anchorY = 0,\n    anchorWidth = 0,\n    anchorHeight = 0;\n\n  if (anchorXY) {\n    anchorX = anchorXY.x;\n    anchorY = anchorXY.y;\n  } else if (anchorEl) {\n    const anchorR = anchorEl.getBoundingClientRect();\n    anchorX = anchorR.x - anchorPadding;\n    anchorY = anchorR.y - anchorPadding;\n    anchorHeight = anchorR.height + 2 * anchorPadding;\n    anchorWidth = anchorR.width + 2 * anchorPadding;\n  } else {\n    anchorX = (window.innerWidth - width) / 2;\n    anchorY = (window.innerHeight - height) / 2;\n\n    if (positioning && ![\"top-center\", \"center\"].includes(positioning)) {\n      console.warn(\n        \"Popup positioning provided without an anchorEl or anchorXY\",\n      );\n    }\n  }\n\n  let x = anchorX;\n  let y = anchorY;\n  const xLeft = Math.max(0, anchorX);\n  const xCenter = Math.max(0, anchorX + anchorWidth / 2);\n\n  if (positioning === \"center\") {\n    y = 0.5 * (window.document.body.offsetHeight - height);\n    x = 0.5 * (window.document.body.offsetWidth - width);\n  } else if (positioning === \"top-center\") {\n    y = 50;\n    x = 0.5 * (window.document.body.offsetWidth - width);\n  } else if (positioning === \"beneath-center\") {\n    y = anchorY + anchorHeight;\n    x = xCenter - width / 2;\n  } else if (\n    positioning === \"beneath-left\" ||\n    positioning === \"beneath-left-minfill\"\n  ) {\n    if (positioning === \"beneath-left\") {\n      return getPopupPosition({\n        opacity: commonState.opacity,\n        anchorX,\n        anchorHeight,\n        anchorWidth,\n        anchorY,\n        popup: this,\n      });\n    }\n    y = anchorY + anchorHeight;\n    x = xLeft;\n  } else if (positioning === \"left\") {\n    y = anchorY;\n    x = anchorX - width;\n  } else if (positioning === \"beneath-right\") {\n    y = anchorY + anchorHeight;\n    x = anchorX + anchorWidth - width;\n\n    /* Bottom left of point */\n  } else if (positioning === \"tooltip\") {\n    y = anchorY + PADDING_Y;\n    x = anchorX - PADDING_X - width;\n  } else if (\n    positioning === \"inside-top-center\" ||\n    positioning === \"above-center\"\n  ) {\n    x = xCenter;\n    y = anchorY || 0;\n\n    if (positioning === \"above-center\") {\n      y = anchorY - height;\n    }\n  }\n\n  /* Right side is out of screen */\n  let right: number | undefined;\n  if (x + width + PADDING_X > window.innerWidth) {\n    x = Math.max(PADDING_X, window.innerWidth - width - PADDING_X);\n    right = 0;\n  } else if (x < 0) {\n    x = PADDING_X;\n  }\n\n  /* Bottom side is out of screen */\n  let bottom: number | undefined;\n  const bottomOverflow = y + height + PADDING_Y - window.innerHeight;\n\n  // TODO ensure that this is + fixedTopLeft overflow compensation logic does not loop\n  if (bottomOverflow > 0) {\n    if (bottomOverflow < height / 9) {\n      bottom = PADDING_Y;\n    }\n    y = Math.max(PADDING_Y, window.innerHeight - height - PADDING_Y);\n  } else if (y < 0) {\n    y = PADDING_Y;\n  }\n\n  if (\n    this.position?.x !== x ||\n    this.position.y !== y ||\n    this.position.opacity !== commonState.opacity ||\n    bottom\n  ) {\n    this.position ??= { x, y, xMin: x, yMin: y, opacity: commonState.opacity };\n    // This makes popup height grow to max\n    if (fixedTopLeft) {\n      this.position.xMin = Math.round(Math.min(this.position.xMin, x));\n      this.position.yMin = Math.round(Math.min(this.position.yMin, y));\n      x = this.position.xMin;\n      y = this.position.yMin;\n    }\n    this.position.x = x;\n    this.position.y = y;\n\n    const left = `${Math.round(x)}px`;\n    const newState: PopupState = {\n      stateStyle: {\n        ...commonState,\n        ...(Number.isFinite(bottom) && { bottom: `${bottom}px` }),\n        ...(Number.isFinite(right) ?\n          {\n            right,\n            // ...(fixedTopLeft && { left }), // This makes popup width grow to max\n          }\n        : { left }),\n        top: `${Math.round(y)}px`,\n      },\n    };\n\n    if (persistInitialSize) {\n      newState.stateStyle.width = `${width}px`;\n      newState.stateStyle.height = `${height}px`;\n      /** If content width is less than anchor then fill width */\n    } else if (\n      positioning?.endsWith(\"-minfill\") &&\n      anchorWidth &&\n      width < anchorWidth\n    ) {\n      newState.stateStyle.width = `${anchorWidth}px`;\n    } else if (positioning?.endsWith(\"-fill\")) {\n      newState.stateStyle.width = `${anchorWidth}px`;\n    }\n    // TODO - add a better way to prevent height recursion when deleting computed columns from Linked column (beneath-left positioning)\n    // if(isEqual(newState.stateStyle, this.prevStateStyles[1])) return;\n\n    this.setState(newState);\n    this.prevStateStyles.unshift(newState.stateStyle);\n    this.prevStateStyles = this.prevStateStyles.slice(0, 3);\n  }\n};\n"
  },
  {
    "path": "client/src/components/PopupMenu.tsx",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { Command } from \"../Testing\";\nimport type { PopupProps } from \"./Popup/Popup\";\nimport Popup, { POPUP_ZINDEX } from \"./Popup/Popup\";\nimport { classOverride } from \"./Flex\";\n\ntype P<State extends AnyObject> = {\n  button: React.ReactNode;\n  render?: (\n    close: () => void,\n    state: State,\n    setState: (newState: Partial<State>) => void,\n  ) => React.ReactNode;\n  footer?: (close: VoidFunction) => React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n  initialState?: State;\n\n  /**\n   * If true then will increase button zIndex when popup is open\n   */\n  raiseButton?: boolean;\n};\n\nexport default function <S extends AnyObject>(\n  props: P<S> & Partial<Omit<PopupProps, \"footer\">>,\n) {\n  const [ref, setRef] = useState<HTMLElement | null>();\n  const [refBtn, setRefBtn] = useState<HTMLElement>();\n  const {\n    render,\n    content,\n    style = {},\n    className = \"\",\n    onClickClose,\n    initialState = {},\n    footer,\n    onClose,\n    raiseButton,\n    ...otherProps\n  } = props;\n\n  const [state, setState] = useState<S>(initialState as S);\n\n  const open = Boolean(ref);\n\n  const popupClose = () => setRef(null);\n\n  return (\n    <>\n      <div\n        ref={(e) => {\n          if (e) {\n            setRefBtn(e);\n          }\n        }}\n        style={{\n          ...style,\n          ...(raiseButton && open && { zIndex: POPUP_ZINDEX + 1 }),\n        }}\n        className={classOverride(\n          `PopupMenu_triggerWrapper h-fit w-fit ${open ? \"is-open\" : \"\"} `,\n          className,\n        )}\n        data-command={\n          open ? undefined : (\n            (props[\"data-command\"] satisfies Command | undefined)\n          )\n        }\n        onClick={(e) => {\n          if (refBtn?.contains(e.nativeEvent.target as HTMLElement)) {\n            setRef(e.nativeEvent.target as HTMLElement);\n          } else {\n            setRef(null);\n          }\n        }}\n      >\n        {props.button}\n      </div>\n      {ref && (\n        <Popup\n          positioning=\"inside\"\n          {...otherProps}\n          onClose={(e) => {\n            {\n              onClose?.(e);\n              setRef(null);\n            }\n          }}\n          onClickClose={onClickClose ?? !render}\n          content={\n            render ?\n              render(popupClose, state, (newState) =>\n                setState({ ...state, ...newState }),\n              )\n            : content || props.children\n          }\n          footer={footer?.(popupClose)}\n          anchorEl={refBtn}\n        />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/PopupMenuList.tsx",
    "content": "import React from \"react\";\nimport type { BtnProps } from \"./Btn\";\nimport Btn from \"./Btn\";\nimport type { MenuListitem } from \"./MenuListItem\";\nimport { MenuList } from \"./MenuList\";\nimport PopupMenu from \"./PopupMenu\";\nimport type { Command, TestSelectors } from \"../Testing\";\n\ntype PopupMenuListProps = (\n  | { btnProps: BtnProps<void> }\n  | { button: React.ReactChild }\n) & {\n  items: MenuListitem[];\n  listStyle?: React.CSSProperties;\n  \"data-command\": Command;\n};\nexport const PopupMenuList = (props: PopupMenuListProps) => {\n  const theButton =\n    \"button\" in props ? props.button : <Btn {...props.btnProps} />;\n\n  return (\n    <PopupMenu\n      positioning=\"beneath-left\"\n      contentStyle={{ padding: 0, borderRadius: 0 }}\n      clickCatchStyle={{ opacity: 0 }}\n      rootStyle={{ borderRadius: 0 }}\n      button={theButton}\n    >\n      <MenuList\n        activeKey=\"\"\n        style={{ borderRadius: 0, ...props.listStyle }}\n        items={props.items}\n      />\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/components/PostgresInstallationInstructions.tsx",
    "content": "import {\n  mdiApple,\n  mdiInformationVariant,\n  mdiLinux,\n  mdiMicrosoftWindows,\n} from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { ExpandSection } from \"@components/ExpandSection\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { DEFAULT_ELECTRON_CONNECTION } from \"@common/electronInitTypes\";\n\nconst OPERATING_SYSTEMS = [\n  { key: \"linux\", label: \"Linux\", icon: mdiLinux },\n  { key: \"macosx\", label: \"MacOs\", icon: mdiApple },\n  { key: \"windows\", label: \"Windows\", icon: mdiMicrosoftWindows },\n] as const;\nexport type OS = (typeof OPERATING_SYSTEMS)[number][\"key\"];\n\ntype P = {\n  os: OS;\n  placement: \"state-db\" | \"add-connection\" | \"state-db-quick-setup\";\n  className?: string;\n};\nexport const PostgresInstallationInstructions = ({\n  os,\n  className = \"\",\n  placement,\n}: P) => {\n  const { db_user, db_name } = DEFAULT_ELECTRON_CONNECTION;\n\n  return (\n    <PopupMenu\n      title=\"Postgres server installation\"\n      positioning=\"center\"\n      className={className}\n      contentClassName=\"\"\n      clickCatchStyle={{ opacity: 0.5 }}\n      rootStyle={{ maxWidth: \"750px\" }}\n      data-command=\"PostgresInstallationInstructions\"\n      button={\n        <Btn\n          variant={\"outline\"}\n          color=\"action\"\n          iconPath={mdiInformationVariant}\n        >\n          Installation steps\n        </Btn>\n      }\n      render={() => (\n        <div className=\"flex-col p-2 font-18 gap-2 ta-left\">\n          <div>\n            <h3>Postgres downloads:</h3>\n            <ul className=\"no-decor flex-row gap-1 jc-start\">\n              {OPERATING_SYSTEMS.map(({ key, label, icon }) => (\n                <li key={key}>\n                  <Btn\n                    href={`https://www.postgresql.org/download/${key}/`}\n                    target=\"_blank\"\n                    color=\"action\"\n                    variant={os === key ? \"filled\" : \"outline\"}\n                    iconPath={icon}\n                  >\n                    {label}\n                  </Btn>\n                </li>\n              ))}\n            </ul>\n          </div>\n\n          <div>\n            {placement === \"state-db-quick-setup\" ?\n              <h3>Postgres user creation:</h3>\n            : <h3>Postgres user & database creation:</h3>}\n\n            <ExpandSection label=\"Linux/MacOs\" expanded={os !== \"windows\"}>\n              <code className=\"bg-terminal text-white p-1 flex-col ta-left\">\n                sudo -u postgres createuser -P --superuser {db_user}\n              </code>\n              {placement !== \"state-db-quick-setup\" && (\n                <code className=\"bg-terminal text-white p-1 flex-col ta-left\">\n                  sudo -u postgres createdb {db_name} -O {db_user}\n                </code>\n              )}\n            </ExpandSection>\n            <ExpandSection label=\"Windows\" expanded={os === \"windows\"}>\n              <p>\n                PowerShell command. Change &quot;15&quot; to your actual\n                postgres version number\n              </p>\n              <code className=\"bg-terminal text-white p-1 flex-col ta-left\">\n                & &apos;C:\\Program Files\\PostgreSQL\\15\\bin\\createuser.exe&apos;\n                -U postgres -P --superuser {db_user}\n              </code>\n              {placement !== \"state-db-quick-setup\" && (\n                <code className=\"bg-terminal text-white p-1 flex-col ta-left\">\n                  & &apos;C:\\Program Files\\PostgreSQL\\15\\bin\\createdb.exe&apos;\n                  -U postgres -O {db_user} {db_name}\n                </code>\n              )}\n            </ExpandSection>\n          </div>\n        </div>\n      )}\n      // footerButtons={[\n      //   {\n      //     onClickClose: true,\n      //     label: \"Close\",\n      //     variant: \"filled\",\n      //     color: \"action\",\n      //     \"data-command\": \"PostgresInstallationInstructions.Close\",\n      //   },\n      // ]}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/components/ProgressBar.css",
    "content": "/* @keyframes indeterminateTranslateX {\n  0% {\n    background-position-x: 0px;\n  }\n  50% {\n    background-position-x: 100px;\n  }\n  100% {\n    background-position-x: 200px;\n  }\n} */\n\n@keyframes indeterminateTranslateX {\n  0% {\n    transform: translateX(-150%);\n  }\n  25% {\n    transform: translateX(-100%);\n  }\n  50% {\n    transform: translateX(-50%);\n  }\n  75% {\n    transform: translateX(0%);\n  }\n  100% {\n    transform: translateX(200%);\n  }\n}\n"
  },
  {
    "path": "client/src/components/ProgressBar.tsx",
    "content": "import React from \"react\";\nimport \"./ProgressBar.css\";\nimport type { DivProps } from \"./Flex\";\nimport { classOverride } from \"./Flex\";\n\ntype P = {\n  message?: React.ReactNode;\n  style?: React.CSSProperties;\n  value: number;\n  totalValue: number;\n};\nexport const MINI_BARCHART_COLOR = \"var(--active)\";\n\nexport const ProgressBar = ({ message, value, totalValue, style }: P) => {\n  const perc = totalValue > value ? Math.round((100 * value) / totalValue) : -1;\n  const lightColor = \"var(--bg-action)\";\n  const height = 4;\n  const isIndeterminate = perc === -1;\n\n  return (\n    <div className=\"ProgressBar flex-col gap-p25\" style={style}>\n      <div\n        className=\"ProgressBarOuter shadow\"\n        style={{\n          background: lightColor,\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          className=\"ProgressBarInner shadow\"\n          style={{\n            borderRadius: `${height / 2}px`,\n            height: `${height}px`,\n            background: MINI_BARCHART_COLOR,\n            minHeight: `${height}px`,\n            minWidth: \"2px\",\n            ...(isIndeterminate ?\n              {\n                animation: \"indeterminateTranslateX 1s infinite linear\",\n                willChange: \"transform\",\n                width: `${50}%`,\n              }\n            : {\n                width: `${perc}%`,\n              }),\n          }}\n        />\n      </div>\n      <div className={\"text-1 \"}>{message}</div>\n    </div>\n  );\n};\n\ntype CellBarchartProps = {\n  min: number | Date;\n  max: number | Date;\n  value: number | Date;\n  message: React.ReactNode;\n  barColor: string | undefined;\n  textColor: string | undefined;\n} & DivProps;\nexport const CellBarchart = ({\n  min,\n  max,\n  value,\n  message,\n  barColor = MINI_BARCHART_COLOR,\n  textColor,\n  style,\n  className,\n  ...divProps\n}: CellBarchartProps) => {\n  const delta = +max - +min;\n  const valDelta = +value - +min;\n  const perc = Math.round((100 * valDelta) / delta);\n  const height = 8;\n  return (\n    <div\n      {...divProps}\n      className={classOverride(\"ProgressBar flex-col gap-p25\", className)}\n      style={style}\n    >\n      <div\n        className={\"shadow\"}\n        style={{\n          borderRadius: `${height / 2}px`,\n          height: `${height}px`,\n          background: barColor,\n          flex: 1,\n          minHeight: `${height}px`,\n          width: `${perc}%`,\n          minWidth: \"2px\",\n        }}\n      ></div>\n      <div\n        className={\"text-1 ta-left\"}\n        style={textColor ? { color: textColor } : undefined}\n      >\n        {message}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/QRCodeImage.tsx",
    "content": "import React, { useEffect, useRef } from \"react\";\nimport QRCode from \"qrcode\";\nimport PopupMenu from \"./PopupMenu\";\n\ntype QRCodeImageProps = {\n  url: string | undefined;\n  size?: number;\n  variant: \"href-wrapped\" | \"table-cell\" | \"canvas-only\";\n};\n\nexport const QRCodeImage = ({ size, url, variant }: QRCodeImageProps) => {\n  const canvasNode = useRef<HTMLCanvasElement>(null);\n  useEffect(() => {\n    const canvas = canvasNode.current;\n    if (!canvas) return;\n\n    if (url) {\n      const sizeOpts =\n        Number.isFinite(size) ?\n          { width: size }\n        : ({\n            width: Math.min(canvas.offsetWidth, canvas.offsetHeight),\n          } as const);\n      QRCode.toCanvas(canvas, url, sizeOpts);\n    } else {\n      const context = canvas.getContext(\"2d\");\n      context?.clearRect(0, 0, canvas.width, canvas.height);\n    }\n  }, [size, url, canvasNode]);\n\n  if (!url) return null;\n\n  const canvas = <canvas ref={canvasNode} style={{}} />;\n\n  if (variant === \"canvas-only\") return canvas;\n\n  const wrappedCanvas = (\n    <a href={url} target={\"_blank\"} rel=\"noreferrer\">\n      {/* {window.isMobileDevice && <div>Tap image</div>} */}\n      {canvas}\n    </a>\n  );\n\n  if (variant === \"href-wrapped\") {\n    return wrappedCanvas;\n  }\n\n  return (\n    <PopupMenu\n      positioning=\"center\"\n      title={url}\n      button={canvas}\n      contentClassName=\"ai-center\"\n      render={() => <QRCodeImage size={400} url={url} variant=\"canvas-only\" />}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/components/Scheduler.css",
    "content": ".scheduler .item {\n  background: rgb(95, 180, 241);\n}\n.scheduler .item:hover,\n.scheduler .item:focus {\n  background: rgb(73 173 245);\n}\n"
  },
  {
    "path": "client/src/components/Scheduler.tsx",
    "content": "// import React, { FunctionComponent, ReactChild, useEffect, useState } from 'react';\n// import \"./Scheduler.css\";\n\n// type P = {\n//   style?: object;\n//   className?: string;\n//   items: {\n//     content: ReactChild;\n//     from: Date;\n//     to: Date;\n//     onClick?: any;\n//     post_code: string;\n//   }[]\n// }\n\n// export const SECOND = 1000,\n// MINUTE = 60 * SECOND,\n// HOUR = 60 * MINUTE,\n// DAY = 24 * HOUR;\n\n// const Scheduler:FunctionComponent<P> = (props) => {\n//   const {\n//     className = \"\",\n//     style = {}\n//   } = props;\n//   const locale = enGB;\n\n//   let items = props.items.sort((a, b) => +a.from - (+b.from));\n\n//   const [day, setDay] = useState<any>(null);\n//   const [days, setDays] = useState<any[]>([]);\n\n//   const getFromHour = (f: Date, int = false) => {\n//     if(f <= startOfDay(new Date())){\n//       return 0;\n//     } else {\n//       let res = getHours(f);\n//       if(!int) res += getMinutes(f)/60;\n//       return res;\n//     }\n//   }\n//   const getToHour = (f: Date, int = false, continueEnd = false) => {\n//     if(f >= startOfDay(Date.now() + DAY)){\n//       if(continueEnd) return 24 + getHours(f);\n//       return 24;\n//     } else {\n//       let res = getHours(f);\n//       if(!int) res += getMinutes(f)/60;\n//       return res;\n//     }\n//   }\n\n//   const HOUR_HEIGHT = 80;\n\n//   useEffect(()=>{\n//     let start = new Date();\n//     if(items && items.length){\n//       start = items[0]!.from;\n//     }\n//     let end = new Date(+start + 7 * 24 * 3600 * 1000)\n\n//     var days = eachDayOfInterval({ start, end });\n//     setDay(days[0]);\n//     setDays(days);\n//   }, [items]);\n\n//   const getDayItems = (day: Date) => {\n//     return items.filter(d => +d.from < +day + DAY - MINUTE && +d.to > +day);\n//   }\n\n//   let dayItems = getDayItems(day);\n\n//   let startHour = 7;\n//   let endHour = 21;\n//   if(dayItems && dayItems.length){\n//     let f = dayItems[0]!;\n//     startHour = getFromHour(f.from, true) || startHour;\n//     if(startHour) startHour--;\n\n//     let l = dayItems[dayItems.length-1]!;\n//     endHour = getToHour(l.to, true) || endHour;\n//     if(endHour < 24) endHour++;\n//   }\n//   let nt = endHour - startHour + 1;\n//   let ticks = new Array(Math.max(1, nt)).fill(startHour).map((d,i)=> d + i);\n\n//   const getDaySelector = () => (\n//     <div className=\"day-wrapper flex-row p-p25 f-0\" style={{ overflow: \"auto\", maxWidth: \"100vw\" }}>\n//       {days.map((d, i) => (\n//         <button key={i} className={\"day flex-col f-0 b \" + (i? \"ml-p5\" : \"\") + (day === d? \" active \" : \" secondary bg-color-0 \")}\n//           onClick={()=>{\n//             setDay(d);\n//           }}\n//         >\n//           <div>{format(d, \"dd MMM\", { locale } )}</div>\n//           <div className=\" text-xl font-medium\" >{format(d, \"EEE\", { locale } )}</div>\n//           <div className=\"\">{getDayItems(d).length} </div>\n//         </button>\n//       ))}\n//     </div>\n//   );\n\n//   if(!day) return null;\n//   return (\n//     <div className={\"scheduler min-h-0 flex-col\" + className} style={style}>\n//       {getDaySelector()}\n//       <div className={\"flex-row p-1 f-1 o-auto min-h-0 relative b\"}>\n\n//         <div className={\"flex-col f-1\"}>\n//           {ticks.map((h, i) => (\n//             <div key={i} className=\"flex-row f-0\" style={{ height: HOUR_HEIGHT + \"px\"}}>\n//               <div className=\"f-0 text-2\">{h === 24? \"00\" : h}:00</div>\n//               <div className=\"f-1  ml-p5 mt-p5\" style={{ height: \"1px\"}} ></div>\n//             </div>\n//           ))}\n//         </div>\n\n//         {dayItems.map(({ content, from, to, post_code, onClick }, i) => {\n//           const topOffset = HOUR_HEIGHT * (getFromHour(from) - startHour);\n//           const btmOffset = HOUR_HEIGHT * (getToHour(to) - startHour);\n//           console.log(getFromHour(from), getToHour(to))\n//           const hours = +(differenceInMinutes(to, from)/60).toFixed(1);\n\n//           // debugger\n//           return (\n//             <button key={i} onClick={onClick} className=\"item rounded shadow-xl p-p5 absolute bg-color-0 w-fit flex-row text-white\"\n//             style={{\n//               left: \"70px\",\n//               top: `${topOffset + 2 + 24}px`,\n//               height: `${btmOffset - topOffset - 2 }px`\n//             }}>\n//               <div className=\"flex-col-wrap mr-2 h-full jc-between ai-start\" >\n//                 <div className=\"mr-1\">{format(from, \"HH:mm\", { locale } )}</div>\n//                 <div>{format(to, \"HH:mm\", { locale } )}</div>\n//               </div>\n//               <div className=\"flex-col-wrap ai-start\" style={{ }}>\n//                 <div className=\"text-medium mb-1 mr-1\">{`${hours} hour${hours > 1? \"s\" : \"\"}`}</div>\n//                 <div className=\"text-medium\">{post_code}</div>\n//               </div>\n//             </button>\n//           )\n//         })}\n\n//       </div>\n//     </div>\n//   );\n// }\n\n// export default Scheduler;\n"
  },
  {
    "path": "client/src/components/ScrollFade/ScrollFade.tsx",
    "content": "import React, { useCallback, useEffect, useState } from \"react\";\nimport type { TestSelectors } from \"../../Testing\";\nimport { classOverride, type DivProps } from \"../Flex\";\nimport { useResizeObserver } from \"./useResizeObserver\";\nimport { fixIndent, getEntries } from \"@common/utils\";\nimport { isDefined, scrollIntoViewIfNeeded } from \"../../utils/utils\";\nimport { isEqual } from \"prostgles-types\";\nimport { useLocation } from \"react-router-dom\";\n\ntype P = TestSelectors &\n  DivProps & {\n    children: React.ReactNode;\n    className?: string;\n    scrollRestore?: boolean;\n  };\n\ntype Sides = Record<\"top\" | \"bottom\" | \"left\" | \"right\", boolean>;\n\n/**\n * Given a list of children, this component will add a fade effect to the bottom of the children if the children are scrollable\n */\nexport const ScrollFade = ({ children, scrollRestore, ...divProps }: P) => {\n  const [elem, setElem] = useState<HTMLDivElement | null>(null);\n  const handleRef = useCallback((el: HTMLDivElement | null) => {\n    setElem(el);\n  }, []);\n  useScrollFade(elem);\n  useScrollRestore(scrollRestore ? elem : undefined);\n\n  return (\n    <div\n      ref={handleRef}\n      {...divProps}\n      className={classOverride(\"ScrollFade\", divProps.className ?? \"\")}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport const useScrollFade = (elem: HTMLElement | null) => {\n  const [overflows, setOverflows] = React.useState({ x: false, y: false });\n  const onScroll = useCallback(() => {\n    if (!elem) return;\n    const {\n      scrollHeight,\n      clientHeight,\n      scrollTop,\n      scrollWidth,\n      clientWidth,\n      scrollLeft,\n    } = elem;\n    const fadeClasses = {\n      bottom: false,\n      top: false,\n      right: false,\n      left: false,\n    };\n    const threshold = 10;\n    if (scrollHeight >= clientHeight) {\n      fadeClasses.top = scrollTop > threshold;\n      fadeClasses.bottom = scrollTop + clientHeight < scrollHeight - threshold;\n    }\n    if (scrollWidth >= clientWidth) {\n      fadeClasses.left = scrollLeft > threshold;\n      fadeClasses.right = scrollLeft + clientWidth < scrollWidth;\n    }\n    const finalMask = getEntries(fadeClasses)\n      .map(([k, v]) => {\n        if (v) return getGradient(k);\n      })\n      .filter(isDefined)\n      .join(\",\\n\");\n    elem.style.mask = finalMask;\n    /** Required to ensure the masks colors stack correctly */\n    elem.style[\"-webkit-mask-composite\"] = \"source-in\";\n  }, [elem]);\n\n  React.useEffect(() => {\n    if (!elem) return;\n    elem.addEventListener(\"scroll\", onScroll);\n    return () => elem.removeEventListener(\"scroll\", onScroll);\n  }, [elem, onScroll]);\n\n  const onResize = useCallback(() => {\n    onScroll();\n    const thresholdPx = 2;\n    if (!elem) return;\n    const newOverflows = {\n      x: elem.scrollWidth > elem.clientWidth + thresholdPx,\n      y: elem.scrollHeight > elem.clientHeight + thresholdPx,\n    };\n    if (!isEqual(newOverflows, overflows)) {\n      setOverflows(newOverflows);\n    }\n  }, [onScroll, elem, overflows]);\n\n  useResizeObserver({\n    elem,\n    box: \"border-box\",\n    onResize,\n  });\n\n  return overflows;\n};\n\nconst getGradient = (side: keyof Sides) => {\n  return fixIndent(`\n    linear-gradient(\n      to ${\n        side === \"bottom\" ? \"top\"\n        : side === \"left\" ? \"right\"\n        : side === \"right\" ? \"left\"\n        : \"bottom\"\n      },\n      rgba(0, 0, 0, 0.1) 0px,\n      rgba(0, 0, 0, 0.9) 40px,\n      rgba(0, 0, 0, 1) 80px,\n      rgba(0, 0, 0, 1) 100%\n    )`);\n};\n\n/**\n * On fragment change will scroll to the appropriate element and restore scroll position on unmount\n */\nconst useScrollRestore = (list: HTMLElement | undefined | null) => {\n  const { hash } = useLocation();\n  const noHashScrollRef = React.useRef(0);\n\n  useEffect(() => {\n    if (!list || hash) return;\n    const onScroll = () => {\n      const currentHash = window.location.hash;\n      if (currentHash) return;\n      noHashScrollRef.current = list.scrollTop;\n    };\n    list.addEventListener(\"scroll\", onScroll);\n    return () => list.removeEventListener(\"scroll\", onScroll);\n  }, [list, hash]);\n\n  useEffect(() => {\n    if (!list) return;\n    if (!hash) {\n      list.scrollTop = noHashScrollRef.current;\n    } else {\n      try {\n        const el = list.querySelector<HTMLHeadElement>(\n          `[id='${hash.slice(1)}']`,\n        );\n        if (el) {\n          scrollIntoViewIfNeeded(el);\n        }\n      } catch {}\n    }\n  }, [hash, list]);\n};\n"
  },
  {
    "path": "client/src/components/ScrollFade/useResizeObserver.ts",
    "content": "import { useIsMounted } from \"prostgles-client\";\nimport { useEffect, useRef, useState } from \"react\";\n\ntype Size = {\n  width: number | undefined;\n  height: number | undefined;\n};\n\ntype UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {\n  elem: T | null;\n  onResize?: (size: Size) => void;\n  box?: \"border-box\" | \"content-box\" | \"device-pixel-content-box\";\n};\n\nconst initialSize: Size = {\n  width: undefined,\n  height: undefined,\n};\n\nexport function useResizeObserver<T extends HTMLElement = HTMLElement>(\n  options: UseResizeObserverOptions<T>,\n): Size {\n  const { elem, box = \"content-box\" } = options;\n  const [{ width, height }, setSize] = useState<Size>(initialSize);\n  const getIsMounted = useIsMounted();\n  const previousSize = useRef<Size>({ ...initialSize });\n  const onResize = useRef<((size: Size) => void) | undefined>(undefined);\n  onResize.current = options.onResize;\n\n  useEffect(() => {\n    if (!elem) return;\n\n    if (typeof window === \"undefined\" || !(\"ResizeObserver\" in window)) return;\n\n    const observer = new ResizeObserver((d) => {\n      const [entry] = d;\n      const boxProp =\n        box === \"border-box\" ? \"borderBoxSize\"\n        : box === \"device-pixel-content-box\" ? \"devicePixelContentBoxSize\"\n        : \"contentBoxSize\";\n\n      const newWidth = extractSize(entry, boxProp, \"inlineSize\");\n      const newHeight = extractSize(entry, boxProp, \"blockSize\");\n\n      const hasChanged =\n        previousSize.current.width !== newWidth ||\n        previousSize.current.height !== newHeight;\n\n      if (hasChanged) {\n        const newSize: Size = { width: newWidth, height: newHeight };\n        previousSize.current.width = newWidth;\n        previousSize.current.height = newHeight;\n\n        if (onResize.current) {\n          onResize.current(newSize);\n        } else {\n          if (getIsMounted()) {\n            setSize(newSize);\n          }\n        }\n      }\n    });\n\n    observer.observe(elem, { box });\n\n    return () => {\n      observer.disconnect();\n    };\n  }, [box, elem, getIsMounted]);\n\n  return { width, height };\n}\n\ntype BoxSizesKey = keyof Pick<\n  ResizeObserverEntry,\n  \"borderBoxSize\" | \"contentBoxSize\" | \"devicePixelContentBoxSize\"\n>;\n\nfunction extractSize(\n  entry: ResizeObserverEntry,\n  box: BoxSizesKey,\n  sizeType: keyof ResizeObserverSize,\n): number | undefined {\n  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n  if (!entry[box]) {\n    if (box === \"contentBoxSize\") {\n      return entry.contentRect[sizeType === \"inlineSize\" ? \"width\" : \"height\"];\n    }\n    return undefined;\n  }\n\n  return Array.isArray(entry[box]) ?\n      entry[box][0]![sizeType]\n      // @ts-ignore Support Firefox's non-standard behavior\n    : (entry[box][sizeType] as number);\n}\n"
  },
  {
    "path": "client/src/components/SearchList/SearchInput.tsx",
    "content": "import { mdiFormatLetterCase } from \"@mdi/js\";\nimport React from \"react\";\nimport type { TestSelectors } from \"../../Testing\";\nimport Btn from \"../Btn\";\nimport { classOverride, FlexRow } from \"../Flex\";\nimport { Input } from \"../Input\";\nimport Loading from \"../Loader/Loading\";\n\nexport const SearchInputZIndex = 2;\n\nexport type SearchInputProps = Pick<\n  React.HTMLProps<HTMLInputElement>,\n  \"type\" | \"title\" | \"autoFocus\"\n> &\n  TestSelectors & {\n    placeholder?: string;\n    className?: string;\n    wrapperStyle?: React.CSSProperties;\n    onClickWrapper?: (e: React.MouseEvent<HTMLDivElement>) => void;\n    style?: React.CSSProperties;\n    value: string;\n    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n    withShadow: boolean | undefined;\n    isLoading?: boolean;\n    matchCase?: {\n      value: boolean;\n      onChange: (value: boolean) => void;\n    };\n    inputWrapperRef?: React.LegacyRef<HTMLDivElement>;\n    inputRef?: React.LegacyRef<HTMLInputElement>;\n    mode: undefined | { \"!listNode\": boolean; \"!noList\": boolean };\n    leftContent?: React.ReactNode;\n  };\n\nexport const SearchInput = (props: SearchInputProps) => {\n  const {\n    type = \"text\",\n    className,\n    onClickWrapper,\n    wrapperStyle,\n    inputWrapperRef,\n    inputRef,\n    matchCase,\n    isLoading,\n    withShadow,\n    style,\n    mode,\n    leftContent,\n    ...inputProps\n  } = props;\n\n  const size = window.isLowWidthScreen ? \"small\" : undefined;\n\n  return (\n    <FlexRow\n      ref={inputWrapperRef}\n      className={\n        \"SearchList_InputWrapper bg-color-0 gap-0 h-fit f-1 relative o-hidden relative rounded focus-border b b-color \" +\n        (withShadow ? \" shadow \" : \" \")\n      }\n      style={{\n        ...wrapperStyle,\n        ...(mode && {\n          zIndex: mode[\"!listNode\"] ? \"unset\" : SearchInputZIndex,\n        }),\n      }}\n      onClick={onClickWrapper}\n    >\n      {leftContent}\n      <Input\n        className={classOverride(\"Input f-1 w-full\", className)}\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        type={type}\n        ref={inputRef}\n        style={{\n          minWidth: \"5em\",\n          ...(mode?.[\"!noList\"] && {\n            borderBottomLeftRadius: 0,\n            borderBottomRightRadius: 0,\n            zIndex: 3,\n          }),\n          ...(matchCase && {\n            borderTopRightRadius: 0,\n            borderBottomRightRadius: 0,\n          }),\n          ...style,\n          ...(size !== \"small\" && {\n            padding: \"0.75em\",\n            paddingRight: 0,\n          }),\n          ...(leftContent && {\n            paddingLeft: 0,\n          }),\n        }}\n        autoComplete=\"off\"\n        title={\"Search\"}\n        {...inputProps}\n      />\n      <FlexRow\n        className=\"relative rounded f-0 ai-center jc-center gap-0 bg-color-0 \"\n        style={{\n          borderTopLeftRadius: 0,\n          borderBottomLeftRadius: 0,\n          overflow: \"visible\",\n          margin: \"0px\",\n        }}\n      >\n        {isLoading && (\n          <Loading\n            className=\"noselect mr-p5 bg-color-0\"\n            sizePx={24}\n            variant=\"cover\"\n          />\n        )}\n\n        {matchCase && (\n          <Btn\n            data-command=\"SearchList.MatchCase\"\n            title={\"Match case\"}\n            iconPath={mdiFormatLetterCase}\n            style={{\n              margin: \"1px\",\n              visibility: isLoading ? \"hidden\" : \"visible\",\n            }}\n            color={matchCase.value ? \"action\" : undefined}\n            onClick={() => {\n              matchCase.onChange(!matchCase);\n            }}\n          />\n        )}\n      </FlexRow>\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/components/SearchList/SearchList.css",
    "content": "::placeholder {\n  /* Chrome, Firefox, Opera, Safari 10.1+ */\n  color: var(--gray-400);\n  opacity: 1; /* Firefox */\n  user-select: none;\n}\n\n:-ms-input-placeholder {\n  /* Internet Explorer 10-11 */\n  color: var(--gray-400);\n  user-select: none;\n}\n\n::-ms-input-placeholder {\n  /* Microsoft Edge */\n  color: var(--gray-400);\n  user-select: none;\n}\n\n.SearchList_InputWrapper {\n  border-color: var(--gray-300);\n}\n.dark-theme .SearchList_InputWrapper {\n  background-color: #1c1c1c;\n  border-color: var(--gray-500);\n}\n.dark-theme .SearchList_InputWrapper input {\n  background-color: #1c1c1c;\n  color: var(--gray-100);\n}\n\n.dark-theme .search-text-endings {\n  color: #969696;\n}\n.dark-theme .search-text-match {\n  color: #dddddd;\n}\n"
  },
  {
    "path": "client/src/components/SearchList/SearchList.tsx",
    "content": "import type { Primitive } from \"d3\";\nimport { type AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type { TestSelectors } from \"../../Testing\";\nimport \"../List.css\";\nimport type { OptionKey } from \"../Select/Select\";\nimport type { SearchInputProps } from \"./SearchInput\";\nimport \"./SearchList.css\";\nimport { SearchListContent } from \"./SearchListContent\";\n\nexport type SearchListItemContent = {\n  content?: React.ReactNode;\n  contentLeft?: React.ReactNode;\n  contentRight?: React.ReactNode;\n  contentBottom?: React.ReactNode;\n  contentTop?: React.ReactNode;\n};\nexport type SearchListItem = TestSelectors & {\n  key: OptionKey;\n  label?: string | React.ReactNode;\n  /**\n   * Parent labels are used to group items in the search list.\n   */\n  parentLabels?: string[];\n  rowStyle?: React.CSSProperties;\n  rowClassname?: string;\n  title?: string;\n  subLabel?: string;\n  ranking?: number | ((searchTerm: string) => number);\n  checked?: boolean;\n  selected?: boolean;\n  onPress?: (\n    e:\n      | React.MouseEvent<HTMLLIElement, globalThis.MouseEvent>\n      | React.KeyboardEvent<HTMLLIElement>,\n    term?: string,\n  ) => void;\n  style?: React.CSSProperties;\n  styles?: {\n    rowInner?: React.CSSProperties;\n    labelWrapper?: React.CSSProperties;\n    label?: React.CSSProperties;\n    subLabel?: React.CSSProperties;\n    labelRootWrapperStyle?: React.CSSProperties;\n  };\n  data?: AnyObject | Primitive | null;\n  disabledInfo?: string;\n} & SearchListItemContent;\n\nexport type ParsedListItem = SearchListItem & {\n  node?: React.ReactNode;\n  rank?: number;\n};\n\nexport type SearchListProps<M extends boolean = false> = TestSelectors & {\n  defaultSearch?: string;\n  defaultValue?: string;\n\n  onChange?: (val: M extends true ? OptionKey[] : OptionKey, e: any) => void;\n  onSearch?: (term: string, e: any) => void;\n  onSearchItems?: (\n    term: string,\n    opts?: { matchCase?: boolean },\n    onPartialResult?: (\n      searchItems: SearchListItem[],\n      finished: boolean,\n      cancel: VoidFunction,\n    ) => any,\n  ) => Promise<SearchListItem[]>;\n  onType?: (term: string, setTerm: (newTerm: string) => void) => void;\n  items?: SearchListItem[];\n  onReorder?: (newItems: SearchListItem[]) => void;\n  style?: React.CSSProperties;\n  className?: string;\n  /**\n   * The id is used for the header checkbox to ensure it works\n   */\n  id?: string;\n\n  inputID?: string;\n  checkboxed?: boolean;\n  placeholder?: string;\n  autoFocus?: boolean;\n  inputProps?: Pick<\n    SearchInputProps,\n    \"type\" | \"leftContent\" | keyof TestSelectors | \"autoFocus\"\n  >;\n\n  leftContent?: React.ReactNode;\n\n  /**\n   * If provided then allows toggling all values\n   */\n  onMultiToggle?: (\n    items: SearchListItem[],\n    e: React.ChangeEvent<HTMLInputElement>,\n  ) => void;\n  inputEl?: HTMLElement;\n  dontHighlight?: boolean;\n  label?: React.ReactNode;\n  variant?: \"search\" | \"search-no-shadow\";\n  noBorder?: boolean;\n  selectedKey?: OptionKey;\n  rootStyle?: React.CSSProperties;\n\n  /**\n   * Number of rows to slice from result\n   */\n  limit?: number;\n\n  /**\n   * If number of items is below this value then hide search bar\n   */\n  noSearchLimit?: number;\n\n  /**\n   * If specified then pressing Enter will trigger this instead of the first result UNLESS arrow keys have been moved\n   */\n  onPressEnter?: (term: string) => any;\n\n  matchCase?:\n    | {\n        hide?: undefined;\n        value?: boolean;\n        onChange: (matchCase: boolean) => any;\n      }\n    | {\n        hide: true;\n        value?: undefined;\n        onChange?: undefined;\n      };\n\n  searchStyle?: React.CSSProperties;\n  searchEmpty?: boolean;\n\n  /* Used to identify when to refresh the data */\n  dataSignature?: string;\n\n  inputStyle?: React.CSSProperties;\n\n  noResultsContent?: React.ReactNode;\n  endOfResultsContent?: React.ReactNode;\n\n  rowStyleVariant?: \"row-wrap\";\n};\n\nexport const SearchList = <M extends boolean = false>(\n  props: SearchListProps<M>,\n) => {\n  return <SearchListContent {...props} />;\n};\n"
  },
  {
    "path": "client/src/components/SearchList/SearchListContent.tsx",
    "content": "import { isObject } from \"@common/publishUtils\";\nimport React, { useEffect, useMemo } from \"react\";\nimport { Checkbox } from \"../Checkbox\";\nimport ErrorComponent from \"../ErrorComponent\";\nimport { generateUniqueID } from \"../FileInput/FileInput\";\nimport { Label } from \"../Label\";\nimport { SearchInput } from \"./SearchInput\";\nimport type { SearchListProps } from \"./SearchList\";\nimport { SearchListItems } from \"./SearchListItems\";\nimport { useSearchListItems } from \"./hooks/useSearchListItems\";\nimport { useSearchListOnClick } from \"./hooks/useSearchListOnClick\";\nimport {\n  SEARCH_LIST_INPUT_CLASSNAME,\n  useSearchListOnKeyUpDown,\n} from \"./hooks/useSearchListOnKeyUpDown\";\nimport { useSearchListSearch } from \"./hooks/useSearchListSearch\";\n\nexport const SearchListContent = <M extends boolean = false>(\n  props: SearchListProps<M>,\n) => {\n  const {\n    rootStyle = {},\n    label,\n    searchStyle = {},\n    variant,\n    onMultiToggle,\n    items = [],\n    inputID,\n    className,\n    onSearchItems,\n    searchEmpty = false,\n    placeholder,\n    inputProps,\n    inputEl,\n    noSearchLimit = 5,\n    style = {},\n    autoFocus,\n    onChange,\n    onPressEnter,\n    dataSignature,\n    leftContent,\n    noBorder,\n  } = props;\n  const multiSelect = !!onMultiToggle;\n  const noShadow = variant?.includes(\"no-shadow\");\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const inputWrapperRef = React.useRef<HTMLDivElement>(null);\n  const refList = React.useRef<HTMLUListElement>(null);\n  const idRef = React.useRef(props.id ?? inputID ?? generateUniqueID());\n  const id = idRef.current;\n  const showHover = Boolean(onChange || onMultiToggle || onPressEnter);\n  const {\n    matchCase,\n    setMatchCase,\n    onSetTerm,\n    searchTerm,\n    endSearch,\n    searchClosed,\n    searchItems = [],\n    error,\n    searchingItems,\n    onStartSearch,\n    searching,\n    setSearchClosed,\n    isSearch,\n  } = useSearchListSearch(props);\n\n  const { renderedItems, allSelected, renderedSelected } = useSearchListItems({\n    ...props,\n    searchItems,\n    searchTerm,\n    matchCase,\n  });\n\n  const noList = isSearch ? searchClosed : false; // !renderedItems.length && !searchTerm;\n\n  const wrapperStyleFinal = useMemo(() => {\n    if (noBorder) {\n      return {\n        borderRadius: 0,\n        borderTop: \"unset\",\n        borderBottom: \"unset\",\n      };\n    }\n    if (searchClosed) return {};\n    return {\n      ...{\n        borderBottomLeftRadius: 0,\n        borderBottomRightRadius: 0,\n      },\n    };\n  }, [noBorder, searchClosed]);\n\n  useSearchListOnClick({\n    isSearch,\n    dataSignature,\n    onStartSearch,\n    refInput: inputRef,\n    refList: refList,\n    searching,\n    searchClosed,\n    setSearchClosed,\n  });\n\n  useEffect(() => {\n    if (autoFocus) {\n      setTimeout(() => {\n        inputRef.current?.focus();\n      }, 5);\n    }\n  }, [autoFocus]);\n\n  const { onKeyDown } = useSearchListOnKeyUpDown({\n    refList,\n    onPressEnter,\n    endSearch,\n    searchTerm,\n    refInput: inputRef,\n  });\n\n  const noSearch =\n    !onSearchItems &&\n    items.length < noSearchLimit &&\n    !searchTerm &&\n    /** Ensure leftContent is shown even if search is not necessary */\n    !leftContent;\n\n  if (props.noSearchLimit && leftContent) {\n    console.warn(\n      \"SearchList: noSearchLimit is ignored when leftContent is provided\",\n    );\n  }\n\n  const hasSearch = !(noSearch || inputEl);\n\n  const listNode =\n    error ? <ErrorComponent error={error} />\n    : noList ? null\n    : <SearchListItems\n        {...props}\n        ref={refList}\n        renderedItems={renderedItems}\n        isSearch={isSearch}\n        searchTerm={searchTerm}\n        inputWrapperRef={inputWrapperRef}\n        searchingItems={searchingItems}\n        endSearch={endSearch}\n        showHover={showHover}\n      />;\n  return (\n    <div\n      data-command=\"SearchList\"\n      className={\"SearchList list-comp ta-left flex-col min-h-0 \" + className}\n      ref={rootRef}\n      onKeyDown={onKeyDown}\n      style={{ ...style, ...(!isSearch ? rootStyle : {}) }}\n    >\n      {!!label && (\n        <Label className=\"mb-p25\" variant=\"normal\">\n          {label}\n        </Label>\n      )}\n      <div\n        className={\n          \"SearchListInput f-0 min-h-0 min-w-0 flex-row gap-p5 jc-start relative \" +\n          (!hasSearch && multiSelect ? \" bg-color-2 \" : \"\") +\n          (isSearch ? \" \" : \"  ai-center  \") +\n          (!hasSearch && !multiSelect ? \" hidden\" : \"\")\n        }\n        style={searchStyle}\n      >\n        {!!multiSelect && (\n          <Checkbox\n            title=\"Toggle all\"\n            className={!renderedItems.length ? \"hidden\" : \"\"}\n            data-command=\"SearchList.toggleAll\"\n            checked={Boolean(renderedSelected.length)}\n            onChange={(e) => {\n              const checked = e.currentTarget.checked;\n\n              const newItems = items.map((d) => {\n                /** If filteted then only update the visible items */\n                const filteredItem =\n                  !searchTerm ? d : (\n                    renderedItems.find((_d) => _d.key === d.key)\n                  );\n                return {\n                  ...d,\n                  checked:\n                    filteredItem ?\n                      d.disabledInfo ?\n                        d.checked\n                      : checked\n                    : d.checked,\n                };\n              });\n\n              onMultiToggle(newItems, e);\n            }}\n          />\n        )}\n        {!hasSearch ?\n          multiSelect ?\n            <div className=\"pl-1 py-p5 noselect text-1p5 ws-nowrap\">\n              {allSelected.length || 0} selected\n            </div>\n          : null\n        : <SearchInput\n            id={id}\n            leftContent={leftContent}\n            withShadow={isSearch && !noShadow}\n            inputRef={inputRef}\n            inputWrapperRef={inputWrapperRef}\n            onClickWrapper={(e) => {\n              if (!listNode && searchEmpty) {\n                onSetTerm(searchTerm || \"\", e);\n              }\n            }}\n            wrapperStyle={wrapperStyleFinal}\n            className={SEARCH_LIST_INPUT_CLASSNAME}\n            data-command=\"SearchList.Input\"\n            {...inputProps}\n            placeholder={placeholder}\n            title={\"Search\"}\n            value={searchTerm}\n            onChange={(e) => {\n              const searchTerm = e.currentTarget.value;\n              onSetTerm(searchTerm, e);\n            }}\n            mode={\n              isSearch ?\n                {\n                  \"!listNode\": !listNode,\n                  \"!noList\": !noList,\n                }\n              : undefined\n            }\n            matchCase={\n              props.matchCase?.hide ?\n                undefined\n              : {\n                  value: !!matchCase,\n                  onChange: (matchCase) => {\n                    if (props.matchCase?.onChange) {\n                      props.matchCase.onChange(matchCase);\n                    } else {\n                      setMatchCase(matchCase);\n                    }\n                  },\n                }\n            }\n            isLoading={isSearch && searchingItems}\n          />\n        }\n      </div>\n      {listNode}\n    </div>\n  );\n};\n\nexport const getValueAsText = (v: unknown): string | null | undefined =>\n  v && (isObject(v) || Array.isArray(v)) ? JSON.stringify(v) : v?.toString();\n"
  },
  {
    "path": "client/src/components/SearchList/SearchListItems.tsx",
    "content": "import { useScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport React, {\n  forwardRef,\n  useCallback,\n  useImperativeHandle,\n  useMemo,\n  useState,\n} from \"react\";\nimport { ClickCatchOverlay } from \"../ClickCatchOverlay\";\nimport { DraggableLI } from \"../DraggableLI\";\nimport { classOverride, FlexCol } from \"../Flex\";\nimport { POPUP_CLASSES } from \"../Popup/Popup\";\nimport type {\n  ParsedListItem,\n  SearchListItem,\n  SearchListProps,\n} from \"./SearchList\";\nimport { SearchListRowContent } from \"./SearchListRowContent\";\n\nexport type SearchListItemsProps = Pick<\n  SearchListProps,\n  | \"items\"\n  | \"onSearch\"\n  | \"data-command\"\n  | \"data-key\"\n  | \"endOfResultsContent\"\n  | \"noResultsContent\"\n  | \"onReorder\"\n> & {\n  renderedItems: ParsedListItem[];\n  isSearch: boolean | undefined;\n  searchTerm: string | undefined;\n  inputWrapperRef: React.RefObject<HTMLDivElement>;\n  searchingItems: boolean;\n  endSearch: (force?: boolean) => void;\n  showHover: boolean;\n};\nexport const SearchListItems = forwardRef<\n  HTMLUListElement | null,\n  SearchListItemsProps\n>((props: SearchListItemsProps, ref) => {\n  const {\n    renderedItems,\n    items = [],\n    searchTerm,\n    isSearch,\n    onSearch,\n    inputWrapperRef,\n    endOfResultsContent,\n    noResultsContent,\n    searchingItems,\n    endSearch,\n    onReorder,\n  } = props;\n  const inputWrapper = inputWrapperRef.current;\n  const notAllItemsShown =\n    renderedItems.length && renderedItems.length < items.length && !searchTerm;\n\n  const [node, setNode] = useState<HTMLUListElement | null>(null);\n  const handleRef = useCallback((el: HTMLUListElement | null) => {\n    setNode(el);\n  }, []);\n  useImperativeHandle(ref, () => node as HTMLUListElement, [node]);\n  useScrollFade(node);\n\n  const listStyle = useMemo(() => {\n    return {\n      padding: 0,\n      ...(!isSearch ?\n        {}\n      : {\n          position: \"absolute\",\n          zIndex: 1,\n          left: 0,\n          right: 0,\n          maxHeight: \"400px\",\n          ...(inputWrapper &&\n            (() => {\n              const bbox = inputWrapper.getBoundingClientRect();\n              const outlineSize = 1;\n              let top = bbox.top + bbox.height + outlineSize;\n              let left = bbox.left;\n              const popup = inputWrapper.closest<HTMLDivElement>(\n                `.${POPUP_CLASSES.root}`,\n              );\n              if (popup && popup.style.transform) {\n                const pRect = popup.getBoundingClientRect();\n                top -= Math.round(pRect.top);\n                left -= Math.round(pRect.left);\n              }\n              return {\n                position: \"fixed\",\n                top,\n                left,\n                zIndex: 3,\n                right: bbox.right,\n                width: `${bbox.width}px`,\n              };\n            })()),\n        }),\n    } satisfies React.CSSProperties;\n  }, [inputWrapper, isSearch]);\n\n  return (\n    <div\n      className={\n        \"SearchList_Suggestions flex-col relative w-full f-1  min-h-0 min-w-0 \" +\n        (isSearch ? \" o-visible \" : \" \")\n      }\n      data-command={props[\"data-command\"]}\n      data-key={props[\"data-key\"]}\n    >\n      {isSearch && !!renderedItems.length && <ClickCatchOverlay />}\n      <FlexCol\n        className={\n          \"f-1 max-h-fit min-h-0 min-w-0  rounded-b\" +\n          (isSearch ? \"  shadow bg-color-0 \" : \"\")\n        }\n        style={listStyle}\n      >\n        <ul\n          className={\n            \"no-decor f-1 max-h-fit min-h-0 min-w-0 ul-search-list o-auto rounded-b  no-scroll-bar \" +\n            (isSearch ? \"  shadow bg-color-0 \" : \"\")\n          }\n          role=\"listbox\"\n          ref={handleRef}\n          data-command={\"SearchList.List\"}\n        >\n          {onSearch && !props.items ? null : (\n            renderedItems.map((renderedItem, i) => {\n              const onPress: SearchListItem[\"onPress\"] =\n                !renderedItem.onPress || renderedItem.disabledInfo ?\n                  undefined\n                : (e) => {\n                    e.stopPropagation();\n                    e.preventDefault();\n                    renderedItem.onPress!(e, searchTerm);\n                    endSearch();\n                  };\n\n              const asStringIfPossible = (v: any) => {\n                if (typeof v === \"string\" || typeof v === \"number\") {\n                  return v.toString();\n                }\n                if (v === null) {\n                  return \"null\";\n                }\n                return \"\";\n              };\n              return (\n                <React.Fragment key={i}>\n                  {renderedItem.contentTop}\n                  <DraggableLI\n                    role={onPress ? \"option\" : \"listitem\"}\n                    data-command={renderedItem[\"data-command\"]}\n                    data-key={asStringIfPossible(renderedItem.key)}\n                    data-label={asStringIfPossible(renderedItem.label)}\n                    aria-disabled={!!renderedItem.disabledInfo}\n                    aria-selected={!!renderedItem.selected}\n                    title={renderedItem.disabledInfo ?? renderedItem.title}\n                    style={{\n                      ...renderedItem.rowStyle,\n                      ...(renderedItem.disabledInfo ?\n                        {\n                          cursor: \"not-allowed\",\n                          opacity: 0.4,\n                          touchAction: \"none\",\n                          // pointerEvents: \"none\",\n                        }\n                      : {}),\n                    }}\n                    tabIndex={-1}\n                    idx={i}\n                    items={items.slice(0)}\n                    onReorder={onReorder}\n                    className={classOverride(\n                      \"noselect bg-li flex-row ai-start p-p5 min-w-0 \" +\n                        (renderedItem.selected ? \" selected \" : \"\") +\n                        (renderedItem.disabledInfo ? \" not-allowed \"\n                        : renderedItem.onPress ? \" pointer \"\n                        : \"\") +\n                        (!renderedItem.onPress && !props.showHover ?\n                          \" no-hover \"\n                        : \" \"),\n                      renderedItem.rowClassname,\n                    )}\n                    onClick={(e) => {\n                      return onPress?.(e, searchTerm);\n                    }}\n                    onKeyUp={\n                      !onPress ? undefined : (\n                        (e) => {\n                          if (e.key === \"Enter\") {\n                            onPress(e, searchTerm);\n                          }\n                        }\n                      )\n                    }\n                  >\n                    <SearchListRowContent item={renderedItem} />\n                  </DraggableLI>\n                </React.Fragment>\n              );\n            })\n          )}\n          {!renderedItems.length && !searchingItems && (\n            <div className=\"p-p5 text-1 no-data\">\n              {noResultsContent ??\n                (!endOfResultsContent ? <div>No results</div> : null)}\n            </div>\n          )}\n          {notAllItemsShown ?\n            <div className=\"p-p5 pl-1 noselect text-2\">\n              Not all items shown...\n            </div>\n          : endOfResultsContent}\n        </ul>\n      </FlexCol>\n    </div>\n  );\n});\n"
  },
  {
    "path": "client/src/components/SearchList/SearchListRowContent.tsx",
    "content": "import { Checkbox } from \"@components/Checkbox\";\nimport React from \"react\";\nimport type { ParsedListItem } from \"./SearchList\";\n\nexport const SearchListRowContent = ({ item }: { item: ParsedListItem }) => {\n  if (\"content\" in item) return item.content;\n  const { contentLeft, contentBottom, contentRight } = item;\n\n  return (\n    <div\n      className=\"ROWINNER flex-row ai-center f-1 gap-p5 \"\n      style={item.styles?.rowInner}\n    >\n      {typeof item.checked === \"boolean\" && (\n        <Checkbox\n          id={item.id}\n          className=\"f-0 no-pointer-events\"\n          checked={item.checked}\n          onChange={() => {}}\n        />\n      )}\n      {contentLeft || null}\n      <div\n        className=\"LABELWRAPPER flex-col ai-start f-1\"\n        style={item.styles?.labelWrapper}\n      >\n        <label\n          className={\n            \"ws-pre mr-p5 f-1 flex-row noselect min-w-0 w-full \" +\n            (item.disabledInfo ? \" not-allowed \"\n            : item.onPress ? \" pointer \"\n            : \" \")\n          }\n          style={item.style}\n        >\n          {item.node}\n        </label>\n        {contentBottom}\n      </div>\n      {contentRight || null}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/SearchList/getSearchListMatchAndHighlight.tsx",
    "content": "import React from \"react\";\nimport { isDefined } from \"src/utils/utils\";\n\nexport const getSearchListMatchAndHighlight = (args: {\n  ranking?: number;\n  term: string;\n  text: string;\n  key?: any;\n  subLabel?: string;\n  matchCase?: boolean;\n  style?: React.CSSProperties;\n  subLabelStyle?: React.CSSProperties;\n  rootStyle?: React.CSSProperties;\n}): { node: React.ReactNode; rank: number } => {\n  const getNode = (r: {\n    ranking?: number;\n    term: string;\n    text: string;\n    key?: any;\n    isSublabel?: boolean;\n    matchCase?: boolean;\n    style?: React.CSSProperties;\n  }) => {\n    const { term, text, key, isSublabel = false, matchCase = false } = r;\n    const style: React.CSSProperties =\n        !isSublabel ?\n          {\n            fontSize: \"18px\",\n            fontWeight: term ? undefined : 500,\n          }\n        : {\n            fontSize: \"14px\",\n            marginTop: \".25em\",\n          },\n      rootColorClass = isSublabel ? `SubLabel text-1` : `text-0`;\n\n    const rootStyle = { whiteSpace: \"normal\", ...style, ...r.style };\n    let rank = 0,\n      label = text || \"\";\n    const noTermLabel =\n      isSublabel ? label.split(\"\\n\").slice(0, 3).join(\"\\n\") : label;\n    let node = (\n      <span className={rootColorClass + ` text-ellipsis`} style={rootStyle}>\n        {noTermLabel}\n      </span>\n    );\n    if (term) {\n      const lbl = matchCase ? label : label.toLowerCase(),\n        strm = matchCase ? term : term.toLowerCase();\n      let idx = lbl.indexOf(strm);\n\n      rank = r.ranking ?? idx;\n      if (idx > -1) {\n        let prevLines: string[] | undefined;\n        let nextLines: string[] | undefined;\n        if (isSublabel) {\n          const lines = label.split(\"\\n\");\n          const matchingLineIdx = lines.findIndex((l) =>\n            matchCase ?\n              l.includes(term)\n            : l.toLowerCase().includes(term.toLowerCase()),\n          );\n          label = lines[matchingLineIdx] ?? \"\";\n          idx =\n            matchCase ?\n              label.indexOf(term)\n            : label.toLowerCase().indexOf(term.toLowerCase());\n\n          if (matchingLineIdx > 2) {\n            prevLines = lines.slice(matchingLineIdx - 2, matchingLineIdx - 1);\n          }\n          nextLines = lines.slice(matchingLineIdx + 1, matchingLineIdx + 2);\n        } else {\n          /** Join lines into one */\n          label = label.split(\"\\n\").join(\" \");\n        }\n        const shortenText = label.length > 40;\n        node = (\n          <div\n            className=\"MatchRoot flex-col  f-1\"\n            style={rootStyle}\n            title={text}\n          >\n            {prevLines !== undefined && prevLines.length > 0 && (\n              <span className=\"f-0 text-2 text-ellipsis\">\n                {prevLines.join(\"\\n\")}\n              </span>\n            )}\n            <div\n              className=\"MatchRow flex-row f-1\"\n              style={{ whiteSpace: \"normal\" }}\n            >\n              <div\n                className={`${shortenText ? \"f-1\" : \"f-0\"} search-text-endings text-ellipsis`}\n                style={{ maxWidth: \"fit-content\" }}\n              >\n                <span>{label.slice(0, idx)}</span>\n                <strong className=\"f-0 search-text-match\">\n                  {label.slice(idx, idx + strm.length)}\n                </strong>\n                <span>{label.slice(idx + strm.length)}</span>\n              </div>\n            </div>\n            {nextLines !== undefined && nextLines.length > 0 && (\n              <span className=\"f-0 text-2 text-ellipsis\">\n                {nextLines.join(\"\\n\")}\n              </span>\n            )}\n          </div>\n        );\n      } else {\n        rank = Infinity;\n      }\n    }\n\n    if (!label) {\n      if (key === \"\") node = <i>[Empty]</i>;\n      if (key === null) node = <i>[NULL]</i>;\n    }\n\n    return { rank, node };\n  };\n\n  const {\n    term,\n    text,\n    key,\n    subLabel,\n    matchCase,\n    style,\n    subLabelStyle,\n    rootStyle,\n    ranking,\n  } = args;\n  const titleNode = getNode({ term, text, key, matchCase, style, ranking });\n\n  const result = titleNode;\n  if (subLabel) {\n    const subTitleNode = getNode({\n      term,\n      text: subLabel,\n      key: subLabel,\n      isSublabel: true,\n      matchCase,\n      style: subLabelStyle,\n    });\n    result.node = (\n      <div className=\"flex-col f-1\" style={rootStyle}>\n        {titleNode.node}\n        {subTitleNode.node}\n      </div>\n    );\n    let rank = ranking ?? Math.min(titleNode.rank, subTitleNode.rank + 5);\n    if (isDefined(ranking)) {\n    } else if (titleNode.rank !== Infinity && subTitleNode.rank !== Infinity) {\n      rank /= 2;\n    }\n\n    result.rank = rank;\n  }\n  return result;\n};\n"
  },
  {
    "path": "client/src/components/SearchList/hooks/useSearchListItems.tsx",
    "content": "import { Icon } from \"@components/Icon/Icon\";\nimport { mdiPlus } from \"@mdi/js\";\nimport { isEqual } from \"prostgles-types\";\nimport React from \"react\";\nimport { FlexRow, FlexRowWrap } from \"../../Flex\";\nimport type { SearchListItem, SearchListProps } from \"../SearchList\";\nimport { useSearchListItemsSorting } from \"./useSearchListItemsSorting\";\nimport type { SearchListState } from \"./useSearchListSearch\";\n\nexport type SearchListItemsState = ReturnType<typeof useSearchListItems>;\nexport const useSearchListItems = (\n  props: Pick<\n    SearchListProps,\n    | \"rowStyleVariant\"\n    | \"limit\"\n    | \"onSearchItems\"\n    | \"onSearch\"\n    | \"items\"\n    | \"dontHighlight\"\n  > &\n    Pick<SearchListState, \"searchItems\" | \"searchTerm\"> & {\n      matchCase: boolean;\n    },\n) => {\n  const { rowStyleVariant, items = [] } = props;\n  const styles = rowStyleVariant === \"row-wrap\" ? rowWrapStyle : undefined;\n\n  const { itemGroupHeaders, renderedItemsWithoutHeaders, getFullItem } =\n    useSearchListItemsSorting(props);\n\n  const renderedSelected = renderedItemsWithoutHeaders.filter(\n    (d) => !d.disabledInfo && d.checked,\n  );\n\n  const allSelected = items.filter((d) => d.checked);\n\n  const renderedItems =\n    itemGroupHeaders.length ?\n      itemGroupHeaders.flatMap(({ parentLabels }) => {\n        const groupItems = renderedItemsWithoutHeaders.filter((d) =>\n          isEqual(d.parentLabels, parentLabels),\n        );\n        const [firstGroupItem] = groupItems;\n        if (!firstGroupItem) return [];\n        const leftSpacer =\n          !parentLabels.length ? undefined : (\n            <Icon path={mdiPlus} style={{ opacity: 0 }} />\n          );\n\n        return groupItems.map(({ contentLeft, ...item }, index) => {\n          const isFirst = index === 0;\n          const contentTop =\n            !isFirst ? undefined\n            : !parentLabels.length ? undefined\n            : <FlexRowWrap\n                className=\"SearchList_GroupHeader p-p5 text-1 bg-color-0 bb b-color gap-p25\"\n                style={{ position: \"sticky\", top: 0 }}\n              >\n                {parentLabels.map((label, i) => (\n                  <React.Fragment key={i}>\n                    {i > 0 && <span className=\"text-2\">{\">\"}</span>}\n                    <span key={i} className=\"bold\" style={{ opacity: 0.75 }}>\n                      {label}\n                    </span>\n                  </React.Fragment>\n                ))}\n              </FlexRowWrap>;\n          return {\n            ...item,\n            contentTop,\n            contentLeft:\n              !leftSpacer && !contentLeft ?\n                undefined\n              : <FlexRow className=\"gap-0\">\n                  {contentLeft ?? leftSpacer}\n                </FlexRow>,\n          };\n        });\n      })\n    : renderedItemsWithoutHeaders;\n\n  return {\n    renderedItems,\n    renderedSelected,\n    allSelected,\n    itemGroupHeaders,\n    styles,\n    getFullItem,\n  };\n};\n\nconst rowWrapStyle = {\n  subLabel: {\n    flex: \"none\",\n    textTransform: \"uppercase\",\n  },\n  labelRootWrapperStyle: {\n    flexDirection: \"row\",\n    flexWrap: \"wrap\",\n    justifyContent: \"space-between\",\n    columnGap: \"1em\",\n  },\n} satisfies SearchListItem[\"styles\"];\n"
  },
  {
    "path": "client/src/components/SearchList/hooks/useSearchListItemsSorting.ts",
    "content": "import { isEqual } from \"prostgles-types\";\nimport { useCallback, useMemo } from \"react\";\nimport { getSearchListMatchAndHighlight } from \"../getSearchListMatchAndHighlight\";\nimport type {\n  ParsedListItem,\n  SearchListItem,\n  SearchListProps,\n} from \"../SearchList\";\nimport { getValueAsText } from \"../SearchListContent\";\nimport type { SearchListState } from \"./useSearchListSearch\";\n\nexport const useSearchListItemsSorting = (\n  props: Pick<\n    SearchListProps,\n    | \"rowStyleVariant\"\n    | \"limit\"\n    | \"onSearchItems\"\n    | \"onSearch\"\n    | \"items\"\n    | \"dontHighlight\"\n  > &\n    Pick<SearchListState, \"searchItems\" | \"searchTerm\"> & {\n      matchCase: boolean;\n    },\n) => {\n  const {\n    rowStyleVariant,\n    searchTerm,\n    onSearchItems,\n    searchItems,\n    items = [],\n    dontHighlight,\n    onSearch,\n    matchCase,\n    limit = 34,\n  } = props;\n  const styles = rowStyleVariant === \"row-wrap\" ? rowWrapStyle : undefined;\n\n  const getFullItem = useCallback(\n    (d: SearchListItem): ParsedListItem => {\n      const match = getSearchListMatchAndHighlight({\n        matchCase,\n        ranking:\n          typeof d.ranking === \"function\" ? d.ranking(searchTerm) : d.ranking,\n        term: searchTerm,\n        style: d.styles?.label,\n        subLabelStyle: { ...styles?.subLabel, ...d.styles?.subLabel },\n        rootStyle: {\n          ...styles?.labelRootWrapperStyle,\n          ...d.styles?.labelRootWrapperStyle,\n        },\n        text: getValueAsText(d.label !== undefined ? d.label : d.key) ?? \"\",\n        key: d.key,\n        subLabel: d.subLabel,\n      });\n      return {\n        ...d,\n        ...match,\n      };\n    },\n    [matchCase, searchTerm, styles],\n  );\n\n  const sortDisabledLast = (a: ParsedListItem, b: ParsedListItem) =>\n    Number(!!a.disabledInfo) - Number(!!b.disabledInfo);\n  const renderedItemsWithoutHeaders: ParsedListItem[] = useMemo(\n    () =>\n      onSearchItems ? searchItems\n      : dontHighlight ? items\n      : onSearch ? items.map(getFullItem)\n      : items\n          .map(getFullItem)\n          .filter(({ rank }) => !searchTerm || rank !== Infinity)\n          .sort(\n            (a, b) =>\n              sortDisabledLast(a, b) ||\n              (!searchTerm ? 0 : (\n                (a.rank ?? 0) - (b.rank ?? 0) ||\n                (a.parentLabels?.length ?? 0) - (b.parentLabels?.length ?? 0) ||\n                getLen(a.label) - getLen(b.label)\n              )),\n          )\n          .slice(0, (searchTerm ? 2 : 1) * limit),\n    [\n      dontHighlight,\n      getFullItem,\n      items,\n      limit,\n      onSearch,\n      onSearchItems,\n      searchItems,\n      searchTerm,\n    ],\n  );\n\n  const itemGroupHeaders = renderedItemsWithoutHeaders\n    .reduce((a, v) => {\n      if (\n        !v.parentLabels ||\n        a.some((labels) => isEqual(labels, v.parentLabels))\n      ) {\n        return a;\n      }\n      return [...a, v.parentLabels];\n    }, [] as string[][])\n    .map((parentLabels) => ({\n      header: parentLabels.join(\" > \"),\n      parentLabels,\n    }));\n  // .sort((a, b) => a.header.localeCompare(b.header));\n\n  // const dd = renderedItemsWithoutHeaders.sort((a, b) => a.rank! - b.rank!);\n  // console.log(\"Sorted items by ranking:\", dd);\n  return { itemGroupHeaders, renderedItemsWithoutHeaders, getFullItem };\n};\n\nconst getLen = (v) => {\n  if (typeof v === \"string\") {\n    return v.length;\n  }\n  return 0;\n};\n\nconst rowWrapStyle = {\n  subLabel: {\n    flex: \"none\",\n    textTransform: \"uppercase\",\n  },\n  labelRootWrapperStyle: {\n    flexDirection: \"row\",\n    flexWrap: \"wrap\",\n    justifyContent: \"space-between\",\n    columnGap: \"1em\",\n  },\n} satisfies SearchListItem[\"styles\"];\n"
  },
  {
    "path": "client/src/components/SearchList/hooks/useSearchListOnClick.ts",
    "content": "import { useEffect } from \"react\";\nimport type { useSearchListSearch } from \"./useSearchListSearch\";\n\ntype P = {\n  isSearch: boolean | undefined;\n  refList: React.RefObject<HTMLUListElement>;\n  refInput: React.RefObject<HTMLInputElement>;\n  dataSignature: string | undefined;\n} & Pick<\n  ReturnType<typeof useSearchListSearch>,\n  \"onStartSearch\" | \"searching\" | \"searchClosed\" | \"setSearchClosed\"\n>;\nexport const useSearchListOnClick = (props: P) => {\n  const {\n    isSearch,\n    onStartSearch,\n    searching,\n    searchClosed,\n    refList,\n    dataSignature,\n    refInput,\n    setSearchClosed,\n  } = props;\n\n  useEffect(() => {\n    /** Toggle search items when clicking the input */\n    const onClick: EventListenerOrEventListenerObject = (e) => {\n      const {\n        dataSignature: searchedDataSignature,\n        term,\n        cancelCurrentSearch,\n      } = searching.current ?? {};\n      if (isSearch && refList.current && e.target) {\n        if (\n          !searchClosed &&\n          !refList.current.contains(e.target as Node) &&\n          !refInput.current?.contains(e.target as Node)\n        ) {\n          setSearchClosed(true);\n          cancelCurrentSearch?.();\n        } else if (\n          searchClosed &&\n          refInput.current &&\n          refInput.current.contains(e.target as Node)\n        ) {\n          if (term && searchedDataSignature !== dataSignature) {\n            onStartSearch(term);\n          } else {\n            setSearchClosed(false);\n          }\n        }\n      }\n    };\n\n    window.addEventListener(\"click\", onClick);\n    return () => {\n      window.removeEventListener(\"click\", onClick);\n    };\n  }, [\n    dataSignature,\n    isSearch,\n    onStartSearch,\n    refInput,\n    refList,\n    searchClosed,\n    searching,\n    setSearchClosed,\n  ]);\n};\n"
  },
  {
    "path": "client/src/components/SearchList/hooks/useSearchListOnKeyUpDown.ts",
    "content": "import { useCallback, useEffect, useRef, type RefObject } from \"react\";\nimport type { SearchListProps } from \"../SearchList\";\n\nexport const SEARCH_LIST_INPUT_CLASSNAME = \"search-list-comp-input\";\n\nexport type SearchListOnKeyUpDownProps = Pick<\n  SearchListProps,\n  \"onPressEnter\"\n> & {\n  refInput: React.RefObject<HTMLInputElement>;\n  refList: RefObject<HTMLUListElement>;\n  searchTerm: string | undefined;\n  endSearch: (force?: boolean) => void;\n};\nexport const useSearchListOnKeyUpDown = ({\n  refList,\n  refInput,\n  onPressEnter,\n  endSearch,\n  searchTerm,\n}: SearchListOnKeyUpDownProps) => {\n  const focusedRowIndexRef = useRef<number | undefined>(undefined);\n  useEffect(() => {\n    focusedRowIndexRef.current = undefined;\n  }, [searchTerm]);\n\n  const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback(\n    (e) => {\n      const list = refList.current;\n      const input = refInput.current;\n      if (!list) return;\n      const { activeElement } = document;\n      if (!activeElement || !e.currentTarget.contains(document.activeElement))\n        return;\n      const listItems = [...list.children].filter(\n        (el) => el instanceof HTMLLIElement,\n      );\n      const firstListItem = listItems.at(0);\n      const lastListItem = listItems.at(-1);\n\n      const listNotFocused = !list.contains(activeElement);\n\n      const inputIsFocused = !!activeElement.closest(\n        \".\" + SEARCH_LIST_INPUT_CLASSNAME,\n      );\n\n      /* Prevent annoying select all when not within input */\n      if (e.key === \"a\" && e.ctrlKey && !inputIsFocused) {\n        input?.focus();\n        input?.select();\n        e.preventDefault();\n        return;\n      }\n\n      if (e.key === \"Enter\" && inputIsFocused) {\n        e.preventDefault();\n        if (\n          onPressEnter &&\n          focusedRowIndexRef.current === undefined &&\n          searchTerm\n        ) {\n          onPressEnter(searchTerm);\n          endSearch(true);\n        } else if (firstListItem) {\n          firstListItem.click();\n        }\n      }\n\n      if (\n        !listNotFocused &&\n        e.key.length === 1 &&\n        !e.shiftKey &&\n        !e.ctrlKey &&\n        !e.altKey\n      ) {\n        input?.focus();\n        return;\n      }\n\n      if (e.key === \"ArrowUp\") {\n        if (activeElement === firstListItem || listNotFocused) {\n          lastListItem?.focus();\n        } else if (listItems.length && activeElement instanceof HTMLLIElement) {\n          listItems[listItems.indexOf(activeElement) - 1]?.focus();\n        }\n        e.preventDefault();\n      } else if (e.key === \"ArrowDown\") {\n        if (activeElement === lastListItem || listNotFocused) {\n          firstListItem?.focus();\n        } else if (listItems.length && activeElement instanceof HTMLLIElement) {\n          listItems[listItems.indexOf(activeElement) + 1]?.focus();\n        }\n        e.preventDefault();\n      }\n\n      if (e.key === \"ArrowUp\" || e.key === \"ArrowDown\") {\n        focusedRowIndexRef.current =\n          activeElement instanceof HTMLLIElement ?\n            listItems.indexOf(activeElement)\n          : -1;\n      }\n    },\n    [endSearch, onPressEnter, refInput, refList, searchTerm],\n  );\n\n  return { onKeyDown };\n};\n"
  },
  {
    "path": "client/src/components/SearchList/hooks/useSearchListSearch.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { SearchListItem, SearchListProps } from \"../SearchList\";\nimport { getValueAsText } from \"../SearchListContent\";\nimport { useIsMounted } from \"prostgles-client\";\n\nexport const useSearchListSearch = (\n  props: Pick<\n    SearchListProps,\n    | \"onSearchItems\"\n    | \"searchEmpty\"\n    | \"dataSignature\"\n    | \"onSearch\"\n    | \"onType\"\n    | \"matchCase\"\n    | \"defaultValue\"\n    | \"defaultSearch\"\n    | \"variant\"\n  >,\n) => {\n  const [searchTerm, setSearchTerm] = useState(\"\");\n  const getIsMounted = useIsMounted();\n  const {\n    onSearchItems,\n    searchEmpty,\n    dataSignature,\n    onSearch,\n    onType,\n    variant,\n    defaultValue,\n    defaultSearch,\n  } = props;\n  const isSearch = variant?.startsWith(\"search\");\n\n  const searching = useRef<{\n    term: string;\n    dataSignature?: string;\n    timeout: NodeJS.Timeout;\n    cancelCurrentSearch: VoidFunction | undefined;\n  }>();\n\n  const [searchState, setSearchState] = useState<{\n    searchItems: SearchListItem[];\n    searchingItems: boolean;\n    searchClosed: boolean;\n    error?: unknown;\n    dataSignature?: string;\n    term?: string;\n  }>({\n    searchClosed: true,\n    searchingItems: false,\n    searchItems: [],\n  });\n  const searchClosed = searchState.searchClosed;\n  const updateSearchState = useCallback(\n    (newState: Partial<typeof searchState>) => {\n      setSearchState((prevState) => ({\n        ...prevState,\n        ...newState,\n      }));\n    },\n    [setSearchState],\n  );\n\n  const [matchCaseValue, setMatchCaseValue] = useState(\n    props.matchCase?.value ?? false,\n  );\n  const matchCase = props.matchCase?.value ?? matchCaseValue;\n\n  const setSearchClosed = useCallback(\n    (closed: boolean) => updateSearchState({ searchClosed: closed }),\n    [updateSearchState],\n  );\n  const endSearch = useCallback(\n    (force = false) => {\n      if (force || (isSearch && !searchClosed)) {\n        searching.current?.cancelCurrentSearch?.();\n        setSearchTerm(\"\");\n        setSearchClosed(true);\n      }\n    },\n    [isSearch, searchClosed, setSearchClosed],\n  );\n\n  const onStartSearch = useCallback(\n    (term: string) => {\n      if (!onSearchItems) return;\n\n      if (searching.current) {\n        if (searching.current.term === term) {\n          return;\n        } else {\n          clearTimeout(searching.current.timeout);\n        }\n      }\n\n      if (typeof term !== \"string\" || (!searchEmpty && !term)) {\n        updateSearchState({ searchItems: [], searchingItems: false });\n\n        void onSearchItems(term);\n      } else {\n        updateSearchState({ searchingItems: true });\n\n        searching.current = {\n          dataSignature,\n          term,\n          cancelCurrentSearch: undefined,\n          timeout: setTimeout(() => {\n            if (!getIsMounted()) return;\n\n            updateSearchState({ searchingItems: true });\n            try {\n              void onSearchItems(\n                term,\n                { matchCase },\n                (searchItems, finished, cancel) => {\n                  if (!searching.current) return;\n                  searching.current.cancelCurrentSearch = () => {\n                    updateSearchState({\n                      searchingItems: false,\n                      error: undefined,\n                    });\n                    cancel();\n                    searching.current = undefined;\n                  };\n\n                  if (term === searching.current.term) {\n                    updateSearchState({\n                      searchItems,\n                      searchClosed: false,\n                      error: undefined,\n                      dataSignature,\n                      term,\n                    });\n                    if (finished) {\n                      searching.current.cancelCurrentSearch();\n                    }\n                  }\n                },\n              );\n            } catch (error) {\n              updateSearchState({\n                searchingItems: false,\n                error,\n                searchClosed: true,\n              });\n            }\n          }, 400),\n        };\n      }\n    },\n    [\n      dataSignature,\n      getIsMounted,\n      matchCase,\n      onSearchItems,\n      searchEmpty,\n      updateSearchState,\n    ],\n  );\n\n  const onSetTerm = useCallback(\n    (\n      searchTerm: string,\n      e?:\n        | React.ChangeEvent<HTMLInputElement>\n        | React.MouseEvent<HTMLDivElement, MouseEvent>,\n    ) => {\n      onType?.(searchTerm, (newTerm) => {\n        searchTerm = newTerm;\n      });\n      setSearchTerm(searchTerm);\n      onSearch?.(searchTerm, e);\n      onStartSearch(searchTerm);\n    },\n    [onStartSearch, onSearch, onType],\n  );\n\n  useEffect(() => {\n    const defaultValueText = getValueAsText(defaultValue);\n    if (typeof defaultValueText === \"string\" && defaultValueText) {\n      setSearchTerm(defaultValueText);\n    } else if (typeof defaultSearch === \"string\" && defaultSearch) {\n      onSetTerm(defaultSearch, undefined);\n    }\n  }, [defaultSearch, defaultValue, onSetTerm, setSearchTerm]);\n\n  const setMatchCase = useCallback(\n    (value: boolean) => {\n      setMatchCaseValue(value);\n      updateSearchState({ searchClosed: false });\n      if (searchTerm) {\n        onStartSearch(searchTerm);\n      }\n    },\n    [onStartSearch, searchTerm, updateSearchState],\n  );\n\n  return {\n    matchCase,\n    setMatchCase,\n    onSetTerm,\n    searching,\n    onStartSearch,\n    searchTerm,\n    endSearch,\n    setSearchClosed,\n    isSearch,\n    ...searchState,\n  };\n};\nexport type SearchListState = ReturnType<typeof useSearchListSearch>;\n"
  },
  {
    "path": "client/src/components/SearchList/searchMatchUtils/getItemSearchRank.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\n\ntype SearchItem = { title: string; subTitle: string; level: number };\nexport const getItemSearchRank = (\n  item: SearchItem,\n  query: string,\n  titleWeight = 2,\n  descWeight = 1,\n) => {\n  const titleScore = getSearchScore(item.title, query, { level: item.level });\n  const descScore = getSearchScore(item.subTitle, query, { level: item.level });\n  const titleScoreWeighted = titleScore * titleWeight;\n  const descScoreWeighted = descScore * descWeight;\n  if (titleScore === 0 && descScore === 0) {\n    return Infinity;\n  }\n  if (titleScore === 0) {\n    return 100 - descScoreWeighted;\n  }\n  if (descScore === 0) {\n    return 100 - titleScoreWeighted;\n  }\n\n  const bestScore = Math.max(titleScoreWeighted, descScoreWeighted);\n\n  // const score = titleScore * titleWeight + descScore * descWeight;\n  return 100 - bestScore;\n};\n\nconst levenshtein = (a: string, b: string) => {\n  const dp: number[][] = Array.from({ length: a.length + 1 }, () => []);\n  for (let i = 0; i <= a.length; i++) dp[i]![0] = i;\n  for (let j = 0; j <= b.length; j++) dp[0]![j] = j;\n\n  for (let i = 1; i <= a.length; i++) {\n    for (let j = 1; j <= b.length; j++) {\n      dp[i]![j] =\n        a[i - 1] === b[j - 1] ?\n          dp[i - 1]![j - 1]!\n        : 1 + Math.min(dp[i - 1]![j]!, dp[i]![j - 1]!, dp[i - 1]![j - 1]!);\n    }\n  }\n  return dp[a.length]![b.length]!;\n};\n\nconst similarity = (a: string, b: string) => {\n  const distance = levenshtein(a, b);\n  const maxLen = Math.max(a.length, b.length);\n  return (1 - distance / maxLen) * 100;\n};\n\nconst getSearchScore = (\n  itemValue: string,\n  query: string,\n  levelOpts?: { level: number; penalty?: number },\n) => {\n  if (itemValue === query) return 100;\n  const itemValueLower = itemValue.toLowerCase();\n  const queryLower = query.toLowerCase();\n\n  if (itemValueLower === queryLower) return 99.99;\n  const index = itemValueLower.indexOf(queryLower);\n  if (index === -1) return 0;\n\n  let score = similarity(itemValueLower, queryLower);\n\n  if (isDefined(levelOpts)) {\n    const { level, penalty = 5 } = levelOpts;\n    const levelPenalty = (level - 1) * penalty;\n    score -= levelPenalty;\n  }\n\n  return score;\n};\n"
  },
  {
    "path": "client/src/components/SearchList/searchMatchUtils/getSearchRanking.ts",
    "content": "import { isDefined } from \"src/utils/utils\";\n\nexport const getSearchRanking = (searchTerm: string, labels: string[]) => {\n  if (searchTerm) {\n    const matchedLabelRank = labels\n      .map((l, i) => {\n        const idx = l.toLowerCase().indexOf(searchTerm.toLowerCase());\n        const rank =\n          idx === -1 ? undefined : (\n            Number(`${i}.${idx.toString().padStart(3, \"0\")}`)\n          );\n        return rank;\n      })\n      .filter(isDefined)[0];\n    return matchedLabelRank ?? Infinity;\n  }\n  return Infinity;\n};\n"
  },
  {
    "path": "client/src/components/Section.tsx",
    "content": "import { mdiChevronDown, mdiChevronRight, mdiFullscreen } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport { omitKeys } from \"prostgles-types\";\nimport type { BtnProps } from \"./Btn\";\nimport Btn from \"./Btn\";\nimport { classOverride, FlexRow } from \"./Flex\";\nimport Popup from \"./Popup/Popup\";\nimport type { Command, TestSelectors } from \"../Testing\";\n\ntype SectionProps = TestSelectors & {\n  title: React.ReactNode;\n  titleRightContent?: React.ReactNode;\n  children: React.ReactNode;\n  buttonStyle?: React.CSSProperties;\n  btnProps?: BtnProps<void>;\n  className?: string;\n  disabledInfo?: string;\n  contentStyle?: React.CSSProperties;\n  contentClassName?: string;\n  open?: boolean;\n  titleIconPath?: string;\n  disableFullScreen?: boolean;\n} & (\n    | {\n        style?: React.CSSProperties;\n      }\n    | {\n        getStyle?: (expanded: boolean) => React.CSSProperties;\n      }\n  ) &\n  (\n    | {\n        titleIconPath?: string;\n        titleIcon?: undefined;\n      }\n    | {\n        titleIcon?: React.ReactNode;\n        titleIconPath?: undefined;\n      }\n  );\n\nexport const Section = (props: SectionProps) => {\n  const {\n    children,\n    title,\n    className = \"\",\n    disableFullScreen,\n    disabledInfo,\n    contentClassName = \"\",\n    contentStyle = {},\n    buttonStyle = {},\n    open: oDef,\n    titleRightContent,\n    titleIcon,\n    titleIconPath,\n    btnProps,\n    \"data-command\": dataCommand,\n    \"data-key\": dataKey,\n    ...otherProps\n  } = props;\n  const [open, toggle] = useState(oDef);\n  const [fullscreen, setFullscreen] = useState(false);\n\n  return (\n    <div\n      data-command={dataCommand satisfies Command | undefined}\n      data-key={dataKey}\n      className={classOverride(\n        \"Section flex-col min-h-0 f-0 relative bg-inherit \" +\n          (open ? \"bb b-color\" : \"\"),\n        className,\n      )}\n      style={\n        \"getStyle\" in otherProps ? otherProps.getStyle?.(!!open)\n        : \"style\" in otherProps ?\n          otherProps.style\n        : undefined\n      }\n    >\n      <div\n        className=\"Section__Header flex-row ai-center noselect pointer f-0 bb b-color bg-inherit\"\n        style={\n          !open ?\n            {\n              borderBottom: \"unset\", // \"1px solid var(--b-color)\", It looks better without border when closed?\n              borderTop: \"unset\",\n            }\n          : {\n              position: \"sticky\",\n              top: 0,\n              zIndex: 1,\n              borderBottom: \"unset\",\n              marginBottom: \".5em\",\n            }\n        }\n      >\n        <Btn\n          className={\n            (titleRightContent ? \"\" : \"f-1d\") +\n            \" p-p5 ta-left font-20 bold jc-start mr-1\"\n          }\n          title=\"Expand section\"\n          disabledInfo={disabledInfo}\n          style={{\n            width: undefined,\n            ...buttonStyle,\n          }}\n          iconPath={\n            titleIcon ? undefined : (\n              (titleIconPath ?? (!open ? mdiChevronRight : mdiChevronDown))\n            )\n          }\n          iconNode={titleIcon}\n          {...(omitKeys(btnProps ?? {}, [\"onClick\"]) as BtnProps<void>)}\n          onClick={fullscreen ? undefined : () => toggle(!open)}\n        >\n          {title}\n        </Btn>\n        {titleRightContent}\n        {!disableFullScreen && (\n          <Btn\n            className={fullscreen ? \"\" : \"show-on-parent-hover\"}\n            iconPath={mdiFullscreen}\n            data-command=\"Section.toggleFullscreen\"\n            onClick={() => setFullscreen(!fullscreen)}\n            color={fullscreen ? \"action\" : undefined}\n          />\n        )}\n      </div>\n\n      {(open || fullscreen) && (\n        <div style={contentStyle} className={contentClassName}>\n          {children}\n        </div>\n      )}\n\n      {fullscreen && (\n        <Popup\n          positioning=\"fullscreen\"\n          title={\n            <FlexRow className=\"trigger-hover-force\">\n              {titleIcon}\n              {title}\n              {titleRightContent}\n            </FlexRow>\n          }\n          contentClassName={contentClassName}\n          contentStyle={contentStyle}\n          onClose={() => {\n            setFullscreen(false);\n          }}\n        >\n          {children}\n        </Popup>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Select/Select.css",
    "content": ".select-wrapper {\n  /* border-color: #d2d6dc;\n  border-width: 1px;\n  border-style: solid; */\n  box-sizing: border-box;\n  border-radius: 0.375rem;\n  overflow: hidden;\n}\n\n.select-wrapper select {\n  background: white;\n  padding: 0.5rem 0.75rem;\n  border: none;\n  outline: 0;\n  flex: 1;\n  text-decoration: none;\n  cursor: pointer;\n}\n"
  },
  {
    "path": "client/src/components/Select/Select.tsx",
    "content": "import React from \"react\";\nimport \"./Select.css\";\nimport { sliceText } from \"@common/utils\";\nimport type { TestSelectors } from \"../../Testing\";\nimport RTComp from \"../../dashboard/RTComp\";\nimport type { BtnProps } from \"../Btn\";\nimport Chip from \"../Chip\";\nimport { generateUniqueID } from \"../FileInput/FileInput\";\nimport { FlexCol, FlexRow } from \"../Flex\";\nimport { Icon } from \"../Icon/Icon\";\nimport type { LabelProps } from \"../Label\";\nimport { Label } from \"../Label\";\nimport Popup from \"../Popup/Popup\";\nimport type { SearchListItem, SearchListProps } from \"../SearchList/SearchList\";\nimport { SearchList } from \"../SearchList/SearchList\";\nimport { SelectTriggerButton } from \"./SelectTriggerButton\";\nimport Btn from \"../Btn\";\n\nexport type OptionKey = string | number | boolean | Date | null | undefined;\nexport type FullOption<O extends OptionKey = string> = Pick<\n  SearchListItem,\n  \"ranking\" | \"parentLabels\"\n> & {\n  key: O;\n  label?: string | React.ReactElement;\n  subLabel?: string;\n  checked?: boolean;\n  disabledInfo?: string;\n  rightContent?: React.ReactNode;\n} & TestSelectors &\n  (\n    | {\n        iconPath?: string;\n        leftContent?: undefined;\n      }\n    | {\n        iconPath?: undefined;\n        leftContent?: React.ReactNode;\n      }\n  );\n\ntype E =\n  | React.MouseEvent<HTMLDivElement, MouseEvent>\n  | React.MouseEvent<HTMLButtonElement, MouseEvent>\n  | React.MouseEvent<HTMLLIElement, MouseEvent>\n  | React.KeyboardEvent<HTMLLIElement>\n  | React.KeyboardEvent<HTMLButtonElement>\n  | React.ChangeEvent<HTMLInputElement>\n  | React.ChangeEvent<HTMLSelectElement>\n  | React.KeyboardEvent\n  | React.ChangeEvent\n  | KeyboardEvent;\n\n// type P<O extends FullOption, Multi extends boolean = false> = {\nexport type SelectProps<\n  O extends OptionKey,\n  Multi extends boolean = false,\n  Optional extends boolean = false,\n> = TestSelectors & {\n  onChange?: (\n    val: Multi extends true ? O[]\n    : O | Optional extends true ? undefined\n    : O,\n    e: E,\n    option: FullOption<O> | undefined,\n  ) => void;\n  onSearch?: (term: string) => void;\n  onOpen?: (buttonAnchorEl: HTMLButtonElement) => void;\n  onClose?: VoidFunction;\n  name?: string;\n  value?: any;\n  style?: React.CSSProperties;\n  className?: string;\n  id?: string;\n  required?: boolean;\n  label?: string | Pick<LabelProps, \"label\" | \"info\">;\n  /**\n   * If true then will show the selected option sublabel underneath\n   */\n  showSelectedSublabel?: boolean;\n  showIconOnly?: boolean;\n  title?: string;\n  placeholder?: string;\n  variant?:\n    | \"search-list-only\"\n    | \"div\"\n    | \"chips-lg\"\n    | \"button-group\"\n    | \"button-group-vertical\";\n  multiSelect?: Multi;\n  labelAsValue?: boolean;\n  emptyLabel?: string;\n  asRow?: boolean;\n  iconPath?: string;\n  size?: \"small\";\n  btnProps?: BtnProps<void>;\n  /**\n   * Number of rows to slice from result\n   */\n  limit?: number;\n  noSearchLimit?: number;\n\n  showTop?: number;\n  sliceMax?: number;\n\n  disabledInfo?: string;\n  optional?: Optional;\n  nullable?: boolean;\n  endOfResultsContent?: SearchListProps[\"endOfResultsContent\"];\n} & (\n    | {\n        options: readonly O[];\n      }\n    | {\n        fullOptions: readonly FullOption<O>[];\n      }\n  );\n\nexport type SelectState = {\n  popupAnchor: HTMLElement | null;\n  defaultSearch?: string;\n\n  /**\n   * To prevent things moving around during multi selection\n   *    we will fire onChange after the popup is closed\n   */\n  multiSelection?: any[];\n  fixedBtnWidth?: number;\n};\n\nexport class Select<\n  O extends OptionKey,\n  Multi extends boolean = false,\n  Optional extends boolean = false,\n> extends RTComp<SelectProps<O, Multi, Optional>, SelectState> {\n  state: SelectState = {\n    popupAnchor: null,\n    defaultSearch: undefined,\n  };\n\n  btnRef?: HTMLButtonElement;\n\n  id = generateUniqueID();\n\n  render() {\n    const {\n      onChange: _onChange,\n      className = \"\",\n      title,\n      value: _value,\n      id = this.id,\n      style = {},\n      required,\n      label,\n      variant = \"div\",\n      onSearch,\n      noSearchLimit = 15,\n      endOfResultsContent,\n      multiSelect,\n      asRow,\n      limit,\n      showTop = 3,\n      sliceMax = 150,\n      disabledInfo,\n      optional = false,\n      showSelectedSublabel = false,\n      placeholder = \"Search...\",\n    } = this.props;\n\n    const value = this.state.multiSelection ?? _value;\n\n    let fullOptions: FullOption[] = [];\n\n    if (\"options\" in this.props) {\n      fullOptions = (this.props.options as any).map((key) => ({\n        key,\n        label: key,\n        ...(multiSelect ?\n          {\n            checked: Boolean(value && (value as string[]).includes(key)),\n          }\n        : {}),\n      }));\n    } else {\n      if (\n        this.props.value &&\n        this.props.fullOptions.some((d) => typeof d.checked === \"boolean\")\n      ) {\n        console.warn(\n          \"fullOptions checked AND value provided to Select\",\n          this.props.fullOptions,\n        );\n      }\n      fullOptions = (this.props.fullOptions as any as FullOption[]).map(\n        (o) => ({\n          ...o,\n          label: o.label ?? (o.key as OptionKey)?.toString(),\n          ...(multiSelect && !(\"checked\" in o) ?\n            {\n              checked: Boolean(value && (value as OptionKey[]).includes(o.key)),\n            }\n          : {}),\n        }),\n      );\n    }\n\n    fullOptions = fullOptions.map((o) => ({\n      ...o,\n      label: o.label ?? ((o.label as any) === null ? \"NULL\" : \"\"),\n    }));\n\n    const selectStyle: React.CSSProperties =\n      !label ?\n        {\n          ...style,\n        }\n      : {};\n    const selectClass =\n      !label ?\n        disabledInfo ? \" disabled \"\n        : \"\"\n      : \"\";\n    let selectedFullOptions: typeof fullOptions = [];\n\n    const onChange: typeof _onChange = (newValue, e: E) => {\n      if (JSON.stringify(value) === JSON.stringify(newValue)) {\n        return;\n      }\n\n      if (Array.isArray(newValue) && this.state.popupAnchor) {\n        this.setState({ multiSelection: newValue });\n      } else {\n        _onChange?.(\n          newValue,\n          e,\n          Array.isArray(newValue) ? undefined : (\n            (fullOptions.find((fo) => fo.key === newValue) as\n              | FullOption<O>\n              | undefined)\n          ),\n        );\n      }\n    };\n    const toggleOne = (key: string, e: E) => {\n      if (multiSelect) {\n        const selected = fullOptions.filter((d) => d.checked).map((d) => d.key);\n        if (selected.includes(key)) {\n          onChange(\n            selected.filter((k) => k !== key) as Multi extends true ? O[]\n            : O | Optional extends true ? undefined\n            : O,\n            e,\n            undefined,\n          );\n        } else {\n          onChange(\n            [...selected, key] as Multi extends true ? O[]\n            : O | Optional extends true ? undefined\n            : O,\n            e,\n            undefined,\n          );\n        }\n      } else {\n        onChange(\n          key as any,\n          e,\n          fullOptions.find((fo) => fo.key === key) as FullOption<O> | undefined,\n        );\n      }\n    };\n\n    let select: React.ReactNode = null;\n    const { popupAnchor, defaultSearch, fixedBtnWidth } = this.state;\n\n    const closeDropDown = (e: E) => {\n      this.props.onSearch?.(\"\");\n      if (this.state.multiSelection && _onChange) {\n        _onChange(this.state.multiSelection as any, e, undefined);\n      }\n      this.setState({\n        popupAnchor: null,\n        multiSelection: undefined,\n        fixedBtnWidth: undefined,\n        defaultSearch: \"\",\n      });\n      /** Maintain the same form element focused for convenience */\n      setTimeout(() => {\n        if (document.activeElement !== document.body) {\n          this.btnRef?.focus();\n        }\n      }, 2);\n    };\n\n    let btnLabel: string | undefined = \"Select...\";\n    if (Array.isArray(value)) {\n      if (value.length) {\n        selectedFullOptions = fullOptions.filter((o) => value.includes(o.key));\n        const labels = selectedFullOptions.map(\n          (o) => (o.label as string) || (o.key as OptionKey),\n        );\n        const firstValues = labels\n          .map((v) =>\n            sliceText((v === null ? \"NULL\" : v)?.toString(), sliceMax),\n          )\n          .slice(0, showTop)\n          .join(\", \");\n        btnLabel =\n          firstValues +\n          (labels.length > showTop ? ` + ${labels.length - showTop}` : \"\");\n      }\n    } else {\n      selectedFullOptions = fullOptions.filter((o) => o.key === value);\n      btnLabel = fullOptions.find((o) => o.key === value)?.label ?? value;\n    }\n\n    const chipMode = variant === \"chips-lg\";\n    const trigger = (\n      <SelectTriggerButton\n        {...this.props}\n        setRef={(r) => {\n          this.btnRef = r;\n        }}\n        selectedFullOptions={selectedFullOptions}\n        onChange={onChange}\n        optional={optional}\n        selectClass={selectClass}\n        chipMode={chipMode}\n        fixedBtnWidth={fixedBtnWidth}\n        fullOptions={fullOptions}\n        selectStyle={selectStyle}\n        multiSelection={this.state.multiSelection}\n        popupAnchor={popupAnchor}\n        onPress={(btn, defaultSearch) => {\n          const maxBtnWidth = btn.getBoundingClientRect().width;\n          this.props.onOpen?.(btn);\n          this.setState({\n            popupAnchor: btn,\n            defaultSearch,\n            fixedBtnWidth: maxBtnWidth,\n          });\n        }}\n        btnLabel={btnLabel}\n      />\n    );\n    let chips: React.ReactNode = null;\n    if (chipMode) {\n      const chipValues =\n        multiSelect && popupAnchor ?\n          fullOptions.filter((v) => {\n            return this.props.value.includes(v.key);\n          })\n        : selectedFullOptions;\n      chips = (\n        <div\n          className={\"Select_Chips flex-row-wrap gap-p5 ai-center \" + className}\n          style={style}\n        >\n          {chipValues.map(({ key, label }) => (\n            <Chip\n              key={key}\n              color=\"blue\"\n              style={{\n                fontSize: \"18px\",\n              }}\n              onDelete={(e) => {\n                toggleOne(key, e);\n              }}\n            >\n              {label ?? key}\n            </Chip>\n          ))}\n          {trigger}\n        </div>\n      );\n    }\n\n    const searchList = (\n      <SearchList\n        id={id}\n        style={{\n          maxHeight: \"500px\",\n          ...(variant === \"button-group-vertical\" && {\n            border: \"1px solid var(--b-color)\",\n            borderRadius: \"var(--rounded)\",\n          }),\n        }}\n        searchStyle={variant === \"search-list-only\" ? {} : { margin: \"0.5em\" }}\n        placeholder={placeholder}\n        defaultSearch={defaultSearch}\n        noSearchLimit={noSearchLimit}\n        data-command={this.props[\"data-command\"]}\n        data-key={this.props[\"data-key\"]}\n        autoFocus={true}\n        // noSearch={!onSearch && options.length < 15}\n        endOfResultsContent={endOfResultsContent}\n        onSearch={onSearch}\n        onMultiToggle={\n          multiSelect ?\n            (items, e) => {\n              onChange(\n                items\n                  .filter((d) => d.checked)\n                  .map((d) => d.key) as Multi extends true ? O[]\n                : O | Optional extends true ? undefined\n                : O,\n                e,\n                undefined,\n              );\n            }\n          : undefined\n        }\n        onChange={multiSelect ? undefined : (onChange as any)}\n        limit={limit}\n        items={fullOptions.map(\n          ({\n            key,\n            label,\n            subLabel,\n            checked,\n            disabledInfo,\n            ranking,\n            iconPath,\n            leftContent,\n            rightContent,\n            ...selectorProps\n          }) => {\n            return {\n              key: key as OptionKey,\n              label,\n              subLabel,\n              ranking,\n              contentLeft:\n                leftContent ? leftContent\n                : iconPath ? <Icon path={iconPath} className=\"text-1\" />\n                : undefined,\n              contentRight: rightContent,\n              styles: {\n                subLabel: {\n                  whiteSpace: \"pre-wrap\",\n                },\n              },\n              onPress: (e) => {\n                toggleOne(key, e);\n                if (!multiSelect) {\n                  closeDropDown(e);\n                }\n              },\n              selected: key === value,\n              checked,\n              disabledInfo,\n              ...selectorProps,\n            };\n          },\n        )}\n      />\n    );\n\n    if (variant === \"search-list-only\") {\n      return searchList;\n    }\n\n    const labelNode =\n      typeof label === \"string\" ?\n        <label\n          htmlFor={id}\n          className={\n            \"noselect f-0 text-1 ta-left \" + (asRow ? \" mr-p5 \" : \" mb-p5 \")\n          }\n        >\n          {label}\n        </label>\n      : <Label {...label} variant=\"normal\" className={\"mb-p5\"} />;\n    if (variant === \"button-group-vertical\") {\n      return (\n        <FlexCol className=\"gap-p25\">\n          {labelNode}\n          {searchList}\n        </FlexCol>\n      );\n    }\n    if (variant === \"button-group\") {\n      select = (\n        <FlexRow className=\"rounded gap-0 b b-color\">\n          {fullOptions.map((fullOption, index) => {\n            const hasPrevItem = index > 0;\n            const hasNextItem = index < fullOptions.length - 1;\n            const { key, label, disabledInfo, ...otherProps } = fullOption;\n            return (\n              <Btn\n                key={key}\n                data-key={otherProps[\"data-key\"] ?? key}\n                style={{\n                  ...(hasPrevItem && {\n                    borderTopLeftRadius: 0,\n                    borderBottomLeftRadius: 0,\n                  }),\n                  ...(hasNextItem && {\n                    borderTopRightRadius: 0,\n                    borderBottomRightRadius: 0,\n                  }),\n                }}\n                color=\"default\"\n                size={this.props.size}\n                variant={value === key ? \"faded\" : undefined}\n                disabledInfo={disabledInfo}\n                onClick={(e) => {\n                  //@ts-ignore\n                  onChange(key, e, fullOption);\n                }}\n                value={key.toString()}\n              >\n                {label ?? key}\n              </Btn>\n            );\n          })}\n        </FlexRow>\n      );\n    } else {\n      select = (\n        <>\n          {chips || trigger}\n          {popupAnchor && (\n            <Popup\n              rootStyle={{\n                padding: 0,\n                maxWidth: \"min(99vw, 600px)\",\n                boxSizing: \"border-box\",\n              }}\n              anchorEl={popupAnchor}\n              positioning=\"beneath-left-minfill\"\n              clickCatchStyle={{ opacity: 0 }}\n              onClose={closeDropDown}\n              contentClassName=\"rounded p-0\"\n              persistInitialSize={true}\n            >\n              {searchList}\n            </Popup>\n          )}\n        </>\n      );\n    }\n\n    const [selectedFullOption] = selectedFullOptions;\n    if (!label) {\n      return select;\n    }\n\n    return (\n      <div\n        className={\n          \"Select w-fit \" +\n          (asRow ? \" flex-row ai-center \" : \" flex-col \") +\n          className\n        }\n        style={style}\n      >\n        {labelNode}\n        {select}\n        {showSelectedSublabel &&\n          selectedFullOption &&\n          !!selectedFullOption.subLabel?.length && (\n            <FlexRow className=\"w-fit p-p5 text-1p5 gap-p5 font-14\">\n              {!!selectedFullOption.iconPath && (\n                <Icon path={selectedFullOption.iconPath} size={1} />\n              )}\n              {selectedFullOption.subLabel}\n            </FlexRow>\n          )}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/Select/SelectTriggerButton.tsx",
    "content": "import { classOverride } from \"@components/Flex\";\nimport { mdiClose, mdiMenuDown, mdiPencil } from \"@mdi/js\";\nimport React from \"react\";\nimport { RenderValue } from \"../../dashboard/SmartForm/SmartFormField/RenderValue\";\nimport { getCommandElemSelector } from \"../../Testing\";\nimport Btn from \"../Btn\";\nimport type { FullOption, OptionKey, SelectProps } from \"./Select\";\n\ntype P<\n  O extends OptionKey,\n  Multi extends boolean = false,\n  Optional extends boolean = false,\n> = Omit<SelectProps<O, Multi, Optional>, \"fullOptions\" | \"onOpen\"> & {\n  fullOptions: FullOption<string>[];\n  multiSelection: any[] | undefined;\n  selectStyle: React.CSSProperties;\n  fixedBtnWidth: number | undefined;\n  chipMode: boolean;\n  selectClass: string;\n  popupAnchor: HTMLElement | null;\n  onPress: (btn: HTMLButtonElement, defaultSearch: string | undefined) => void;\n  setRef: (ref: HTMLButtonElement) => void;\n  btnLabel: string | undefined;\n  selectedFullOptions: FullOption<string>[];\n};\nexport const SelectTriggerButton = <\n  O extends OptionKey,\n  Multi extends boolean = false,\n  Optional extends boolean = false,\n>(\n  props: P<O, Multi, Optional>,\n) => {\n  const {\n    onChange,\n    className = \"\",\n    title,\n    value: _value,\n    label,\n    labelAsValue,\n    emptyLabel = \"Select...\",\n    iconPath,\n    size,\n    btnProps,\n    disabledInfo,\n    optional = false,\n    showIconOnly,\n    fullOptions,\n    multiSelection,\n    fixedBtnWidth,\n    selectStyle,\n    chipMode,\n    selectClass,\n    popupAnchor,\n    onPress,\n    setRef,\n    btnLabel,\n    selectedFullOptions,\n  } = props;\n\n  const value = multiSelection ?? _value;\n  const options: OptionKey[] = fullOptions.map(({ key }) => key);\n  const noOtherOption =\n    !options.length || (options.length === 1 && value === options[0]);\n\n  if (!onChange) return null;\n\n  const showSelectedIcon =\n    showIconOnly ? selectedFullOptions[0]?.iconPath : undefined;\n\n  const btnText = btnLabel ?? emptyLabel;\n  const btnChildren =\n    chipMode || showSelectedIcon ? null\n    : iconPath || btnProps?.children !== undefined ?\n      (btnProps?.children ?? null)\n    : <>\n        <div className={\" text-ellipsis \"} style={{ lineHeight: \"18px\" }}>\n          {!labelAsValue ?\n            btnText\n          : <RenderValue\n              column={undefined}\n              value={btnText}\n              showTitle={!noOtherOption}\n              maxLength={150}\n            />\n          }\n        </div>\n      </>;\n\n  const triggerButton = (\n    <Btn\n      title={title}\n      style={{\n        borderRadius: \"6px\",\n        position: \"relative\",\n        /* Ensure the dropdown end icon is aligned with end when no value is selected */\n        justifyContent: \"space-between\",\n        minHeight: \"32px\",\n\n        ...selectStyle,\n        ...(fixedBtnWidth && { width: `${fixedBtnWidth}px` }),\n      }}\n      /** Use \"data-command\" for content when button not needed anymore */\n      data-command={popupAnchor ? undefined : props[\"data-command\"]}\n      data-key={popupAnchor ? undefined : props[\"data-key\"]}\n      size={size}\n      variant={chipMode ? \"icon\" : \"faded\"}\n      color={chipMode ? \"action\" : \"default\"}\n      iconPath={\n        showSelectedIcon ?? iconPath ?? (chipMode ? mdiPencil : mdiMenuDown)\n      }\n      iconPosition={!btnProps?.iconPath ? \"right\" : \"left\"}\n      disabledInfo={\n        disabledInfo ?? (noOtherOption ? \"No other option\" : undefined)\n      }\n      disabledVariant={noOtherOption ? \"no-fade\" : undefined}\n      {...btnProps}\n      className={classOverride(\n        `${label ? \"  \" : className} Select w-fit f-0 select-button ${selectClass} ${popupAnchor ? \"is-open\" : \"\"} `,\n        btnProps?.className,\n      )}\n      onClick={\n        noOtherOption ? undefined : (\n          (e) => {\n            onPress(e.currentTarget, undefined);\n          }\n        )\n      }\n      //@ts-ignore\n      _ref={(r) => {\n        if (r) {\n          setRef(r);\n        }\n      }}\n      onKeyDown={\n        noOtherOption ? undefined : (\n          (e) => {\n            const { key, currentTarget } = e;\n            const isFocused = document.activeElement === currentTarget;\n            if (!isFocused) return;\n            const isAlphanumeric = key.match(/^[a-z0-9]$/i);\n            if (isAlphanumeric && !popupAnchor) {\n              onPress(e.currentTarget, key);\n            } else if ([\"Enter\", \" \"].includes(key) && !popupAnchor) {\n              e.preventDefault();\n              e.stopPropagation();\n              onPress(e.currentTarget, undefined);\n            } else if ([\"ArrowUp\", \"ArrowDown\"].includes(key)) {\n              const selectOpt = document.querySelector<HTMLUListElement>(\n                getCommandElemSelector(\"Popup.content\") +\n                  \" ul > li:first-child\",\n              );\n              if (selectOpt) {\n                selectOpt.focus();\n                return;\n              }\n              const increm = key === \"ArrowUp\" ? -1 : 1;\n              let selIdx = options.indexOf(value) + increm;\n              if (selIdx < 0) selIdx = options.length - 1;\n              else if (selIdx > options.length - 1) selIdx = 0;\n              onChange(options[selIdx] as any, e, fullOptions[selIdx] as any);\n\n              e.preventDefault();\n            }\n          }\n        )\n      }\n      children={btnChildren}\n    />\n  );\n\n  const trigger =\n    !optional ? triggerButton : (\n      <div\n        className={`${label ? \"  \" : className} flex-row gap-0 ai-center ${selectClass} `}\n      >\n        {triggerButton}\n        {![undefined, null].includes(value) && (\n          <Btn\n            iconPath={mdiClose}\n            title=\"Reset selection\"\n            size={size}\n            onClick={(e) => onChange(undefined as any, e, undefined)}\n          />\n        )}\n      </div>\n    );\n\n  return trigger;\n};\n"
  },
  {
    "path": "client/src/components/ShorterText.tsx",
    "content": "import React, { useState } from \"react\";\nimport { getColumnDataColor } from \"../dashboard/SmartForm/SmartFormField/RenderValue\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport { useIsMounted } from \"prostgles-client\";\n\ntype P = {\n  column?: Pick<\n    Partial<ValidatedColumnInfo>,\n    \"is_pkey\" | \"tsDataType\" | \"udt_name\"\n  >;\n  value: string | null;\n  style?: React.CSSProperties;\n};\nexport const ShorterText = ({ value: guid, column, style }: P) => {\n  const [copied, setCopied] = useState(false);\n  const getIsMounted = useIsMounted();\n  return (\n    <div className=\"ShorterText flex-row gap-1 ai-center\">\n      {guid === null ?\n        <i>NULL</i>\n      : <>\n          <div\n            className=\"f-1 pointer relative\"\n            style={{\n              color: getColumnDataColor(\n                column ?? { tsDataType: \"string\", udt_name: \"uuid\" },\n              ),\n              ...style,\n            }}\n            title=\"Click to copy value\"\n            onClick={() => {\n              navigator.clipboard.writeText(guid);\n              setCopied(true);\n              setTimeout(() => {\n                if (!getIsMounted()) return;\n                setCopied(false);\n              }, 2000);\n            }}\n          >\n            {copied && (\n              <div\n                className=\"absolute bg-color-0 w-full h-full\"\n                style={{ zIndex: 1 }}\n              >\n                Copied!\n              </div>\n            )}\n            {guid.substring(0, 8)}\n            ...\n            {guid.substring(guid.length - 8)}\n          </div>\n        </>\n      }\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/SidePanel.tsx",
    "content": "import React from \"react\";\nimport Popup from \"./Popup/Popup\";\n\ntype P = {\n  onClose?: () => any;\n  title?: string;\n  children: any;\n  footerButtons?: JSX.Element | JSX.Element[];\n};\n\nexport default class SidePanel extends React.Component<P, any> {\n  render() {\n    const {\n      onClose,\n      title = \"Title\",\n      children,\n      footerButtons = [],\n    } = this.props;\n\n    return (\n      <Popup\n        onClose={onClose}\n        positioning=\"as-is\"\n        title={title}\n        rootStyle={{\n          top: 0,\n          right: 0,\n          bottom: 0,\n          overflow: \"hidden\",\n          position: \"absolute\",\n        }}\n      >\n        {title ?\n          <div\n            className=\"text-lg leading-6 font-medium p-1 noselect\"\n            style={{\n              boxShadow: \"rgb(64 64 64) 0px -5px 9px 0px\",\n              clipPath: \"inset(0px 0px -10px)\",\n            }}\n          >\n            {title}\n          </div>\n        : null}\n        <div className=\"flex-col f-1 o-auto\">{children}</div>\n        <div className=\"flex-row f-0 bt p-1 jc-around\">{footerButtons}</div>\n      </Popup>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/Slider.css",
    "content": ".slidecontainer {\n  width: 100%; /* Width of the outside container */\n  margin-bottom: 1em;\n}\n\n/* The slider itself */\n.slider {\n  -webkit-appearance: none; /* Override default CSS styles */\n  appearance: none;\n  width: 100%; /* Full-width */\n  height: 8px; /* Specified height */\n  background: #d3d3d3; /* Grey background */\n  outline: none; /* Remove outline */\n  opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */\n  -webkit-transition: 0.2s; /* 0.2 seconds transition on hover */\n  transition: opacity 0.2s;\n  border-radius: 4px;\n}\n\n/* Mouse-over effects */\n.slider:hover {\n  opacity: 1;\n}\n\n/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */\n.slider::-webkit-slider-thumb {\n  -webkit-appearance: none; /* Override default look */\n  appearance: none;\n  width: 16px; /* Set a specific slider handle width */\n  height: 16px; /* Slider handle height */\n  border-radius: 50%;\n  border: 1px solid gray;\n  background: white; /* Green background */\n  cursor: pointer; /* Cursor on hover */\n}\n.slider::-webkit-slider-thumb:hover {\n  border: 1px solid rgb(0, 208, 255);\n}\n\n.slider::-moz-range-thumb {\n  width: 16px; /* Set a specific slider handle width */\n  height: 16px; /* Slider handle height */\n  background: white; /* Green background */\n  cursor: pointer; /* Cursor on hover */\n}\n.slider::-moz-range-thumb:hover {\n  border: 1px solid rgb(0, 208, 255);\n}\n\n@media only screen and (max-width: 600px) {\n  .slidecontainer {\n    margin-bottom: 1.5em;\n  }\n\n  /* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */\n  .slider::-webkit-slider-thumb {\n    width: 26px; /* Set a specific slider handle width */\n    height: 26px; /* Slider handle height */\n  }\n\n  .slider::-moz-range-thumb {\n    width: 26px; /* Set a specific slider handle width */\n    height: 26px; /* Slider handle height */\n  }\n}\n"
  },
  {
    "path": "client/src/components/Slider.tsx",
    "content": "import React from \"react\";\nimport { isDefined } from \"../utils/utils\";\nimport { FlexCol, classOverride } from \"./Flex\";\nimport { Label } from \"./Label\";\nimport \"./Slider.css\";\n\ntype P = {\n  style?: React.CSSProperties;\n  className?: string;\n\n  onChange: (\n    val: number,\n    event?: Parameters<React.ChangeEventHandler<HTMLInputElement>>[0],\n  ) => void;\n  min: number;\n  max: number;\n  step?: number;\n  value?: number;\n\n  defaultValue?: number;\n  label?: string;\n};\n\nexport const Slider = (props: P) => {\n  const {\n    style = {},\n    className = \"\",\n    min,\n    max,\n    value,\n    label,\n    onChange,\n    step,\n    defaultValue,\n  } = props;\n\n  const percentage =\n    !isDefined(value) ? 0 : ((value - min) / (max - min)) * 100;\n  return (\n    <FlexCol\n      className={classOverride(\"slidecontainer gap-p25\", className)}\n      style={style}\n      onDoubleClick={\n        !Number.isFinite(defaultValue) ?\n          undefined\n        : () => {\n            onChange(defaultValue!);\n          }\n      }\n    >\n      {label && <Label label={label} variant=\"normal\" />}\n      <input\n        type=\"range\"\n        min={min}\n        max={max}\n        value={value ?? min}\n        step={Math.min(1, step ?? Math.abs(min - max) / 60)}\n        className=\"slider pointer\"\n        style={{\n          background: `linear-gradient(to right, var(--action) ${percentage}%, var(--bg-color-3) ${percentage}%)`,\n        }}\n        onChange={(e) => onChange(+e.target.value, e)}\n      />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/components/StatusChip.tsx",
    "content": "import React from \"react\";\nimport { FlexRow } from \"./Flex\";\nimport type { TestSelectors } from \"src/Testing\";\n\nexport const StatusChip = ({\n  text,\n  color,\n  ...testSelectors\n}: {\n  text: string;\n  color: \"yellow\" | \"red\" | \"green\" | \"blue\" | \"gray\";\n} & TestSelectors) => {\n  return (\n    <FlexRow\n      {...testSelectors}\n      className=\"gap-p25 px-p5 py-p25 \"\n      style={{\n        backgroundColor: `var(--faded-${color})`,\n        color: `var(--${color})`,\n        borderRadius: \"12px\",\n      }}\n    >\n      <div\n        style={{\n          width: \"8px\",\n          height: \"8px\",\n          borderRadius: \"50%\",\n          backgroundColor: `var(--${color})`,\n        }}\n      />\n      <div>{text}</div>\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Stepper.tsx",
    "content": "import React from \"react\";\n\ntype MyProps = {\n  steps: {\n    completed?: boolean;\n    current?: boolean;\n    onClick?: () => any;\n  }[];\n};\n\nexport default class Stepper extends React.Component<MyProps, any> {\n  render() {\n    const { steps = [] } = this.props;\n    const style = { width: \".625em\", height: \".625em\" },\n      currentStep = steps.findIndex((s) => s.current);\n\n    const getDividerLine = (i: number) => (\n      <div className=\"absolute inset-0 flex ai-center\">\n        <div\n          className={\n            \"h-p125 w-full \" + (i < currentStep ? \"bg-color-1\" : \"bg-color-2\")\n          }\n        ></div>\n      </div>\n    );\n\n    return (\n      <nav>\n        <ul\n          className=\"no-decor flex ai-center\"\n          style={{ listStyle: \"none\", padding: 0 }}\n        >\n          {steps.map((s, i) => (\n            <li\n              key={i}\n              className={\n                \"relative \" + (i < steps.length - 1 ? \" pr-2 f-1 \" : \"\")\n              }\n            >\n              {s.completed && !s.current ?\n                <>\n                  {getDividerLine(i)}\n                  <div\n                    onClick={s.onClick}\n                    className=\"pointer relative w-2 h-2 flex ai-center jc-center rounded-full  \"\n                  >\n                    <svg\n                      className=\"w-1p25 h-1p25 text-white\"\n                      viewBox=\"0 0 20 20\"\n                      fill=\"currentColor\"\n                    >\n                      <path\n                        fillRule=\"evenodd\"\n                        d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n                        clipRule=\"evenodd\"\n                      />\n                    </svg>\n                  </div>\n                </>\n              : s.current ?\n                <>\n                  {getDividerLine(i)}\n                  <span\n                    onClick={s.onClick}\n                    className=\"relative w-2 h-2 flex ai-center jc-center bg-color-0 border-2 border-indigo-600 rounded-full \"\n                  >\n                    <span\n                      style={style}\n                      className=\"h-2.5 w-2.5 rounded-full\"\n                    ></span>\n                  </span>\n                </>\n              : <>\n                  {getDividerLine(i)}\n                  <div\n                    onClick={s.onClick}\n                    className=\"pointer relative w-2 h-2 flex ai-center jc-center bg-color-0 border-2 rounded-full   \"\n                  >\n                    <span\n                      style={style}\n                      className=\"h-2.5 w-2.5 bg-transparent rounded-full \"\n                    ></span>\n                  </div>\n                </>\n              }\n            </li>\n          ))}\n        </ul>\n      </nav>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/components/SvgIcon.tsx",
    "content": "import { useIsMounted, usePromise } from \"prostgles-client\";\nimport React, { useEffect } from \"react\";\nimport sanitizeHtml from \"sanitize-html\";\nimport { classOverride, type DivProps } from \"./Flex\";\n\nexport const cachedSvgs = new Map<string, string>();\n\nexport const fetchNamedSVG = async (iconName: string) => {\n  const iconNameContainsOnlyLettersAndMaybeEndWithDigits =\n    /^[a-zA-Z]+(\\d+)?$/.test(iconName);\n  if (!iconNameContainsOnlyLettersAndMaybeEndWithDigits) {\n    console.error(\n      `Icon name \"${iconName}\" iconNameContainsOnlyLettersAndMaybeEndWithDigits`,\n    );\n    return;\n  }\n  const iconPath = `/icons/${iconName}.svg`;\n  const cached = cachedSvgs.get(iconPath);\n  if (cached) {\n    return cached;\n  }\n  return fetchIconAndCache(iconPath);\n};\n\nexport const SvgIcon = ({\n  icon,\n  className,\n  size,\n  style,\n}: {\n  icon: string;\n  className?: string;\n  style?: React.CSSProperties;\n  size?: number;\n}) => {\n  const getIsMounted = useIsMounted();\n  const iconPath = `/icons/${icon}.svg`;\n  const [svg, setSvg] = React.useState(cachedSvgs.get(iconPath));\n  useEffect(() => {\n    fetchIconAndCache(iconPath).then((fetchedSvg) => {\n      if (!getIsMounted()) return;\n      setSvg(fetchedSvg);\n    });\n  }, [iconPath, getIsMounted, icon]);\n\n  const sizePx = `${size || 24}px`;\n  return (\n    <div\n      className={className}\n      style={{\n        width: sizePx,\n        height: sizePx,\n        ...style,\n      }}\n      dangerouslySetInnerHTML={!svg ? undefined : { __html: svg }}\n    />\n  );\n};\n\nconst fetchIconAndCache = (iconPath: string) => {\n  return fetch(iconPath)\n    .then((res) => res.text())\n    .then((svgRaw) => {\n      const svg = sanitizeHtml(svgRaw, {\n        allowedTags: [\"svg\", \"path\"],\n        allowedAttributes: {\n          svg: [\"width\", \"height\", \"view\", \"style\", \"xmlns\", \"viewBox\", \"role\"],\n          path: [\"d\", \"style\"],\n        },\n        parser: {\n          lowerCaseTags: false,\n          lowerCaseAttributeNames: false,\n        },\n      });\n      cachedSvgs.set(iconPath, svg);\n      return svg;\n    });\n};\n\nexport const getIcon = async (icon: string) => {\n  const iconPath = `/icons/${icon}.svg`;\n  if (!cachedSvgs.has(iconPath)) {\n    const res = await fetchIconAndCache(iconPath);\n    return res;\n  }\n  return cachedSvgs.get(iconPath)!;\n};\n\ntype SvgIconFromURLProps = DivProps & {\n  url: string;\n  /**\n   * @default \"mask\"\n   * mask = uses currentColor\n   * background = maintains original colours\n   */\n  mode?: \"background\" | \"mask\" | \"auto\";\n};\nexport const SvgIconFromURL = ({\n  url,\n  className,\n  style,\n  mode: propsMode = \"auto\",\n  ...divProps\n}: SvgIconFromURLProps) => {\n  const modeOverride = usePromise(async () => {\n    if (propsMode !== \"auto\") return;\n    const res = await fetch(url);\n    const contentType = res.headers.get(\"content-type\");\n    if (contentType?.includes(\"image/svg+xml\")) {\n      const svg = await res.text();\n      if (svg.includes(\"currentColor\")) {\n        return \"mask\";\n      }\n    }\n    return \"background\";\n  }, [propsMode, url]);\n  const mode = propsMode === \"auto\" ? modeOverride : propsMode;\n\n  return (\n    <div\n      {...divProps}\n      className={classOverride(\"SvgIconFromURL\", className)}\n      style={{\n        ...style,\n        ...(mode === \"mask\" ?\n          {\n            backgroundColor: \"currentColor\",\n            maskImage: `url(${JSON.stringify(url)})`,\n            maskSize: \"cover\",\n          }\n        : {\n            backgroundImage: `url(${JSON.stringify(url)})`,\n            backgroundSize: \"cover\",\n          }),\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/components/SwitchToggle.css",
    "content": ".Switch-root {\n  -webkit-font-smoothing: antialiased;\n  -webkit-text-size-adjust: 100%;\n  font-size: 1rem;\n  line-height: 1.5;\n  letter-spacing: 0;\n  font-weight: 400;\n  font-family: Arial;\n  display: inline-flex;\n  width: 58px;\n  height: 38px;\n  overflow: hidden;\n  padding: 12px;\n  box-sizing: border-box;\n  position: relative;\n  flex-shrink: 0;\n  z-index: 0;\n  vertical-align: middle;\n}\n\n.SwitchBase-root {\n  -webkit-font-smoothing: antialiased;\n  -webkit-text-size-adjust: 100%;\n  color-scheme: light;\n  font-size: 1rem;\n  line-height: 1.5;\n  letter-spacing: 0;\n  font-weight: 400;\n  font-family: Arial;\n  display: inline-flex;\n  -webkit-box-align: center;\n  align-items: center;\n  -webkit-box-pack: center;\n  justify-content: center;\n  box-sizing: border-box;\n  -webkit-tap-highlight-color: transparent;\n  background-color: transparent;\n  outline: 0;\n  margin: 0;\n  user-select: none;\n  vertical-align: middle;\n  -webkit-appearance: none;\n  text-decoration: none;\n  padding: 9px;\n  border-radius: 50%;\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 1;\n  transition:\n    left 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,\n    transform 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\n}\n\n.Switch-input {\n  -webkit-tap-highlight-color: transparent;\n  user-select: none;\n  box-sizing: inherit;\n  cursor: inherit;\n  position: absolute;\n  opacity: 0;\n  height: 100%;\n  top: 0;\n  margin: 0;\n  padding: 0;\n  z-index: 1;\n  left: 0;\n  width: 100%;\n  /* left: -100%;\n  width: 300%; */\n}\n\n.Switch-thumb {\n  font-size: 1rem;\n  line-height: 1.5;\n  letter-spacing: 0;\n  font-weight: 400;\n  --google-font-color-materialiconstwotone: none;\n  font-family: Arial;\n  -webkit-tap-highlight-color: transparent;\n  cursor: pointer;\n  user-select: none;\n  box-sizing: inherit;\n  box-shadow:\n    0px 2px 1px -1px rgba(0, 0, 0, 0.2),\n    0px 1px 1px 0px rgba(0, 0, 0, 0.14),\n    0px 1px 3px 0px rgba(0, 0, 0, 0.12);\n  background-color: currentColor;\n  width: 20px;\n  height: 20px;\n  border-radius: 50%;\n}\n.Switch-thumb.loading {\n  background-color: color-mix(in srgb, currentColor 50%, transparent);\n}\n\n.Switch-track {\n  font-size: 1rem;\n  line-height: 1.5;\n  letter-spacing: 0;\n  font-weight: 400;\n  font-family: Arial;\n  box-sizing: inherit;\n  height: 100%;\n  width: 100%;\n  border-radius: 7px;\n  opacity: 0.38;\n  z-index: -1;\n  transition:\n    opacity 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,\n    background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;\n  background-color: #999999;\n}\n\n.Switch-root.checked .Switch-track {\n  background-color: #76a9fa;\n}\n\n.Switch-root {\n  color: white;\n}\n\n.dark-mode .Switch-root {\n  color: #d3d3d3;\n}\n\n.Switch-root.checked {\n  color: #3f83f8;\n}\n.dark-theme .Switch-root.checked {\n  color: #76a9fa;\n}\n\n.Switch-root.checked > .SwitchBase-root {\n  transform: translateX(20px);\n}\n"
  },
  {
    "path": "client/src/components/SwitchToggle.tsx",
    "content": "import { omitKeys, pickKeys } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { TestSelectors } from \"../Testing\";\nimport { generateUniqueID } from \"./FileInput/FileInput\";\nimport { classOverride } from \"./Flex\";\nimport type { LabelProps } from \"./Label\";\nimport { Label } from \"./Label\";\nimport Loading from \"./Loader/Loading\";\nimport \"./SwitchToggle.css\";\n\nexport type SwitchToggleProps = TestSelectors & {\n  title?: string;\n  checked: boolean;\n  onChange: (\n    checked: boolean,\n    e: React.ChangeEvent<HTMLInputElement>,\n  ) => Promise<void> | void;\n  className?: string;\n  style?: React.CSSProperties;\n  disabledInfo?: string | false;\n  variant?: \"row\" | \"col\" | \"row-reverse\";\n  disableOnChangeDuringLoading?: boolean;\n  label?: string | LabelProps;\n};\n\nexport const SwitchToggle: React.FC<SwitchToggleProps> = ({\n  id = generateUniqueID(),\n  checked,\n  onChange,\n  label,\n  style,\n  title = \"\",\n  className = \"\",\n  disabledInfo,\n  variant = \"row-reverse\",\n  disableOnChangeDuringLoading = true,\n  ...props\n}) => {\n  const [isLoading, setIsLoading] = useState(false);\n  const cursorStyle = {\n    cursor: disabledInfo ? \"not-allowed\" : \"pointer\",\n  };\n  const testSelectors = pickKeys(props, [\"data-key\", \"data-command\"]);\n  const labelP: LabelProps =\n    typeof label === \"string\" ? { label }\n    : label ? label\n    : {};\n  const labelProps: LabelProps | undefined =\n    !label ? undefined : (\n      ({\n        variant: labelP.variant ?? \"normal\",\n        className: classOverride(\n          `text-1p5 noselect w-fit flex-row ai-center`,\n          labelP.className,\n        ),\n        ...omitKeys(labelP, [\"style\", \"className\"]),\n        style: { ...cursorStyle, ...labelP.style },\n        htmlFor: disabledInfo ? undefined : id,\n        ...testSelectors,\n      } as LabelProps)\n    );\n\n  return (\n    <label\n      className={classOverride(\n        `SwitchToggle flex-${variant} w-fit ai-${variant === \"col\" ? \"start\" : \"center\"} gap-p25 ${disabledInfo || (isLoading && disableOnChangeDuringLoading) ? \"disabled \" : \"\"}`,\n        className,\n      )}\n      style={{\n        ...cursorStyle,\n        ...style,\n        ...(disabledInfo && { opacity: 0.7 }),\n        padding: \"1px\" /** to ensure active border stays visible */,\n      }}\n      role=\"switch\"\n      title={(disabledInfo ? disabledInfo : undefined) ?? title}\n    >\n      {labelProps && <Label {...labelProps} aria-checked={checked} />}\n      <div\n        className={`Switch-root rounded focusable ${checked ? \"checked\" : \" \"}`}\n      >\n        <span className={\"SwitchBase-root \"}>\n          <input\n            id={id}\n            className=\"Switch-input\"\n            type=\"checkbox\"\n            checked={checked}\n            disabled={!!disabledInfo}\n            {...(!labelProps ? testSelectors : {})}\n            onChange={\n              disabledInfo ? undefined : (\n                async (e) => {\n                  if (isLoading && disableOnChangeDuringLoading) return;\n                  const isPromise =\n                    \"then\" in onChange && typeof onChange.then === \"function\";\n                  if (isPromise) {\n                    setIsLoading(true);\n                  }\n                  try {\n                    await onChange(e.target.checked, e);\n                  } finally {\n                    setIsLoading(false);\n                  }\n                }\n              )\n            }\n          />\n          <span className={`Switch-thumb ${isLoading ? \"loading\" : \"\"}`}>\n            {isLoading && (\n              <Loading sizePx={21} style={{ color: \"var(--blue)\" }} />\n            )}\n          </span>\n        </span>\n        <span className={\"Switch-track\"}></span>\n      </div>\n    </label>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Table/Pagination.tsx",
    "content": "import React from \"react\";\nimport { PAGE_SIZES } from \"./Table\";\nimport {\n  mdiChevronLeft,\n  mdiChevronRight,\n  mdiPageFirst,\n  mdiPageLast,\n} from \"@mdi/js\";\nimport Btn from \"../Btn\";\nimport FormField from \"../FormField/FormField\";\nimport { classOverride, FlexRow } from \"../Flex\";\nimport type { Command } from \"../../Testing\";\n\nexport type PaginationProps = {\n  pageSize?: number;\n  page?: number;\n  totalRows?: number;\n  onPageChange?: (newPage: number) => any;\n  onPageSizeChange?: (newPageSize: number) => any;\n  className?: string;\n};\n\nexport const Pagination = (props: PaginationProps) => {\n  const {\n    onPageChange: onPC,\n    page: zeroBasedPage = 0,\n    onPageSizeChange,\n    pageSize = PAGE_SIZES[0],\n    totalRows,\n    className = \"\",\n  } = props;\n  const onPageChange = (newPage) => {\n    if (zeroBasedPage !== newPage) onPC?.(newPage);\n  };\n\n  let maxPage = 0;\n  if (totalRows) {\n    maxPage = Math.ceil(totalRows / pageSize) - 1;\n  }\n\n  if (!maxPage) return null;\n\n  const noPrev = zeroBasedPage === 0 ? \"Already at first page\" : undefined;\n  const noNext = zeroBasedPage === maxPage ? \"Already at last page\" : undefined;\n  const totalPages = maxPage + 1;\n  const totalRowCount = +(totalRows ?? 0);\n  const pageCountInfoNode = (\n    <div\n      className=\"text-2 text-sm p-p5 noselect\"\n      data-command={\"Pagination.pageCountInfo\" satisfies Command}\n    >\n      {totalPages.toLocaleString()} page{totalPages === 1 ? \"\" : \"s\"}{\" \"}\n      {` (${totalRowCount.toLocaleString()} rows)`}\n    </div>\n  );\n  if (noPrev && noNext) {\n    return (\n      <FlexRow className=\"p-1\">\n        <div style={{ opacity: 0.5 }}>End of results</div>\n        {pageCountInfoNode}\n      </FlexRow>\n    );\n  }\n  const displayPage = zeroBasedPage + 1;\n  return (\n    <FlexRow\n      data-command={\"Pagination\"}\n      className={classOverride(\"gap-0 p-p5 mt-auto ai-center\", className)}\n    >\n      <Btn\n        data-command=\"Pagination.firstPage\"\n        iconPath={mdiPageFirst}\n        disabledInfo={noPrev}\n        onClick={() => {\n          onPageChange(0);\n        }}\n      />\n      <Btn\n        data-command=\"Pagination.prevPage\"\n        iconPath={mdiChevronLeft}\n        disabledInfo={noPrev}\n        onClick={() => {\n          onPageChange(Math.max(0, zeroBasedPage - 1));\n        }}\n      />\n\n      <input\n        data-command={\"Pagination.page\" satisfies Command}\n        type=\"number\"\n        className=\"h-fit min-w-0 p-p5\"\n        style={{\n          width: `${(displayPage || 0).toString().length + 5}ch`,\n        }}\n        value={displayPage}\n        min={1}\n        max={maxPage + 1}\n        onChange={(e) => {\n          const p = +e.target.value - 1;\n          if (p >= 0 && p <= maxPage) {\n            onPageChange(p);\n          }\n        }}\n      />\n\n      <Btn\n        data-command=\"Pagination.nextPage\"\n        iconPath={mdiChevronRight}\n        disabledInfo={noNext}\n        onClick={() => {\n          onPageChange(Math.min(maxPage, zeroBasedPage + 1));\n        }}\n      />\n      <Btn\n        data-command=\"Pagination.lastPage\"\n        iconPath={mdiPageLast}\n        disabledInfo={noNext}\n        onClick={() => {\n          onPageChange(maxPage);\n        }}\n      />\n\n      {onPageSizeChange && (\n        <FormField\n          title=\"Page size\"\n          value={pageSize}\n          data-command=\"Pagination.pageSize\"\n          options={PAGE_SIZES.map((s) => `${s}`)}\n          onChange={(e) => {\n            const newPageSize = +e;\n            if (newPageSize === pageSize) return;\n            /**\n             * Re-adjust current page if it will become out of bounds\n             */\n            if (newPageSize * (zeroBasedPage + 1) > totalRowCount) {\n              onPageChange(Math.ceil(totalRowCount / newPageSize) - 1);\n            }\n            onPageSizeChange(newPageSize);\n          }}\n        />\n      )}\n      {pageCountInfoNode}\n    </FlexRow>\n  );\n};\n\nexport const usePagination = (defaultPageSize: number = PAGE_SIZES[0]) => {\n  const [page, setPage] = React.useState(0);\n  const [pageSize, setPageSize] = React.useState<number>(defaultPageSize);\n\n  return {\n    page,\n    pageSize,\n    onPageChange: setPage,\n    onPageSizeChange: setPageSize,\n    limit: pageSize,\n    offset: Math.max(0, page * pageSize),\n  };\n};\n"
  },
  {
    "path": "client/src/components/Table/Table.css",
    "content": ".table-component .sort-asc::after {\n  content: \"\\25b2\";\n  color: var(--text-0);\n  margin-left: 0.25em;\n  height: fit-content;\n}\n.table-component .sort-desc::after {\n  content: \"\\25bc\";\n  color: var(--text-0);\n  margin-left: 0.25em;\n  height: fit-content;\n}\n.table-component .sort-none::after {\n  /*\n    used to avoid content shift. Disabled to preserve space \n    content: \"\\25bc\";     \n  */\n  color: transparent;\n  margin-left: 0.25em;\n  height: fit-content;\n}\n\n.table-component .hover:hover {\n  background-color: rgba(0, 183, 255, 0.04);\n}\n\n.table-component .d-row {\n  max-height: 100px;\n}\n\n.table-component .d-row.active-row {\n  border: 1px solid #00d0ff;\n  background: #def9ff;\n}\n\n.resizing-ew {\n  border-right: 2px solid #00d0ff !important;\n}\n\n.table-component .table-column-label {\n  -webkit-line-clamp: 2;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  min-height: 0;\n  display: block;\n  -webkit-box-orient: vertical;\n}\n\n.table-component div[role=\"table\"] {\n  background-color: white;\n}\n\n.dark-theme .table-component div[role=\"table\"] {\n  background-color: #1e1e1e;\n}\n"
  },
  {
    "path": "client/src/components/Table/Table.tsx",
    "content": "import React, { useRef, useState } from \"react\";\nimport \"./Table.css\";\n\nimport type { AnyObject } from \"prostgles-types\";\nimport type {\n  ColumnSort,\n  ColumnSortSQL,\n} from \"../../dashboard/W_Table/ColumnMenu/ColumnMenu\";\nimport type { ColumnSortMenuProps } from \"../../dashboard/W_Table/ColumnMenu/ColumnSortMenu\";\nimport type { ProstglesColumn } from \"../../dashboard/W_Table/W_Table\";\nimport { classOverride } from \"../Flex\";\nimport type { PaginationProps } from \"./Pagination\";\nimport { TableBody } from \"./TableBody\";\nimport type { TableHeaderState } from \"./TableHeader\";\nimport { TableHeader } from \"./TableHeader\";\nexport const PAGE_SIZES = [5, 10, 15, 20, 25, 50, 100, 200] as const;\nexport type PageSize = (typeof PAGE_SIZES)[number];\nexport const TableRootClassname = \"table-component\";\nexport type OnColRenderRowInfo = {\n  row: AnyObject;\n  value: any;\n  renderedVal: any;\n  rowIndex: number;\n  prevRow: AnyObject | undefined;\n  nextRow: AnyObject | undefined;\n};\n/**\n * Renders inner cell node\n */\ntype OnColRender = (rowInfo: OnColRenderRowInfo) => any;\n\nexport type TableColumn = {\n  key: string | number;\n  title?: string;\n  label: React.ReactNode;\n  subLabel?: React.ReactNode;\n  subLabelTitle?: string;\n\n  /**\n   * Applied to headers only\n   */\n  headerClassname?: string;\n\n  /**\n   * Applied to cells\n   */\n  className?: string;\n  blur?: boolean;\n  hidden?: boolean;\n  noRightBorder?: boolean;\n  /** If it's a joined column then a string array of sortable columns */\n  sortable: boolean | ColumnSortMenuProps;\n  onClick?: (row: AnyObject, col: TableColumn, e: React.MouseEvent) => void;\n\n  onRender?: OnColRender;\n\n  /**\n   * Renders outer cell node\n   */\n  onRenderNode?: (row: AnyObject, value: any) => any;\n\n  onResize?: (width: number) => any;\n  getCellStyle?: (\n    row: AnyObject,\n    value: any,\n    renderedVal: any,\n  ) => React.CSSProperties;\n  onContextMenu?: (\n    e: React.MouseEvent,\n    n: HTMLElement,\n    col: TableColumn,\n    setPopup: (popup: TableHeaderState[\"popup\"]) => void,\n  ) => boolean;\n  width?: number;\n  flex?: number;\n};\n\nexport type TableProps<Sort extends ColumnSortSQL> = {\n  rowKeys?: string[];\n  rows: AnyObject[];\n  cols: ProstglesColumn[];\n  onColumnReorder?: (newOrder: ProstglesColumn[]) => void;\n  onSort?: (newSort: Sort[]) => any;\n  sort?: Sort[];\n  onRowHover?: (row: any, e: React.MouseEvent<HTMLDivElement>) => any;\n  onRowClick?: (\n    row: AnyObject | undefined,\n    e: React.MouseEvent<HTMLDivElement>,\n  ) => any;\n  rounded?: boolean;\n  tableStyle?: React.CSSProperties;\n  maxCharsPerCell?: number | string;\n  maxRowHeight?: number;\n  maxRowsPerPage?: number;\n  pagination?: PaginationProps | \"virtual\";\n  activeRowIndex?: number;\n  activeRowStyle?: React.CSSProperties;\n  bodyClass?: string;\n  rowClass?: string;\n  rowStyle?: React.CSSProperties;\n  className?: string;\n  showSubLabel?: boolean;\n  /**\n   * Used to render add row button\n   */\n  afterLastRowContent?: React.ReactNode;\n  enableExperimentalVirtualisation?: boolean;\n};\n\nexport type TableState = {\n  draggedCol?: { node: HTMLDivElement; idx: number; targetIdx?: number };\n};\nexport const Table = <Sort extends ColumnSortSQL>(\n  props: TableProps<Sort> & React.HTMLAttributes<HTMLDivElement>,\n) => {\n  const {\n    rows = [],\n    cols: allCols = [],\n    tableStyle = {},\n    maxRowsPerPage = 100,\n    className = \"\",\n  } = props;\n\n  const ref = useRef<HTMLDivElement>(null);\n\n  const [draggedCol, setDraggedCol] = useState<TableState[\"draggedCol\"]>();\n\n  const cols = allCols.filter((c) => !c.hidden);\n\n  const tableKey =\n    cols.map((c) => `${c.key}${c.width}`).join() + draggedCol?.idx;\n  return (\n    <div\n      key={tableKey}\n      className={classOverride(\n        TableRootClassname + \" o-auto flex-col f-1 min-h-0 min-w-0 \",\n        className,\n      )}\n      ref={ref}\n    >\n      <div\n        role=\"table\"\n        className={\"min-w-fit min-h-0 b b-default flex-col h-full\"}\n        style={tableStyle}\n      >\n        <TableHeader\n          {...props}\n          rootRef={ref}\n          setDraggedCol={(draggedCol) => setDraggedCol(draggedCol)}\n        />\n        <TableBody\n          cols={cols}\n          rows={rows}\n          activeRowIndex={props.activeRowIndex}\n          maxCharsPerCell={props.maxCharsPerCell}\n          maxRowHeight={props.maxRowHeight}\n          maxRowsPerPage={maxRowsPerPage}\n          activeRowStyle={props.activeRowStyle}\n          afterLastRowContent={props.afterLastRowContent}\n          bodyClass={props.bodyClass}\n          rowClass={props.rowClass}\n          rowStyle={props.rowStyle}\n          onRowClick={props.onRowClick}\n          onRowHover={props.onRowHover}\n          draggedCol={draggedCol}\n          pagination={props.pagination}\n          enableExperimentalVirtualisation={\n            props.enableExperimentalVirtualisation\n          }\n        />\n      </div>\n    </div>\n  );\n};\n\nexport function closest<Num extends number>(\n  v: number,\n  arr: readonly Num[] | Num[],\n): Num | undefined {\n  return arr\n    .map((av) => ({ av, diff: Math.abs(v - av) }))\n    .sort((a, b) => a.diff - b.diff)[0]?.av;\n}\nexport function closestIndexOf<Num extends number>(\n  v: number,\n  arr: readonly Num[] | Num[],\n): Num | undefined {\n  return arr\n    .map((av, i) => ({ i, av, diff: Math.abs(v - av) }))\n    .sort((a, b) => a.diff - b.diff)[0]?.i;\n}\n\nexport const onWheelScroll =\n  (parentClassname?: string): React.WheelEventHandler<HTMLElement> =>\n  (e: React.WheelEvent<HTMLElement>) => {\n    if (e.shiftKey || e.ctrlKey || !e.currentTarget.contains(e.target as any))\n      return;\n\n    const oFlowY = (el?: HTMLElement | null) => {\n      return el && el.scrollWidth > el.clientWidth;\n    };\n    let maxDepth = 5;\n    let el =\n      !parentClassname ?\n        e.currentTarget\n      : e.currentTarget.parentElement?.closest<HTMLElement>(\n          \".\" + parentClassname,\n        );\n    let sel;\n    while (maxDepth > 0) {\n      if (oFlowY(el)) {\n        sel = el;\n        maxDepth = 0;\n      } else {\n        el = el?.parentElement;\n        maxDepth--;\n      }\n    }\n    // if(sel) sel.scrollLeft += Math.sign(e.deltaY) * 50\n    if (sel) {\n      sel.scrollLeft -= (e.nativeEvent as any).wheelDeltaY || -e.deltaY;\n      // e.preventDefault();\n    }\n  };\n"
  },
  {
    "path": "client/src/components/Table/TableBody.tsx",
    "content": "import React, { useRef } from \"react\";\nimport \"./Table.css\";\n\nimport type { ColumnSortSQL } from \"../../dashboard/W_Table/ColumnMenu/ColumnMenu\";\nimport { classOverride } from \"../Flex\";\nimport { Pagination } from \"./Pagination\";\nimport { closest, PAGE_SIZES, type TableProps, type TableState } from \"./Table\";\nimport { TableRow } from \"./TableRow\";\nimport { useVirtualisedRows } from \"./useVirtualisedRows\";\n\nexport const TableBody = <Sort extends ColumnSortSQL>(\n  props: Pick<\n    TableProps<Sort>,\n    | \"rowClass\"\n    | \"onRowClick\"\n    | \"onRowHover\"\n    | \"activeRowIndex\"\n    | \"activeRowStyle\"\n    | \"maxRowHeight\"\n    | \"rowStyle\"\n    | \"rows\"\n    | \"cols\"\n    | \"maxCharsPerCell\"\n    | \"maxRowsPerPage\"\n    | \"pagination\"\n    | \"bodyClass\"\n    | \"rowKeys\"\n    | \"afterLastRowContent\"\n    | \"enableExperimentalVirtualisation\"\n  > &\n    Pick<TableState, \"draggedCol\">,\n) => {\n  const {\n    rows = [],\n    cols: c = [],\n    onRowClick,\n    onRowHover,\n    maxCharsPerCell: rawMaxCharsPerCell = 100,\n    maxRowsPerPage = 100,\n    pagination: rawPagination,\n    activeRowIndex = -1,\n    bodyClass = \"\",\n    activeRowStyle = {},\n    rowClass = \"\",\n    rowStyle = {},\n    maxRowHeight,\n    rowKeys,\n    afterLastRowContent = null,\n    draggedCol,\n    enableExperimentalVirtualisation,\n  } = props;\n\n  const scrollBodyRef = useRef<HTMLDivElement>(null);\n\n  const maxCharsPerCell =\n    !!rawMaxCharsPerCell && Number.isFinite(parseInt(rawMaxCharsPerCell + \"\")) ?\n      parseInt(rawMaxCharsPerCell + \"\")\n    : 100;\n\n  const visibleCols = c.filter((c) => !c.hidden);\n  const pagination = rawPagination === \"virtual\" ? undefined : rawPagination;\n  const { page = 0 } = pagination || {};\n  const pageSize = closest(pagination?.pageSize ?? 15, PAGE_SIZES) || 25;\n\n  let rowIndexOffset = 0;\n  let _rows = rows.map((r) => ({ ...r }));\n  if (rawPagination !== \"virtual\") {\n    if (pagination && !pagination.onPageChange) {\n      rowIndexOffset = page * pageSize;\n      _rows = _rows.slice(rowIndexOffset, (page + 1) * pageSize);\n    }\n    if (_rows.length > maxRowsPerPage) {\n      console.warn(\"Exceeded maxRowsPerPage\");\n      _rows = _rows.slice(0, maxRowsPerPage);\n    }\n  }\n\n  const { onScroll } = useVirtualisedRows({\n    rows: _rows,\n    scrollBodyRef,\n    mode: enableExperimentalVirtualisation ? \"auto\" : \"off\",\n  });\n\n  return (\n    <div\n      data-command=\"TableBody\"\n      className={classOverride(\n        \"TableBody b-y b-default f-1 oy-auto ox-hidden flex-col min-w-fit relative \" +\n          (rawPagination === \"virtual\" ? \"\" : \"no-scroll-bar\"),\n        bodyClass,\n      )}\n      role=\"rowgroup\"\n      onScroll={onScroll}\n      onPointerUp={\n        !onRowClick ? undefined : (\n          (e) => {\n            if (e.target === e.currentTarget) {\n              onRowClick(undefined, e);\n            }\n          }\n        )\n      }\n    >\n      <div ref={scrollBodyRef} className=\"relative f-0\">\n        {!_rows.length ?\n          <div className=\"text-3 p-2 noselect\">No data</div>\n        : _rows.map((row, iRow) => {\n            const rowKey = rowKeys?.map((key) => row[key]).join(\"-\") || iRow; // + \" \" + Date.now();\n            return (\n              <TableRow\n                visibleCols={visibleCols}\n                key={rowKey}\n                row={row}\n                iRow={iRow}\n                _rows={_rows}\n                rows={rows}\n                maxCharsPerCell={maxCharsPerCell}\n                rowIndexOffset={rowIndexOffset}\n                draggedCol={draggedCol}\n                activeRowIndex={activeRowIndex}\n                activeRowStyle={activeRowStyle}\n                maxRowHeight={maxRowHeight}\n                onRowClick={onRowClick}\n                onRowHover={onRowHover}\n                rowClass={rowClass}\n                rowStyle={rowStyle}\n              />\n            );\n          })\n        }\n      </div>\n      {afterLastRowContent}\n      {!pagination ? null : (\n        <Pagination\n          key=\"Pagination\"\n          {...pagination}\n          totalRows={pagination.totalRows ?? rows.length}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Table/TableHeader.tsx",
    "content": "import { isObject } from \"prostgles-types\";\nimport React from \"react\";\nimport { vibrateFeedback } from \"../../dashboard/Dashboard/dashboardUtils\";\nimport type { ColumnSortSQL } from \"../../dashboard/W_Table/ColumnMenu/ColumnMenu\";\nimport type { ColumnSortMenuProps } from \"../../dashboard/W_Table/ColumnMenu/ColumnSortMenu\";\nimport {\n  ColumnSortMenu,\n  getDefaultSort,\n} from \"../../dashboard/W_Table/ColumnMenu/ColumnSortMenu\";\nimport { getSortColumn } from \"../../dashboard/W_Table/tableUtils/tableUtils\";\nimport type { ProstglesColumn } from \"../../dashboard/W_Table/W_Table\";\nimport { quickClone } from \"../../utils/utils\";\nimport { classOverride } from \"../Flex\";\nimport { Pan } from \"../Pan\";\nimport type { PopupProps } from \"../Popup/Popup\";\nimport Popup from \"../Popup/Popup\";\nimport type { TableProps, TableState } from \"./Table\";\nimport { TableRootClassname, onWheelScroll } from \"./Table\";\n\ntype TableHeaderProps<Sort extends ColumnSortSQL> = Pick<\n  TableProps<Sort>,\n  \"cols\" | \"sort\" | \"onSort\" | \"onColumnReorder\" | \"showSubLabel\"\n> & {\n  setDraggedCol: (newCol: TableState[\"draggedCol\"]) => void;\n  rootRef: React.RefObject<HTMLDivElement>;\n} & Pick<TableState, \"draggedCol\">;\n\nexport type TableHeaderState = Pick<TableState, \"draggedCol\"> & {\n  popup?: PopupProps & { key: string };\n  showNestedSortOptions?: {\n    anchorEl: HTMLElement;\n  } & ColumnSortMenuProps;\n};\nexport class TableHeader<Sort extends ColumnSortSQL> extends React.Component<\n  TableHeaderProps<Sort>,\n  TableHeaderState\n> {\n  state: Readonly<TableHeaderState> = {\n    draggedCol: undefined,\n  };\n\n  render(): React.ReactNode {\n    const { cols, sort: s = [], onColumnReorder, onSort } = this.props;\n    const setDraggedCol = (draggedCol: TableHeaderState[\"draggedCol\"]) =>\n      this.setState({ draggedCol });\n    const { draggedCol, showNestedSortOptions, popup } = this.state;\n\n    let sort: Required<TableProps<Sort>>[\"sort\"] = [];\n    if (Array.isArray(s)) {\n      sort = s.map((s) => ({ ...s }));\n    }\n\n    return (\n      <>\n        {showNestedSortOptions && (\n          <Popup\n            title=\"Sort by\"\n            clickCatchStyle={{ opacity: 0 }}\n            anchorEl={showNestedSortOptions.anchorEl}\n            positioning=\"beneath-left\"\n            onClose={() => this.setState({ showNestedSortOptions: undefined })}\n          >\n            <ColumnSortMenu {...showNestedSortOptions} />\n          </Popup>\n        )}\n\n        {!popup ? null : (\n          <Popup\n            contentStyle={{\n              overflow: \"unset\",\n            }}\n            anchorEl={popup.anchorEl}\n            positioning={popup.positioning}\n            clickCatchStyle={{ opacity: 0.25, backdropFilter: \"blur(1px)\" }}\n            contentClassName=\"\"\n            {...popup}\n            onClose={() => {\n              this.setState({ popup: undefined });\n            }}\n          >\n            {popup.content}\n          </Popup>\n        )}\n        <div\n          data-command=\"TableHeader\"\n          role=\"row\"\n          className=\"noselect f-0 flex-row shadow bg-color-1\"\n          onWheel={onWheelScroll(TableRootClassname)}\n        >\n          {cols.map((col, iCol) => {\n            const mySort = sort.find((s) => getSortColumn(s, [col]));\n            const className =\n              \"flex-col h-full min-w-0 px-p5 py-p5 text-left font-14 relative \" +\n              \" font-medium text-0 tracking-wider to-ellipsis jc-center \" +\n              (onSort && col.sortable ? \" pointer \" : \"\") +\n              (col.width ? \" f-0 \" : \" f-1 \");\n\n            const contextMenuStyles =\n              !col.onContextMenu ? undefined : (\n                ({\n                  /* disable selection/Copy of UIWebView */\n                  WebkitUserSelect: \"none\",\n                  /* disable the IOS popup when long-press on a link */\n                  WebkitTouchCallout: \"none\",\n                } satisfies React.CSSProperties)\n              );\n\n            return (\n              <div\n                key={iCol}\n                className={classOverride(className, \"br b-color-1\")}\n                {...(col.onContextMenu ? iosContextMenuPolyfill() : {})}\n                onContextMenu={\n                  !col.onContextMenu ? undefined : (\n                    (e) => {\n                      if (\n                        col.onContextMenu &&\n                        !e.currentTarget.classList.contains(\"resizing-ew\")\n                      ) {\n                        vibrateFeedback(25);\n                        return col.onContextMenu(\n                          e,\n                          e.nativeEvent.target as HTMLElement,\n                          col,\n                          (popup) => this.setState({ popup }),\n                        );\n                      }\n                    }\n                  )\n                }\n                data-key={col.key}\n                role=\"columnheader\"\n                style={{\n                  ...getDraggedTableColStyle(col, iCol, draggedCol),\n                  ...contextMenuStyles,\n                }}\n                draggable={true}\n                onDragStart={(e) => {\n                  e.dataTransfer.setData(\"text/plain\", col.name + \"\");\n                  if (onColumnReorder) {\n                    setDraggedCol({ idx: iCol, node: e.currentTarget });\n                  }\n                }}\n                onDragOver={\n                  !onColumnReorder ? undefined : (\n                    (e) => {\n                      if (\n                        draggedCol &&\n                        draggedCol.idx !== iCol &&\n                        draggedCol.targetIdx !== iCol\n                      ) {\n                        setDraggedCol({ ...draggedCol, targetIdx: iCol });\n                      }\n                      e.preventDefault();\n                      return false;\n                    }\n                  )\n                }\n                onDragLeave={\n                  !onColumnReorder ? undefined : (\n                    (e) => {\n                      if (e.target === e.currentTarget) {\n                        setDraggedCol({ ...draggedCol!, targetIdx: undefined });\n                      }\n                    }\n                  )\n                }\n                onDragEnd={\n                  !onColumnReorder ? undefined : (\n                    (e) => {\n                      setDraggedCol(undefined);\n                    }\n                  )\n                }\n                onDrop={\n                  !onColumnReorder ? undefined : (\n                    (e) => {\n                      const droppedOnOtherColumn = draggedCol?.idx !== iCol;\n                      if (draggedCol && droppedOnOtherColumn) {\n                        let newCols = cols.slice(0);\n                        const sourceCol = newCols[draggedCol.idx];\n                        const targetCol = col;\n\n                        if (!sourceCol) return;\n                        newCols = newCols.filter(\n                          (c) => c.key !== sourceCol.key,\n                        );\n                        const targetIdx = newCols.findIndex(\n                          (c) => c.key === targetCol.key,\n                        );\n                        newCols.splice(\n                          targetIdx + (draggedCol.idx < targetIdx + 1 ? 1 : 0),\n                          0,\n                          sourceCol,\n                        );\n\n                        onColumnReorder(newCols);\n                      }\n                      setDraggedCol(undefined);\n                    }\n                  )\n                }\n                onClick={\n                  !onSort || !col.sortable ?\n                    undefined\n                  : (e) => {\n                      if (\n                        e.button ||\n                        (e.target as HTMLDivElement).classList.contains(\n                          \"resize-handle\",\n                        )\n                      )\n                        return;\n\n                      let newSort = quickClone(mySort);\n                      if (!newSort) {\n                        if (isObject(col.sortable)) {\n                          const [col1, col2] =\n                            col.sortable.column.nested?.columns.filter(\n                              (c) => c.show,\n                            ) ?? [];\n                          const onlyOneActiveColumn = col1 && !col2;\n                          if (onlyOneActiveColumn) {\n                            newSort = getDefaultSort(\n                              `${col.key}.${col1.name}`,\n                            ) as Sort; // { key: `${col.key}.${onlyOneActiveColumn.name}`, asc: true };\n                          } else {\n                            this.setState({\n                              showNestedSortOptions: {\n                                anchorEl: e.currentTarget,\n                                ...col.sortable,\n                              },\n                            });\n                            return;\n                          }\n                        } else {\n                          newSort = getDefaultSort(col.key as string) as Sort;\n                        }\n                      } else if (newSort.asc) newSort.asc = false;\n                      else newSort = undefined;\n                      onSort(\n                        (e.shiftKey ?\n                          sort.filter(\n                            (s) => s.key !== col.key && s.key !== mySort?.key,\n                          )\n                        : []\n                        ).concat(newSort ? [newSort] : []),\n                      );\n                    }\n                }\n              >\n                <div\n                  className={\n                    \"flex-row fs-1 h-fit \" +\n                    (!col.sortable ? \"\"\n                    : typeof mySort?.asc !== \"boolean\" ? \" sort-none \"\n                    : [false].includes(mySort.asc as any) ? \"sort-desc\"\n                    : \"sort-asc\") +\n                    (col.headerClassname || \"\")\n                  }\n                >\n                  <div className=\"flex-col min-w-0 f-shrink\">\n                    <div\n                      title={\n                        col.title ??\n                        (typeof col.label === \"string\" ? col.label : undefined)\n                      }\n                      className=\"table-column-label text-ellipsis \"\n                    >\n                      {col.label}\n                    </div>\n                    {col.subLabel !== undefined && this.props.showSubLabel ?\n                      <div\n                        className=\"table-column-sublabel text-2 mt-p25 font-normal ws-nowrap text-ellipsis \"\n                        title={col.subLabelTitle}\n                      >\n                        {col.subLabel}\n                      </div>\n                    : null}\n                  </div>\n                </div>\n\n                {iCol <= cols.length - 1 ?\n                  <Pan\n                    className=\"resize-handle noselect\"\n                    data-command=\"TableHeader.resizeHandle\"\n                    style={{\n                      position: \"absolute\",\n                      right: 0,\n                      top: 0,\n                      bottom: 0,\n                      width: \"30px\",\n                      cursor: \"ew-resize\",\n                      marginRight: iCol === cols.length - 1 ? 0 : \"-15px\",\n                    }}\n                    onDoubleTap={(_, __, node) => {\n                      const tableBody = node.closest(`.table-component`);\n                      const headerCell: HTMLDivElement | null = node.closest(\n                        `[role=\"columnheader\"]`,\n                      );\n                      if (headerCell && col.onResize && tableBody) {\n                        const [firstNode, ...otherRowNodes] =\n                          tableBody.querySelectorAll<HTMLElement>(\n                            `div[role=\"rowgroup\"] [role=\"row\"] > *:nth-child(${iCol + 1})`,\n                          );\n\n                        console.log({ firstNode, otherRowNodes });\n                        if (firstNode) {\n                          const font = window.getComputedStyle(firstNode).font;\n                          const max = [firstNode, ...otherRowNodes].reduce(\n                            (a, n) =>\n                              Math.max(\n                                a,\n                                getTextWidth(\n                                  n.innerText.split(\"\\n\")[0] ?? \"\",\n                                  font,\n                                ),\n                              ),\n                            9,\n                          );\n                          const newWidth = Math.min(\n                            document.body.offsetWidth / 2,\n                            Math.max(30, max + 20),\n                          );\n                          col.onResize(newWidth);\n                        }\n                      }\n                    }}\n                    threshold={0}\n                    onPanStart={(opts, ev) => {\n                      const colh: HTMLDivElement | null = opts.node.closest(\n                        `[role=\"columnheader\"]`,\n                      );\n                      if (colh) {\n                        (opts.node as any)._w = colh.offsetWidth;\n                        // ev.target._flex = +colh.style.flex;\n                      }\n                    }}\n                    onPan={(opts, ev) => {\n                      const headerCell: HTMLDivElement | null =\n                        opts.node.closest(`[role=\"columnheader\"]`);\n                      if (headerCell) {\n                        const { xDiff } = opts;\n                        const w_px = Math.max(\n                          30,\n                          (opts.node as any)._w + xDiff,\n                        );\n                        headerCell.style.width = `${w_px}px`;\n                        headerCell.style.flex = \"none\";\n\n                        const tableBody =\n                          this.props.rootRef.current?.querySelector(\n                            `[role=\"rowgroup\"]`,\n                          );\n                        const rowCols =\n                          tableBody?.querySelectorAll<HTMLElement>(\n                            `[role=\"row\"] > *:nth-child(${iCol + 1})`,\n                          );\n                        rowCols?.forEach((rc) => {\n                          rc.style.width = `${w_px}px`;\n                          rc.style.flex = \"none\";\n                        });\n                      }\n                    }}\n                    onPanEnd={(opts, ev) => {\n                      const colh: HTMLDivElement | null = opts.node.closest(\n                        `[role=\"columnheader\"]`,\n                      );\n                      if (colh && col.onResize) {\n                        col.onResize(colh.offsetWidth);\n                      }\n                    }}\n                    onPress={(ev) => {\n                      const colh = (ev.target as HTMLElement).closest(\n                        `[role=\"columnheader\"]`,\n                      );\n                      if (colh) {\n                        colh.classList.toggle(\"resizing-ew\", true);\n                        vibrateFeedback(15);\n                        ev.preventDefault();\n                        ev.stopPropagation();\n                      }\n                    }}\n                    onRelease={(ev) => {\n                      const colh = (ev.target as HTMLElement).closest(\n                        `[role=\"columnheader\"]`,\n                      );\n                      if (colh) {\n                        colh.classList.toggle(\"resizing-ew\", false);\n                      }\n                    }}\n                  />\n                : null}\n              </div>\n            );\n          })}\n        </div>\n      </>\n    );\n  }\n}\n\nexport const getDraggedTableColStyle = (\n  col: ProstglesColumn,\n  iCol?: number,\n  draggedCol?: TableState[\"draggedCol\"],\n): React.CSSProperties => {\n  const result: React.CSSProperties =\n    col.width ?\n      {\n        flex: \"none\",\n        width: `${col.width}px`,\n        // minWidth: `${col.width}px`\n      }\n    : { flex: 1 }; // width: \"100px\", minWidth: \"100px\"\n\n  if (iCol !== undefined && draggedCol?.idx === iCol) {\n    result.opacity = 0.5;\n  }\n  if (iCol !== undefined && draggedCol?.targetIdx === iCol) {\n    // result.transition = \".3s all\";\n    result.paddingLeft = \"2em\";\n    result.background = \"var(--blue-100)\";\n  }\n\n  return result;\n};\n\nfunction iosContextMenuPolyfill(): {\n  onTouchStart: React.TouchEventHandler<HTMLDivElement>;\n  onTouchMove: React.TouchEventHandler<HTMLDivElement>;\n  onTouchEnd: React.TouchEventHandler<HTMLDivElement>;\n} {\n  const stop: React.TouchEventHandler<HTMLDivElement> = (e) => {\n    if (window.isIOSDevice) {\n      const element: any = e.currentTarget;\n      if (element._isPressed) {\n        clearTimeout(element._isPressed);\n      }\n      element._isPressed = null;\n    }\n  };\n\n  return {\n    onTouchStart: (e) => {\n      if (window.isIOSDevice) {\n        const element: any = e.currentTarget;\n        e.preventDefault();\n        element._isPressed = setTimeout(() => {\n          // const element = e.currentTarget;\n          if (element._isPressed) {\n            const ev3 = new MouseEvent(\"contextmenu\", {\n              bubbles: true,\n              cancelable: false,\n              view: window,\n              button: 2,\n              buttons: 0,\n              clientX: element.getBoundingClientRect().x,\n              clientY: element.getBoundingClientRect().y,\n            });\n            element.dispatchEvent(ev3);\n          }\n        }, 500);\n      }\n    },\n    onTouchMove: stop,\n    onTouchEnd: stop,\n  };\n}\n\nconst getTextWidth = (text: string, font: string) => {\n  // Create a canvas element\n  const canvas = document.createElement(\"canvas\");\n  const context = canvas.getContext(\"2d\");\n\n  if (!context) {\n    return text.length * 10;\n  }\n\n  // Set the font\n  context.font = font;\n\n  // Measure the text width\n  const metrics = context.measureText(text);\n  return metrics.width;\n};\n"
  },
  {
    "path": "client/src/components/Table/TableRow.tsx",
    "content": "import { sliceText } from \"@common/utils\";\nimport React from \"react\";\nimport type { ColumnSortSQL } from \"../../dashboard/W_Table/ColumnMenu/ColumnMenu\";\nimport Btn from \"../Btn\";\nimport { classOverride } from \"../Flex\";\nimport type { TableProps, TableState } from \"./Table\";\nimport { getDraggedTableColStyle } from \"./TableHeader\";\n\nexport type TableRowProps<Sort extends ColumnSortSQL> = {\n  row: any;\n  iRow: number;\n  rowIndexOffset: number;\n  maxCharsPerCell: number;\n  draggedCol: TableState[\"draggedCol\"];\n  _rows: any[];\n  rows: any[];\n  visibleCols: TableProps<Sort>[\"cols\"];\n} & Pick<\n  TableProps<Sort>,\n  | \"rowClass\"\n  | \"onRowClick\"\n  | \"onRowHover\"\n  | \"activeRowIndex\"\n  | \"activeRowStyle\"\n  | \"maxRowHeight\"\n  | \"rowStyle\"\n>;\nexport const TableRow = <Sort extends ColumnSortSQL>({\n  row,\n  iRow,\n  rowClass,\n  onRowClick,\n  onRowHover,\n  activeRowIndex,\n  activeRowStyle,\n  visibleCols,\n  maxCharsPerCell,\n  maxRowHeight,\n  rowStyle,\n  rowIndexOffset,\n  draggedCol,\n  rows,\n  _rows,\n}: TableRowProps<Sort>) => {\n  return (\n    <div\n      role=\"row\"\n      className={\n        \"d-row flex-row f-0 \" +\n        (rowClass ? rowClass : \" \") +\n        (onRowClick ? \" pointer \" : \"\") +\n        (onRowHover || onRowClick ? \" hover \" : \"\")\n      } // + ((activeRowIndex === iRow && !activeRowStyle)? \" active-row \" : \"\")}\n      style={{\n        ...(activeRowIndex === iRow ? activeRowStyle : {}),\n        ...rowStyle,\n        maxHeight: `${maxRowHeight || 100}px`,\n      }}\n      onClick={\n        !onRowClick ? undefined : (\n          (e) => {\n            /** Do not interrupt user from selecting text */\n            if (\n              e.currentTarget.contains(e.target as Node) &&\n              !window.getSelection()?.toString().trim()\n            ) {\n              onRowClick(row, e);\n            }\n          }\n        )\n      }\n    >\n      {visibleCols.map((col, i) => {\n        if (col.onRenderNode) {\n          return col.onRenderNode(row, row[col.key]);\n        }\n        const actualRowIdx = iRow + rowIndexOffset;\n        const cellText = parseCell(row[col.key], maxCharsPerCell, true);\n        const cellTextVal =\n          col.onRender ? null : parseCell(row[col.key], maxCharsPerCell);\n\n        const cellClassName = classOverride(\n          `text-sm leading-5 flex-col text-0 o-auto no-scroll-bar ta-left ${col.noRightBorder ? \"\" : \"br\"} b-color-1 ` + //  ws-no-wrap  o-hidden to-ellipsis\n            `${iRow < _rows.length - 1 ? \"bt\" : \"bt bb\"} ` +\n            (col.blur ? \" blur \" : \" \") +\n            (col.width ? \" f-0 \" : \" f-1 \") +\n            (col.onClick ? \"  \" : \" p-p5 \"),\n          col.className || \"\",\n        );\n\n        return (\n          <div\n            key={i}\n            className={cellClassName}\n            style={{\n              ...getDraggedTableColStyle(col, i, draggedCol),\n              ...(col.getCellStyle?.(row, row[col.key], cellText) || {}),\n            }}\n            role=\"cell\"\n            title={col.onRender ? \"\" : cellText}\n          >\n            {col.onClick ?\n              <Btn\n                onClick={(e) => {\n                  col.onClick && col.onClick(row, col, e);\n                }}\n                className=\"text-indigo-600 hover:text-indigo-900 h-fit w-fit b-color\"\n              >\n                {cellTextVal}\n              </Btn>\n            : col.onRender ?\n              col.onRender({\n                row: row,\n                value: row[col.key],\n                renderedVal: cellText,\n                rowIndex: actualRowIdx,\n                prevRow: rows[actualRowIdx - 1],\n                nextRow: rows[actualRowIdx + 1],\n              })\n            : cellTextVal}\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\nconst parseCell = <FT extends boolean = false>(\n  d: any,\n  maxCharsPerCell = 100,\n  getText?: FT,\n): FT extends true ? string : React.ReactNode => {\n  let node: React.ReactNode;\n  let txt: string | undefined;\n  if (typeof d === \"string\") {\n    txt = sliceText(d, maxCharsPerCell);\n  } else if (typeof d === \"number\") {\n    txt = d.toString();\n  } else if (d === null) {\n    txt = \"NULL\";\n    node = <i className=\"text-3\">NULL</i>;\n  } else if (d === undefined) {\n    txt = \"\";\n  } else if (d && Array.isArray(d)) {\n    txt = sliceText(\n      `[${d.map((c) => parseCell(c, undefined, true)).join(\", \\n\")}]`,\n      maxCharsPerCell,\n    );\n  } else if (d && typeof d.toISOString === \"function\") {\n    txt = d.toISOString();\n  } else if (d && Object.prototype.toString.call(d) === \"[object Object]\") {\n    txt = sliceText(JSON.stringify(d, null, 2), maxCharsPerCell);\n  } else if (d && d.toString) {\n    txt = sliceText(d.toString(), maxCharsPerCell);\n  } else {\n    txt = `${d}`;\n  }\n\n  if (getText) {\n    return txt as any;\n  }\n\n  return node || (txt as any);\n};\n"
  },
  {
    "path": "client/src/components/Table/useVirtualisedRows.ts",
    "content": "import { useMemoDeep } from \"prostgles-client\";\nimport { useCallback, useEffect, useMemo } from \"react\";\n\ntype RowNodeWithInfo = HTMLDivElement & {\n  nodeRect?: DOMRect | undefined;\n  initialStyle: Pick<CSSStyleDeclaration, \"display\"> | undefined;\n};\n\ntype P = {\n  scrollBodyRef: React.RefObject<HTMLDivElement>;\n  rows: any[];\n  mode: \"auto\" | \"off\";\n};\n\n/**\n * Given a table body with rows, render only visible\n * VERY experimental. react-virtuoso not used because scrolling is not smooth\n * */\nexport const useVirtualisedRows = ({\n  scrollBodyRef,\n  rows: nonMemoRows,\n  mode,\n}: P) => {\n  const rows = useMemoDeep(() => nonMemoRows, [nonMemoRows]);\n  const getParentNodes = useCallback(() => {\n    const xScrollParent =\n      scrollBodyRef.current?.closest<HTMLDivElement>(\".table-component\");\n    const scrollContentWrapper = scrollBodyRef.current;\n    const scrollBody = scrollContentWrapper?.parentElement;\n    if (!scrollBody || !xScrollParent) {\n      return;\n    }\n    return { xScrollParent, scrollContentWrapper, scrollBody };\n  }, [scrollBodyRef]);\n\n  const onScroll = useCallback(() => {\n    const pNodes = getParentNodes();\n    if (!pNodes) return;\n    const { xScrollParent, scrollContentWrapper, scrollBody } = pNodes;\n    const offsetTop = scrollBody.getBoundingClientRect().top;\n    const offsetLeft = xScrollParent.getBoundingClientRect().left;\n    const threshold = 100;\n    const setRectAndSize = (node: RowNodeWithInfo, isRow: boolean) => {\n      const nodeRect = node.getBoundingClientRect();\n      node.nodeRect = nodeRect;\n      const display = node.style.display;\n      node.initialStyle = { display };\n      node.style.width = `${nodeRect.width}px`;\n      node.style.height = `${nodeRect.height}px`;\n      node.style.top = !isRow ? \"0px\" : `${nodeRect.top - offsetTop}px`;\n      node.style.left = `${nodeRect.left - offsetLeft}px`;\n      node.style.position = \"absolute\";\n    };\n    const checkChildNodes = (\n      parentNode: HTMLDivElement,\n      isRow = true,\n    ): boolean => {\n      let isFirstRun = false;\n      Array.from(parentNode.children as unknown as RowNodeWithInfo[])\n        .slice(0)\n        .reverse() // This is to ensure changing to absolute position does not affect the previous rows\n        .forEach((node, i) => {\n          if (\n            (isRow && node.role !== \"row\") ||\n            (!isRow && node.role !== \"cell\")\n          )\n            return;\n          if (!(\"nodeRect\" in node) || !node.initialStyle) {\n            setRectAndSize(node, isRow);\n            if (isRow) {\n              /** Prevent items resizing due to flex */\n              Array.from(node.children as unknown as RowNodeWithInfo[]).forEach(\n                (child) => {\n                  const childRect = child.getBoundingClientRect();\n                  child.style.maxWidth = `${childRect.width}px`;\n                  child.style.width = `${childRect.width}px`;\n                },\n              );\n              checkChildNodes(node, false);\n            }\n            isFirstRun = true;\n          }\n          const nodeRect = node.nodeRect || node.getBoundingClientRect();\n\n          const isTooUp =\n            nodeRect.bottom - offsetTop < scrollBody.scrollTop - threshold;\n          const isTooDown =\n            nodeRect.top - offsetTop >\n            scrollBody.scrollTop + scrollBody.clientHeight + threshold;\n          const isOutOfView = isTooUp || isTooDown;\n\n          const rowDisplay = isOutOfView ? \"none\" : node.initialStyle!.display;\n          if (rowDisplay !== node.style.display) {\n            node.style.display = rowDisplay;\n            if (isOutOfView && isRow) {\n              return;\n            }\n          }\n\n          if (!isRow) return;\n          Array.from(node.children as unknown as RowNodeWithInfo[]).forEach(\n            (child) => {\n              const childRect = child.nodeRect!;\n              const isTooLeftOfView =\n                childRect.right - offsetLeft <\n                xScrollParent.scrollLeft - threshold;\n              const isTooRightOfView =\n                childRect.left - offsetLeft >\n                xScrollParent.scrollLeft +\n                  xScrollParent.clientWidth +\n                  threshold;\n              const cellIsOutOfView = isTooLeftOfView || isTooRightOfView;\n\n              const display =\n                cellIsOutOfView ? \"none\" : node.initialStyle!.display;\n              if (child.style.display !== display) {\n                child.style.display = display;\n              }\n            },\n          );\n        });\n      return isFirstRun;\n    };\n    const sizeWasSet = checkChildNodes(scrollContentWrapper);\n    if (sizeWasSet) {\n      checkChildNodes(scrollContentWrapper);\n    }\n  }, [getParentNodes]);\n\n  const disabled = useMemo(() => {\n    const disabled =\n      mode === \"off\" ||\n      (rows.length < 20 && Object.keys(rows[0] ?? {}).length < 30);\n    return disabled;\n  }, [rows, mode]);\n\n  useEffect(() => {\n    if (disabled) {\n      return;\n    }\n    const pNodes = getParentNodes();\n    if (!pNodes) {\n      return;\n    }\n    const { xScrollParent, scrollContentWrapper, scrollBody } = pNodes;\n    scrollContentWrapper.style.height = scrollBody.scrollHeight + \"px\";\n    scrollContentWrapper.style.width = scrollBody.scrollWidth + \"px\";\n    onScroll();\n    xScrollParent.addEventListener(\"scroll\", onScroll, {\n      passive: true,\n    });\n    return () => {\n      xScrollParent.removeEventListener(\"scroll\", onScroll);\n    };\n  }, [rows, disabled, onScroll, getParentNodes]);\n\n  return {\n    onScroll: disabled ? undefined : onScroll,\n  };\n};\n"
  },
  {
    "path": "client/src/components/Tabs.tsx",
    "content": "import { mdiArrowLeft } from \"@mdi/js\";\nimport React from \"react\";\nimport { isObject } from \"@common/publishUtils\";\nimport type { DeltaOf, DeltaOfData } from \"../dashboard/RTComp\";\nimport RTComp from \"../dashboard/RTComp\";\nimport Btn from \"./Btn\";\nimport { classOverride } from \"./Flex\";\nimport { Icon } from \"./Icon/Icon\";\nimport type { MenuListitem } from \"./MenuListItem\";\nimport { MenuList } from \"./MenuList\";\nimport { useConnectionConfigSearchParams } from \"../dashboard/ConnectionConfig/useConnectionConfigSearchParams\";\nimport { getKeys } from \"../utils/utils\";\n\nexport type TabItem = Partial<\n  Omit<MenuListitem, \"contentRight\" | \"onPress\">\n> & {\n  content?: React.ReactNode;\n};\n\nexport type TabItems<K extends string = string> = {\n  [P in K]: TabItem;\n};\n\nexport type TabsProps<T extends TabItems = TabItems> = {\n  items: T;\n  style?: React.CSSProperties;\n  className?: string;\n  listClassName?: string;\n  menuStyle?: React.CSSProperties;\n  variant?:\n    | \"horizontal\"\n    | \"vertical\"\n    | {\n        /**\n         * If available width is less than this then controls will use a select drop down\n         */\n        controlsCollapseWidth: number;\n\n        /**\n         * If available width is less than the sum of breakpoints then will switch to vertical.\n         */\n        controlsBreakpoint: number;\n        contentBreakpoint: number;\n      };\n\n  /**\n   * If true then non active controls will be hidden\n   */\n  compactMode?: \"hide-label\" | \"hide-inactive\";\n  activeKey?: keyof T;\n  defaultActiveKey?: keyof T;\n  onChange?: (itemLabel: keyof T | undefined) => void;\n  contentClass?: string;\n  onRender?: (activeItem: TabItem) => React.ReactNode;\n};\n\ntype S<T> = {\n  activeKey?: keyof T;\n  variant?: \"horizontal\" | \"vertical\";\n  controlsCollapsed: boolean;\n};\n\nexport default class Tabs<T extends TabItems = TabItems> extends RTComp<\n  TabsProps<T>,\n  S<T>\n> {\n  state: S<T> = {\n    controlsCollapsed: false,\n  };\n\n  rootDiv?: HTMLDivElement;\n  sizeObeserver?: ResizeObserver;\n\n  onUnmount() {\n    this.rootDiv && this.sizeObeserver?.unobserve(this.rootDiv);\n  }\n\n  checkVariant = () => {\n    if (!this.mounted) return;\n\n    const { variant = \"horizontal\" } = this.props;\n    if (this.rootDiv && isObject(variant)) {\n      const { contentBreakpoint, controlsBreakpoint, controlsCollapseWidth } =\n        variant;\n      let newVariant = this.state.variant;\n      const width = document.body.offsetWidth; // this.rootDiv.offsetWidth\n      const newCCollapse = width < controlsCollapseWidth;\n      if (width < contentBreakpoint + controlsBreakpoint) {\n        newVariant = \"horizontal\";\n      } else {\n        newVariant = \"vertical\";\n      }\n\n      if (newVariant !== this.state.variant) {\n        this.setState({ variant: newVariant });\n      }\n      if (newCCollapse !== this.state.controlsCollapsed) {\n        this.setState({ controlsCollapsed: newCCollapse });\n      }\n    }\n  };\n\n  onDelta(\n    deltaP?: DeltaOf<TabsProps<T>>,\n    deltaS?: DeltaOf<S<T>>,\n    deltaD?: DeltaOfData<{ [x: string]: any }>,\n  ): void {\n    const { variant = \"horizontal\" } = this.props;\n    if (deltaP?.variant) {\n      if (this.rootDiv) {\n        this.sizeObeserver?.unobserve(this.rootDiv);\n        if (isObject(variant)) {\n          this.sizeObeserver = new ResizeObserver(this.checkVariant);\n          this.sizeObeserver.observe(this.rootDiv);\n        }\n      }\n    }\n\n    if (!this.state.variant && !isObject(variant)) {\n      this.setState({ variant });\n    }\n  }\n\n  render() {\n    const {\n      className = \"\",\n      listClassName = \"\",\n      style = {},\n      contentClass = \"\",\n      onChange,\n      compactMode,\n      onRender,\n      menuStyle,\n    } = this.props;\n\n    const { variant, controlsCollapsed } = this.state;\n\n    const items = this.props.items;\n\n    const activeKey =\n      this.props.activeKey ??\n      this.state.activeKey ??\n      this.props.defaultActiveKey;\n\n    let activeContent: React.ReactNode = null;\n    if (typeof activeKey === \"string\" && items[activeKey]?.content) {\n      activeContent =\n        onRender ? onRender(items[activeKey]) : items[activeKey].content;\n    }\n    const isHoriz = variant === \"horizontal\";\n\n    let itemStyle: React.CSSProperties = {\n      borderColor: \"transparent\",\n      borderRightStyle: \"solid\",\n      borderRightWidth: \"2px\",\n    };\n    let posClass = \"flex-row \";\n    if (isHoriz) {\n      posClass = \"flex-col \";\n      itemStyle = {\n        borderColor: \"transparent\",\n        borderBottomStyle: \"solid\",\n        borderBottomWidth: \"2px\",\n      };\n    }\n    if (compactMode === \"hide-inactive\") {\n      itemStyle = { borderColor: \"transparent\" };\n    }\n\n    const activeItemStyle = { ...itemStyle };\n\n    let showBackBtn = false;\n    if (compactMode === \"hide-inactive\" && activeKey) {\n      posClass = \"flex-col \";\n      showBackBtn = true;\n    }\n\n    const activeItemIconPath =\n      activeKey ? items[activeKey]?.leftIconPath : undefined;\n    const activeKeyAndContent = !!(activeKey && items[activeKey]?.content);\n    return (\n      <div\n        ref={(rootDiv) => {\n          if (rootDiv) this.rootDiv = rootDiv;\n        }}\n        style={{\n          ...style,\n          ...(!variant && { opacity: 0 }),\n        }}\n        className={classOverride(\n          \"Tabs \" + posClass + \" min-w-0 min-h-0 f-1 \",\n          className,\n        )}\n      >\n        {showBackBtn ?\n          <div\n            className=\"flex-row ai-center\"\n            style={{ backgroundColor: \"aliceblue\" }}\n          >\n            <Btn\n              iconPath={mdiArrowLeft}\n              onClick={() => {\n                this.setState({ activeKey: undefined });\n                if (onChange) {\n                  onChange(undefined);\n                }\n              }}\n            >\n              {activeKey as string}\n            </Btn>\n            {activeItemIconPath && (\n              <Icon className=\"text-2 mr-1\" path={activeItemIconPath} />\n            )}\n          </div>\n        : <MenuList\n            style={{\n              zIndex: 1,\n              ...(isObject(this.props.variant) && { borderRadius: 0 }),\n              ...menuStyle,\n            }}\n            /* Ensure active items without content do not take white space to right */\n            className={classOverride(\n              `Tabs_Menu ${activeKeyAndContent ? \"bg-color-1 shadow\" : \"max-w-unset\"} f-0 w-full noselect`,\n              listClassName,\n            )}\n            variant={\n              controlsCollapsed ? \"dropdown\"\n              : variant === \"horizontal\" ?\n                \"horizontal-tabs\"\n              : variant\n            }\n            compactMode={this.props.compactMode === \"hide-label\"}\n            activeKey={activeKey as string | undefined}\n            items={Object.keys(items)\n              .filter((k) => !items[k]!.hide)\n              .map((key) => ({\n                key,\n                label: items[key]?.label || key,\n                leftIconPath: items[key]?.leftIconPath,\n                disabledText: items[key]?.disabledText,\n                style: {\n                  // ...(key === activeKey ? activeItemStyle : itemStyle),\n                  ...(items[key]?.style || {}),\n                },\n                listProps: items[key]?.listProps,\n                onPress:\n                  items[key]?.disabledText ?\n                    undefined\n                  : () => {\n                      if (onChange) {\n                        onChange(key);\n                      }\n                      this.setState({ activeKey: key });\n                    },\n              }))}\n          />\n        }\n        {!activeContent ? null : (\n          <div className={`Tabs_Content ${contentClass}`}>{activeContent}</div>\n        )}\n      </div>\n    );\n  }\n}\n\nexport const TabsWithDefaultStyle = (props: Pick<TabsProps, \"items\">) => {\n  const { activeSection, setSection } = useConnectionConfigSearchParams(\n    getKeys(props.items),\n  );\n  return (\n    <div\n      className=\"flex-col f-1 min-h-0 pt-1 w-full\"\n      style={{ maxWidth: \"800px\" }}\n    >\n      <Tabs\n        style={{ minHeight: \"100vh\" }}\n        menuStyle={{ maxHeight: undefined }}\n        variant={{\n          controlsBreakpoint: 200,\n          contentBreakpoint: 500,\n          controlsCollapseWidth: 350,\n        }}\n        className=\"f-1 shadow\"\n        activeKey={activeSection ?? props.items[0]?.key}\n        onChange={(section) => {\n          setSection({ section });\n        }}\n        items={props.items}\n        contentClass=\"f-1 o-autdo flex-row jc-center bg-color-2 \"\n        onRender={(item) => (\n          <div className=\"flex-col f-1 max-w-800 min-w-0 bg-color-0 shadow w-full\">\n            <h2 style={{ paddingLeft: \"18px\" }} className=\" max-h-fit\">\n              {item.label}\n            </h2>\n            <div\n              className={\n                \" f-1 o-auto flex-row \" + (window.isLowWidthScreen ? \"\" : \" \")\n              }\n              style={{\n                alignSelf: \"stretch\",\n              }}\n            >\n              {item.content}\n            </div>\n          </div>\n        )}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/Tooltip.tsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport Popup from \"./Popup/Popup\";\n\ntype P = {\n  children: React.ReactNode;\n  tooltip: React.ReactNode;\n};\nexport const Tooltip = ({ children, tooltip }: P) => {\n  const ref = useRef<HTMLSpanElement>(null);\n  const [open, setOpen] = useState(false);\n  useEffect(() => {\n    const onMouseEnter = (ev: MouseEvent) => {\n      setOpen(true);\n    };\n    const onMouseLeave = (ev: MouseEvent) => {\n      setOpen(false);\n    };\n\n    const el = ref.current;\n    el?.addEventListener(\"mouseenter\", onMouseEnter);\n    el?.addEventListener(\"mouseleave\", onMouseLeave);\n\n    return () => {\n      el?.removeEventListener(\"mouseenter\", onMouseEnter);\n      el?.removeEventListener(\"mouseleave\", onMouseLeave);\n    };\n  }, [setOpen]);\n\n  return (\n    <>\n      <span ref={ref} style={open ? { cursor: \"help\", opacity: 0.8 } : {}}>\n        {children}\n      </span>\n      {ref.current && open && (\n        <Popup\n          anchorEl={ref.current}\n          anchorPadding={20}\n          positioning=\"above-center\"\n          clickCatchStyle={{ opacity: 0 }}\n          onClickClose={false}\n        >\n          {tooltip}\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/API/zip.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unnecessary-condition */\n/* eslint-disable no-var */\n// @ts-nocheck\nexport class Zip {\n  constructor(name: string) {\n    this.name = name;\n    this.zip = [];\n    this.file = [];\n  }\n\n  private dec2bin = (dec, size) => dec.toString(2).padStart(size, \"0\");\n  private str2dec = (str) => Array.from(new TextEncoder().encode(str));\n  private str2hex = (str) =>\n    [...new TextEncoder().encode(str)].map((x) =>\n      x.toString(16).padStart(2, \"0\"),\n    );\n  private hex2buf = (hex) =>\n    new Uint8Array(hex.split(\" \").map((x) => parseInt(x, 16)));\n  private bin2hex = (bin) =>\n    parseInt(bin.slice(8), 2).toString(16).padStart(2, \"0\") +\n    \" \" +\n    parseInt(bin.slice(0, 8), 2).toString(16).padStart(2, \"0\");\n\n  reverse = (hex) => {\n    const hexArray = [];\n    for (let i = 0; i < hex.length; i = i + 2)\n      hexArray[i] = hex[i] + \"\" + hex[i + 1];\n    return hexArray\n      .filter((a) => a)\n      .reverse()\n      .join(\" \");\n  };\n\n  private crc32 = (r) => {\n    for (var a, o = [], c = 0; c < 256; c++) {\n      a = c;\n      for (let f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1;\n      o[c] = a;\n    }\n    for (var n = -1, t = 0; t < r.length; t++)\n      n = (n >>> 8) ^ o[255 & (n ^ r[t])];\n    return this.reverse(((-1 ^ n) >>> 0).toString(16).padStart(8, \"0\"));\n  };\n\n  fecth2zip(filesArray, folder = \"\") {\n    filesArray.forEach((fileUrl) => {\n      let resp;\n      fetch(fileUrl)\n        .then((response) => {\n          resp = response;\n          return response.arrayBuffer();\n        })\n        .then((blob) => {\n          new Response(blob).arrayBuffer().then((buffer) => {\n            console.log(`File: ${fileUrl} load`);\n            const uint = [...new Uint8Array(buffer)];\n            uint.modTime = resp.headers.get(\"Last-Modified\");\n            uint.fileUrl = `${this.name}/${folder}${fileUrl}`;\n            this.zip[fileUrl] = uint;\n          });\n        });\n    });\n  }\n\n  str2zip(name: string, content: string, folder = \"\") {\n    const uint = [...new Uint8Array(this.str2dec(content))];\n    uint.name = name;\n    uint.modTime = new Date();\n    uint.fileUrl = `${this.name}/${folder}${name}`;\n    this.zip[name] = uint;\n  }\n\n  files2zip(files, folder = \"\") {\n    for (let i = 0; i < files.length; i++) {\n      files[i].arrayBuffer().then((data) => {\n        const uint = [...new Uint8Array(data)];\n        uint.name = files[i].name;\n        uint.modTime = files[i].lastModifiedDate;\n        uint.fileUrl = `${this.name}/${folder}${files[i].name}`;\n        this.zip[uint.fileUrl] = uint;\n      });\n    }\n  }\n\n  makeZip() {\n    let count = 0;\n    const fileHeader = \"\";\n    let centralDirectoryFileHeader = \"\";\n    let directoryInit = 0;\n    let offSetLocalHeader = \"00 00 00 00\";\n    const zip = this.zip;\n    for (const name in zip) {\n      const getModTime = () => {\n        const lastMod = new Date(zip[name].modTime);\n        const hour = this.dec2bin(lastMod.getHours(), 5);\n        const minutes = this.dec2bin(lastMod.getMinutes(), 6);\n        const seconds = this.dec2bin(Math.round(lastMod.getSeconds() / 2), 5);\n        const year = this.dec2bin(lastMod.getFullYear() - 1980, 7);\n        const month = this.dec2bin(lastMod.getMonth() + 1, 4);\n        const day = this.dec2bin(lastMod.getDate(), 5);\n        return (\n          this.bin2hex(`${hour}${minutes}${seconds}`) +\n          \" \" +\n          this.bin2hex(`${year}${month}${day}`)\n        );\n      };\n      const modTime = getModTime();\n      const crc = this.crc32(zip[name]);\n      const size = this.reverse(\n        parseInt(zip[name].length).toString(16).padStart(8, \"0\"),\n      );\n      const nameFile = this.str2hex(zip[name].fileUrl).join(\" \");\n      const nameSize = this.reverse(\n        zip[name].fileUrl.length.toString(16).padStart(4, \"0\"),\n      );\n      const fileHeader = `50 4B 03 04 14 00 00 00 00 00 ${modTime} ${crc} ${size} ${size} ${nameSize} 00 00 ${nameFile}`;\n      const fileHeaderBuffer = this.hex2buf(fileHeader);\n      directoryInit =\n        directoryInit + fileHeaderBuffer.length + zip[name].length;\n      centralDirectoryFileHeader = `${centralDirectoryFileHeader}50 4B 01 02 14 00 14 00 00 00 00 00 ${modTime} ${crc} ${size} ${size} ${nameSize} 00 00 00 00 00 00 01 00 20 00 00 00 ${offSetLocalHeader} ${nameFile} `;\n      offSetLocalHeader = this.reverse(\n        directoryInit.toString(16).padStart(8, \"0\"),\n      );\n      this.file.push(fileHeaderBuffer, new Uint8Array(zip[name]));\n      count++;\n    }\n    centralDirectoryFileHeader = centralDirectoryFileHeader.trim();\n    const entries = this.reverse(count.toString(16).padStart(4, \"0\"));\n    const dirSize = this.reverse(\n      centralDirectoryFileHeader\n        .split(\" \")\n        .length.toString(16)\n        .padStart(8, \"0\"),\n    );\n    const dirInit = this.reverse(directoryInit.toString(16).padStart(8, \"0\"));\n    const centralDirectory = `50 4b 05 06 00 00 00 00 ${entries} ${entries} ${dirSize} ${dirInit} 00 00`;\n\n    this.file.push(\n      this.hex2buf(centralDirectoryFileHeader),\n      this.hex2buf(centralDirectory),\n    );\n\n    const a = document.createElement(\"a\");\n    a.href = URL.createObjectURL(\n      new Blob([...this.file], { type: \"application/octet-stream\" }),\n    );\n    console.log(a.href);\n    a.download = `${this.name}.zip`;\n    a.click();\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/AccessControl.tsx",
    "content": "import React from \"react\";\n\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Loading from \"@components/Loader/Loading\";\nimport { AccessControlRuleEditor } from \"./AccessControlRuleEditor\";\nimport { AccessControlRules } from \"./ExistingAccessRules\";\nimport type { useAccessControlSearchParams } from \"./useAccessControlSearchParams\";\nimport { mdiAccountCog, mdiPlus } from \"@mdi/js\";\nimport { UserSyncConfig } from \"./UserSyncConfig\";\nimport { ROUTES } from \"@common/utils\";\n\ntype P = ReturnType<typeof useAccessControlSearchParams> & {\n  prgl: Prgl;\n  className?: string;\n};\n\nexport type AccessRule = Required<DBSSchema[\"access_control\"]> & {\n  access_control_user_types: {\n    ids: DBSSchema[\"user_types\"][\"id\"][];\n  }[];\n  isApplied: boolean | undefined;\n  published_methods: Required<DBSSchema[\"published_methods\"]>[];\n  access_control_allowed_llm: Omit<\n    Required<DBSSchema[\"access_control_allowed_llm\"]>,\n    \"access_control_id\"\n  >[];\n  access_control_methods: Omit<\n    Required<DBSSchema[\"access_control_methods\"]>,\n    \"access_control_id\"\n  >[];\n};\n\nexport type EditedAccessRule = Omit<\n  AccessRule,\n  \"database_id\" | \"id\" | \"isApplied\"\n>;\n\nexport type AccessControlAction =\n  | {\n      type: \"create\";\n      selectedRuleId?: undefined;\n    }\n  | {\n      type: \"create-default\";\n      selectedRuleId?: undefined;\n    }\n  | {\n      type: \"edit\";\n      selectedRuleId: number;\n      tableName?: string;\n    };\n\nexport const AccessControl = (props: P) => {\n  const { workspaces, connection, rules, dbsConnection, database_config } =\n    useGetAccessRules(props.prgl);\n  const { className, prgl, action, setAction } = props;\n\n  if (!(prgl.dbs.access_control_user_types as any)?.subscribe) {\n    return (\n      <InfoRow className=\"f-0 h-fit\">\n        Must be admin to access this section{\" \"}\n      </InfoRow>\n    );\n  }\n  if (!connection || !dbsConnection || !database_config || !rules)\n    return <Loading />;\n\n  return (\n    <div className={\"flex-col f-1 \" + className}>\n      <div className=\"f-1 flex-row min-h-0 \">\n        {action ?\n          <AccessControlRuleEditor\n            {...props}\n            database_config={database_config}\n            action={action}\n            connection={connection}\n            dbsConnection={dbsConnection}\n            onCancel={() => {\n              setAction(undefined);\n            }}\n          />\n        : <FlexCol className=\"f-1 relative\">\n            <FlexRow>\n              <Btn\n                variant=\"filled\"\n                color=\"action\"\n                data-command=\"config.ac.create\"\n                onClick={() => {\n                  setAction({ type: \"create\" });\n                }}\n                iconPath={mdiPlus}\n              >\n                Create new rule\n              </Btn>\n              <UserSyncConfig {...props.prgl} />\n\n              <Btn\n                iconPath={mdiAccountCog}\n                variant=\"faded\"\n                asNavLink={true}\n                href={ROUTES.USERS}\n              >\n                Manage users\n              </Btn>\n            </FlexRow>\n\n            <AccessControlRules\n              workspaces={workspaces ?? []}\n              rules={rules}\n              prgl={prgl}\n              onSelect={(r) => {\n                setAction({ type: \"edit\", selectedRuleId: r.id });\n              }}\n            />\n          </FlexCol>\n        }\n      </div>\n    </div>\n  );\n};\n\nconst useGetAccessRules = (prgl: Prgl) => {\n  const { connectionId, dbs } = prgl;\n  const { data: appliedRules } = dbs.access_control_connections.useSubscribe({\n    connection_id: connectionId,\n  });\n  const { data: _rules } = dbs.access_control.useSubscribe(\n    {\n      database_id: prgl.databaseId,\n    },\n    ACCESS_CONTROL_SELECT,\n  );\n  const rules = _rules?.map((r) => ({\n    ...r,\n    isApplied: appliedRules?.some((ar) => ar.access_control_id === r.id),\n  }));\n  const { data: connection } = dbs.connections.useFindOne({ id: connectionId });\n  const { data: dbsConnection } = dbs.connections.useFindOne({\n    is_state_db: true,\n  });\n  const { data: database_config } = dbs.database_configs.useFindOne({\n    id: prgl.databaseId,\n  });\n  const { data: workspaces } = dbs.workspaces.useFind();\n\n  return { workspaces, connection, rules, dbsConnection, database_config };\n};\n\nexport const ACCESS_CONTROL_SELECT = {\n  select: {\n    \"*\": 1,\n    access_control_user_types: { ids: { $array_agg: [\"user_type\"] } },\n    published_methods: \"*\",\n    access_control_allowed_llm: \"*\",\n    access_control_methods: \"*\",\n  },\n} as const;\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/AccessControlRuleEditor.tsx",
    "content": "import type { PropsWithChildren } from \"react\";\nimport React, { useState } from \"react\";\n\nimport {\n  mdiAccount,\n  mdiArrowLeft,\n  mdiClose,\n  mdiTableAccount,\n  mdiTableLock,\n} from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport type {\n  ContextDataObject,\n  DBSSchema,\n  TableRulesErrors,\n} from \"@common/publishUtils\";\nimport { dataCommand } from \"../../Testing\";\nimport Btn from \"@components/Btn\";\nimport ButtonGroup from \"@components/ButtonGroup\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow, classOverride } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport Loading from \"@components/Loader/Loading\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport { PublishedMethods } from \"../W_Method/PublishedMethods\";\nimport type {\n  AccessControlAction,\n  AccessRule,\n  EditedAccessRule,\n} from \"./AccessControl\";\nimport { AccessRuleEditorFooter } from \"./AccessRuleEditorFooter\";\nimport { PAllTables } from \"./PermissionTypes/PAllTables\";\nimport { PCustomTables } from \"./PermissionTypes/PCustomTables\";\nimport { PRunSQL } from \"./PermissionTypes/PRunSQL\";\nimport { PublishedWorkspaceSelector } from \"./PublishedWorkspaceSelector\";\nimport { ComparablePGPolicies } from \"./RuleTypeControls/ComparablePGPolicies\";\nimport { UserStats } from \"./UserStats\";\nimport { UserTypeSelect } from \"./UserTypeSelect\";\nimport { useAccessControlSearchParams } from \"./useAccessControlSearchParams\";\nimport type { ValidEditedAccessRuleState } from \"./useEditedAccessRule\";\nimport { useEditedAccessRule } from \"./useEditedAccessRule\";\nimport { AskLLMAccessControl } from \"../AskLLM/Setup/AskLLMAccessControl\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\n\nconst ACCESS_TYPES = [\"Custom\", \"All views/tables\", \"Run SQL\"] as const;\nexport type PermissionEditProps = Pick<\n  UserGroupRuleEditorProps,\n  \"prgl\" | \"dbsConnection\"\n> & {\n  action: AccessControlAction;\n  contextData: ContextDataObject | undefined;\n  userTypes: string[];\n  editedRule: ValidEditedAccessRuleState | undefined;\n};\n\nexport type TableErrors = Record<string, TableRulesErrors> | string;\n\ntype UserGroupRuleEditorProps = Pick<CommonWindowProps, \"prgl\"> & {\n  action: AccessControlAction;\n  connection: DBSSchema[\"connections\"];\n  database_config: DBSSchema[\"database_configs\"];\n  dbsConnection: DBSSchema[\"connections\"];\n  onCancel: VoidFunction;\n};\n\nexport const AccessControlRuleEditor = ({\n  action,\n  database_config,\n  prgl,\n  dbsConnection,\n  onCancel,\n}: UserGroupRuleEditorProps) => {\n  const { db, dbs, dbsTables, dbsMethods, connection, tables } = prgl;\n  const editedRule = useEditedAccessRule({ action, prgl });\n  const { setAction } = useAccessControlSearchParams();\n  const [wspErrors, setWspErrors] = useState<string>();\n\n  const currentSQLUser: string | undefined = usePromise(\n    async () =>\n      await db.sql?.(`SELECT \"current_user\"()`, {}, { returnType: \"value\" }),\n    [db],\n  );\n  const type = editedRule?.type;\n  if (!editedRule) {\n    return <Loading />;\n  }\n  if (type === \"edit-not-found\") {\n    return (\n      <FlexCol className=\"f-1\">\n        <ErrorComponent\n          error={`Access rule id ${action.selectedRuleId} was not found`}\n        />\n        <Btn\n          iconPath={mdiArrowLeft}\n          color=\"action\"\n          variant=\"faded\"\n          onClick={() => {\n            setAction(undefined);\n          }}\n        >\n          Show all rules\n        </Btn>\n      </FlexCol>\n    );\n  }\n\n  const { userTypes, onChange, newRule, rule } = editedRule;\n  const title =\n    action.selectedRuleId ? `Edit rule`\n    : action.type === \"create-default\" ? \"Create default rule\"\n    : \"Create new rule\";\n  const { dbPermissions, dbsPermissions } = newRule ?? {};\n\n  const tablesWithRules = tables\n    .map((t) => {\n      const rule =\n        dbPermissions?.type === \"Custom\" ?\n          dbPermissions.customTables.find((ct) => ct.tableName === t.name)\n        : dbPermissions?.type === \"All views/tables\" ?\n          Object.fromEntries(dbPermissions.allowAllTables.map((r) => [r, true]))\n        : undefined;\n      return {\n        ...t,\n        rule,\n      };\n    })\n    .sort((a, b) => +!!b.rule - +!!a.rule || a.name.localeCompare(b.name));\n\n  const permEditorProps = {\n    action,\n    prgl,\n    tablesWithRules,\n    contextData: editedRule.contextData,\n    dbsConnection,\n    connection,\n    userTypes,\n    onChange: (newDBPerm: AccessRule[\"dbPermissions\"]) =>\n      onChange({ ...rule, dbPermissions: newDBPerm }),\n    rule,\n    editedRule,\n  };\n\n  return (\n    <FlexCol className={\"AccessRuleEditor f-1 gap-2 min-s-0 jc-none\"}>\n      <FlexRow className=\"bt b-color text-1\">\n        <div className=\"py-1 font-20 bold noselect f-1 ws-nowrap text-ellipsis\">\n          {title}\n        </div>\n        <Btn className=\"ml-auto f-0\" iconPath={mdiClose} onClick={onCancel} />\n      </FlexRow>\n      <ScrollFade className={\"flex-col gap-2 f-1 min-s-0 jc-none o-auto\"}>\n        <FlexRow>\n          <SectionHeader icon={mdiAccount} className=\" \">\n            Target user types\n          </SectionHeader>\n\n          <UserStats\n            theme={prgl.theme}\n            dbs={dbs}\n            dbsTables={dbsTables}\n            dbsMethods={dbsMethods}\n          />\n        </FlexRow>\n\n        <div className=\"pl-2\">\n          <UserTypeSelect\n            data-command={\"config.ac.edit.user\"}\n            connectionId={connection.id}\n            database_id={database_config.id}\n            userTypes={editedRule.userTypes}\n            fromEditedRule={editedRule.initialUserTypes}\n            dbs={dbs}\n            onChange={(newUserGroupNames) => {\n              editedRule.onChange({\n                access_control_user_types: [{ ids: newUserGroupNames }],\n              });\n            }}\n          />\n        </div>\n\n        <FlexCol\n          className={\n            \"_RuleTypeSection flex-col  \" +\n            (editedRule.userTypes.length || action.type === \"create-default\" ?\n              \"\"\n            : \" hidden \")\n          }\n        >\n          <SectionHeader icon={mdiTableAccount} className=\" mt-2 \">\n            Access type\n          </SectionHeader>\n          <div className=\"pl-2 mt-p5 \" {...dataCommand(\"config.ac.edit.type\")}>\n            <ButtonGroup\n              value={(newRule ?? rule)?.dbPermissions?.type}\n              options={ACCESS_TYPES}\n              onChange={(accessType) => {\n                let dbPermissions: EditedAccessRule[\"dbPermissions\"] = {\n                  type: \"Custom\",\n                  customTables: [],\n                };\n                const dbsPermissions: EditedAccessRule[\"dbsPermissions\"] = {\n                  ...rule?.dbsPermissions,\n                  createWorkspaces: true,\n                };\n                if (accessType === \"Run SQL\") {\n                  dbPermissions = { type: accessType, allowSQL: false };\n                } else if (accessType === \"All views/tables\") {\n                  dbPermissions = { type: accessType, allowAllTables: [] };\n                }\n\n                editedRule.onChange({\n                  dbPermissions,\n                  dbsPermissions,\n                });\n\n                setWspErrors(undefined);\n              }}\n            />\n          </div>\n          {dbPermissions && (\n            <div className=\"pl-2 \">\n              {dbPermissions.type === \"Run SQL\" ?\n                <FlexCol className=\"gap-p25\">\n                  <span>\n                    The selected user groups will be allowed to run SQL queries.\n                  </span>\n                  <span>\n                    This is the same level of access to the database as the\n                    current user{\" \"}\n                    <strong>{JSON.stringify(currentSQLUser)}</strong>\n                  </span>\n                </FlexCol>\n              : dbPermissions.type === \"All views/tables\" ?\n                \"Allow read and/or write access to specified tables\"\n              : \"Fine grained access control to specified tables\"}\n            </div>\n          )}\n        </FlexCol>\n\n        {dbPermissions && (\n          <FlexCol\n            className={\n              \"_DataSection pb-4 \" +\n              (dbPermissions.type === \"Custom\" ? \" f-1 \" : \" f-0 \")\n            }\n          >\n            {/* {dbPermissions.type !== \"Run SQL\" &&  <- no point. Must allow sharing dashboards with any user */}\n            <PublishedWorkspaceSelector\n              tables={tables}\n              className=\"mb-1\"\n              prgl={prgl}\n              dbsPermissions={dbsPermissions ?? null}\n              onSetError={(err) => {\n                setWspErrors(err);\n              }}\n              wspError={wspErrors}\n              dbPermissions={dbPermissions}\n              onChange={(newDbsPermissions) => {\n                onChange({\n                  dbsPermissions: {\n                    ...dbsPermissions,\n                    ...newDbsPermissions,\n                  },\n                });\n              }}\n              onChangeRule={(newRule) => {\n                onChange({\n                  ...rule,\n                  ...newRule,\n                } as any);\n              }}\n            />\n\n            {dbPermissions.type !== \"Run SQL\" && (\n              <SectionHeader icon={mdiTableLock}>Data access</SectionHeader>\n            )}\n            <div\n              className=\"pl-2 f-0 flex-col min-h-0 h-fit\"\n              style={{ maxHeight: \"min(60vh, 450px)\" }}\n              {...dataCommand(\"config.ac.edit.dataAccess\")}\n            >\n              {dbPermissions.type === \"Run SQL\" ?\n                <PRunSQL {...permEditorProps} dbPermissions={dbPermissions} />\n              : dbPermissions.type === \"All views/tables\" ?\n                <PAllTables\n                  {...permEditorProps}\n                  dbPermissions={dbPermissions}\n                />\n              : <PCustomTables\n                  {...permEditorProps}\n                  dbPermissions={dbPermissions}\n                  onChange={(newDBPerm) =>\n                    onChange({ ...rule, dbPermissions: newDBPerm })\n                  }\n                />\n              }\n              {dbPermissions.type !== \"Run SQL\" && rule && (\n                <ComparablePGPolicies prgl={prgl} rule={rule} />\n              )}\n            </div>\n\n            <PublishedMethods\n              className=\"my-2 PublishedMethods\"\n              prgl={prgl}\n              editedRule={editedRule}\n              accessRuleId={\n                action.type === \"edit\" ? action.selectedRuleId : undefined\n              }\n            />\n\n            <AskLLMAccessControl\n              {...prgl}\n              editedRule={editedRule}\n              accessRuleId={\n                action.type === \"edit\" ? action.selectedRuleId : undefined\n              }\n            />\n          </FlexCol>\n        )}\n      </ScrollFade>\n      {dbPermissions && (\n        <AccessRuleEditorFooter\n          editedRule={editedRule}\n          dbs={dbs}\n          database_id={database_config.id}\n          action={action}\n          onCancel={onCancel}\n          connectionId={connection.id}\n          error={wspErrors}\n        />\n      )}\n    </FlexCol>\n  );\n};\n\nexport const SectionHeader = ({\n  icon,\n  children,\n  className,\n  size,\n}: PropsWithChildren<{\n  icon?: string;\n  className?: string;\n  size?: \"small\";\n}>) => {\n  const isSmall = size === \"small\";\n  const content =\n    isSmall ?\n      <h4 className=\"m-0 p-0\">{children}</h4>\n    : <h3 className=\"m-0 p-0\">{children}</h3>;\n  return (\n    <div\n      className={classOverride(\n        `SectionHeader flex-row gap-1 ai-center ${isSmall ? \"font-16\" : \"\"}`,\n        className,\n      )}\n    >\n      {icon && <Icon path={icon} size={1} />}\n      {content}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/AccessRuleEditorFooter.tsx",
    "content": "import { isDefined, omitKeys } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { SuccessMessage } from \"@components/Animations\";\nimport type { BtnProps } from \"@components/Btn\";\nimport { ButtonBar } from \"@components/ButtonBar\";\nimport ClickCatch from \"@components/ClickCatch\";\nimport type { DBS } from \"../Dashboard/DBS\";\nimport type {\n  AccessControlAction,\n  AccessRule,\n  EditedAccessRule,\n} from \"./AccessControl\";\nimport { FlexCol } from \"@components/Flex\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport type { ValidEditedAccessRuleState } from \"./useEditedAccessRule\";\n\ntype P = {\n  onCancel: VoidFunction;\n  action: AccessControlAction;\n  dbs: DBS;\n  connectionId: string;\n  database_id: number;\n  error: any;\n  editedRule: ValidEditedAccessRuleState | undefined;\n};\nexport const AccessRuleEditorFooter = (props: P) => {\n  const {\n    dbs,\n    action,\n    database_id,\n    connectionId,\n    onCancel,\n    error: wspError,\n    editedRule,\n  } = props;\n\n  const [localError, setLocalError] = useState<any>();\n  const [success, setSuccess] = useState(\"\");\n\n  const { selectedRuleId } = action;\n\n  if (success) {\n    return (\n      <ClickCatch style={{ zIndex: 1 }}>\n        <SuccessMessage\n          message={success}\n          className=\"absolute-centered bg-color-0 rounded\"\n          style={{ padding: \"4em\" }}\n        />\n      </ClickCatch>\n    );\n  }\n  const { newRule, onChange, ruleWasEdited, type, ruleErrorMessage } =\n    editedRule ?? {};\n  const error = wspError || localError || ruleErrorMessage;\n\n  return (\n    <FlexCol className=\"AccessRuleEditorFooter\">\n      <ErrorComponent\n        error={error}\n        variant=\"outlined\"\n        autoScrollIntoView={false}\n      />\n      {newRule && onChange && type !== \"create\" && (\n        <ButtonBar\n          error={localError}\n          buttons={(\n            [\n              {\n                children: \"Cancel\",\n                className: \"mr-auto\",\n                variant: \"faded\",\n                \"data-command\": \"config.ac.cancel\",\n                onClick: onCancel,\n              },\n              selectedRuleId ?\n                ({\n                  children: \"Remove rule\",\n                  className: \"w-fit\",\n                  variant: \"faded\",\n                  \"data-command\": \"config.ac.removeRule\",\n                  color: \"danger\",\n                  clickConfirmation: {\n                    color: \"danger\",\n                    message: \"Are you sure you want to remove this rule?\",\n                    buttonText: \"Remove rule\",\n                  },\n                  onClickPromise: async () => {\n                    try {\n                      await dbs.access_control_user_types.delete({\n                        access_control_id: selectedRuleId,\n                      });\n                      await dbs.access_control.delete({ id: selectedRuleId });\n                      onChange({ ...newRule, access_control_user_types: [] });\n                      onCancel();\n                    } catch (error) {\n                      setLocalError(error);\n                    }\n                  },\n                } satisfies BtnProps<void>)\n              : undefined,\n              {\n                children: !selectedRuleId ? `Create rule` : `Update rule`,\n                variant: \"filled\",\n                color: \"action\",\n                disabledInfo:\n                  wspError ? \"Must fix errors\" : (\n                    ruleErrorMessage ||\n                    localError ||\n                    (ruleWasEdited ? undefined : \"Nothing to update\")\n                  ),\n                \"data-command\": \"config.ac.save\",\n                onClickPromise: async () => {\n                  try {\n                    await upsertRule({\n                      action,\n                      newRule,\n                      database_id,\n                      connectionId,\n                      dbs,\n                    });\n                    setLocalError(undefined);\n\n                    setSuccess(\n                      !selectedRuleId ? `Rule created!` : `Rule updated!`,\n                    );\n                    setTimeout(onCancel, 1000);\n                  } catch (e: any) {\n                    setLocalError(e);\n                  }\n                },\n              } satisfies BtnProps<void>,\n            ] as const\n          ).filter(isDefined)}\n        />\n      )}\n    </FlexCol>\n  );\n};\n\nconst upsertRule = async (\n  args: Pick<P, \"dbs\" | \"connectionId\" | \"action\" | \"database_id\"> & {\n    newRule: EditedAccessRule;\n  },\n) => {\n  const { action, newRule, dbs, connectionId, database_id } = args;\n  const {\n    access_control_user_types = [],\n    published_methods = [],\n    access_control_allowed_llm = [],\n    access_control_methods = [],\n  } = newRule;\n\n  const connection = await dbs.connections.findOne({ id: connectionId });\n  if (!connection || !connectionId) {\n    throw `Connection not found (id = ${connectionId})`;\n  } else {\n    const userGroupNames = access_control_user_types.flatMap((ids) => ids.ids);\n\n    const insertRelatedData = async (access_control_id: number) => {\n      await dbs.access_control_user_types.delete({ access_control_id });\n      if (userGroupNames.length) {\n        await dbs.access_control_user_types.insert(\n          userGroupNames.map((user_type) => ({ access_control_id, user_type })),\n        );\n      }\n\n      await dbs.access_control_methods.delete({ access_control_id });\n      if (published_methods.length) {\n        await dbs.access_control_methods.insert(\n          published_methods.map((m) => ({\n            access_control_id,\n            published_method_id: m.id,\n          })),\n        );\n      }\n\n      await dbs.access_control_allowed_llm.delete({ access_control_id });\n      if (access_control_allowed_llm.length) {\n        await dbs.access_control_allowed_llm.insert(\n          access_control_allowed_llm.map((m) => ({ ...m, access_control_id })),\n        );\n      }\n\n      await dbs.access_control_methods.delete({ access_control_id });\n      if (access_control_methods.length) {\n        await dbs.access_control_methods.insert(\n          access_control_methods.map((m) => ({ ...m, access_control_id })),\n        );\n      }\n    };\n\n    const newRuleWithoutSomeExtraKeys = omitKeys(newRule as AccessRule, [\n      \"access_control_user_types\",\n      \"database_id\",\n      \"published_methods\",\n      \"id\",\n      \"created\",\n      \"access_control_allowed_llm\",\n      \"access_control_methods\",\n    ]);\n\n    if (action.type === \"edit\") {\n      const { selectedRuleId } = action;\n      await dbs.access_control.update(\n        { id: selectedRuleId },\n        newRuleWithoutSomeExtraKeys,\n      );\n      await insertRelatedData(selectedRuleId);\n    } else {\n      const overLappingUserGroups = await dbs.access_control_user_types.find(\n        {\n          $existsJoined: { \"**.connections\": { id: connectionId } } as any,\n          user_type: { $in: userGroupNames },\n        },\n        {\n          select: { user_type: 1 },\n          returnType: \"values\",\n        },\n      );\n      if (overLappingUserGroups.length) {\n        throw `Cannot have rules with overlapping user group names: ${overLappingUserGroups.flat().join(\", \")}.\\nRemove these group names from this rule or from the other rules`;\n      } else {\n        const acontrol = await dbs.access_control.insert(\n          {\n            ...(newRuleWithoutSomeExtraKeys as AccessRule),\n            database_id,\n            access_control_connections: [\n              {\n                connection_id: connectionId,\n              },\n            ],\n          },\n          { returning: \"*\" },\n        );\n\n        await insertRelatedData(acontrol.id);\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/AccessRuleSummary.tsx",
    "content": "import { mdiTextBoxSearchOutline } from \"@mdi/js\";\nimport type { FieldFilter } from \"prostgles-types\";\nimport { isObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type { CustomTableRules } from \"@common/publishUtils\";\nimport Chip from \"@components/Chip\";\nimport { LabeledRow } from \"@components/LabeledRow\";\nimport type { EditedAccessRule } from \"./AccessControl\";\n\ntype TableRuleSummary = {\n  tableName: string;\n  s: string;\n  i: string;\n  d: string;\n  u: string;\n};\n\ntype P = {\n  rule: EditedAccessRule[\"dbPermissions\"];\n  className?: string;\n  style?: React.CSSProperties;\n};\nexport const ACCESS_RULE_METHODS = [\n  \"insert\",\n  \"select\",\n  \"update\",\n  \"delete\",\n] as const;\n\nexport const AccessRuleSummary = ({\n  rule: r,\n  style,\n  className = \"\",\n}: P): JSX.Element => {\n  const CLASSES = {\n    i: \"text-green\",\n    s: \"text-1p5\",\n    u: \"text-warning\",\n    d: \"text-danger\",\n  } as const;\n\n  const summarizeFieldFilter = (f: FieldFilter | undefined) => {\n    let fieldList = \"\";\n    if (f === \"*\") {\n      fieldList = \"*\";\n    } else if (Array.isArray(f)) {\n      fieldList = f.join(\", \");\n    } else if (isObject(f)) {\n      const fList = Object.keys(f).join(\", \");\n      fieldList = Object.values(f).some((v) => !v) ? `except ${fList}` : fList;\n    }\n    return fieldList;\n  };\n\n  if (r.type === \"Run SQL\") {\n    return (\n      <span className={\"AccessRuleSummary bold \" + className} style={style}>\n        Full database access\n      </span>\n    );\n  } else if (r.type === \"All views/tables\") {\n    const allowedMethods = ACCESS_RULE_METHODS.filter((v) =>\n      r.allowAllTables.includes(v),\n    );\n\n    return (\n      <div\n        className={\"AccessRuleSummary flex-row-wrap gap-p25 \" + className}\n        style={style}\n      >\n        <div className=\"flex-row gap-p25\">\n          {allowedMethods.map((a) => (\n            <div\n              key={a}\n              className={CLASSES[a[0] as any].replace(\"text\", \"bb-2 b\")}\n            >\n              {a}\n            </div>\n          ))}\n        </div>\n        actions allowed to\n        <div className=\"bold\">all tables/views</div>\n        within public schema of the database\n      </div>\n    );\n  } else if ((r as any).type === \"Custom\") {\n    const c: CustomTableRules[\"customTables\"] = r.customTables;\n\n    const tableRules: TableRuleSummary[] = c\n      .filter((t) => t.select || t.delete || t.insert || t.update)\n      .map((t) => {\n        return {\n          tableName: t.tableName,\n          // s: !t.select? \"\" : `${summarizeFieldFilter((t.select === true)? \"*\" : t.select? t.select.fields : undefined)} ${isObject(t.select) && t.select.forcedFilterDetailed? \" filtered\" : \" all\"}`,\n          // i: !t.insert? \"\" : `${summarizeFieldFilter((t.insert === true)? \"*\" : t.insert? t.insert.fields : undefined)}`,\n          // d: !t.delete? \"\" : `${isObject(t.delete) && t.delete.forcedFilterDetailed? \" filtered\" : \" all\"}`,\n          // u: !t.update? \"\" : `${summarizeFieldFilter((t.update === true)? \"*\" : t.update? t.update.fields : undefined)} ${isObject(t.update) && t.update.forcedFilterDetailed? \" filtered\" : \" all\"}`\n          s: !t.select ? \"\" : \"\\u25A0\",\n          u: !t.update ? \"\" : \"\\u25A0\",\n          i: !t.insert ? \"\" : \"\\u25A0\",\n          d: !t.delete ? \"\" : \"\\u25A0\",\n        };\n      });\n\n    const chipWrapper = (tableRules: TableRuleSummary[]) => (\n      <div\n        className=\"pl-1 pt-p5 flex-row-wrap gap-p5 o-auto no-scroll-bar\"\n        style={{\n          maxHeight: \"200px\",\n          overflow: \"auto\",\n        }}\n      >\n        {tableRules.map((r, i) => (\n          <Chip key={i}>\n            <div className=\"flex-row-wrap gap-p5\">\n              {r.tableName}\n              <div className=\"flex-row gap-p25 ai-center\">\n                {r.i && (\n                  <span title=\"Insert/add data\" className={CLASSES.i}>\n                    {r.i}\n                  </span>\n                )}\n                {r.s && (\n                  <span title=\"Select/view data\" className={CLASSES.s}>\n                    {r.s}\n                  </span>\n                )}\n                {r.u && (\n                  <span title=\"Update/edit data\" className={CLASSES.u}>\n                    {r.u}\n                  </span>\n                )}\n                {r.d && (\n                  <span title=\"Delete/remove data\" className={CLASSES.d}>\n                    {r.d}\n                  </span>\n                )}\n              </div>\n            </div>\n          </Chip>\n        ))}\n      </div>\n    );\n\n    return (\n      <LabeledRow\n        className={\"AccessRuleSummary \" + className}\n        style={style}\n        icon={mdiTextBoxSearchOutline}\n        label={\"Tables/views: (\" + tableRules.length + \")\"}\n        noContentWrapper={true}\n      >\n        {chipWrapper(tableRules)}\n      </LabeledRow>\n    );\n  }\n\n  return <></>;\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/ContextDataSelector.tsx",
    "content": "import { mdiCardAccountDetailsOutline, mdiClose } from \"@mdi/js\";\nimport React from \"react\";\nimport type { ContextValue } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { classOverride } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport type { FilterColumn } from \"../SmartFilter/smartFilterUtils\";\nimport type { ContextDataSchema } from \"./OptionControllers/FilterControl\";\n\ntype P = {\n  className?: string;\n  onChange: (contextValue?: ContextValue  ) => void;\n  value: ContextValue | undefined;\n  contextData: ContextDataSchema;\n  column: FilterColumn;\n};\n\nexport const ContextDataSelector = ({\n  className = \"\",\n  onChange,\n  value,\n  contextData,\n  column,\n}: P) => {\n  const ctxCols = contextData.flatMap((t) =>\n    t.columns\n      .filter((c) => c.tsDataType === column.tsDataType)\n      .map((c) => ({\n        id: t.name + \".\" + c.name,\n        tableName: t.name,\n        ...c,\n      })),\n  );\n  const valueId =\n    value ? `${value.objectName}.${value.objectPropertyName}` : undefined;\n\n  return (\n    <div\n      className={classOverride(\n        \"ContextDataSelector flex-row gap-0 ai-center \",\n        className,\n      )}\n    >\n      <Select\n        title=\"From session data\"\n        btnProps={\n          value ?\n            {\n              variant: \"default\",\n            }\n          : {\n              iconPath: mdiCardAccountDetailsOutline,\n              children: null,\n              color: \"action\",\n              variant: \"default\",\n            }\n        }\n        data-command=\"ContextDataSelector\"\n        value={valueId}\n        className={className}\n        iconPath=\"\"\n        style={{ maxHeight: \"unset\" }}\n        fullOptions={ctxCols.map((ctxCol) => ({\n          key: ctxCol.id,\n          label: `{{${ctxCol.tableName}.${ctxCol.name}}}`,\n          subLabel: ctxCol.data_type,\n        }))}\n        onChange={(id) => {\n          const ctxCol = ctxCols.find((c) => c.id === id);\n          if (!ctxCol) return;\n\n          onChange({\n            objectName: ctxCol.tableName,\n            objectPropertyName: ctxCol.name,\n          });\n        }}\n      />\n      {value && (\n        <Btn\n          iconPath={mdiClose}\n          title=\"Clear session value\"\n          onClick={() => {\n            onChange(undefined);\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/ContextFilter.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useEffect, useState } from \"react\";\nimport type { FilterType, DetailedFilter } from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\n\nexport const CONTEXT_FILTER_OPERANDS = [\n  \"=\",\n  \"<>\",\n  \">=\",\n  \"<=\",\n  \">\",\n  \"<\",\n] as const;\n\ntype ContextFilterVal = {\n  fieldName: string;\n  objectPropertyName: string;\n  type: (typeof CONTEXT_FILTER_OPERANDS)[number];\n};\n\ntype ContextFilterProps = {\n  columns: ValidatedColumnInfo[];\n  contextCols: ValidatedColumnInfo[];\n  onChange: (val: ContextFilterVal) => void;\n  filter?: ContextFilterVal;\n};\n\nexport const ContextFilter = ({\n  columns,\n  contextCols,\n  onChange,\n  filter,\n}: ContextFilterProps) => {\n  const [value, setValue] = useState<Partial<ContextFilterVal>>({\n    ...filter,\n    type: \"=\",\n  });\n  const col = columns.find((c) => c.name === value.fieldName);\n  const ctxCol = contextCols.find((c) => c.name === value.objectPropertyName);\n\n  useEffect(() => {\n    const isNewOrUpdated =\n      !filter || JSON.stringify(filter) !== JSON.stringify(value);\n    const finalFilter: ContextFilterVal | undefined =\n      isNewOrUpdated && col && ctxCol && col.tsDataType === ctxCol.tsDataType ?\n        (value as ContextFilterVal)\n      : undefined;\n    if (finalFilter) {\n      onChange(finalFilter);\n    }\n  }, [value, col, ctxCol, filter, onChange]);\n\n  return (\n    <div className=\"flex-col gap-1\">\n      <div className=\"flex-row gap-p5 ai-end\">\n        <Select\n          label=\"Column\"\n          value={value.fieldName}\n          fullOptions={columns\n            .filter((c) =>\n              contextCols.some((ctxCol) => ctxCol.tsDataType === c.tsDataType),\n            )\n            .map((c) => ({\n              key: c.name,\n              label: c.label,\n              subLabel: c.data_type,\n            }))}\n          onChange={(fieldName) => setValue({ ...value, fieldName })}\n        />\n        {!!value.fieldName && (\n          <>\n            <Select\n              options={CONTEXT_FILTER_OPERANDS}\n              value={value.type}\n              onChange={(type) => {\n                setValue({ ...value, type });\n              }}\n            />\n            <Select\n              label=\"User field\"\n              value={value.objectPropertyName}\n              fullOptions={contextCols.map((c) => ({\n                key: c.name,\n                label: `user.${c.label}`,\n                subLabel: c.data_type,\n              }))}\n              onChange={(objectPropertyName) => {\n                setValue({ ...value, objectPropertyName });\n              }}\n            />\n          </>\n        )}\n      </div>\n    </div>\n  );\n};\n\ntype AddContextFilterProps = Pick<\n  ContextFilterProps,\n  \"columns\" | \"contextCols\"\n> & {\n  onAdd: (newFilter: DetailedFilter) => void;\n};\n\nexport const AddContextFilter = ({\n  columns,\n  contextCols,\n  onAdd,\n}: AddContextFilterProps) => {\n  const [value, setValue] = useState<ContextFilterVal | void>();\n\n  if (!contextCols.length) return <>No context columns</>;\n\n  return (\n    <PopupMenu\n      button={\n        <Btn size=\"small\" className=\"shadow bg-color-0\" iconPath={mdiPlus}>\n          User filter\n        </Btn>\n      }\n      positioning=\"beneath-left\"\n      clickCatchStyle={{ opacity: 0 }}\n      render={(pClose) => (\n        <div className=\"flex-col gap-2\">\n          <ContextFilter\n            columns={columns}\n            contextCols={contextCols}\n            onChange={(f) => {\n              setValue(f);\n            }}\n          />\n\n          {!!value && (\n            <Btn\n              size=\"small\"\n              variant=\"filled\"\n              color=\"action\"\n              iconPath={mdiPlus}\n              onClick={() => {\n                onAdd({\n                  type: value.type as FilterType,\n                  fieldName: value.fieldName,\n                  contextValue: {\n                    objectName: \"user\",\n                    objectPropertyName: value.objectPropertyName,\n                  },\n                });\n                pClose();\n              }}\n            >\n              Add\n            </Btn>\n          )}\n        </div>\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/ExistingAccessRules.tsx",
    "content": "import React from \"react\";\nimport type { AccessRule } from \"./AccessControl\";\nimport Chip from \"@components/Chip\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { mdiAccount, mdiFunction, mdiViewCarousel } from \"@mdi/js\";\nimport { LabeledRow } from \"@components/LabeledRow\";\nimport { pluralise } from \"../../pages/Connections/Connection\";\nimport { AccessRuleSummary } from \"./AccessRuleSummary\";\nimport type { Workspace } from \"../Dashboard/dashboardUtils\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { Prgl } from \"../../App\";\nimport { FlexRow } from \"@components/Flex\";\n\ntype ExistingAccessRulesProps = {\n  onSelect: (rule: AccessRule) => void;\n  rules: AccessRule[];\n  workspaces: Workspace[];\n  prgl: Prgl;\n};\n\nexport const AccessControlRules = ({\n  rules,\n  onSelect,\n  workspaces,\n  prgl: { dbs, connectionId },\n}: ExistingAccessRulesProps) => {\n  const userTypesWithAccess = rules.flatMap((r) =>\n    r.access_control_user_types.flatMap((u) => u.ids),\n  );\n\n  return (\n    <div className=\"ExistingAccessRules \">\n      <InfoRow\n        variant=\"naked\"\n        color=\"info\"\n        style={{ alignItems: \"center\" }}\n        iconPath=\"\"\n      >\n        <p>\n          {!userTypesWithAccess.length ? \"Only users\" : \"Users\"} of type{\" \"}\n          <strong>\"admin\"</strong> have full access to this database.\n        </p>\n\n        {!!userTypesWithAccess.length && (\n          <p>\n            Users of {pluralise(userTypesWithAccess.length, \"type\")}{\" \"}\n            <strong>\n              {userTypesWithAccess.map((v) => JSON.stringify(v)).join(\", \")}\n            </strong>{\" \"}\n            can access according to the rules below:\n          </p>\n        )}\n      </InfoRow>\n\n      {!!rules.length && (\n        <>\n          <h3 className=\"m-0 mt-1 mb-1\">\n            Access rules {rules.length > 5 ? `(${rules.length})` : \"\"}\n          </h3>\n          <div className=\"flex-col gap-1 w-fit max-w-full\">\n            {rules.map((r, ri) => {\n              const publishedWorkspaceNames = workspaces\n                .filter((w) =>\n                  r.dbsPermissions?.viewPublishedWorkspaces?.workspaceIds.includes(\n                    w.id,\n                  ),\n                )\n                .map((w) => w.name);\n              const userTypes = r.access_control_user_types[0]?.ids;\n              return (\n                <div\n                  key={ri}\n                  className={\n                    \"ExistingAccessRules_Item flex-col active-shadow-hover gap-p5 pointer rounded p-p5 bg-color-0 shadow b b-color o-auto\"\n                  }\n                  data-key={userTypes}\n                  onClick={({ target }) => {\n                    if (\n                      target instanceof HTMLElement &&\n                      target.closest(\".SwitchToggle\")\n                    )\n                      return;\n                    onSelect(r);\n                  }}\n                >\n                  <FlexRow>\n                    <LabeledRow\n                      icon={mdiAccount}\n                      title=\"User types\"\n                      className=\"ExistingAccessRules_Item_Header ai-center f-1\"\n                    >\n                      <span className=\"text-0 font-20 bold\">\n                        {userTypes?.join(\", \")}\n                      </span>\n                    </LabeledRow>\n                    <SwitchToggle\n                      checked={!!r.isApplied}\n                      title={\n                        r.isApplied ? \"Click to disable\" : \"Click to enable\"\n                      }\n                      onChange={(isApplied, e) => {\n                        e.stopPropagation();\n                        e.preventDefault();\n                        if (isApplied) {\n                          dbs.access_control_connections.insert({\n                            connection_id: connectionId,\n                            access_control_id: r.id,\n                          });\n                        } else {\n                          dbs.access_control_connections.delete({\n                            connection_id: connectionId,\n                            access_control_id: r.id,\n                          });\n                        }\n                      }}\n                    />\n                  </FlexRow>\n\n                  <AccessRuleSummary rule={r.dbPermissions} />\n\n                  {!!publishedWorkspaceNames.length && (\n                    <LabeledRow\n                      icon={mdiViewCarousel}\n                      label=\"Workspaces\"\n                      labelStyle={{ minWidth: \"152px\" }}\n                      className=\"ai-center\"\n                      contentClassName=\"pl-1\"\n                      onClick={() => onSelect(r)}\n                    >\n                      {publishedWorkspaceNames.map((w, i) => (\n                        <Chip title=\"Published workspace\" color=\"blue\" key={i}>\n                          {w}\n                        </Chip>\n                      ))}\n                    </LabeledRow>\n                  )}\n\n                  {!!r.published_methods.length && (\n                    <LabeledRow\n                      icon={mdiFunction}\n                      label=\"Functions\"\n                      className=\"ai-center\"\n                      contentClassName=\"pl-1\"\n                      onClick={() => onSelect(r)}\n                    >\n                      <div className=\"flex-row-wrap gap-p5\">\n                        {r.published_methods.map((m, i) => (\n                          <Chip\n                            key={m.name}\n                            className=\"pointer\"\n                            title=\"Click to edit\"\n                            value={m.name}\n                          />\n                        ))}\n                      </div>\n                    </LabeledRow>\n                  )}\n                </div>\n              );\n            })}\n          </div>\n        </>\n      )}\n    </div>\n  );\n};\n\nexport const WspIconPath = mdiViewCarousel;\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/Methods/ArgumentDefinition.tsx",
    "content": "import { mdiClose } from \"@mdi/js\";\nimport React from \"react\";\nimport type { ArgDef } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport { ReferencesDefinition } from \"./ReferencesDefinition\";\n\nexport type ArgumentDefinitionProps = ArgDef & {\n  onChange: (newArg: ArgDef | undefined) => void;\n} & Pick<CommonWindowProps, \"tables\">;\nexport const ArgumentDefinition = (props: ArgumentDefinitionProps) => {\n  const { onChange, ...arg } = props;\n\n  return (\n    <div className=\"flex-row ai-start gap-p5\">\n      <FormField\n        label=\"Name\"\n        value={arg.name}\n        onChange={(name) => {\n          onChange({ ...arg, name });\n        }}\n      />\n      <FormField\n        label=\"Type\"\n        disabledInfo={arg.references ? \"Must remove reference\" : undefined}\n        options={[\"string\", \"number\", \"Date\"]}\n        value={arg.type}\n        onChange={(type) => {\n          onChange({ ...arg, type });\n        }}\n      />\n      <FormField\n        label=\"Default value\"\n        type=\"text\"\n        name={arg.name}\n        value={arg.defaultValue}\n        onChange={(defaultValue) => {\n          onChange({ ...arg, defaultValue });\n        }}\n      />\n      <FormField\n        label=\"Optional\"\n        type=\"checkbox\"\n        value={arg.optional}\n        onChange={(optional) => {\n          onChange({ ...arg, optional });\n        }}\n      />\n\n      <ReferencesDefinition {...props} />\n\n      <Btn\n        title=\"Remove argument\"\n        color=\"danger\"\n        variant=\"faded\"\n        className=\"show-on-parent-hover as-end\"\n        iconPath={mdiClose}\n        onClick={() => onChange(undefined)}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/Methods/MethodDefinition.tsx",
    "content": "import { mdiCodeJson } from \"@mdi/js\";\nimport React, { useMemo, useState } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Prgl } from \"../../../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { JSONBSchema } from \"@components/JSONBSchema/JSONBSchema\";\nimport { Section } from \"@components/Section\";\nimport { MethodDefinitionEditAsJson } from \"./MethodDefinitionEditAsJson\";\nimport { MethodFunctionDefinition } from \"./MethodFunctionDefinition\";\n\nexport type MethodDefinitionProps = {\n  onChange: (newMethod: MethodDefinitionProps[\"method\"]) => void;\n  method: Partial<DBSSchema[\"published_methods\"]>;\n  renderMode?: \"Code\";\n} & Pick<\n  Prgl,\n  | \"tables\"\n  | \"dbsTables\"\n  | \"db\"\n  | \"connectionId\"\n  | \"dbsMethods\"\n  | \"dbKey\"\n  | \"dbs\"\n>;\n\nexport const MethodDefinition = (props: MethodDefinitionProps) => {\n  const {\n    onChange,\n    method,\n    tables,\n    dbsTables,\n    db,\n    connectionId,\n    renderMode,\n    dbs,\n  } = props;\n\n  const [editAsJSON, seteditAsJSON] = useState(false);\n\n  const { methodArgsCol } = useMemo(() => {\n    const methodsTable = dbsTables.find((t) => t.name === \"published_methods\");\n    const methodArgsCol = methodsTable?.columns.find(\n      (c) => c.name === \"arguments\",\n    );\n\n    return {\n      methodArgsCol,\n    };\n  }, [dbsTables]);\n\n  const renderCode = renderMode === \"Code\";\n  const methodName = method.name;\n\n  const codeEditorNode = <MethodFunctionDefinition {...props} />;\n\n  const { data: clashingMethod } = dbs.published_methods.useFindOne({\n    ...(method.id && { id: { $ne: method.id } }),\n    name: methodName,\n    connection_id: connectionId,\n  });\n\n  if (renderCode) return codeEditorNode;\n\n  return (\n    <FlexCol className=\"MethodDefinition f-1 gap-p5\">\n      <div className=\"flex-row ai-center gap-1\">\n        <Btn\n          className=\"ml-auto\"\n          iconPath={mdiCodeJson}\n          color={editAsJSON ? \"action\" : undefined}\n          variant={editAsJSON ? \"filled\" : undefined}\n          onClick={() => {\n            seteditAsJSON(!editAsJSON);\n          }}\n        >\n          {!editAsJSON ? \"Edit as JSON\" : \"Edit as form\"}\n        </Btn>\n      </div>\n      {editAsJSON ?\n        <MethodDefinitionEditAsJson {...props} />\n      : <>\n          <FlexCol className=\"p-1\">\n            <FormField\n              id=\"function_name\"\n              label=\"Name\"\n              value={method.name}\n              error={clashingMethod ? \"Name already exists\" : undefined}\n              onChange={(name) => {\n                onChange({ ...method, name });\n              }}\n            />\n            <FormField\n              id=\"function_description\"\n              type=\"text\"\n              value={method.description}\n              label={\"Description\"}\n              optional={true}\n              onChange={(description) => onChange({ ...method, description })}\n            />\n          </FlexCol>\n          {method.name && (\n            <FlexCol className=\"flex-col gap-1 f-1\">\n              <JSONBSchema\n                className=\"mt-1\"\n                schema={methodArgsCol!.jsonbSchema!}\n                value={method.arguments}\n                onChange={(a) => {\n                  onChange({ ...method, arguments: a });\n                }}\n                db={db}\n                tables={tables}\n              />\n              <Section\n                className=\"f-1\"\n                title=\"Function definition\"\n                contentClassName=\"flex-col gap-1  f-1\"\n                open={true}\n              >\n                {codeEditorNode}\n              </Section>\n              <Section title=\"Result\" contentClassName=\"flex-col gap-1 p-1 f-1\">\n                <p className=\"ta-start m-0\">\n                  Any returned value will be shown as JSON to the client\n                </p>\n                <JSONBSchema\n                  className=\"mt-1\"\n                  schema={{\n                    title: \"Display table\",\n                    description:\n                      \"Table that will be displayed below controls and inputs\",\n                    optional: true,\n                    lookup: {\n                      type: \"schema\",\n                      object: \"table\",\n                    },\n                  }}\n                  value={method.outputTable}\n                  onChange={(outputTable) => {\n                    onChange({ ...method, outputTable: outputTable ?? null });\n                  }}\n                  db={db}\n                  tables={tables}\n                />\n              </Section>\n            </FlexCol>\n          )}\n        </>\n      }\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/Methods/MethodDefinitionEditAsJson.tsx",
    "content": "import { getJSONBSchemaAsJSONSchema } from \"prostgles-types\";\nimport React, { useCallback, useMemo } from \"react\";\nimport { CodeEditor, type LanguageConfig } from \"../../CodeEditor/CodeEditor\";\nimport type { MethodDefinitionProps } from \"./MethodDefinition\";\n\nexport const MethodDefinitionEditAsJson = (props: MethodDefinitionProps) => {\n  const { onChange, method, dbsTables } = props;\n\n  const { language, methodArgsCol } = useMemo(() => {\n    const methodsTable = dbsTables.find((t) => t.name === \"published_methods\");\n    const methodArgsCol = methodsTable?.columns.find(\n      (c) => c.name === \"arguments\",\n    );\n    const jsonSchema = getJSONBSchemaAsJSONSchema(\n      \"published_methods\",\n      \"arguments\",\n      {\n        type: {\n          ...methodsTable?.columns\n            // .filter((c) => [\"arguments\"].includes(c.name))\n            .filter((c) => ![\"id\"].includes(c.name))\n            .reduce(\n              (a, v) => ({\n                ...a,\n                [v.name]: { type: v.tsDataType, nullable: v.is_nullable },\n              }),\n              {},\n            ),\n          arguments: methodArgsCol?.jsonbSchema as any,\n        },\n      },\n    );\n    const jsonSchemas = [\n      {\n        id: \"published_methods\",\n        schema: jsonSchema,\n      },\n    ];\n    const language: LanguageConfig = {\n      lang: \"json\",\n      jsonSchemas,\n    };\n\n    return {\n      language,\n      methodArgsCol,\n    };\n  }, [dbsTables]);\n\n  const onCodeChange = useCallback(\n    (val: string) => {\n      try {\n        const newMethod = JSON.parse(val);\n        onChange(newMethod);\n      } catch (err) {\n        // console.error(err);\n      }\n    },\n    [onChange],\n  );\n\n  return (\n    <CodeEditor\n      style={{\n        minWidth: \"600px\",\n        minHeight: \"400px\",\n      }}\n      language={language}\n      value={JSON.stringify(method, null, 2)}\n      onChange={onCodeChange}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/Methods/MethodFunctionDefinition.tsx",
    "content": "import { isEqual } from \"prostgles-types\";\nimport React, { useCallback, useMemo, useRef } from \"react\";\nimport { CodeEditorWithSaveButton } from \"../../CodeEditor/CodeEditorWithSaveButton\";\nimport type { MethodDefinitionProps } from \"./MethodDefinition\";\nimport { useCodeEditorTsTypes } from \"./useCodeEditorTsTypes\";\nimport Loading from \"@components/Loader/Loading\";\n\nexport const MethodFunctionDefinition = (props: MethodDefinitionProps) => {\n  const {\n    onChange,\n    method,\n    tables,\n    connectionId,\n    dbsMethods,\n    dbKey,\n    renderMode,\n    dbs,\n  } = props;\n\n  const languageObj = useCodeEditorTsTypes({\n    connectionId,\n    dbsMethods,\n    dbKey,\n    method,\n    tables,\n    dbs,\n  });\n\n  const onSave = useCallbackDeep(\n    (run: string) => {\n      onChange({ ...method, run });\n    },\n    [method, onChange],\n  );\n\n  const options = useMemo(() => {\n    return {\n      glyphMargin: false,\n      padding: { top: 16, bottom: 0 },\n      lineNumbersMinChars: 4,\n    };\n  }, []);\n\n  const renderCode = renderMode === \"Code\";\n  if (!languageObj) {\n    return <Loading style={{ margin: \"4em\" }} />;\n  }\n  return (\n    <CodeEditorWithSaveButton\n      label={\n        renderCode ? undefined : (\n          \"Server-side TypeScript function triggered by a button press\"\n        )\n      }\n      language={languageObj}\n      value={method.run ?? \"\"}\n      options={options}\n      autoSave={!renderCode}\n      onSave={onSave}\n      codeEditorClassName={renderCode ? \"b-none\" : \"\"}\n    />\n  );\n};\n\nfunction useCallbackDeep<T extends (...args: any[]) => any>(\n  callback: T,\n  dependencies: any[],\n): T {\n  const ref = useRef<{\n    deps: any[];\n    cb: T;\n    wrapper: T;\n  }>();\n\n  if (!ref.current) {\n    ref.current = {\n      deps: dependencies,\n      cb: callback,\n      wrapper: callback,\n    };\n  }\n\n  // Update stored callback if it changes\n  ref.current.cb = callback;\n\n  const memoizedCallback = useCallback(\n    (...args: any[]) => ref.current!.cb(...args),\n    [ref],\n  );\n\n  if (!isEqual(dependencies, ref.current.deps)) {\n    ref.current.deps = dependencies;\n    ref.current.wrapper = memoizedCallback as T;\n  }\n\n  return ref.current.wrapper;\n}\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/Methods/ReferencesDefinition.tsx",
    "content": "import React, { useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport Popup from \"@components/Popup/Popup\";\nimport { Select } from \"@components/Select/Select\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { omitKeys } from \"prostgles-types\";\nimport type { ArgumentDefinitionProps } from \"./ArgumentDefinition\";\n\nexport const ReferencesDefinition = ({\n  onChange,\n  tables,\n  ...arg\n}: ArgumentDefinitionProps) => {\n  const [showRef, setShowRef] = useState(false);\n\n  const refTable = tables.find((t) => t.name === arg.references?.table);\n\n  const updateRef = (newRef: Partial<(typeof arg)[\"references\"]>) => {\n    if (!newRef) {\n      onChange(omitKeys(arg, [\"references\"]));\n    } else {\n      onChange({\n        ...arg,\n        references: { column: \"\", ...(arg.references || {}), ...newRef } as any,\n      });\n    }\n  };\n\n  return (\n    <>\n      <Btn\n        variant=\"faded\"\n        style={{ alignSelf: \"end\" }}\n        color={arg.references ? \"action\" : undefined}\n        onClick={() => {\n          setShowRef(true);\n        }}\n      >\n        References\n      </Btn>\n      {showRef && (\n        <Popup\n          onClickClose={false}\n          focusTrap={true}\n          contentClassName=\"flex-col gap-1 p-1\"\n          footerButtons={[\n            {\n              label: \"Remove reference\",\n              variant: \"faded\",\n              color: \"danger\",\n              onClick: () => {\n                onChange(omitKeys(arg, [\"references\"]));\n                setShowRef(false);\n              },\n            },\n            {\n              label: \"Done\",\n              color: \"action\",\n              variant: \"filled\",\n              onClick: () => {\n                setShowRef(false);\n              },\n            },\n          ]}\n        >\n          <Select\n            label=\"table\"\n            value={arg.references?.table}\n            options={tables.map((t) => t.name)}\n            onChange={(table) => {\n              updateRef({ table });\n            }}\n          />\n          {refTable && (\n            <Select\n              label={arg.references?.isFullRow ? \"Display column\" : \"column\"}\n              value={arg.references?.column || undefined}\n              fullOptions={\n                refTable.columns\n                  .filter((c) =>\n                    [\"string\", \"number\", \"Date\"].includes(c.tsDataType),\n                  )\n                  .map((t) => ({\n                    key: t.name,\n                    label: t.name,\n                    subLabel: t.data_type,\n                  }))\n              }\n              onChange={(column) => {\n                const colInfo = refTable.columns.find(\n                  (c) => c.name === column,\n                )!;\n                onChange({\n                  ...arg,\n                  type: colInfo.tsDataType as any,\n                  references: { ...arg.references!, column },\n                });\n              }}\n            />\n          )}\n          {!!arg.references?.table && (\n            <>\n              <SwitchToggle\n                label={{\n                  label: \"Include full row\",\n                  variant: \"normal\",\n                  info: (\n                    <>Entire row will be passed as the argument ({arg.name})</>\n                  ),\n                }}\n                className=\"mr-auto\"\n                checked={!!arg.references.isFullRow}\n                onChange={(isFullRow) => {\n                  updateRef({ isFullRow });\n                }}\n              />\n              {!!arg.references.isFullRow && (\n                <>\n                  <SwitchToggle\n                    label={{\n                      label: \"Show in row card actions\",\n                      variant: \"normal\",\n                      info: (\n                        <>\n                          After clicking on row view/edit the row panel footer\n                          actions will include this method\n                        </>\n                      ),\n                    }}\n                    className=\"mr-auto\"\n                    checked={!!arg.references.showInRowCard}\n                    onChange={(v) => {\n                      updateRef({ showInRowCard: v ? {} : undefined });\n                    }}\n                  />\n\n                  {!!arg.references.showInRowCard && (\n                    <FormField\n                      label=\"Action label\"\n                      value={arg.references.showInRowCard.actionLabel}\n                      type=\"text\"\n                      onChange={(actionLabel) =>\n                        updateRef({ showInRowCard: { actionLabel } })\n                      }\n                    />\n                  )}\n                </>\n              )}\n            </>\n          )}\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/Methods/useCodeEditorTsTypes.ts",
    "content": "import { useMemoDeep, usePromise } from \"prostgles-client\";\nimport { useRef } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { fixIndent } from \"../../../demo/scripts/sqlVideoDemo\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { LanguageConfig, TSLibrary } from \"../../CodeEditor/CodeEditor\";\nimport { dboLib, pgPromiseDb } from \"../../CodeEditor/monacoTsLibs\";\nimport type { MethodDefinitionProps } from \"./MethodDefinition\";\n\ntype Props = Pick<\n  MethodDefinitionProps,\n  \"dbsMethods\" | \"connectionId\" | \"dbKey\" | \"tables\" | \"dbs\"\n> & {\n  method: MethodDefinitionProps[\"method\"] | undefined;\n};\n\nlet nodeLibs: TSLibrary[] | undefined;\n\nexport const useCodeEditorTsTypes = (\n  props: Props,\n): LanguageConfig | undefined => {\n  const { connectionId, dbsMethods, dbKey, method, tables, dbs } = props;\n  const dbSchemaTypes = usePromise(async () => {\n    const dbSchemaTypes = await dbsMethods.getConnectionDBTypes?.(connectionId);\n    return dbSchemaTypes;\n  }, [dbsMethods, connectionId]);\n\n  /**\n   * Reduce re-renders\n   */\n  const newMethodId = useRef(\"new_method_\" + Date.now().toString());\n  const { id, arguments: args, description: desc } = method ?? {};\n  const methodOpts = useMemoDeep(() => {\n    if (!method) return undefined;\n    return {\n      id: id ?? newMethodId.current,\n      args,\n      desc,\n    };\n  }, [id, args, desc]);\n\n  const tsLibrariesAndModelName = usePromise(async () => {\n    if (dbSchemaTypes && dbsMethods.getNodeTypes && connectionId && dbKey) {\n      const methodTsLib =\n        methodOpts &&\n        (await fetchMethodDefinitionTypes({\n          dbs,\n          arguments: methodOpts.args ?? [],\n          description: methodOpts.desc ?? \"\",\n          tables,\n        }));\n      const libs = nodeLibs ?? (await dbsMethods.getNodeTypes());\n      nodeLibs = libs;\n      const tsLibraries: TSLibrary[] = [\n        ...libs.map((l) => ({\n          ...l,\n          filePath: `file://${l.filePath}`,\n        })),\n        /** Required to ensure dbo types work */\n        {\n          filePath: \"file:///node_modules/@types/dbo/index.d.ts\",\n          content: `declare global { ${dboLib} }; export {}`,\n        },\n        {\n          filePath: \"file:///pgPromiseDb.ts\",\n          content: pgPromiseDb,\n        },\n        {\n          filePath: \"file:///DBGeneratedSchema.ts\",\n          content: `declare global {   ${dbSchemaTypes} }; export {}`,\n        },\n        {\n          filePath: \"file:///node_modules/@types/ProstglesOnMount/index.d.ts\",\n          content: `declare global { \n          /**\n           * Function that will be called after the table is created and server started or schema changed\n           */\n          export type ProstglesOnMount = (args: { dbo: Required<DBOFullyTyped<DBGeneratedSchema>>; db: pgPromise.DB; }) => void | Promise<void>; \n        }; \n        export {} `,\n        },\n        methodTsLib,\n      ].filter(isDefined);\n      return {\n        tsLibraries,\n        /**\n         * Using the same name for onmount and method will result in all editors showing the same content (first value)\n         */\n        modelFileName:\n          methodOpts ?\n            `method_${connectionId}${methodOpts.id}`\n          : `onMount_${connectionId}`,\n      };\n    }\n  }, [dbsMethods, connectionId, dbKey, tables, dbs, dbSchemaTypes, methodOpts]);\n\n  if (!tsLibrariesAndModelName) return;\n\n  return {\n    lang: \"typescript\",\n    ...tsLibrariesAndModelName,\n  };\n};\n\ntype FetchMethodDefinitionTypesArgs = Pick<Props, \"tables\" | \"dbs\"> &\n  Pick<DBSSchema[\"published_methods\"], \"arguments\" | \"description\">;\nconst fetchMethodDefinitionTypes = async ({\n  tables,\n  arguments: args,\n  description,\n  dbs,\n}: FetchMethodDefinitionTypesArgs) => {\n  const userTypes = await dbs.user_types.find();\n  const argumentTypes = args.map((a) => {\n    let type: string = a.type;\n    if (a.type === \"Lookup\" && (a.lookup as any)) {\n      const refT = tables.find((t) => t.name === a.lookup.table);\n      if (refT) {\n        // TODO: fix type. Maybe add a LookupDefinition type?\n        //@ts-ignore\n        if (a.lookup.isFullRow) {\n          type = `{ ${refT.columns.map((c) => `${c.name}: ${c.tsDataType}`).join(\"; \")} }`;\n        } else {\n          const col = refT.columns.find((c) => c.name === a.lookup.column);\n          if (col) {\n            type = col.tsDataType;\n          }\n        }\n      }\n    }\n    return `    ${a.name}${a.optional ? \"?\" : \"\"}: ${type};\\n`;\n  });\n  const argumentType =\n    argumentTypes.length ? `{ \\n${argumentTypes.join(\"\")} \\n}` : \"never\";\n\n  const userTypesTs = userTypes.map((t) => JSON.stringify(t.id)).join(\" | \");\n  const tsMethodDef = fixIndent(`\n    /**\n     * Server-side function\n     * ${description}\n     */\n    type ProstglesMethod = (\n      args: ${argumentType},\n      ctx: {\n        db: pgPromise.DB;\n        dbo: DBOFullyTyped<DBGeneratedSchema>; \n        tables: any[];\n        user: { id: string; type: ${userTypesTs}; };\n        /**\n         * Call an MCP server tool\n         */\n        callMCPServerTool: (serverName: string, toolName: string, args?: any) => Promise<any>;\n      }\n    ) => Promise<any>`);\n\n  return {\n    filePath: `file:///ProstglesMethod.ts`,\n    content: tsMethodDef,\n  } satisfies TSLibrary;\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/OptionControllers/DynamicFields.tsx",
    "content": "import type {\n  ContextDataObject,\n  TableRules,\n  UpdateRule,\n} from \"@common/publishUtils\";\nimport { validateDynamicFields } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexRow } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport { mdiClose, mdiPlus, mdiTableFilter } from \"@mdi/js\";\nimport { useEffectAsync } from \"prostgles-client\";\nimport React, { useEffect, useState } from \"react\";\nimport type { TablePermissionControlsProps } from \"../TableRules/TablePermissionControls\";\nimport { FieldFilterControl } from \"./FieldFilterControl\";\nimport type { ContextDataSchema } from \"./FilterControl\";\nimport { FilterControl } from \"./FilterControl\";\n\ntype P = Pick<\n  Required<TablePermissionControlsProps>,\n  \"prgl\" | \"table\" | \"tableRules\"\n> & {\n  rule: TableRules[\"update\"];\n  onChange: (rule: UpdateRule) => void;\n  contextDataSchema: ContextDataSchema;\n  contextData: ContextDataObject;\n};\n\nexport const DynamicFields = ({\n  rule: r,\n  contextDataSchema,\n  table,\n  onChange,\n  prgl: { db, tables, methods },\n  contextData,\n}: P) => {\n  const rule: UpdateRule = r === true || !r ? { fields: \"*\" } : r;\n\n  const setValue = (\n    df: Required<UpdateRule>[\"dynamicFields\"][number] | undefined,\n    index: number | undefined,\n  ) => {\n    onChange({\n      ...rule,\n      dynamicFields:\n        df === undefined ? rule.dynamicFields?.filter((_, i) => i !== index)\n        : index === undefined ? [...(rule.dynamicFields ?? []), df]\n        : rule.dynamicFields?.map((_df, _i) => (_i === index ? df : _df)),\n    });\n  };\n\n  const [error, setError] = useState<any>();\n  useEffect(() => {\n    void (async () => {\n      const valid = await validateDynamicFields(\n        rule.dynamicFields,\n        db[table.name],\n        contextData,\n        table.columns.map((c) => c.name),\n      );\n      setError(valid.error);\n    })();\n  }, [rule.dynamicFields, contextData, db, table.columns, table.name]);\n\n  return (\n    <div className=\"DynamicFields flex-col gap-p5 min-s-0 f-0\">\n      <FlexRow>\n        <Label\n          label={\"Dynamic fields\"}\n          iconPath={mdiTableFilter}\n          info={`Any update targeting records matching these filters will be allowed to update the custom fields provided\\n\\nExample use case: allow updating message.content if user.id = message.user_id. \\n\\nBy default only message.seen can be updated`}\n          popupTitle={\"UPDATE dynamic fields rule\"}\n        />\n        <Btn\n          iconPath={mdiPlus}\n          variant=\"filled\"\n          color=\"action\"\n          size=\"small\"\n          onClick={() => {\n            setValue(\n              { fields: rule.fields, filterDetailed: { $and: [] } },\n              undefined,\n            );\n          }}\n        />\n      </FlexRow>\n      {!!error && (\n        <div\n          className=\"o-auto\"\n          style={{\n            flex: 1,\n            minWidth: 0,\n            maxWidth: \"80%\",\n            padding: \"2em\",\n          }}\n        >\n          <ErrorComponent error={error} />\n        </div>\n      )}\n      <div className=\"flex-col gap-p5 o-auto  f-1 min-s-0 p-1 ml-3\">\n        {rule.dynamicFields?.map(({ fields, filterDetailed }, i) => {\n          return (\n            <div\n              key={i}\n              className={\"p-1 shadow flex-row gap-p5 b b-color w-fit rounded \"}\n            >\n              <div className=\"flex-col gap-p5\">\n                <FieldFilterControl\n                  label=\"Fields\"\n                  columns={table.columns}\n                  value={fields}\n                  onChange={(newFields) => {\n                    setValue({ fields: newFields, filterDetailed }, i);\n                  }}\n                />\n                <FilterControl\n                  db={db}\n                  methods={methods}\n                  tableName={table.name}\n                  tables={tables}\n                  detailedFilter={filterDetailed as any}\n                  label={\"Filter\"}\n                  onChange={(newFilter) => {\n                    setValue(\n                      !newFilter ? undefined : (\n                        { fields, filterDetailed: newFilter }\n                      ),\n                      i,\n                    );\n                  }}\n                  onSetError={setError}\n                  contextData={contextDataSchema}\n                  containerClassname={\"\"}\n                />\n              </div>\n              <Btn\n                iconPath={mdiClose}\n                onClick={() => {\n                  setValue(undefined, i);\n                }}\n              ></Btn>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/OptionControllers/FieldFilterControl.tsx",
    "content": "import React, { useCallback, useEffect, useMemo } from \"react\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport { isObject, getKeys } from \"prostgles-types\";\nimport { Select } from \"@components/Select/Select\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport type { FieldFilter } from \"@common/publishUtils\";\nimport { mdiFilter } from \"@mdi/js\";\nimport { Label } from \"@components/Label\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\n\ntype FieldFilterControlProps = {\n  iconPath?: string;\n  label: string;\n  info?: React.ReactNode;\n  value: FieldFilter;\n  columns: ValidatedColumnInfo[];\n  onChange: (val: FieldFilterControlProps[\"value\"]) => any;\n  expectAtLeastOne?: boolean;\n  excluded?: {\n    fields?: string[];\n    message: string;\n  };\n  title?: React.ReactNode;\n};\n\nexport const FieldFilterControl = ({\n  value,\n  columns,\n  onChange,\n  label,\n  excluded,\n  info,\n  iconPath = mdiFilter,\n  expectAtLeastOne = false,\n  title,\n}: FieldFilterControlProps) => {\n  const fieldOpts = [\n    {\n      key: \"only these fields\",\n      label: \"Custom fields\",\n      [\"data-command\"]: \"FieldFilterControl.type.custom\",\n    },\n    {\n      key: \"all EXCEPT these fields\",\n      label: \"All fields except\",\n      [\"data-command\"]: \"FieldFilterControl.type.except\",\n    },\n    {\n      key: \"all fields\",\n      label: \"All fields\",\n      disabledInfo: excluded?.fields?.length ? excluded.message : undefined,\n    },\n  ] as const;\n\n  const isExcept = isObject(value) && Object.values(value).some((v) => !v);\n  const fieldOpt =\n    value === \"*\" ? fieldOpts[2].key\n    : isExcept ? fieldOpts[1].key\n    : fieldOpts[0].key;\n\n  const setFields = useCallback(\n    (fields: string[] | true, include: boolean) => {\n      const newFields =\n        fields === true ? \"*\" : (\n          fields\n            .filter((f) => columns.some((c) => c.name === f))\n            .reduce((a, v) => ({ ...a, [v]: include ? true : false }), {})\n        );\n\n      onChange(newFields);\n    },\n    [columns, onChange],\n  );\n\n  const fieldList = columns.map((c) => ({\n    key: c.name,\n    subLabel: c.udt_name,\n    disabledInfo:\n      excluded?.fields?.includes(c.name) ? excluded.message : undefined,\n  }));\n  const fields = useMemo(\n    () =>\n      Array.isArray(value) ? value\n      : isObject(value) ? getKeys(value)\n      : [],\n    [value],\n  );\n\n  useEffect(() => {\n    if (fieldList.some((f) => f.disabledInfo && fields.includes(f.key))) {\n      setFields(\n        fieldList.filter((f) => !f.disabledInfo).map((f) => f.key),\n        true,\n      );\n    }\n  }, [fields, fieldList, setFields]);\n\n  const error = getError(value, columns, expectAtLeastOne);\n\n  return (\n    <FlexCol className=\"gap-0\" data-command=\"FieldFilterControl\">\n      <FlexRow>\n        <Label\n          iconPath={iconPath}\n          label={label}\n          info={info}\n          popupTitle={title}\n        />\n        <Select\n          className=\"mr-p5\"\n          data-command=\"FieldFilterControl.type\"\n          fullOptions={fieldOpts}\n          value={fieldOpt}\n          onChange={(opt) => {\n            if (opt === \"all fields\") {\n              setFields(true, true);\n            } else {\n              setFields(\n                fields.length ? fields : columns.map((c) => c.name),\n                opt === fieldOpts[0].key,\n              );\n            }\n          }}\n        />\n      </FlexRow>\n      {(fieldOpt !== \"all fields\" || error) && (\n        <FlexRow\n          className=\"gap-0 p-1 ml-3\"\n          style={{ maxWidth: \"min(800px, 100vw)\" }}\n        >\n          {fieldOpt !== \"all fields\" && (\n            <Select\n              id=\"Select\"\n              data-command=\"FieldFilterControl.select\"\n              value={fields}\n              fullOptions={fieldList}\n              multiSelect={true}\n              onChange={(fields) => {\n                setFields(fields, !isExcept);\n              }}\n              variant=\"chips-lg\"\n            />\n          )}\n\n          {error && <ErrorComponent error={error} />}\n        </FlexRow>\n      )}\n    </FlexCol>\n  );\n};\n\nconst getError = (\n  v: FieldFilter,\n  columns: ValidatedColumnInfo[],\n  expectAtLeastOne = true,\n) => {\n  if (!v) return;\n  if (isObject(v)) {\n    const fields = v;\n    const fieldKeys = getKeys(fields);\n    if (!fieldKeys.length && expectAtLeastOne)\n      return \"Must select at least a field\";\n\n    const badCols = fieldKeys.filter(\n      (fieldName) => !columns.some((c) => c.name === fieldName),\n    );\n    if (badCols.length) {\n      return `Invalid/disallowed columns found: ${badCols}`;\n    }\n\n    const fieldVals = Object.values(fields);\n    if (\n      fieldVals.some((v) => v === true) &&\n      fieldVals.some((v) => v === false)\n    ) {\n      return \"Cannot combine included and excluded fields\";\n    }\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/OptionControllers/FilterControl.tsx",
    "content": "import { getSmartGroupFilter, type DetailedFilter } from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiCheckAll, mdiTableEye, mdiTableFilter } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client\";\nimport { usePromise } from \"prostgles-client\";\nimport {\n  omitKeys,\n  type MethodHandler,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { appTheme, useReactiveState } from \"../../../App\";\nimport { pluralise } from \"../../../pages/Connections/Connection\";\nimport { quickClone } from \"../../../utils/utils\";\nimport type { DBSchemaTablesWJoins } from \"../../Dashboard/dashboardUtils\";\nimport { RenderFilter } from \"../../RenderFilter\";\nimport SmartTable from \"../../SmartTable\";\n\nexport type ContextDataSchema = {\n  name: string;\n  columns: ValidatedColumnInfo[];\n}[];\n\nexport type SingleGroupFilter =\n  | { $and: DetailedFilter[] }\n  | { $or: DetailedFilter[] };\n\nexport type ForcedFilterControlProps = {\n  detailedFilter?: SingleGroupFilter;\n  db: DBHandlerClient;\n  methods: MethodHandler;\n  tables: DBSchemaTablesWJoins;\n  tableName: string;\n  onChange: (val?: SingleGroupFilter) => any;\n  contextData: ContextDataSchema;\n  iconPath?: string;\n  label: string;\n  info?: React.ReactNode;\n  title?: React.ReactNode;\n  containerClassname?: string;\n  onSetError: (error?: string) => void;\n  mode?: \"forcedFilter\" | \"checkFilter\";\n};\n\nconst OPTS = [\n  {\n    key: \"disabled\",\n    label: \"all data\",\n    [\"data-command\"]: \"ForcedFilterControl.type.disabled\",\n  },\n  {\n    key: \"enabled\",\n    label: \"filtered data\",\n    [\"data-command\"]: \"ForcedFilterControl.type.enabled\",\n  },\n] as const;\nconst OPTS_CHECK = [\n  {\n    key: \"disabled\",\n    label: \"Disabled\",\n    [\"data-command\"]: \"CheckFilterControl.type.disabled\",\n  },\n  {\n    key: \"enabled\",\n    label: \"Enabled\",\n    [\"data-command\"]: \"CheckFilterControl.type.enabled\",\n  },\n] as const;\n\nexport const FilterControl = (props: ForcedFilterControlProps) => {\n  const {\n    onChange: setF,\n    detailedFilter: _detailedFilter,\n    label,\n    info,\n    title,\n    containerClassname = \"  \",\n    tableName,\n    db,\n    mode = \"forcedFilter\",\n  } = props;\n  const { state: theme } = useReactiveState(appTheme);\n  const iconPath =\n    props.iconPath ?? (mode === \"checkFilter\" ? mdiCheckAll : mdiTableFilter);\n\n  const detailedFilter = quickClone(_detailedFilter);\n\n  const [o, setO] = useState<(typeof OPTS)[number][\"key\"]>(\n    detailedFilter ? \"enabled\" : \"disabled\",\n  );\n\n  useEffect(() => {\n    setO(detailedFilter ? \"enabled\" : \"disabled\");\n  }, [detailedFilter]);\n\n  const isAnd = detailedFilter && \"$and\" in detailedFilter;\n  const filters = useMemo(\n    () =>\n      !detailedFilter ? []\n      : isAnd ? detailedFilter.$and\n      : detailedFilter.$or,\n    [detailedFilter, isAnd],\n  );\n\n  const tableHandler = db[tableName];\n  const rowCount = usePromise(async () => {\n    const filter = getSmartGroupFilter(\n      filters,\n      undefined,\n      isAnd ? \"and\" : \"or\",\n    );\n    const rowCount = await tableHandler!.count!(filter);\n    return rowCount;\n  }, [tableHandler, filters, isAnd]);\n  const isCheck = mode === \"checkFilter\";\n  const ViewDataBtn =\n    isCheck ? null : (\n      <div className=\"ml-2  flex-row \">\n        <PopupMenu\n          title=\"Filtered records\"\n          contentStyle={{\n            padding: 0,\n            minHeight: \"200px\",\n          }}\n          className=\"ml-auto\"\n          positioning=\"center\"\n          button={\n            <Btn iconPath={mdiTableEye}>\n              {rowCount} {pluralise(+rowCount!, \"row\")}\n            </Btn>\n          }\n          render={(pClose) => (\n            <SmartTable\n              title={({ filteredRows, totalRows }) => (\n                <div className=\"flex-row ai-center gap-p25 ws-pre jc-center bg-color-2 p-1\">\n                  <div className=\"bold\">\n                    {filteredRows.toLocaleString()} records\n                  </div>\n                  {filters.length > 0 && (\n                    <>\n                      from a total of\n                      <div>{totalRows.toLocaleString()}</div>\n                    </>\n                  )}\n                </div>\n              )}\n              db={props.db}\n              methods={props.methods}\n              tableName={props.tableName}\n              tables={props.tables}\n              filterOperand={isAnd ? \"and\" : \"or\"}\n              filter={filters}\n              onFilterChange={(newFilter) => {\n                props.onChange({ $and: newFilter });\n              }}\n            />\n          )}\n        />\n      </div>\n    );\n\n  const compType = isCheck ? \"CheckFilterControl\" : \"ForcedFilterControl\";\n  return (\n    <FlexCol\n      className={`${compType} gap-p5`}\n      style={{ minWidth: \"500px\" }}\n      data-command={compType}\n    >\n      <FlexRowWrap>\n        <Label\n          iconPath={iconPath}\n          label={label}\n          info={info}\n          popupTitle={title}\n        />\n        <Select\n          data-command={`${compType}.type`}\n          fullOptions={isCheck ? OPTS_CHECK : OPTS}\n          value={o}\n          onChange={(o) => {\n            if (o === \"disabled\") {\n              setF(undefined);\n            } else {\n              setF({ $and: [] });\n            }\n          }}\n        />\n        {ViewDataBtn}\n      </FlexRowWrap>\n\n      {o !== \"disabled\" && (\n        <FlexRowWrap\n          className={\n            \"FilterList ai-center p-1 ml-3 pointer \" + containerClassname\n          }\n        >\n          <RenderFilter\n            {...omitKeys(props, [\"title\"])}\n            selectedColumns={undefined}\n            filter={detailedFilter}\n            itemName={mode === \"forcedFilter\" ? \"filter\" : \"condition\"}\n            mode=\"compact\"\n            onChange={setF}\n          />\n        </FlexRowWrap>\n      )}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/PasswordlessSetup.tsx",
    "content": "import React, { useState } from \"react\";\nimport type { ClientUser, ExtraProps } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { pageReload } from \"@components/Loader/Loading\";\n\nexport const PasswordlessSetup = ({ dbsMethods }: ExtraProps) => {\n  type NewUser = Partial<ClientUser & { passwordconfirm?: string }>;\n  const [{ username, password, passwordconfirm }, setNewUser] =\n    useState<NewUser>({});\n\n  const setU = (u: NewUser) => {\n    setNewUser((user) => ({ ...user, ...u }));\n  };\n  const issues =\n    !username ? \"Username not provided\"\n    : !password ? \"Password not provided\"\n    : !passwordconfirm ? \"Password confirmation not provided\"\n    : password !== passwordconfirm ? \"Passwords do not match\"\n    : undefined;\n\n  return (\n    <div className=\"flex-col gap-1 ai-center p-1\">\n      <div>Passwordless access</div>\n      <div>Only this device and browser can access the dashboard</div>\n      <div>\n        To add users and set access control must first create an admin user with\n        a password. This will disable passwordless access\n      </div>\n\n      <PopupMenu\n        title=\"Create admin user\"\n        positioning=\"top-center\"\n        button={\n          <Btn color=\"action\" variant=\"filled\">\n            Create admin user\n          </Btn>\n        }\n        render={(pClose) => (\n          <div className=\"flex-col gap-1\">\n            <FormField\n              id=\"username\"\n              label=\"Username\"\n              value={username ?? \"\"}\n              onChange={(username) => setU({ username })}\n            />\n            <FormField\n              id=\"new-password\"\n              autoComplete=\"off\"\n              label=\"Password\"\n              type=\"password\"\n              value={password ?? \"\"}\n              onChange={(password) => setU({ password })}\n            />\n            <FormField\n              id=\"confirm_password\"\n              autoComplete=\"false\"\n              label=\"Confirm password\"\n              type=\"password\"\n              value={passwordconfirm ?? \"\"}\n              onChange={(passwordconfirm) => setU({ passwordconfirm })}\n            />\n          </div>\n        )}\n        footerButtons={[\n          {\n            label: \"Create\",\n            disabledInfo: issues,\n            variant: \"filled\",\n            color: \"action\",\n            onClickPromise: async () => {\n              if (!username || !password) {\n                throw \"Username or Password missing\";\n              }\n              await dbsMethods.disablePasswordless!({ username, password });\n              pageReload(\"disablePasswordless\");\n            },\n          },\n        ]}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/PermissionTypes/PAllTables.tsx",
    "content": "import { getKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport {\n  getBasicPermissions,\n  TablePermissionControls,\n} from \"../TableRules/TablePermissionControls\";\nimport type { DBPermissionEditorProps } from \"./PCustomTables\";\nimport type { TableRules } from \"@common/publishUtils\";\n\nexport const PAllTables = ({\n  dbPermissions,\n  onChange,\n  contextData,\n  prgl,\n  userTypes,\n  tablesWithRules,\n}: DBPermissionEditorProps<\"All views/tables\">) => {\n  const { tables } = prgl;\n\n  const tableRules: TableRules = getBasicPermissions(\n    dbPermissions.allowAllTables.reduce((a, v) => ({ ...a, [v]: true }), {}),\n  );\n  return (\n    <div className=\"flex-col gap-1\">\n      <h4 className=\"my-1\">Allowed on {tables.length} tables:</h4>\n      <TablePermissionControls\n        prgl={prgl}\n        userTypes={userTypes}\n        contextData={contextData}\n        tablesWithRules={tablesWithRules}\n        errors={{}}\n        tableRules={tableRules}\n        onChange={(newRules) => {\n          let allowAllTables = [...dbPermissions.allowAllTables];\n          const newRule = getKeys(newRules)[0];\n\n          if (newRule && newRule !== \"subscribe\" && newRule !== \"sync\") {\n            allowAllTables =\n              !newRules[newRule] ?\n                allowAllTables.filter((v) => v !== newRule)\n              : Array.from(new Set(allowAllTables.concat(newRule)));\n          }\n          onChange({\n            type: \"All views/tables\",\n            allowAllTables,\n          });\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/PermissionTypes/PCustomTables.tsx",
    "content": "import { mdiFile, mdiTable, mdiTableEye } from \"@mdi/js\";\nimport { getKeys } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { TableRules } from \"@common/publishUtils\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { EditedAccessRule } from \"../AccessControl\";\nimport type { PermissionEditProps } from \"../AccessControlRuleEditor\";\nimport type { TableInfoWithRules } from \"../TableRules/TablePermissionControls\";\nimport { TablePermissionControls } from \"../TableRules/TablePermissionControls\";\n\ntype DBPermissionCustomTables<\n  T extends EditedAccessRule[\"dbPermissions\"][\"type\"],\n> = Extract<EditedAccessRule[\"dbPermissions\"], { type: T }>;\n\nexport type DBPermissionEditorProps<\n  T extends EditedAccessRule[\"dbPermissions\"][\"type\"],\n> = PermissionEditProps & {\n  dbPermissions: DBPermissionCustomTables<T>;\n  onChange: (n: DBPermissionCustomTables<T>) => void;\n  tablesWithRules: TableInfoWithRules[];\n};\n\nexport const PCustomTables = ({\n  dbPermissions,\n  onChange,\n  contextData,\n  prgl,\n  userTypes,\n  tablesWithRules,\n  editedRule,\n}: DBPermissionEditorProps<\"Custom\">) => {\n  const { tables } = prgl;\n  const [hideNoRules, setHideNoRules] = useState(false);\n\n  const tableRules = Object.fromEntries(\n    ([\"select\", \"insert\", \"update\", \"delete\"] as const).map((ruleType) => {\n      const allCustomTablesMatchRule =\n        dbPermissions.customTables.length > 0 &&\n        dbPermissions.customTables.some((ct) => {\n          const table = tables.find((t) => t.name === ct.tableName);\n          const ruleIsPossible =\n            table?.info.isView ? ruleType === \"select\" : true;\n          return !ruleIsPossible || ct[ruleType];\n        });\n      return [ruleType, allCustomTablesMatchRule];\n    }),\n  );\n\n  const [initialOrder, setInitialOrder] = useState(\n    tablesWithRules.map((t) => t.name),\n  );\n  return (\n    <FlexCol\n      className=\"PCustomTables gap-0 min-h-0 h-fit\"\n      style={{ maxHeight: \"60vh\" }}\n    >\n      <div className=\"f-1 jc-end ai-end flex-row\">\n        <SwitchToggle\n          label=\"Show allowed only\"\n          disabledInfo={\n            dbPermissions.customTables.length ?\n              undefined\n            : \"No allowed tables to show\"\n          }\n          className=\"flex-row ai-center gap-p5\"\n          style={{ flexDirection: \"row-reverse\" }}\n          checked={hideNoRules}\n          onChange={(v) => {\n            setHideNoRules(v);\n            setInitialOrder(tablesWithRules.map((t) => t.name));\n          }}\n        />\n      </div>\n      <div className=\"flex-row-wrap gap-p5 ai-center p-p5 py-1 \">\n        <div className=\"mr-auto bold text-2 noselect\">\n          Toggle All ({tables.length} tables)\n        </div>\n        <TablePermissionControls\n          prgl={prgl}\n          userTypes={userTypes}\n          contextData={contextData}\n          className=\" pr-2  mr-p25 \"\n          style={{\n            gap: \"2.5em\",\n          }}\n          errors={{}}\n          tableRules={tableRules}\n          tablesWithRules={[]}\n          onChange={(newTableRules) => {\n            const newDbPermissions = {\n              ...dbPermissions,\n            };\n            newDbPermissions.customTables = tables\n              .map((t) => {\n                const existingRule = newDbPermissions.customTables.find(\n                  (ct) => ct.tableName === t.name,\n                );\n                const tableRules = existingRule ?? { tableName: t.name };\n                const toggledRuleName = getKeys(newTableRules)[0]!;\n                if (t.info.isView && toggledRuleName !== \"select\") {\n                  // Ignore non-select rule for views\n                } else {\n                  tableRules[toggledRuleName] =\n                    newTableRules[toggledRuleName] ? true : undefined;\n                }\n                return tableRules;\n              })\n              .filter((t) => t.select || t.insert || t.update || t.delete);\n\n            onChange(newDbPermissions);\n          }}\n        />\n      </div>\n      <SearchList\n        id=\"custom-tables\"\n        className=\"shadow\"\n        placeholder={`Search ${tables.length} tables & views`}\n        limit={200}\n        items={tablesWithRules\n          .filter((t) => {\n            if (!hideNoRules || t.info.isFileTable) return true;\n            return (\n              t.rule &&\n              [\"select\", \"insert\", \"update\", \"delete\"].some((ruleType) => {\n                return t.rule?.[ruleType];\n              })\n            );\n          })\n          .toSorted(\n            (a, b) =>\n              initialOrder.indexOf(a.name) - initialOrder.indexOf(b.name),\n          )\n          .map((t) => {\n            const existingRule = dbPermissions.customTables.find(\n              (ct) => ct.tableName === t.name,\n            );\n            const tableRules: TableRules & { tableName: string } =\n              existingRule ?? { tableName: t.name };\n\n            const setTableRules = (rules?: TableRules) => {\n              if (rules) {\n                getKeys(rules).forEach((key) => {\n                  if (key === \"select\") {\n                    tableRules[key] = rules[key];\n                  } else if (key === \"insert\") {\n                    tableRules[key] = rules[key];\n                  } else if (key === \"delete\") {\n                    tableRules[key] = rules[key];\n                  } else if (key !== \"sync\" && key !== \"subscribe\") {\n                    tableRules[key] = rules[key];\n                  }\n                });\n                const newDbPermissions = {\n                  ...dbPermissions,\n                  customTables: dbPermissions.customTables,\n                };\n                if (!existingRule) {\n                  newDbPermissions.customTables.push(tableRules);\n                } else {\n                  newDbPermissions.customTables =\n                    newDbPermissions.customTables.map((ct) =>\n                      ct.tableName === t.name ? tableRules : ct,\n                    );\n                }\n\n                onChange(newDbPermissions);\n              }\n            };\n\n            const icon =\n              t.info.isFileTable ? { path: mdiFile, title: \"File table\" }\n              : t.info.isView ? { title: \"View\", path: mdiTableEye }\n              : { title: \"Table\", path: mdiTable };\n            const isNotFromWorkspaceTables =\n              !t.info.isFileTable &&\n              !editedRule?.newRule?.dbsPermissions?.createWorkspaces &&\n              editedRule?.worspaceTableAndColumns?.length &&\n              !editedRule.worspaceTableAndColumns.some(\n                (wt) => wt.tableName === t.name,\n              );\n            return {\n              key: t.name,\n              styles: {\n                labelWrapper: {\n                  fontWeight: 500,\n                  minWidth: \"60px\",\n                },\n                rowInner:\n                  window.isLowWidthScreen ?\n                    {\n                      flexDirection: \"column\",\n                      gap: \"1em\",\n                      alignItems: \"start\",\n                      overflow: \"auto\",\n                    }\n                  : {},\n              },\n              disabledInfo:\n                isNotFromWorkspaceTables ?\n                  \"Cannot allow non workspace tables\"\n                : undefined,\n              title: t.name,\n              rowStyle: { border: \"1px solid var(--b-default)\" },\n              contentLeft: (\n                <Icon\n                  className=\"mr-p5 text-2\"\n                  title={icon.title}\n                  path={icon.path}\n                />\n              ),\n              contentRight: (\n                <TablePermissionControls\n                  key={t.name}\n                  className={window.isLowWidthScreen ? \"\" : \"ml-1\"}\n                  prgl={prgl}\n                  userTypes={userTypes}\n                  contextData={contextData}\n                  errors={{}}\n                  table={t}\n                  tableRules={tableRules}\n                  tablesWithRules={tablesWithRules}\n                  onChange={(val) => {\n                    setTableRules(val);\n                  }}\n                />\n              ),\n            };\n          })}\n      />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/PermissionTypes/PRunSQL.tsx",
    "content": "import { InfoRow } from \"@components/InfoRow\";\nimport { LabeledRow } from \"@components/LabeledRow\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { usePromise } from \"prostgles-client\";\nimport React from \"react\";\nimport { areEqual } from \"../../../utils/utils\";\nimport type { DBPermissionEditorProps } from \"./PCustomTables\";\n\nexport const PRunSQL = ({\n  dbPermissions,\n  onChange,\n  dbsConnection,\n  prgl: { connection, db },\n}: DBPermissionEditorProps<\"Run SQL\">) => {\n  const roleInfo = usePromise(async () => {\n    return (await db.sql!(\n      `\n      SELECT r.rolname, r.rolsuper, r.rolinherit, r.rolcreaterole, r.rolcreatedb, r.rolcanlogin, r.rolconnlimit, r.rolvaliduntil, r.rolreplication, r.rolbypassrls\n      FROM pg_catalog.pg_roles r\n      WHERE r.rolname = current_user\n      `,\n      {},\n      { returnType: \"row\" },\n    )) as {\n      rolname: string;\n      rolsuper: boolean;\n      rolinherit: boolean;\n      rolcreaterole: boolean;\n      rolcreatedb: boolean;\n      rolcanlogin: boolean;\n      rolconnlimit: number;\n      rolvaliduntil: Date | null;\n      rolreplication: boolean;\n      rolbypassrls: boolean;\n    };\n  });\n\n  const superAccessToSameServerAsDBS = areEqual(connection, dbsConnection, [\n    \"db_host\",\n    \"db_port\",\n  ]);\n\n  return (\n    <>\n      <div className=\"PRunSQL my-1 ws-pre flex-col\">\n        <div className=\"flex-col gap-p5  my-2d\">\n          {roleInfo?.rolsuper && superAccessToSameServerAsDBS ?\n            <InfoRow color=\"warning\">\n              This rule will allow <strong>superuser</strong> access to state\n              database server{\" \"}\n              <strong>\n                {dbsConnection.db_host}:{dbsConnection.db_port}\n              </strong>\n              <br></br>\n              <br></br>\n              User may update their privilege to the same level of access as a\n              type <strong>\"admin\"</strong> user\n            </InfoRow>\n          : roleInfo?.rolsuper ?\n            <InfoRow color=\"warning\">\n              Superuser access (Bypasses all permission checks, except the right\n              to log in. This is a dangerous privilege and should not be used\n              carelessly)\n            </InfoRow>\n          : <LabeledRow label=\"PG Role Attributes\" className=\"ws-wrap\">\n              {[\n                roleInfo?.rolsuper && \"Superuser\",\n                roleInfo?.rolcreatedb && \"CreateDB\",\n                roleInfo?.rolcreaterole && \"CreateRole\",\n                roleInfo?.rolinherit && \"Inherit\",\n                roleInfo?.rolbypassrls && \"BypassRLS\",\n                roleInfo?.rolcanlogin && \"Login\",\n                roleInfo?.rolvaliduntil &&\n                  \"Valid Until\" + roleInfo.rolvaliduntil.toISOString(),\n              ].filter((v) => v)}\n            </LabeledRow>\n          }\n        </div>\n      </div>\n      <SwitchToggle\n        label=\"Run SQL\"\n        checked={!!dbPermissions.allowSQL}\n        onChange={(allowSQL) => {\n          onChange({\n            type: \"Run SQL\",\n            allowSQL,\n          });\n        }}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/PublishedWorkspaceSelector.tsx",
    "content": "import { mdiViewQuilt } from \"@mdi/js\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useEffect } from \"react\";\nimport { FlexCol } from \"@components/Flex\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { isDefined } from \"../../utils/utils\";\nimport type { DBS } from \"../Dashboard/DBS\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type {\n  DBSchemaTablesWJoins,\n  WindowData,\n} from \"../Dashboard/dashboardUtils\";\nimport type { DeepPartial } from \"../RTComp\";\nimport { SmartSelect } from \"../SmartSelect\";\nimport type { AccessRule } from \"./AccessControl\";\nimport { SectionHeader } from \"./AccessControlRuleEditor\";\nimport { useIsMounted } from \"prostgles-client\";\n\ntype P = Pick<CommonWindowProps, \"prgl\"> & {\n  dbsPermissions: DeepPartial<AccessRule[\"dbsPermissions\"]>;\n  dbPermissions: DeepPartial<AccessRule[\"dbPermissions\"]>;\n  onChange: (dbsPermissions: AccessRule[\"dbsPermissions\"]) => void;\n  onChangeRule: (ruleDelta: DeepPartial<AccessRule>) => void;\n  className?: string;\n  style?: React.CSSProperties;\n  wspError: string | undefined;\n  onSetError: (error?: string) => void;\n  tables: DBSchemaTablesWJoins;\n};\n\nexport const PublishedWorkspaceSelector = ({\n  prgl: { dbs, connectionId },\n  dbsPermissions,\n  onChange,\n  onChangeRule,\n  dbPermissions,\n  className = \"\",\n  style,\n  onSetError,\n  tables,\n}: P) => {\n  const { data: publishedWorkspaces } = dbs.workspaces.useSubscribe(\n    { published: true, connection_id: connectionId },\n    {},\n  );\n  const getIsMounted = useIsMounted();\n\n  useEffect(() => {\n    void (async () => {\n      let wspErrors: string | undefined = undefined;\n      const workspaceIds =\n        dbsPermissions?.viewPublishedWorkspaces?.workspaceIds ?? [];\n      if (workspaceIds.length) {\n        const wsps = await dbs.workspaces.find({ \"id.$in\": workspaceIds });\n        if (!getIsMounted()) return;\n        const missingWorkspaceIds = workspaceIds.filter(\n          (wid) => !wsps.some((w) => w.id === wid),\n        );\n        if (missingWorkspaceIds.length) {\n          onSetError(\n            `${missingWorkspaceIds.length} Published workspaces not found: \\n  ${missingWorkspaceIds.join(\"\\n \")}`,\n          );\n          return;\n        }\n        const { msg } = await getWorkspaceTables(\n          dbs,\n          workspaceIds,\n          dbPermissions,\n          tables,\n        );\n        if (!getIsMounted()) return;\n        if (msg) {\n          wspErrors = msg;\n        }\n      }\n\n      if (\n        !wspErrors &&\n        !dbsPermissions?.createWorkspaces &&\n        !dbsPermissions?.viewPublishedWorkspaces?.workspaceIds?.length\n      ) {\n        wspErrors =\n          \"Must allow 'Create workspaces' or select at least one workspace within 'Access published workspaces' \";\n      }\n\n      onSetError(wspErrors);\n    })();\n  }, [dbPermissions, dbs, dbsPermissions, getIsMounted, onSetError, tables]);\n\n  /**\n   * A user must be allowed to:\n   *  - View a published workspace (selected?.length > 0)\n   *  AND/OR\n   *  - Create own workspaces (dbsPermissions?.createWorkspaces === true)\n   */\n  const selected = dbsPermissions?.viewPublishedWorkspaces?.workspaceIds;\n  const showWorkspaceSelect = !!(\n    publishedWorkspaces?.length || selected?.length\n  );\n  const showCreateWorkspaces =\n    !selected?.length || !dbsPermissions?.createWorkspaces;\n\n  if ((!showWorkspaceSelect && !showCreateWorkspaces) || !publishedWorkspaces) {\n    return null;\n  }\n\n  const workspaceOptions = publishedWorkspaces.map((w) => ({\n    key: w.id,\n    label: w.name,\n  }));\n  selected?.map((key) => {\n    if (!workspaceOptions.some((w) => w.key === key)) {\n      workspaceOptions.push({\n        key,\n        label: `Deleted - ${key}`,\n      });\n    }\n  });\n\n  return (\n    <FlexCol\n      className={\"PublishedWorkspaceSelector \" + className}\n      style={style}\n    >\n      <SectionHeader icon={mdiViewQuilt} className=\"mb-p5\">\n        Workspace access\n      </SectionHeader>\n\n      <div className=\"relative bg-color-0 rounded flex-row gap-2 ml-2 \">\n        <SwitchToggle\n          variant=\"col\"\n          data-command=\"config.ac.edit.createWorkspaces\"\n          checked={!!dbsPermissions?.createWorkspaces}\n          onChange={(createWorkspaces) => {\n            onChange({ createWorkspaces });\n          }}\n          disabledInfo={\n            !publishedWorkspaces.length ?\n              \"Cannot change if no published workspaces\"\n            : undefined\n          }\n          label={{\n            variant: \"normal\",\n            label: \"Create own workspaces\",\n            info: \"Grants the user the ability to create, view and edit own workspaces/dashboards. If disabled then only the published workspaces are accessible to the user\",\n          }}\n        />\n\n        <SmartSelect\n          label={{\n            label: \"Published workspaces\",\n            info: `Limits access to only the tables/views found in the chosen workspaces. Must publish workspaces`,\n            variant: \"normal\",\n          }}\n          data-command=\"config.ac.edit.publishedWorkspaces\"\n          disabledInfo={\n            !publishedWorkspaces.length ? \"No published workspaces\" : undefined\n          }\n          tableHandler={dbs.workspaces as any}\n          filter={{ published: true, connection_id: connectionId }}\n          allowCreate={false}\n          fieldName=\"id\"\n          displayField=\"name\"\n          values={selected ?? []}\n          onChange={async (workspaceIds) => {\n            const newDbsRule: AccessRule[\"dbsPermissions\"] = {\n              ...dbsPermissions,\n              viewPublishedWorkspaces: { workspaceIds },\n            };\n            /** Toggle missing tables if required */\n            const { dbPermissionsCorrected } = await getWorkspaceTables(\n              dbs,\n              workspaceIds,\n              dbPermissions,\n              tables,\n            );\n            if (dbPermissionsCorrected) {\n              onChangeRule({\n                dbPermissions: dbPermissionsCorrected,\n                dbsPermissions: newDbsRule,\n              });\n            } else {\n              onChange(newDbsRule);\n            }\n          }}\n        />\n      </div>\n    </FlexCol>\n  );\n};\n\nexport type WorspaceTableAndColumns = {\n  tableName: string;\n  columns: ValidatedColumnInfo[];\n};\nexport const getWorkspaceTables = async (\n  dbs: DBS,\n  workspaceIds: string[],\n  dbPermissions: DeepPartial<AccessRule[\"dbPermissions\"]>,\n  tables: DBSchemaTablesWJoins,\n) => {\n  if (!workspaceIds.length) {\n    return { msg: undefined, missingTables: undefined };\n  }\n  let missingWindowTables: WindowData<\"table\">[] = [];\n  let dbPermissionsCorrected = { ...dbPermissions };\n  const worspaceTableAndColumns: WorspaceTableAndColumns[] = [];\n  if (dbPermissions.type === \"Custom\") {\n    const workspaceWindows = await dbs.windows.find({\n      \"workspace_id.$in\": workspaceIds,\n    });\n    const tableWindows = workspaceWindows.filter(\n      (w) => w.type === \"table\" && !!w.table_name,\n    ) as WindowData<\"table\">[];\n    const { customTables } = dbPermissions;\n    const _missingWindowTables = tableWindows\n      .map((w) => {\n        const table = tables.find((t) => t.name === w.table_name);\n        const tableWindowColumns = table?.columns.filter((tc) =>\n          w.columns?.some(\n            (c) =>\n              tc.name === c.name &&\n              c.show &&\n              (!c.computedConfig ||\n                c.computedConfig.isColumn ||\n                tc.name === c.computedConfig.column),\n          ),\n        );\n        if (!table || !tableWindowColumns) {\n          return undefined;\n        }\n        worspaceTableAndColumns.push({\n          tableName: table.name,\n          columns: tableWindowColumns,\n        });\n        return {\n          ...w,\n          table,\n          tableWindowColumns,\n        };\n      })\n      .filter(\n        (t) => !customTables?.some((ct) => t && ct.tableName === t.table_name),\n      );\n    const validMissingWindowTables = _missingWindowTables.filter(isDefined);\n    missingWindowTables = validMissingWindowTables;\n    const missingTableNames = validMissingWindowTables.map((t) => t.table_name);\n    const invalidMissingTables = _missingWindowTables.filter((t) => !t?.table);\n    if (invalidMissingTables.length) {\n      return {\n        msg: `Workspace contains invalid tables: ${invalidMissingTables.map((t) => t?.table_name)}`,\n      };\n    }\n\n    dbPermissionsCorrected = {\n      type: \"Custom\",\n      customTables: validMissingWindowTables\n        .map(({ table_name, tableWindowColumns, table }) => {\n          const windowFields = tableWindowColumns.map((c) => c.name);\n          const fields =\n            windowFields.length === table.columns.length ?\n              (\"*\" as const)\n            : windowFields;\n          return {\n            tableName: table_name,\n            select: { fields, filterFields: fields },\n            update: { fields, filterFields: fields },\n            insert: { fields },\n            delete: { filterFields: fields },\n          };\n        })\n        .concat(\n          customTables?.filter(\n            (ct) => !missingTableNames.includes(ct.tableName),\n          ) ?? ([] as any),\n        ),\n    };\n  }\n  if (missingWindowTables.length) {\n    const missingTableNames = missingWindowTables.map((t) => t.name);\n    return {\n      msg: `Must allow SELECT on all tables from the published workspaces. Missing tables: ${missingTableNames.join(\", \")}`,\n      missingTables: missingTableNames,\n      dbPermissionsCorrected,\n      worspaceTableAndColumns,\n    };\n  }\n\n  return { msg: undefined, missingTables: undefined, worspaceTableAndColumns };\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/ComparablePGPolicies.tsx",
    "content": "import { usePromise } from \"prostgles-client\";\nimport React from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport CodeExample from \"../../CodeExample\";\nimport type { EditedAccessRule } from \"../AccessControl\";\nimport { ACCESS_RULE_METHODS } from \"../AccessRuleSummary\";\nimport { getComparablePGPolicy } from \"./getComparablePGPolicy\";\nimport { fixIndent } from \"../../../demo/scripts/sqlVideoDemo\";\n\nexport const ComparablePGPolicies = ({\n  prgl,\n  rule,\n}: {\n  prgl: Prgl;\n  rule: EditedAccessRule;\n}) => {\n  const policies = usePromise(async () => {\n    const { tables } = prgl;\n    const userTypes = rule.access_control_user_types[0]?.ids ?? [];\n    const r = rule.dbPermissions;\n    if (r.type === \"Run SQL\") {\n      return [\"\"];\n    }\n    if (r.type === \"All views/tables\") {\n      const table_policies = await Promise.all(\n        tables.map(async (table) => {\n          return await Promise.all(\n            r.allowAllTables.map((command) => {\n              return getComparablePGPolicy({\n                excludeRLSStatement: true,\n                table,\n                userTypes,\n                prgl,\n                forcedFilterDetailed: { $and: [] },\n                checkFilterDetailed: undefined,\n                forcedDataDetail: undefined,\n                ...(command === \"select\" ?\n                  {\n                    command: \"SELECT\",\n                  }\n                : command === \"insert\" ?\n                  {\n                    command: \"INSERT\",\n                    rule: { fields: \"*\" },\n                  }\n                : command === \"update\" ?\n                  {\n                    command: \"UPDATE\",\n                    rule: { fields: \"*\" },\n                  }\n                : {\n                    command: \"DELETE\",\n                    rule: { filterFields: \"*\" },\n                  }),\n              });\n            }),\n          );\n        }),\n      );\n\n      return table_policies.flat();\n    } else {\n      return await Promise.all(\n        r.customTables.map(async (tablePermission) => {\n          const table = tables.find(\n            (t) => t.name === tablePermission.tableName,\n          );\n          if (!table) return \"\";\n          const commands = ACCESS_RULE_METHODS.filter(\n            (m) => tablePermission[m],\n          );\n          const commandPolicies = await Promise.all(\n            commands.map((command) => {\n              const commandRule = tablePermission[command] as any;\n              return getComparablePGPolicy({\n                excludeRLSStatement: true,\n                table,\n                userTypes,\n                prgl,\n                ...(command === \"select\" ?\n                  {\n                    command: \"SELECT\",\n                    ...commandRule,\n                  }\n                : command === \"insert\" ?\n                  {\n                    command: \"INSERT\",\n                    ...commandRule,\n                  }\n                : command === \"update\" ?\n                  {\n                    command: \"UPDATE\",\n                    ...commandRule,\n                  }\n                : {\n                    command: \"DELETE\",\n                    ...commandRule,\n                  }),\n              });\n            }),\n          );\n\n          return commandPolicies.join(\"\\n\\n\");\n        }),\n      );\n    }\n  }, [prgl, rule]);\n\n  const query =\n    fixIndent(`\n    /* Drop all existing policies */\n    DO\n    $$\n    DECLARE\n    pol RECORD;\n    BEGIN\n      FOR pol IN (SELECT polname AS name, polrelid::regclass AS table FROM pg_policy)\n      LOOP\n        EXECUTE format('DROP POLICY %I ON %I', pol.name, pol.table);\n      END LOOP;\n    END;\n    $$;\n\n    /* Enable RLS for all tables */\n    DO $$\n    DECLARE\n      tbl record;\n    BEGIN\n      FOR tbl IN SELECT tablename FROM pg_tables AS t\n        WHERE t.schemaname = CURRENT_SCHEMA \n      LOOP\n        EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY;', tbl.tablename); \n      END LOOP;\n    END; $$;\n\n    /* It is recommended to create a non superuser pg account with read/write permissions to all relevant tables */\n    CREATE USER my_user WITH ENCRYPTED PASSWORD 'my_password';\n    GRANT ${ACCESS_RULE_METHODS.map((a) => a.toUpperCase()).join(\", \")} ON ALL TABLES IN SCHEMA public TO my_user;\n\n    /* Comparable policies */\n  `) +\n    \"\\n\" +\n    (policies ?? []).join(\"\\n\");\n\n  return (\n    <PopupMenu\n      className=\"ComparablePGPolicies mt-1\"\n      positioning=\"fullscreen\"\n      title=\"Comparable Postgres Policies\"\n      clickCatchStyle={{ opacity: 0.5 }}\n      button={\n        <Btn color=\"action\" data-command=\"ComparablePGPolicies\">\n          Comparable PG Policies\n        </Btn>\n      }\n      render={(pClose) => (\n        <FlexCol className=\"f-1\">\n          <InfoRow variant=\"naked\">\n            Some features like limiting read/write access to specific columns\n            cannot be easily achieved with Postgres\n            <br />\n            The policies below limit which rows can be read/modified (USING\n            clause) ensuring updated/inserted rows satisfy specific conditions\n            (CHECK clause)\n          </InfoRow>\n          <CodeExample\n            language=\"sql\"\n            style={{ minWidth: \"500px\", minHeight: \"500px\" }}\n            value={query}\n          />\n        </FlexCol>\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/DeleteRuleControl.tsx",
    "content": "import { getKeys, isObject } from \"prostgles-types\";\nimport React from \"react\";\nimport ErrorComponent from \"@components/ErrorComponent\";\n\nimport type { DeleteRule, TableRules } from \"@common/publishUtils\";\nimport { FieldFilterControl } from \"../OptionControllers/FieldFilterControl\";\nimport type {\n  ContextDataSchema,\n  SingleGroupFilter,\n} from \"../OptionControllers/FilterControl\";\nimport { FilterControl } from \"../OptionControllers/FilterControl\";\nimport type { TablePermissionControlsProps } from \"../TableRules/TablePermissionControls\";\nimport { ExampleComparablePolicy } from \"./ExampleComparablePolicy\";\nimport { RuleToggle } from \"./RuleToggle\";\nimport { RuleExpandSection } from \"./SelectRuleControl\";\n\ntype P = Pick<\n  Required<TablePermissionControlsProps>,\n  \"prgl\" | \"table\" | \"userTypes\"\n> & {\n  rule: TableRules[\"delete\"];\n  onChange: (rule: DeleteRule | undefined) => void;\n  contextDataSchema: ContextDataSchema;\n};\n\nexport const DeleteRuleControl = ({\n  rule: rawRule,\n  onChange,\n  table,\n  prgl,\n  contextDataSchema: contextData,\n  userTypes,\n}: P) => {\n  const rule: DeleteRule | undefined =\n    rawRule === true ? { filterFields: \"*\" }\n    : isObject(rawRule) ? rawRule\n    : undefined;\n  const error =\n    rule && isObject(rule.filterFields) && !getKeys(rule.filterFields).length ?\n      \"Must select at least one field\"\n    : undefined;\n\n  return (\n    <div className=\"flex-col gap-2\">\n      <RuleToggle\n        checked={!!rule}\n        onChange={(v) => {\n          onChange(v ? { filterFields: \"*\" } : undefined);\n        }}\n      />\n      {rule && (\n        <>\n          <FilterControl\n            title={\"DELETE required condition\"}\n            label=\"Records from\"\n            info={\n              <div className=\"flex-col gap-1\">\n                <div>\n                  If specified, a filter is added to each delete to ensure no\n                  other records can be deleted\n                </div>\n              </div>\n            }\n            db={prgl.db}\n            methods={prgl.methods}\n            tables={prgl.tables}\n            contextData={contextData}\n            detailedFilter={rule.forcedFilterDetailed as SingleGroupFilter}\n            tableName={table.name}\n            onSetError={console.error}\n            onChange={(forcedFilterDetailed) => {\n              onChange({\n                ...rule,\n                forcedFilterDetailed,\n              });\n            }}\n          />\n          <RuleExpandSection>\n            <FieldFilterControl\n              title={\"DELETE filter fields\"}\n              label=\"Can filter by\"\n              info={\"Fields that can be used in the delete filter\"}\n              columns={table.columns}\n              value={rule.filterFields}\n              onChange={(filterFields) => {\n                onChange({ ...rule, filterFields });\n              }}\n            />\n            <ExampleComparablePolicy\n              command=\"DELETE\"\n              rule={rule}\n              table={table}\n              userTypes={userTypes}\n              prgl={prgl}\n            />\n          </RuleExpandSection>\n        </>\n      )}\n      <ErrorComponent error={error} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/ExampleComparablePolicy.tsx",
    "content": "import { mdiShieldLockOutline } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport React from \"react\";\nimport type {\n  DeleteRule,\n  InsertRule,\n  SelectRule,\n  UpdateRule,\n} from \"@common/publishUtils\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport CodeExample from \"../../CodeExample\";\nimport type { SelectRuleControlProps } from \"./SelectRuleControl\";\nimport { getComparablePGPolicy } from \"./getComparablePGPolicy\";\n\ntype P = Pick<SelectRuleControlProps, \"table\" | \"userTypes\" | \"prgl\"> &\n  (\n    | {\n        rule: SelectRule;\n        command: \"SELECT\";\n      }\n    | {\n        rule: InsertRule;\n        command: \"INSERT\";\n      }\n    | {\n        rule: UpdateRule;\n        command: \"UPDATE\";\n      }\n    | {\n        rule: DeleteRule;\n        command: \"DELETE\";\n      }\n  ) & {\n    excludeRLSStatement?: boolean;\n  };\n\nexport const ExampleComparablePolicy = (p: P) => {\n  const policy = usePromise(\n    async () =>\n      getComparablePGPolicy({\n        forcedFilterDetailed: undefined,\n        checkFilterDetailed: undefined,\n        forcedDataDetail: undefined,\n        ...p.rule,\n        ...p,\n      }),\n    [p],\n  );\n\n  return (\n    <FlexCol>\n      <Label\n        iconPath={mdiShieldLockOutline}\n        label={\"Comparable postgres policy\"}\n        info={\n          \"Policy that may be used in postgres to replicate some of these rules\"\n        }\n        popupTitle={\"Comparable postgres policy\"}\n      />\n      {policy && (\n        <FlexCol className=\"gap-p5 ml-3 p-1\">\n          <div className=\"text-2 ai-start\">\n            Policy that may be used in postgres to replicate some of the rules\n            above\n          </div>\n          <CodeExample\n            language=\"sql\"\n            style={{ minWidth: \"500px\", minHeight: \"200px\" }}\n            value={policy}\n          />\n        </FlexCol>\n      )}\n    </FlexCol>\n  );\n};\n\n// const getComparableSelectPolicy = async (rule: SelectRule, table: P[\"table\"], userTypes: string[], prgl: Prgl) => {\n//   let query = \"\";\n//   let viewName = \"\";\n//   if(rule.fields !== \"*\"){\n//     viewName = table.name + \"_view\";\n//     query += [\n//       `/** Comparable postgres view to limite select fields */`,\n//       `CREATE VIEW ${JSON.stringify(viewName)} AS`,\n//       `SELECT ${parseFieldFilter({\n//         fieldFilter: rule.fields,\n//         columns: table.columns.map(c => c.name)\n//       }).join(\", \")}`,\n//       `FROM ${JSON.stringify(table.name)};`,\n//       `ALTER TABLE ${JSON.stringify(viewName)}`,\n//       `ENABLE ROW LEVEL SECURITY;`,\n//       `\\n\\n`\n//     ].join(\"\\n\");\n//   }\n\n//   query += (await getPGPolicy({\n//     table: viewName? {\n//       ...table,\n//       name: viewName\n//     } : table,\n//     action: \"SELECT\",\n//     forcedFilterDetailed: rule.forcedFilterDetailed,\n//     userTypes,\n//     prgl,\n//   }));\n\n//   return query;\n// }\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/InsertRuleControl.tsx",
    "content": "import { mdiFileDocumentPlusOutline } from \"@mdi/js\";\nimport { isObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type { InsertRule, TableRules } from \"@common/publishUtils\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FieldFilterControl } from \"../OptionControllers/FieldFilterControl\";\nimport type {\n  ContextDataSchema,\n  SingleGroupFilter,\n} from \"../OptionControllers/FilterControl\";\nimport { FilterControl } from \"../OptionControllers/FilterControl\";\nimport type { TablePermissionControlsProps } from \"../TableRules/TablePermissionControls\";\nimport { ExampleComparablePolicy } from \"./ExampleComparablePolicy\";\nimport { RuleToggle } from \"./RuleToggle\";\nimport { RuleExpandSection } from \"./SelectRuleControl\";\n\ntype P = Pick<\n  Required<TablePermissionControlsProps>,\n  \"prgl\" | \"table\" | \"tableRules\" | \"userTypes\"\n> & {\n  rule: TableRules[\"insert\"];\n  onChange: (rule: InsertRule | undefined) => void;\n  contextDataSchema: ContextDataSchema;\n};\n\nexport const InsertRuleControl = ({\n  rule: rawRule,\n  onChange,\n  table,\n  prgl,\n  tableRules,\n  contextDataSchema: contextData,\n  userTypes,\n}: P) => {\n  const rule: InsertRule | undefined =\n    rawRule === true ? { fields: \"*\" }\n    : isObject(rawRule) ? rawRule\n    : undefined;\n  const error = null;\n\n  return (\n    <div className=\"flex-col gap-2\">\n      <RuleToggle\n        checked={!!rule}\n        onChange={(v) => {\n          onChange(v ? { fields: \"*\" } : undefined);\n        }}\n      />\n      {rule && (\n        <>\n          <FieldFilterControl\n            iconPath={mdiFileDocumentPlusOutline}\n            label=\"Fields\"\n            title=\"INSERT fields\"\n            info={\n              \"Fields that can be used in inserting data. \\nCannot insert fields that are found in the forced data rule\"\n            }\n            columns={table.columns}\n            excluded={{\n              fields: rule.forcedDataDetail?.map((fd) => fd.fieldName),\n              message: \"Cannot insert a field that has forced data\",\n            }}\n            value={rule.fields}\n            onChange={(fields) => {\n              onChange({ ...rule, fields });\n            }}\n          />\n\n          {/* <ForcedDataControl\n        title=\"INSERT forced data\"\n        info={<div className=\"flex-col gap-1\">\n          <div>Data added to each insert. These fields cannot be inserted by the user</div>\n        </div>}\n        table={table}\n        prgl={prgl}\n        tableRules={tableRules}\n        contextData={contextData}\n        forcedDataDetail={rule.forcedDataDetail}\n        onChange={forcedDataDetail => {\n          onChange({ ...rule, forcedDataDetail, })\n        }}\n      /> */}\n\n          <FilterControl\n            label=\"Check\"\n            mode=\"checkFilter\"\n            info={\n              <div className=\"flex-col gap-1\">\n                <div>New records must satisfy a condition</div>\n              </div>\n            }\n            db={prgl.db}\n            methods={prgl.methods}\n            tables={prgl.tables}\n            contextData={contextData}\n            detailedFilter={rule.checkFilterDetailed as SingleGroupFilter}\n            tableName={table.name}\n            onSetError={console.error}\n            onChange={(checkFilterDetailed) => {\n              onChange({\n                ...rule,\n                checkFilterDetailed,\n              });\n            }}\n          />\n          <RuleExpandSection>\n            <ExampleComparablePolicy\n              command=\"INSERT\"\n              rule={rule}\n              table={table}\n              userTypes={userTypes}\n              prgl={prgl}\n            />\n          </RuleExpandSection>\n        </>\n      )}\n      <ErrorComponent error={error} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/RuleToggle.tsx",
    "content": "import React from \"react\";\nimport type { SwitchToggleProps } from \"@components/SwitchToggle\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\n\nexport const RuleToggle = ({\n  checked,\n  onChange,\n  disabledInfo,\n}: Pick<SwitchToggleProps, \"checked\" | \"onChange\" | \"disabledInfo\">) => {\n  return (\n    <SwitchToggle\n      label={{ label: \"Enabled\", variant: \"header\" }}\n      data-command=\"RuleToggle\"\n      checked={checked}\n      onChange={onChange}\n      disabledInfo={disabledInfo}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/SelectRuleControl.tsx",
    "content": "import {\n  mdiDotsHorizontal,\n  mdiFilter,\n  mdiSortReverseVariant,\n  mdiTextBoxSearchOutline,\n} from \"@mdi/js\";\nimport { getKeys, isObject } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport ErrorComponent from \"@components/ErrorComponent\";\n\nimport type { SelectRule, TableRules } from \"@common/publishUtils\";\nimport { ExpandSection } from \"@components/ExpandSection\";\nimport { FlexCol } from \"@components/Flex\";\nimport { FieldFilterControl } from \"../OptionControllers/FieldFilterControl\";\nimport type {\n  ContextDataSchema,\n  SingleGroupFilter,\n} from \"../OptionControllers/FilterControl\";\nimport { FilterControl } from \"../OptionControllers/FilterControl\";\nimport type { TablePermissionControlsProps } from \"../TableRules/TablePermissionControls\";\nimport { ExampleComparablePolicy } from \"./ExampleComparablePolicy\";\nimport { RuleToggle } from \"./RuleToggle\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\n\nexport type SelectRuleControlProps = Pick<\n  Required<TablePermissionControlsProps>,\n  \"prgl\" | \"table\"\n> & {\n  tableRules: TableRules;\n  onChange: (rule: SelectRule | undefined) => void;\n  contextDataSchema: ContextDataSchema;\n  userTypes: string[];\n};\n\nexport const SelectRuleControl = ({\n  tableRules,\n  onChange,\n  table,\n  prgl,\n  contextDataSchema: contextData,\n  userTypes,\n}: SelectRuleControlProps) => {\n  const { db, methods: dbMethods, tables } = prgl;\n  const rawRule = tableRules[\"select\"];\n  const rule: SelectRule | undefined =\n    rawRule === true ? { fields: \"*\", subscribe: {} }\n    : isObject(rawRule) ? rawRule\n    : undefined;\n  const [filterErr, setFilterError] = useState<string | undefined>();\n  const error = getSelectRuleError({ table, tableRules, prgl }) ?? filterErr;\n\n  return (\n    <FlexCol className=\"gap-2\">\n      <RuleToggle\n        checked={!!rule}\n        onChange={(v) => {\n          onChange(v ? { fields: \"*\" } : undefined);\n        }}\n      />\n      {rule && (\n        <FlexCol className=\"gap-2\">\n          <SwitchToggle\n            label={{ label: \"Allow subscribe\", variant: \"header\" }}\n            checked={!!rule.subscribe}\n            onChange={(allowSubscribe) => {\n              onChange({ ...rule, subscribe: allowSubscribe ? {} : undefined });\n            }}\n          />\n          <FieldFilterControl\n            iconPath={mdiTextBoxSearchOutline}\n            title=\"SELECT fields\"\n            label=\"Can view/select\"\n            info={\n              <div className=\"flex-col gap-1\">\n                <div>List of fields that can be viewed/selected</div>\n              </div>\n            }\n            columns={table.columns}\n            value={rule.fields}\n            onChange={(fields) => {\n              onChange({ ...rule, fields });\n            }}\n          />\n          <FilterControl\n            label=\"From\"\n            onSetError={setFilterError}\n            title=\"SELECT filter\"\n            info={\n              <FlexCol>\n                <div>\n                  Filter used in each select to ensure other records cannot be\n                  viewed\n                </div>\n              </FlexCol>\n            }\n            db={db}\n            methods={dbMethods}\n            tables={tables}\n            detailedFilter={rule.forcedFilterDetailed as SingleGroupFilter}\n            tableName={table.name}\n            contextData={contextData}\n            onChange={(forcedFilterDetailed) => {\n              onChange({\n                ...rule,\n                forcedFilterDetailed,\n              });\n            }}\n          />\n          <RuleExpandSection\n            expanded={!!(rule.orderByFields || rule.filterFields)}\n          >\n            <>\n              <FieldFilterControl\n                title=\"SELECT order by fields\"\n                label=\"Can order by\"\n                info=\"List of fields allowed to be used in the ORDER BY\"\n                expectAtLeastOne={false}\n                iconPath={mdiSortReverseVariant}\n                columns={table.columns}\n                value={rule.orderByFields ?? rule.fields}\n                onChange={(orderByFields) => {\n                  onChange({ ...rule, orderByFields });\n                }}\n              />\n              <FieldFilterControl\n                iconPath={mdiFilter}\n                title=\"SELECT filter by fields\"\n                label=\"Can filter by\"\n                info=\"List of fields allowed to be used in the filter condition\"\n                expectAtLeastOne={false}\n                columns={table.columns}\n                value={rule.filterFields ?? rule.fields}\n                onChange={(filterFields) => {\n                  onChange({ ...rule, filterFields });\n                }}\n              />\n              <ExampleComparablePolicy\n                command=\"SELECT\"\n                rule={rule}\n                table={table}\n                userTypes={userTypes}\n                prgl={prgl}\n              />\n            </>\n          </RuleExpandSection>\n          <ErrorComponent error={error} />\n        </FlexCol>\n      )}\n    </FlexCol>\n  );\n};\n\nexport const getSelectRuleError = ({\n  tableRules,\n}: Pick<SelectRuleControlProps, \"table\" | \"prgl\" | \"tableRules\">):\n  | string\n  | undefined => {\n  if (tableRules.select) {\n    const rule = tableRules.select;\n    if (\n      isObject(rule) &&\n      isObject(rule.fields) &&\n      !getKeys(rule.fields).length\n    ) {\n      return \"Must select at least one field\";\n    }\n  }\n\n  return undefined;\n};\n\nexport const RuleExpandSection = ({\n  children,\n  expanded,\n}: {\n  children: React.ReactNode;\n  expanded?: boolean;\n}) => {\n  return (\n    <ExpandSection\n      buttonProps={{\n        className: \"ml-p5\",\n        style: { opacity: 0.75 },\n        children: <div className=\"ml-1\">More options</div>,\n      }}\n      iconPath={mdiDotsHorizontal}\n      expanded={expanded}\n    >\n      {children}\n    </ExpandSection>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/SyncRuleControl.tsx",
    "content": "import React from \"react\";\nimport ErrorComponent from \"@components/ErrorComponent\";\n\nimport type { SyncRule, TableRules } from \"@common/publishUtils\";\nimport { Select } from \"@components/Select/Select\";\nimport type { ContextDataSchema } from \"../OptionControllers/FilterControl\";\nimport type { TablePermissionControlsProps } from \"../TableRules/TablePermissionControls\";\nimport { RuleToggle } from \"./RuleToggle\";\nimport { InfoRow } from \"@components/InfoRow\";\n// import { _PG_date, _PG_numbers, _PG_strings } from \"prostgles-types\";\n\ntype P = Pick<\n  Required<TablePermissionControlsProps>,\n  \"prgl\" | \"table\" | \"userTypes\"\n> & {\n  rule: TableRules[\"sync\"];\n  tableRules: TableRules;\n  onChange: (rule: SyncRule | undefined) => void;\n  contextDataSchema: ContextDataSchema;\n};\n\nexport const SyncRuleControl = ({\n  rule: rawRule,\n  onChange,\n  table,\n  tableRules,\n}: P) => {\n  const rule = rawRule;\n\n  const allowedSyncedFieldUdtNames = [\n    // \"timestamp\", \"timestamptz\", \"int4\",\n    \"int8\",\n  ];\n  const syncFields = table.columns.filter((c) =>\n    allowedSyncedFieldUdtNames.includes(c.udt_name),\n  );\n  const allowedIdFieldsUdtNames = [\n    \"int2\",\n    \"int4\",\n    \"float4\",\n    \"float8\",\n    \"int8\",\n    \"varchar\",\n    \"text\",\n    \"uuid\",\n  ];\n  const idFields = table.columns.filter((c) =>\n    allowedIdFieldsUdtNames.includes(c.udt_name),\n  );\n\n  const error =\n    !rule ? undefined\n    : !syncFields.length ?\n      `No fields that can be used as last updated field. Allowed data types: ${allowedSyncedFieldUdtNames.join(\", \")}`\n    : !idFields.length ?\n      `No fields that can be used as ID fields. Allowed data types: ${allowedIdFieldsUdtNames.join(\", \")}`\n    : !rule.id_fields.length ? \"Must select at least one id field\"\n    : !rule.synced_field ? \"Must select a last updated field\"\n    : rule.id_fields.some((f) => f === rule.synced_field) ?\n      \"ID fields cannot include the last updated field\"\n    : undefined;\n\n  const cannotEnableError =\n    table.info.isView ? \"Only tables can be synced\"\n    : !tableRules.select ? \"Cannot enable sync without select rule\"\n    : !(tableRules.update || tableRules.insert || tableRules.delete) ?\n      \"Cannot enable sync without at least one of the following rules: insert, update, delete\"\n    : undefined;\n\n  return (\n    <div className=\"flex-col gap-2\">\n      <RuleToggle\n        checked={!!rule}\n        onChange={(v) => {\n          onChange(\n            v ?\n              { id_fields: [], synced_field: \"\", allow_delete: false }\n            : undefined,\n          );\n        }}\n        disabledInfo={cannotEnableError}\n      />\n      <InfoRow color=\"info\" variant=\"naked\">\n        Real-time synchronization between server and API clients.\n        <br></br>\n        Exposes a \"sync\" method for the table which allows the client to make\n        instant optimistic changes to their local data which are then synced\n        with the server.\n      </InfoRow>\n      {rule && (\n        <>\n          <Select\n            label={{\n              label: \"ID fields\",\n              info: \"Fields that uniquely identify a record\",\n            }}\n            fullOptions={idFields.map((c) => ({\n              key: c.name,\n              subLabel: c.udt_name,\n            }))}\n            value={rule.id_fields}\n            multiSelect={true}\n            onChange={(id_fields) => {\n              const newUniqueFields = Array.from(\n                new Set([...rule.id_fields, ...id_fields]),\n              );\n              onChange({ ...rule, id_fields: newUniqueFields });\n            }}\n          />\n          <Select\n            label={{\n              label: \"Sync/Last updated field\",\n              info: \"Field that shows the UNIX time when the record was last updated/created\",\n            }}\n            fullOptions={syncFields.map((c) => ({\n              key: c.name,\n              subLabel: c.udt_name,\n              disabledInfo:\n                rule.id_fields.includes(c.name) ?\n                  \"Cannot be an ID field\"\n                : undefined,\n            }))}\n            value={rule.synced_field}\n            onChange={(synced_field) => {\n              onChange({ ...rule, synced_field });\n            }}\n          />\n        </>\n      )}\n      <ErrorComponent error={error} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/UpdateRuleControl.tsx",
    "content": "import { mdiFileDocumentEditOutline } from \"@mdi/js\";\nimport { getKeys, isObject, type AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type {\n  ContextDataObject,\n  TableRules,\n  UpdateRule,\n} from \"@common/publishUtils\";\nimport { parseFieldFilter } from \"@common/publishUtils\";\nimport ErrorComponent from \"@components/ErrorComponent\";\n\nimport { DynamicFields } from \"../OptionControllers/DynamicFields\";\nimport { FieldFilterControl } from \"../OptionControllers/FieldFilterControl\";\nimport type {\n  ContextDataSchema,\n  SingleGroupFilter,\n} from \"../OptionControllers/FilterControl\";\nimport { FilterControl } from \"../OptionControllers/FilterControl\";\nimport type { TablePermissionControlsProps } from \"../TableRules/TablePermissionControls\";\nimport { ExampleComparablePolicy } from \"./ExampleComparablePolicy\";\nimport { RuleToggle } from \"./RuleToggle\";\nimport { RuleExpandSection } from \"./SelectRuleControl\";\n\ntype P = Pick<\n  Required<TablePermissionControlsProps>,\n  \"prgl\" | \"table\" | \"tableRules\" | \"userTypes\"\n> & {\n  rule: TableRules[\"update\"];\n  onChange: (rule: UpdateRule | undefined) => void;\n  contextDataSchema: ContextDataSchema;\n  contextData: ContextDataObject;\n};\n\nexport const UpdateRuleControl = (props: P) => {\n  const {\n    rule: rawRule,\n    onChange,\n    table,\n    contextDataSchema,\n    contextData,\n    prgl,\n    userTypes,\n  } = props;\n  const rule: UpdateRule | undefined =\n    rawRule === true ? { fields: \"*\" }\n    : isObject(rawRule) ? rawRule\n    : undefined;\n  const error = !rawRule ? undefined : getUpdateRuleError(props);\n\n  return (\n    <div className=\"flex-col gap-2 min-h-0\">\n      <RuleToggle\n        checked={!!rule}\n        onChange={(v) => {\n          onChange(v ? { fields: \"*\" } : undefined);\n        }}\n      />\n      {rule && (\n        <>\n          <FieldFilterControl\n            iconPath={mdiFileDocumentEditOutline}\n            label={\"Can update\"}\n            info={\n              \"List of fields that can be edited/updated. \\nIf the Check condition specifies only one possible value for a field then it will be pre-populated and hidden from user\"\n            }\n            columns={table.columns}\n            value={rule.fields}\n            onChange={(fields) => {\n              onChange({ ...rule, fields });\n            }}\n          />\n\n          <FilterControl\n            label=\"From\"\n            info={\n              <div className=\"flex-col gap-1\">\n                <div>\n                  Filter added to each update to ensure other records cannot be\n                  updated\n                </div>\n              </div>\n            }\n            db={prgl.db}\n            methods={prgl.methods}\n            tables={prgl.tables}\n            contextData={contextDataSchema}\n            detailedFilter={rule.forcedFilterDetailed as SingleGroupFilter}\n            tableName={table.name}\n            onSetError={console.error}\n            onChange={(forcedFilterDetailed) => {\n              onChange({\n                ...rule,\n                forcedFilterDetailed,\n              });\n            }}\n          />\n          {/* \n      <ForcedDataControl \n        info={\n          <div className=\"flex-col gap-1\">\n            <div>Data added to each update. These fields cannot be updated by the user</div>\n          </div>\n        }\n        table={table}\n        prgl={prgl} \n        tableRules={tableRules} \n        contextData={contextDataSchema}\n        forcedDataDetail={rule.forcedDataDetail}\n        onChange={forcedDataDetail => onChange({ ...rule, forcedDataDetail })}\n      /> */}\n\n          <FilterControl\n            label=\"Check\"\n            mode=\"checkFilter\"\n            info={\n              <div className=\"flex-col gap-1\">\n                <div>New records must satisfy a condition</div>\n              </div>\n            }\n            db={prgl.db}\n            methods={prgl.methods}\n            tables={prgl.tables}\n            contextData={contextDataSchema}\n            detailedFilter={rule.checkFilterDetailed as SingleGroupFilter}\n            tableName={table.name}\n            onSetError={console.error}\n            onChange={(checkFilterDetailed) => {\n              onChange({\n                ...rule,\n                checkFilterDetailed,\n              });\n            }}\n          />\n\n          <DynamicFields\n            {...props}\n            contextDataSchema={contextDataSchema}\n            contextData={contextData}\n          />\n          <RuleExpandSection>\n            <ExampleComparablePolicy\n              command=\"UPDATE\"\n              rule={rule}\n              table={table}\n              userTypes={userTypes}\n              prgl={prgl}\n            />\n          </RuleExpandSection>\n        </>\n      )}\n      <ErrorComponent error={error} />\n    </div>\n  );\n};\n\nexport const getUpdateRuleError = ({\n  rule: rawRule,\n  table,\n  tableRules,\n}: Pick<P, \"rule\" | \"table\" | \"tableRules\">): string | undefined => {\n  const rule: UpdateRule = isObject(rawRule) ? rawRule : { fields: \"*\" };\n\n  let error =\n    isObject(rule.fields) && !getKeys(rule.fields).length ?\n      \"Must select at least one field\"\n    : undefined;\n\n  if (!error) {\n    const pkeyCols = table.columns.filter((c) => c.is_pkey).map((c) => c.name);\n    const notAllPkeysSelected =\n      pkeyCols.length &&\n      (!tableRules.select ||\n        (isObject(tableRules.select) &&\n          tableRules.select.fields !== \"*\" &&\n          parseFieldFilter({\n            columns: pkeyCols,\n            fieldFilter: tableRules.select.fields,\n          }).length !== pkeyCols.length));\n\n    if (notAllPkeysSelected) {\n      error = `Primary key fields must be allowed in Select to allow Update through the dashboard. Primary key fields: ${pkeyCols}`;\n    } else {\n      const err = `Filter fields must be allowed in Select to allow Update through the dashboard`;\n      if (\n        !tableRules.select ||\n        (isObject(tableRules.select) &&\n          tableRules.select.fields !== \"*\" &&\n          tableRules.select.filterFields !== \"*\")\n      ) {\n        if (tableRules.select && isObject(tableRules.select)) {\n          const columns = table.columns.map((c) => c.name);\n          const selectFields = parseFieldFilter({\n            columns,\n            fieldFilter: tableRules.select.fields,\n          });\n          const selectFilterFields = parseFieldFilter({\n            columns,\n            fieldFilter: tableRules.select.filterFields ?? \"*\",\n          });\n          const noFilterFields = !selectFilterFields.filter((f) =>\n            selectFields.includes(f),\n          ).length;\n          if (noFilterFields) {\n            error = err;\n          }\n          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n        } else if (!tableRules.select) {\n          error = err;\n        }\n      }\n    }\n  }\n\n  return error;\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/RuleTypeControls/getComparablePGPolicy.ts",
    "content": "import { asName } from \"prostgles-client/dist/prostgles\";\nimport {\n  getFinalFilter,\n  parseContextVal,\n  type GroupedDetailedFilter,\n  type DetailedFilter,\n} from \"@common/filterUtils\";\nimport type { ForcedData } from \"@common/publishUtils\";\nimport type { SelectRuleControlProps } from \"./SelectRuleControl\";\ntype GetComparablePGPolicyArgs = Pick<\n  SelectRuleControlProps,\n  \"table\" | \"userTypes\" | \"prgl\"\n> & {\n  command: \"SELECT\" | \"UPDATE\" | \"INSERT\" | \"DELETE\" | undefined;\n  forcedFilterDetailed: GroupedDetailedFilter | undefined;\n  checkFilterDetailed: GroupedDetailedFilter | undefined;\n  forcedDataDetail: ForcedData[] | undefined;\n  excludeRLSStatement?: boolean;\n};\n\nexport const getComparablePGPolicy = async ({\n  forcedFilterDetailed,\n  checkFilterDetailed,\n  command,\n  forcedDataDetail,\n  userTypes,\n  table,\n  prgl,\n  excludeRLSStatement,\n}: GetComparablePGPolicyArgs) => {\n  const columns = table.columns.map((c) => c.name);\n  const getSingleFilterCondition = async (f: DetailedFilter) => {\n    if (\"contextValue\" in f) {\n      const col = table.columns.find((c) => c.name === f.fieldName);\n      if (!col) return \"\";\n      const contextVal: string = parseContextVal(f, undefined, {\n        forInfoOnly: \"pg\",\n      });\n      return `${f.fieldName} ${f.type ?? \"=\"} ${contextVal}::${col.udt_name}`;\n    }\n    const parsedFilter = getFinalFilter(f, undefined, { columns });\n    try {\n      const condition = (await prgl.db[table.name]?.find?.(parsedFilter, {\n        returnType: \"statement-where\",\n      })) as any as string;\n      return condition.trim();\n    } catch (err) {\n      return \"\";\n    }\n  };\n  const getFilterCondition = async (\n    filter: GroupedDetailedFilter | undefined,\n  ) => {\n    if (!filter) return \"\";\n    const isAnd = \"$and\" in filter;\n    const filters = isAnd ? filter.$and : filter.$or;\n    const conditions = await Promise.all(\n      filters.map((f) => getSingleFilterCondition(f as DetailedFilter)),\n    );\n    return `${conditions.filter((v) => v).join(isAnd ? \" AND \" : \" OR \")}`;\n  };\n\n  const indent = (str: string, depth = 1, tab = \"  \") =>\n    str\n      .split(\"\\n\")\n      .map((l) => `${tab.repeat(depth)}${l}`)\n      .join(\"\\n\");\n\n  const usingCondition = await getFilterCondition(forcedFilterDetailed);\n  const checkCondition = await getFilterCondition(checkFilterDetailed);\n\n  const finalAction = command ?? \"ALL\";\n  const using =\n    finalAction === \"INSERT\" ? \"\" : (\n      `\\nUSING (\\n  COALESCE(prostgles.user('type') IN (${userTypes.map((ut) => `'${ut}'`)}), FALSE)${usingCondition ? `\\n AND \\n${indent(usingCondition)}` : \"\"} \\n)`\n    );\n\n  const forcedDataChecks =\n    !forcedDataDetail?.length ?\n      undefined\n    : forcedDataDetail.map((d, i) => {\n        const col = table.columns.find((c) => c.name === d.fieldName);\n        const value =\n          d.type === \"fixed\" ?\n            [\"number\", \"boolean\"].includes(col?.tsDataType as any) ?\n              d.value\n            : `'${d.value}'`\n          : `prostgles.${d.objectName}('${d.objectPropertyName}')::${col?.udt_name}`;\n        return `${i ? \"\" : \"  \"}${d.fieldName} = ${value}`;\n      });\n\n  const withCheck =\n    forcedDataChecks || checkCondition ?\n      [\n        `\\nWITH CHECK (`,\n        (forcedDataChecks ?? [])\n          .concat([checkCondition])\n          .filter((v) => v)\n          .map((v) => `  ${v}`)\n          .join(\"AND \\n\"),\n        `)`,\n      ].join(\"\\n\")\n    : \"\";\n\n  const policyName = asName(\n    `prostgles_${table.name}_${finalAction}_${userTypes.sort().join(\"_\")}`,\n  );\n  const tableName = asName(table.name);\n  let policy = [\n    `DROP POLICY IF EXISTS ${policyName} ON ${tableName};`,\n    `CREATE POLICY ${policyName}`,\n    `ON ${tableName}`,\n    `FOR ${finalAction}`,\n  ].join(\"\\n\");\n  if (using) policy += using;\n  if (withCheck) policy += withCheck;\n  policy += \";\\n\";\n\n  if (excludeRLSStatement) return policy;\n  return (\n    policy +\n    [`ALTER TABLE ${tableName}`, `ENABLE ROW LEVEL SECURITY;`].join(\"\\n\")\n  );\n};\n\nexport const getTableEnableRLSStatement = (tableName: string) => {\n  return `ALTER TABLE ${asName(tableName)} ENABLE ROW LEVEL SECURITY;`;\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/TableRules/FileTableAccessControlInfo.tsx",
    "content": "import { isObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { FieldFilter, TableRules } from \"@common/publishUtils\";\nimport { parseFieldFilter } from \"@common/publishUtils\";\nimport { FlexCol } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { DBSchemaTablesWJoins } from \"../../Dashboard/dashboardUtils\";\nimport type { TableInfoWithRules } from \"./TablePermissionControls\";\n\ntype FileTableAccessControlInfoProps = {\n  table: DBSchemaTablesWJoins[number];\n  tablesWithRules: TableInfoWithRules[];\n  ruleType: keyof TableRules;\n};\nexport const FileTableAccessControlInfo = (\n  props: FileTableAccessControlInfoProps,\n) => {\n  const { refTables } = useFileTableRefTableRules(props);\n  if (!refTables) return null;\n\n  const ruleCols = refTables\n    .map((t) => {\n      const cols = Object.entries(t.colRules).filter(\n        ([c, r]) => r[props.ruleType],\n      );\n      if (!cols.length) return undefined;\n\n      return {\n        tableName: t.name,\n        cols: cols.map((c) => c[0]),\n      };\n    })\n    .filter(isDefined);\n\n  return (\n    <FlexCol>\n      <InfoRow color=\"info\" variant=\"naked\" iconPath=\"\">\n        <FlexCol className=\"gap-p5\">\n          <div>\n            Access to the files is controlled through:\n            <ul className=\"no-decor pl-1\">\n              <li>- access rules for the columns referencing this table</li>\n              <li>- access rules for this table</li>\n            </ul>\n          </div>\n          <div>\n            Any allowed action on the referencing columns is permitted on the\n            referenced file records\n          </div>\n          {ruleCols.length > 0 && (\n            <>\n              <div className=\"mt-1\">\n                <strong>{props.ruleType.toUpperCase()}</strong> is allowed on\n                files{\" \"}\n                {props.ruleType !== \"insert\" ?\n                  \"referenced by\"\n                : \"that will be referenced by\"}{\" \"}\n                the permitted records from following tables and columns:\n              </div>\n              <ul className=\"no-decor pl-1\">\n                {ruleCols.map((t) => (\n                  <li key={t.tableName} className=\"bold\">\n                    {t.tableName} ({t.cols})\n                  </li>\n                ))}\n              </ul>\n            </>\n          )}\n        </FlexCol>\n      </InfoRow>\n    </FlexCol>\n  );\n};\n\nexport const useFileTableRefTableRules = ({\n  table,\n  tablesWithRules,\n}: {\n  table: DBSchemaTablesWJoins[number] | undefined;\n  tablesWithRules: TableInfoWithRules[];\n}) => {\n  const refTables = useMemo(\n    () =>\n      !table?.info.isFileTable ?\n        undefined\n      : tablesWithRules\n          .map((t) => {\n            const refs = t.columns.filter((c) =>\n              c.references?.some((r) => r.ftable === t.info.fileTableName),\n            );\n            if (!t.rule || !refs.length) {\n              return undefined;\n            }\n            return {\n              ...t,\n              rule: t.rule,\n              refs,\n            };\n          })\n          .filter(isDefined)\n          .map((t) => {\n            const r = t.rule;\n            const getCols = (fieldFilter: FieldFilter) => {\n              const fields = parseFieldFilter({\n                columns: t.columns.map((c) => c.name),\n                fieldFilter,\n              });\n              return t.refs.filter((c) => fields.includes(c.name));\n            };\n\n            const selectRefs =\n              r.select === true ? t.refs\n              : isObject(r.select) ? getCols(r.select.fields)\n              : undefined;\n            const insertRefs =\n              r.insert === true ? t.refs\n              : isObject(r.insert) ? getCols(r.insert.fields)\n              : undefined;\n            const deleteRefs = r.delete ? t.refs : undefined;\n            const updateRefs =\n              r.update === true ? t.refs\n              : isObject(r.update) ? getCols(r.update.fields)\n              : undefined;\n\n            if (!selectRefs && !insertRefs && !deleteRefs && !updateRefs) {\n              return undefined;\n            }\n\n            return {\n              ...t,\n              colRules: Object.fromEntries(\n                Array.from(\n                  new Set([\n                    ...(selectRefs ?? []).map((c) => c.name),\n                    ...(insertRefs ?? []).map((c) => c.name),\n                    ...(deleteRefs ?? []).map((c) => c.name),\n                    ...(updateRefs ?? []).map((c) => c.name),\n                  ]),\n                ).map((colName) => [\n                  colName,\n                  {\n                    select: selectRefs?.some((r) => r.name === colName),\n                    insert: insertRefs?.some((r) => r.name === colName),\n                    delete: deleteRefs?.some((r) => r.name === colName),\n                    update: updateRefs?.some((r) => r.name === colName),\n                  },\n                ]),\n              ),\n              selectRefs,\n              insertRefs,\n              deleteRefs,\n              updateRefs,\n            };\n          })\n          .filter(isDefined),\n    [tablesWithRules, table],\n  );\n\n  return { refTables };\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/TableRules/TablePermissionControls.tsx",
    "content": "import { mdiAlertCircleOutline, mdiCog } from \"@mdi/js\";\nimport { getKeys, isObject } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { GroupedDetailedFilter } from \"@common/filterUtils\";\nimport type {\n  BasicTablePermissions,\n  ContextDataObject,\n  SelectRule,\n  TableRules,\n  TableRulesErrors,\n  UpdateRule,\n} from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type { DBSchemaTablesWJoins } from \"../../Dashboard/dashboardUtils\";\nimport { useFileTableRefTableRules } from \"./FileTableAccessControlInfo\";\nimport { TableRulesPopup } from \"./TableRulesPopup\";\nimport { useLocalTableRulesErrors } from \"./useLocalTableRulesErrors\";\n\nexport type TableInfoWithRules = DBSchemaTablesWJoins[number] & {\n  rule?: TableRules;\n};\n\nexport type TablePermissionControlsProps = Pick<CommonWindowProps, \"prgl\"> & {\n  tableRules: TableRules;\n  tablesWithRules: TableInfoWithRules[];\n  variant?: \"mini\" | \"micro\";\n  className?: string;\n  style?: React.CSSProperties;\n  onChange: (newValues: TableRules) => void;\n  table?: DBSchemaTablesWJoins[number];\n  contextData?: ContextDataObject;\n  errors: TableRulesErrors;\n  userTypes: string[];\n};\n\nexport const getBasicPermissions = (\n  rules: TableRules = {},\n): BasicTablePermissions => {\n  return getKeys(rules).reduce(\n    (a, ruleType) => ({\n      ...a,\n      [ruleType]: Boolean(rules[ruleType]),\n    }),\n    {},\n  );\n};\nexport const TABLE_RULE_LABELS = {\n  select: {\n    label: { micro: \"S\", mini: \"Select\", default: \"Select/View all records\" },\n    title: \"View records\",\n  },\n  insert: {\n    label: { micro: \"I\", mini: \"Insert\", default: \"Insert/Add new records\" },\n    title: \"Insert/Add records\",\n  },\n  update: {\n    label: { micro: \"U\", mini: \"Update\", default: \"Update/Edit records\" },\n    title: \"Edit data\",\n  },\n  delete: {\n    label: { micro: \"D\", mini: \"Delete\", default: \"Delete/Remove records\" },\n    title: \"Remove records\",\n  },\n  sync: {\n    label: { micro: \"R\", mini: \"Sync\", default: \"Sync records\" },\n    title: \"Sync records\",\n  },\n} as const;\n\nexport type EditedRuleType = keyof typeof TABLE_RULE_LABELS;\n\nexport const TablePermissionControls = (\n  props: TablePermissionControlsProps,\n) => {\n  const {\n    tableRules,\n    onChange,\n    className = \"\",\n    table,\n    errors,\n    style,\n    tablesWithRules: allRules,\n  } = props;\n  const variant = props.variant ?? (window.innerWidth < 630 ? \"micro\" : \"mini\");\n\n  const [editedRuleType, setEditedRuleType] = useState<EditedRuleType>();\n\n  // const localTableRulesErrors = usePromise(async () => {\n  //   if(!table || !contextData?.user || !localTableRules) return;\n\n  //   const columnNames = table.columns.map(c => c.name);\n  //   const tableRErrs = await getTableRulesErrors(omitKeys(localRules, [\"tableName\" as any]), columnNames, contextData);\n  //   return tableRErrs;\n  // }, [localRules, table, contextData, localTableRules]);\n  const localTableRulesErrors = useLocalTableRulesErrors(props);\n\n  const tableErrors = localTableRulesErrors ?? errors;\n\n  const fileTableRefRules = useFileTableRefTableRules({\n    table,\n    tablesWithRules: allRules,\n  });\n\n  return (\n    <div\n      className={\n        \"TablePermissionControls gap-p25 flex-row ai-center \" + className\n      }\n      style={{\n        ...style,\n        gap: fileTableRefRules.refTables ? \"14px\" : \"8px\",\n        paddingRight: fileTableRefRules.refTables ? \"8px\" : 0,\n      }}\n    >\n      {table && editedRuleType && (\n        <TableRulesPopup\n          {...props}\n          tableErrors={tableErrors}\n          table={table}\n          editedRuleType={editedRuleType}\n          onClose={() => setEditedRuleType(undefined)}\n        />\n      )}\n      {getKeys(TABLE_RULE_LABELS).map((ruleType) => {\n        if (ruleType === \"sync\") {\n          return null;\n        }\n\n        const error = tableErrors[ruleType];\n        const rule = tableRules[ruleType];\n        const fileRefRules = fileTableRefRules.refTables?.filter((rt) =>\n          Object.values(rt.colRules).some((colRule) => colRule[ruleType]),\n        );\n        const isOn =\n          rule ? \"on\"\n          : fileRefRules && fileRefRules.length ? \"file-table-with-refs\"\n          : undefined;\n\n        const disabledInfo =\n          table?.info.isView && ruleType !== \"select\" ?\n            \"Can only select from a view\"\n          : undefined;\n\n        return (\n          <div key={ruleType} className=\"flex-row ai-center gap-p25\">\n            <Btn\n              data-command={`${ruleType}Rule`}\n              disabledInfo={disabledInfo}\n              size=\"small\"\n              className={isOn ? \" \" : \" \"}\n              variant={\n                isOn === \"on\" ? \"filled\"\n                : isOn === \"file-table-with-refs\" ?\n                  \"faded\"\n                : undefined\n              }\n              iconPath={fileRefRules ? mdiCog : undefined}\n              iconPosition={fileRefRules ? \"right\" : undefined}\n              color={isOn ? \"action\" : undefined}\n              style={{\n                padding: \".5em .75em\",\n                ...(fileRefRules ? { gap: \"8px\" } : {}),\n              }}\n              onClick={() => {\n                if (table?.info.isFileTable) {\n                  setEditedRuleType(ruleType);\n                  return;\n                }\n                const userIdField = table?.columns.find(\n                  (c) =>\n                    c.udt_name === \"uuid\" &&\n                    (c.name === \"user_id\" ||\n                      (table.name === \"users\" && c.name === \"id\")),\n                );\n                if (!isOn && userIdField) {\n                  const forcedFilterDetailed: GroupedDetailedFilter = {\n                    $and: [\n                      {\n                        fieldName: userIdField.name,\n                        type: \"=\",\n                        contextValue: {\n                          objectName: \"user\",\n                          objectPropertyName: \"id\",\n                        },\n                      },\n                    ],\n                  };\n                  const checkFilterDetailed: UpdateRule[\"checkFilterDetailed\"] =\n                    forcedFilterDetailed;\n                  if (ruleType === \"delete\") {\n                    onChange({\n                      [ruleType]: {\n                        forcedFilterDetailed,\n                        filterFields: \"*\",\n                      },\n                    });\n                  } else if (ruleType === \"insert\" || ruleType === \"update\") {\n                    onChange({\n                      [ruleType]: {\n                        fields: \"*\",\n                        checkFilterDetailed,\n                        ...(ruleType === \"update\" && {\n                          forcedFilterDetailed,\n                        }),\n                      },\n                    });\n                  } else if ((ruleType as any) === \"select\") {\n                    onChange({\n                      [ruleType]: {\n                        forcedFilterDetailed,\n                        fields: \"*\",\n                        subscribe: {},\n                      },\n                    } satisfies { select: SelectRule });\n                  }\n                } else {\n                  onChange({ [ruleType]: !rule });\n                }\n              }}\n            >\n              {TABLE_RULE_LABELS[ruleType].label[variant].toUpperCase()}\n            </Btn>\n\n            {!!table && !fileRefRules && (\n              <Btn\n                data-command={`${ruleType}RuleAdvanced`}\n                size=\"small\"\n                className={\n                  isObject(rule) || error ? \"\" : \"show-on-parent-hover\"\n                }\n                style={{\n                  visibility: rule ? undefined : \"hidden\",\n                }}\n                iconPath={error ? mdiAlertCircleOutline : mdiCog}\n                color={\n                  error ? \"danger\"\n                  : isObject(rule) ?\n                    \"action\"\n                  : undefined\n                }\n                onClick={() => {\n                  setEditedRuleType(ruleType);\n                }}\n              />\n            )}\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/TableRules/TableRulesPopup.tsx",
    "content": "import React, { useState } from \"react\";\nimport type { TableRules, TableRulesErrors } from \"@common/publishUtils\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { MenuList } from \"@components/MenuList\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { DBSchemaTablesWJoins } from \"../../Dashboard/dashboardUtils\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { FileTableAccessControlInfo } from \"./FileTableAccessControlInfo\";\nimport { DeleteRuleControl } from \"./../RuleTypeControls/DeleteRuleControl\";\nimport { InsertRuleControl } from \"./../RuleTypeControls/InsertRuleControl\";\nimport { SelectRuleControl } from \"./../RuleTypeControls/SelectRuleControl\";\nimport { UpdateRuleControl } from \"./../RuleTypeControls/UpdateRuleControl\";\nimport type {\n  EditedRuleType,\n  TablePermissionControlsProps,\n} from \"./TablePermissionControls\";\nimport { TABLE_RULE_LABELS } from \"./TablePermissionControls\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { mdiFile, mdiTable, mdiTableEye } from \"@mdi/js\";\nimport { getEntries } from \"@common/utils\";\nimport { SyncRuleControl } from \"../RuleTypeControls/SyncRuleControl\";\n\ntype TableRulesPopupProps = TablePermissionControlsProps & {\n  table: DBSchemaTablesWJoins[number];\n  tableErrors: TableRulesErrors;\n  editedRuleType: EditedRuleType;\n  onClose: VoidFunction;\n};\nexport const TableRulesPopup = ({\n  contextData,\n  tablesWithRules: allRules,\n  tableRules,\n  onChange,\n  prgl,\n  table,\n  tableErrors,\n  userTypes,\n  onClose,\n  ...props\n}: TableRulesPopupProps) => {\n  const [editedRuleType, setEditedRuleType] = useState<EditedRuleType>(\n    props.editedRuleType,\n  );\n  const error = tableErrors[editedRuleType] ?? tableErrors.all;\n  const contextDataSchema = [\n    {\n      name: \"user\",\n      columns: prgl.dbsTables.find((t) => t.name === \"users\")!.columns,\n    },\n  ];\n\n  const [localTableRules, setLocalTableRules] = useState<TableRules | void>();\n  const localRules = localTableRules ?? tableRules;\n  const onChangeRule = <K extends keyof TableRules>(newRule: TableRules[K]) => {\n    setLocalTableRules({\n      ...tableRules,\n      ...localTableRules,\n      [editedRuleType]: newRule,\n    });\n  };\n\n  const commonProps = {\n    prgl,\n    table,\n    contextDataSchema,\n    userTypes,\n    tableRules: localRules,\n    onChange: onChangeRule,\n  };\n  const closePopup = () => {\n    onChange(localRules);\n    onClose();\n  };\n\n  const { info } = table;\n  const ruleWasChanged =\n    JSON.stringify(tableRules[editedRuleType]) ===\n    JSON.stringify(localRules[editedRuleType]);\n  return (\n    <Popup\n      title={\n        <FlexRow className=\"gap-p5 ai-start\">\n          <Icon\n            className=\"text-2\"\n            path={\n              info.isFileTable ? mdiFile\n              : info.isView ?\n                mdiTableEye\n              : mdiTable\n            }\n          />\n          <FlexCol className=\"gap-p5\">\n            <div>{table.name}</div>\n            {table.info.isFileTable && (\n              <div className=\"font-14 text-2\">\n                File metadata is stored in this table\n              </div>\n            )}\n          </FlexCol>\n        </FlexRow>\n      }\n      positioning=\"right-panel\"\n      focusTrap={true}\n      onClose={closePopup}\n      clickCatchStyle={{ opacity: 1 }}\n      footerButtons={[\n        {\n          label: \"Cancel\",\n          onClick: closePopup,\n          \"data-command\": \"TablePermissionControls.close\",\n        },\n        {\n          label: \"Done\",\n          color: \"action\",\n          variant: \"filled\",\n          \"data-command\": \"TablePermissionControls.done\",\n          disabledInfo: ruleWasChanged ? \"Nothing to change\" : undefined,\n          onClick: closePopup,\n        },\n      ]}\n    >\n      <FlexCol className=\"gap-2\">\n        <MenuList\n          className=\"m-auto\"\n          style={{ width: \"100%\", minWidth: \"500px\" }}\n          variant=\"horizontal-tabs\"\n          items={getEntries(TABLE_RULE_LABELS).map(([key, { label }]) => ({\n            key,\n            label: label.mini,\n            onPress: () => {\n              setEditedRuleType(key);\n            },\n          }))}\n          activeKey={editedRuleType}\n        />\n        <FileTableAccessControlInfo\n          ruleType={editedRuleType}\n          table={table}\n          tablesWithRules={allRules}\n        />\n        {editedRuleType === \"select\" ?\n          <SelectRuleControl {...commonProps} />\n        : editedRuleType === \"insert\" ?\n          <InsertRuleControl\n            {...commonProps}\n            rule={localRules[editedRuleType]}\n          />\n        : editedRuleType === \"update\" && contextData ?\n          <UpdateRuleControl\n            {...commonProps}\n            rule={localRules[editedRuleType]}\n            contextData={contextData}\n          />\n        : editedRuleType === \"delete\" ?\n          <DeleteRuleControl\n            {...commonProps}\n            rule={localRules[editedRuleType]}\n          />\n        : editedRuleType === \"sync\" ?\n          <SyncRuleControl {...commonProps} rule={localRules[editedRuleType]} />\n        : null}\n        {error && <ErrorComponent error={error} className=\"m-1\" />}\n      </FlexCol>\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/TableRules/useLocalTableRulesErrors.ts",
    "content": "import { usePromise } from \"prostgles-client\";\nimport { getTableRulesErrors } from \"@common/publishUtils\";\nimport { omitKeys } from \"prostgles-types\";\nimport type { TablePermissionControlsProps } from \"./TablePermissionControls\";\n\ntype Args = Pick<TablePermissionControlsProps, \"contextData\" | \"table\"> &\n  Pick<Partial<TablePermissionControlsProps>, \"tableRules\">;\n\nexport const useLocalTableRulesErrors = ({\n  contextData,\n  table,\n  tableRules,\n}: Args) => {\n  const localTableRulesErrors = usePromise(async () => {\n    if (!table || !contextData?.user || !tableRules) return;\n\n    const columnNames = table.columns.map((c) => c.name);\n    const tableRErrs = await getTableRulesErrors(\n      omitKeys(tableRules, [\"tableName\" as any]),\n      columnNames,\n      contextData,\n    );\n    return tableRErrs;\n  }, [tableRules, table, contextData]);\n\n  return localTableRulesErrors;\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/UserStats.tsx",
    "content": "import { mdiMagnify } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport React from \"react\";\nimport type { ExtraProps } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport SmartTable from \"../SmartTable\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\n\ntype UserStatsProps = Pick<\n  ExtraProps,\n  \"dbs\" | \"dbsTables\" | \"dbsMethods\" | \"theme\"\n>;\n\nexport const UserStats = ({\n  dbs,\n  dbsTables,\n  dbsMethods,\n  theme,\n}: UserStatsProps) => {\n  const existingUserStats = usePromise(\n    () =>\n      dbs.users.find(\n        {},\n        {\n          select: { type: 1, count: { $countAll: [] } },\n          orderBy: { count: -1 } as any,\n        },\n      ) as Promise<\n        {\n          type: string;\n          count: number;\n        }[]\n      >,\n  );\n\n  const userCount = existingUserStats?.reduce((a, v) => a + +v.count, 0) ?? 0;\n\n  return (\n    <PopupMenu\n      className=\"UserStats\"\n      button={\n        <Btn\n          title={`Search ${userCount} users`}\n          iconPath={mdiMagnify}\n          color=\"action\"\n          size=\"small\"\n        />\n      }\n      onClickClose={false}\n      positioning=\"center\"\n      clickCatchStyle={{ opacity: 0.5 }}\n      title={\"All users\"}\n      showFullscreenToggle={{}}\n      footerButtons={[\n        {\n          label: \"Close\",\n          onClickClose: true,\n        },\n      ]}\n      render={() => (\n        <div\n          className=\"UserStats flex-col gap-1 pb -1 o-auto\"\n          style={{ minWidth: \"250px\" }}\n        >\n          <SmartTable\n            key={\"selectedRuleId\"}\n            db={dbs as DBHandlerClient}\n            methods={dbsMethods}\n            filter={[\n              { fieldName: \"type\", type: \"$in\", value: [], disabled: true },\n            ]}\n            tableName=\"users\"\n            tables={dbsTables}\n            allowEdit={true}\n            showInsert={true}\n          />\n        </div>\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/UserSyncConfig.tsx",
    "content": "import { mdiSync } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useCallback, useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { getSqlSuggestions } from \"../SQLEditor/SQLEditorSuggestions\";\nimport { SQLSmartEditor } from \"../SQLEditor/SQLSmartEditor\";\n\nconst getColDefs = (columns: ValidatedColumnInfo[]) =>\n  columns.map((c) =>\n    [\n      JSON.stringify(c.name),\n      c.udt_name.toUpperCase(),\n      c.is_pkey ? \"PRIMARY KEY\"\n      : c.is_nullable ? \"\"\n      : \"NOT NULL\",\n      ![null, undefined].includes(c.column_default) ?\n        `DEFAULT ${c.column_default}`\n      : \"\",\n    ]\n      .filter((v) => v)\n      .join(\" \"),\n  );\n\nexport const UserSyncConfig = ({\n  databaseId,\n  dbs,\n  dbsTables,\n  tables,\n  db,\n  dbKey,\n  connectionId,\n}: Prgl) => {\n  const { data: dbConf } = dbs.database_configs.useSubscribeOne({\n    id: databaseId,\n  });\n  const dbsTable = dbsTables.find((t) => t.name === \"users\");\n  const getState = useCallback(() => {\n    const table = tables.find((t) => t.name === \"users\");\n    if (!dbsTable)\n      return {\n        err: \"no-dbs-users-table\" as const,\n      };\n    const createColStatements = getColDefs(\n      dbsTable.columns.filter((c) => !c.is_generated),\n    );\n    const createUsersTableQuery = `CREATE TABLE users (\\n${createColStatements.map((c) => `  ${c}`).join(\",\\n\")}\\n);`;\n    if (!table) {\n      return {\n        err: \"no-users-table\" as const,\n        query: createUsersTableQuery,\n      };\n    }\n    const requiredSyncFields = [\"id\", \"last_updated\"];\n    const missingColumns = dbsTable.columns.filter(\n      (c) =>\n        requiredSyncFields.includes(c.name) &&\n        !table.columns.find(\n          (tc) => tc.name === c.name && tc.udt_name === c.udt_name,\n        ),\n    );\n    if (missingColumns.length) {\n      return {\n        err: \"missing-cols\" as const,\n        query: `ALTER TABLE users ${getColDefs(missingColumns)\n          .map((def) => `\\nADD COLUMN ${def}`)\n          .join(\n            \",\",\n          )};\\n\\n/* Syncable users fields: */\\n${createUsersTableQuery}`,\n      };\n    }\n\n    return { err: \"none\" as const };\n  }, [tables, dbsTable]);\n\n  const suggestions = usePromise(async () => {\n    const suggestions = await getSqlSuggestions({ sql: db.sql! });\n    return suggestions;\n  }, [db.sql]);\n  const [localState, setLocalState] = useState<ReturnType<typeof getState>>();\n\n  if (localState?.err === \"no-dbs-users-table\") {\n    return <ErrorComponent error={\"Unexpected: no dbs users table\"} />;\n  }\n\n  let sqlEditor: React.ReactNode = null;\n  if (localState?.query && suggestions) {\n    const title =\n      localState.err === \"no-users-table\" ?\n        \"Must create the users table\"\n      : \"Must add missing columns to the users table\";\n    sqlEditor = (\n      <SQLSmartEditor\n        asPopup={false}\n        key={localState.query}\n        sql={db.sql!}\n        query={localState.query}\n        title={title}\n        contentTop={<p className=\"ta-left m-0 p-0\">{title}</p>}\n        suggestions={{ dbKey, connectionId, onRenew: () => {}, ...suggestions }}\n        onSuccess={() => {\n          setLocalState(undefined);\n          dbs.database_configs.update({ id: databaseId }, { sync_users: true });\n        }}\n        onCancel={() => {\n          setLocalState(undefined);\n        }}\n      />\n    );\n  }\n\n  return (\n    <PopupMenu\n      title=\"Sync users\"\n      positioning=\"beneath-center\"\n      button={\n        <Btn\n          iconPath={mdiSync}\n          color={dbConf?.sync_users ? \"action\" : undefined}\n          variant={\"faded\"}\n        >\n          Sync users...\n        </Btn>\n      }\n      clickCatchStyle={{ opacity: 1 }}\n      render={(pClose) => (\n        <FlexCol>\n          <p className=\"ta-start\">\n            When enabled, all users with access rights to this database will be\n            upserted from the state database to the \"users\" table from this\n            database.\n            <br></br>\n            Only columns that match the \"users\" table columns by name and data\n            type will be upserted.\n          </p>\n          <SwitchToggle\n            checked={dbConf?.sync_users ?? false}\n            label=\"Sync users\"\n            onChange={(sync_users) => {\n              const doUpdate = () =>\n                dbs.database_configs.update({ id: databaseId }, { sync_users });\n              if (!sync_users) {\n                doUpdate();\n                return;\n              }\n              const state = getState();\n              if (state.err !== \"none\") {\n                setLocalState(state);\n                return;\n              }\n              doUpdate();\n            }}\n          />\n          {sqlEditor}\n        </FlexCol>\n      )}\n      footerButtons={[{ label: \"Close\", onClickClose: true, variant: \"faded\" }]}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/UserTypeSelect.tsx",
    "content": "import { mdiAccountOutline, mdiAccountQuestion, mdiAccountStar } from \"@mdi/js\";\nimport React from \"react\";\nimport type { TestSelectors } from \"../../Testing\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport type { DBS } from \"../Dashboard/DBS\";\nimport { SmartSelect } from \"../SmartSelect\";\nimport type { TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { UserType } from \"./useEditedAccessRule\";\n\ntype P = {\n  dbs: DBS;\n  userTypes: DBSSchema[\"user_types\"][\"id\"][];\n  connectionId: string;\n  database_id: number;\n  onChange: (userTypes: DBSSchema[\"user_types\"][\"id\"][]) => void;\n\n  /**\n   * Excluded from disabledInfo\n   */\n  fromEditedRule?: string[];\n} & TestSelectors;\n\nexport const UserTypeSelect = (props: P) => {\n  const {\n    dbs,\n    userTypes = [],\n    fromEditedRule,\n    onChange,\n    database_id,\n    ...selectors\n  } = props;\n  const subParams = { select: { user_type: 1 }, returnType: \"values\" } as const;\n  const { data: existingACUserTypes } =\n    dbs.access_control_user_types.useSubscribe(\n      { $existsJoined: { access_control: { database_id } } },\n      subParams,\n    );\n\n  return (\n    <SmartSelect\n      {...selectors}\n      popupTitle=\"User types\"\n      placeholder=\"New or existing user type\"\n      fieldName=\"id\"\n      //@ts-ignore\n      onChange={onChange}\n      tableHandler={dbs.user_types as TableHandlerClient}\n      values={userTypes}\n      getLabel={(_id) => {\n        const id = _id as UserType;\n        let subLabel = \"\",\n          disabledInfo = \"\";\n        if (id === \"admin\") {\n          disabledInfo = \"Cannot change admin\";\n          subLabel = \"Can always access everything\";\n        } else {\n          const existingRules =\n            !fromEditedRule?.includes(id) && existingACUserTypes?.includes(id);\n          if (existingRules) {\n            disabledInfo = \"Need to remove from existing access rule first\";\n            subLabel = `Already assigned permissions`;\n          } else if (\n            (userTypes.includes(\"public\") && !userTypes.includes(id)) ||\n            (userTypes.length &&\n              !userTypes.includes(\"public\") &&\n              id === \"public\")\n          ) {\n            disabledInfo = \"Cannot mix 'public' with other user types\";\n          }\n        }\n\n        return {\n          subLabel,\n          disabledInfo,\n          contentLeft: (\n            <Icon\n              className=\"mr-1\"\n              style={{ opacity: 0.75 }}\n              path={\n                id === \"admin\" ? mdiAccountStar\n                : id === \"public\" ?\n                  mdiAccountQuestion\n                : mdiAccountOutline\n              }\n            />\n          ),\n        };\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/useAccessControlSearchParams.ts",
    "content": "import { useSearchParams } from \"react-router-dom\";\nimport type { AccessControlAction } from \"./AccessControl\";\nimport { isEqual } from \"prostgles-types\";\n\nconst SEARCH_PARAMS = {\n  SELECTED_RULE_ID: \"editRuleId\",\n  CREATE_RULE: \"createRule\",\n  // EDITED_TABLE_NAME: \"editedTableName\",\n} as const;\n\nconst getActionFromSearchParams = (\n  searchParams: URLSearchParams,\n): AccessControlAction | undefined => {\n  const selectedRuleId = searchParams.get(SEARCH_PARAMS.SELECTED_RULE_ID);\n  const createRule = searchParams.get(SEARCH_PARAMS.CREATE_RULE);\n  // const editedTableName = searchParams.get(SEARCH_PARAMS.EDITED_TABLE_NAME);\n  const action: AccessControlAction | undefined =\n    selectedRuleId ?\n      {\n        type: \"edit\",\n        selectedRuleId: +selectedRuleId,\n      }\n    : createRule ?\n      {\n        type: \"create\",\n      }\n    : undefined;\n\n  return action;\n};\n\nexport const useAccessControlSearchParams = () => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const action = getActionFromSearchParams(searchParams);\n\n  const setAction = (newAction: AccessControlAction | undefined) => {\n    /** useSearchParams is not instant so must get the actual latest value */\n    const currentAction = getActionFromSearchParams(\n      new URLSearchParams(window.location.search),\n    );\n    if (isEqual(newAction, currentAction)) return;\n\n    if (!newAction) {\n      searchParams.delete(SEARCH_PARAMS.CREATE_RULE);\n      searchParams.delete(SEARCH_PARAMS.SELECTED_RULE_ID);\n      setSearchParams(searchParams);\n    } else if (newAction.type === \"create\") {\n      searchParams.delete(SEARCH_PARAMS.SELECTED_RULE_ID);\n      searchParams.set(SEARCH_PARAMS.CREATE_RULE, \"true\");\n      setSearchParams(searchParams);\n    } else if (newAction.type === \"create-default\") {\n      searchParams.delete(SEARCH_PARAMS.SELECTED_RULE_ID);\n      searchParams.delete(SEARCH_PARAMS.CREATE_RULE);\n      setSearchParams(searchParams);\n    } else {\n      searchParams.delete(SEARCH_PARAMS.CREATE_RULE);\n      searchParams.set(\n        SEARCH_PARAMS.SELECTED_RULE_ID,\n        newAction.selectedRuleId.toString(),\n      );\n      setSearchParams(searchParams);\n    }\n  };\n\n  return { action, setAction };\n};\n\nexport const getAccessControlHref = ({\n  connectionId,\n  ...otherOpts\n}: {\n  connectionId: string;\n  selectedRuleId?: string;\n  tableName?: string;\n}) => {\n  const sp = new URLSearchParams();\n  sp.set(\"section\", \"access_control\");\n  if (otherOpts.selectedRuleId)\n    sp.set(SEARCH_PARAMS.SELECTED_RULE_ID, otherOpts.selectedRuleId);\n  return `/connection-config/${connectionId}?${sp}`;\n};\n"
  },
  {
    "path": "client/src/dashboard/AccessControl/useEditedAccessRule.ts",
    "content": "import { useIsMounted, usePromise } from \"prostgles-client\";\nimport { isEmpty } from \"prostgles-types\";\nimport { useMemo, useState } from \"react\";\nimport type {\n  ContextDataObject,\n  DBSSchema,\n  TableRulesErrors,\n} from \"@common/publishUtils\";\nimport { getTableRulesErrors } from \"@common/publishUtils\";\nimport { areEqual, quickClone } from \"../../utils/utils\";\nimport type { AccessControlAction, EditedAccessRule } from \"./AccessControl\";\nimport { ACCESS_CONTROL_SELECT } from \"./AccessControl\";\nimport type { PermissionEditProps } from \"./AccessControlRuleEditor\";\nimport type { WorspaceTableAndColumns } from \"./PublishedWorkspaceSelector\";\nimport { getWorkspaceTables } from \"./PublishedWorkspaceSelector\";\n\ntype P = Pick<PermissionEditProps, \"action\" | \"prgl\">;\n\ntype TableErrors = Record<string, TableRulesErrors>;\n\nconst defaultRule: EditedAccessRule = {\n  access_control_user_types: [],\n  name: \"\",\n  llm_daily_limit: 0,\n  dbPermissions: {\n    type: \"All views/tables\",\n    allowAllTables: [],\n  },\n  published_methods: [],\n  created: null,\n  dbsPermissions: {\n    createWorkspaces: true,\n  },\n  access_control_allowed_llm: [],\n  access_control_methods: [],\n};\n\nexport type UserType = DBSSchema[\"user_types\"][\"id\"];\n\nexport type ValidEditedAccessRuleState = (\n  | (Extract<AccessControlAction, { type: \"edit\" }> & {\n      rule: EditedAccessRule;\n      newRule: EditedAccessRule;\n      initialUserTypes: UserType[];\n    })\n  | {\n      type: \"create\";\n      newRule:\n        | Partial<\n            Pick<\n              EditedAccessRule,\n              | \"access_control_user_types\"\n              | \"dbPermissions\"\n              | \"dbsPermissions\"\n              | \"access_control_allowed_llm\"\n              | \"access_control_methods\"\n              | \"llm_daily_limit\"\n            >\n          >\n        | undefined;\n      rule: undefined;\n      initialUserTypes?: undefined;\n    }\n  | {\n      type: \"create-complete\";\n      newRule: EditedAccessRule;\n      rule: undefined;\n      initialUserTypes?: undefined;\n    }\n) & {\n  tableErrors: TableErrors | undefined;\n  contextData: ContextDataObject | undefined;\n  onChange: (newRule: Partial<Omit<EditedAccessRule, \"\">>) => void;\n  userTypes: UserType[];\n  ruleWasEdited: boolean;\n  ruleErrorMessage: string | undefined;\n  worspaceTableAndColumns: WorspaceTableAndColumns[] | undefined;\n};\n\nexport type EditedAccessRuleState =\n  | ValidEditedAccessRuleState\n  | {\n      type: \"edit-not-found\";\n    };\n\nexport const useEditedAccessRule = ({\n  action,\n  prgl,\n}: P): EditedAccessRuleState | undefined => {\n  const { dbs, tables } = prgl;\n  const [newRule, setNewRule] = useState<Partial<EditedAccessRule>>();\n  const userTypes = useMemo(\n    () => newRule?.access_control_user_types?.flatMap((d) => d.ids) ?? [],\n    [newRule],\n  );\n\n  const getIsMounted = useIsMounted();\n  const ruleData = usePromise(async () => {\n    if (action.type === \"edit\") {\n      const rule: EditedAccessRule | undefined =\n        await dbs.access_control.findOne(\n          { id: action.selectedRuleId },\n          ACCESS_CONTROL_SELECT,\n        );\n      if (!rule) {\n        return \"edit-not-found\";\n      }\n      const userTypes = rule.access_control_user_types.flatMap((d) => d.ids);\n      const ruleUserFilter =\n        userTypes.length ? { type: { $in: userTypes } } : {};\n      let user = await dbs.users.findOne(ruleUserFilter);\n      /** If no users for the give types then fake an existing one */\n      if (!user) {\n        user = await dbs.users.findOne({});\n        if (user) {\n          user.type = userTypes[0]!;\n        }\n      }\n      if (!getIsMounted()) {\n        return undefined;\n      }\n      const initialUserTypes = rule.access_control_user_types.flatMap(\n        (d) => d.ids,\n      );\n      setNewRule(quickClone(rule));\n      return {\n        ...action,\n        rule: quickClone(rule),\n        initialUserTypes,\n        contextData: !user ? undefined : { user },\n      };\n    }\n    const user = await dbs.users.findOne({ status: \"active\" });\n    return {\n      ...action,\n      type: \"create\" as const,\n      rule: undefined,\n      contextData: !user ? undefined : { user },\n    };\n  }, [getIsMounted, action, dbs.access_control, dbs.users]);\n\n  const wspTables = usePromise(async () => {\n    const workspaceIds =\n      newRule?.dbsPermissions?.viewPublishedWorkspaces?.workspaceIds ?? [];\n    return !newRule?.dbPermissions ?\n        undefined\n      : await getWorkspaceTables(\n          dbs,\n          workspaceIds,\n          newRule.dbPermissions,\n          tables,\n        );\n  }, [newRule, dbs, tables]);\n\n  const tableErrors = usePromise(async () => {\n    if (action.selectedRuleId && newRule) {\n      return await getAccessRuleTableErrors({ prgl }, newRule, userTypes);\n    }\n    return undefined;\n  }, [newRule, action.selectedRuleId, prgl, userTypes]);\n\n  if (!ruleData) {\n    return undefined;\n  }\n\n  if (ruleData === \"edit-not-found\") {\n    return {\n      type: ruleData,\n    };\n  }\n\n  const ruleWasEdited =\n    !newRule ? false : (\n      action.type === \"create\" ||\n      Boolean(\n        ruleData.rule &&\n        !areEqual(newRule, ruleData.rule, [\n          \"access_control_user_types\",\n          \"access_control_allowed_llm\",\n          \"access_control_methods\",\n          \"dbPermissions\",\n          \"dbsPermissions\",\n          \"published_methods\",\n          \"llm_daily_limit\",\n        ]),\n      )\n    );\n\n  const result: ValidEditedAccessRuleState = {\n    ...ruleData,\n    newRule: newRule as any,\n    userTypes,\n    worspaceTableAndColumns: wspTables?.worspaceTableAndColumns,\n    tableErrors,\n    ruleErrorMessage: newRule && getRuleErrorMessage(newRule),\n    ruleWasEdited,\n    onChange: (newRulePart: Partial<EditedAccessRule>) =>\n      setNewRule({\n        ...newRule,\n        ...newRulePart,\n      }),\n  };\n\n  if (\n    result.type === \"create\" &&\n    result.newRule?.access_control_user_types &&\n    result.newRule.dbPermissions\n  ) {\n    return {\n      ...result,\n      type: \"create-complete\",\n      newRule: {\n        ...defaultRule,\n        ...newRule,\n      },\n    };\n  }\n\n  return result;\n};\n\nconst getRuleErrorMessage = (\n  newRule: EditedAccessRule | Partial<EditedAccessRule>,\n) => {\n  const { dbPermissions } = newRule;\n  if (dbPermissions && dbPermissions.type !== \"Run SQL\") {\n    const allowedTables =\n      dbPermissions.type === \"Custom\" ?\n        !!dbPermissions.customTables.filter(\n          (t) => t.select || t.update || t.insert || t.delete,\n        ).length\n      : !!dbPermissions.allowAllTables.length;\n\n    if (!allowedTables && !newRule.access_control_methods?.length) {\n      return \"Empty rule. Must allow at least one table or server function\";\n    }\n  } else if (dbPermissions?.type === \"Run SQL\" && !dbPermissions.allowSQL) {\n    return `Must tick \"Run SQL\" checkbox`;\n  }\n\n  const userTypes =\n    newRule.access_control_user_types?.flatMap((d) => d.ids) ?? [];\n  if (userTypes.includes(\"public\") && userTypes.length !== 1) {\n    return \"Cannot mix 'public' and non-public user types\";\n  }\n\n  return undefined;\n};\n\nconst getAccessRuleTableErrors = async (\n  { prgl: { dbs, tables } }: Pick<P, \"prgl\">,\n  rule: Partial<EditedAccessRule>,\n  userTypes: DBSSchema[\"user_types\"][\"id\"][],\n) => {\n  if (\n    rule.dbPermissions?.type !== \"Custom\" ||\n    !rule.dbPermissions.customTables.length\n  ) {\n    return undefined;\n  }\n\n  const { dbPermissions } = rule;\n  const ruleUser = await dbs.users.findOne({ type: { $in: userTypes } });\n\n  if (ruleUser) {\n    const newContextData: ContextDataObject = { user: ruleUser };\n    const result: Record<string, TableRulesErrors> = {};\n    await Promise.all(\n      dbPermissions.customTables.map(async (tableRules) => {\n        const tableName = tableRules.tableName;\n\n        if (!tables.some((t) => t.name === tableName)) {\n          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n          result[tableName] ??= {};\n          result[tableName].all = `Table ${tableName} could not be found`;\n        } else if (!result[tableName]?.all) {\n          const columnNames = tables\n            .find((t) => t.name === tableName)\n            ?.columns.map((c) => c.name);\n          const errObj = await getTableRulesErrors(\n            tableRules,\n            columnNames!,\n            newContextData,\n          );\n          if (!isEmpty(errObj)) {\n            result[tableName] = errObj;\n          }\n        }\n      }),\n    );\n    return result;\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/AskLLM.tsx",
    "content": "import { mdiAssistant } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport type { LoadedSuggestions } from \"../Dashboard/dashboardUtils\";\nimport { AskLLMChat } from \"./Chat/AskLLMChat\";\nimport { SetupLLMCredentials } from \"./Setup/SetupLLMCredentials\";\nimport { useLLMSetupState } from \"./Setup/useLLMSetupState\";\n\ntype P = Prgl & {\n  workspaceId: string | undefined;\n  loadedSuggestions: LoadedSuggestions | undefined;\n};\n\nexport const AskLLM = (props: P) => {\n  const { workspaceId, loadedSuggestions, ...prgl } = props;\n  const { dbsMethods } = prgl;\n  const { askLLM, stopAskLLM } = dbsMethods;\n\n  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);\n  const onClose = () => {\n    setAnchorEl(null);\n  };\n  const state = useLLMSetupState(prgl);\n\n  return (\n    <>\n      <Btn\n        title={\n          t.AskLLM[\"Chat to an AI Assistant to get help with your queries\"]\n        }\n        variant=\"faded\"\n        color=\"action\"\n        iconPath={mdiAssistant}\n        data-command=\"AskLLM\"\n        onClick={(e) => {\n          setAnchorEl(e.currentTarget);\n        }}\n        loading={state.state === \"loading\"}\n        disabledInfo={\n          !askLLM ?\n            t.AskLLM[\"AI assistant not available. Talk to the admin\"]\n          : undefined\n        }\n      >\n        {/* {window.isMediumWidthScreen ? null : t.AskLLM[\"AI Assistant\"]} */}\n      </Btn>\n\n      {!anchorEl || !askLLM || !stopAskLLM ?\n        null\n      : state.state !== \"ready\" ?\n        <SetupLLMCredentials\n          {...prgl}\n          asPopup={true}\n          setupState={state}\n          onClose={onClose}\n        />\n      : <AskLLMChat\n          loadedSuggestions={loadedSuggestions}\n          prgl={prgl}\n          askLLM={askLLM}\n          stopAskLLM={stopAskLLM}\n          workspaceId={workspaceId}\n          setupState={state}\n          anchorEl={anchorEl}\n          onClose={onClose}\n        />\n      }\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChat.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { Chat } from \"@components/Chat/Chat\";\nimport { FlexCol } from \"@components/Flex\";\nimport Popup from \"@components/Popup/Popup\";\nimport React from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport type { LoadedSuggestions } from \"../../Dashboard/dashboardUtils\";\nimport { AskLLMChatActionBar } from \"../ChatActionBar/AskLLMChatActionBar\";\nimport type { LLMSetupStateReady } from \"../Setup/useLLMSetupState\";\nimport { AskLLMToolApprover } from \"../Tools/AskLLMToolApprover\";\nimport { AskLLMChatHeader } from \"./AskLLMChatHeader\";\nimport { useAskLLMChatSend } from \"./useAskLLMChatSend\";\nimport { useLLMChat } from \"./useLLMChat\";\nimport { useLLMSchemaStr } from \"./useLLMSchemaStr\";\nconst CHAT_WIDTH = 900;\n\nexport type AskLLMChatProps = Pick<\n  Required<Prgl[\"dbsMethods\"]>,\n  \"askLLM\" | \"stopAskLLM\"\n> & {\n  prgl: Prgl;\n  setupState: LLMSetupStateReady;\n  anchorEl: HTMLElement;\n  onClose: VoidFunction;\n  workspaceId: string | undefined;\n  loadedSuggestions: LoadedSuggestions | undefined;\n};\n\nexport const AskLLMChat = (props: AskLLMChatProps) => {\n  const {\n    anchorEl,\n    onClose,\n    prgl,\n    setupState,\n    workspaceId,\n    loadedSuggestions,\n    askLLM,\n    stopAskLLM,\n  } = props;\n  const { tables, db, user, connectionId, connection, dbs, methods } = prgl;\n  const chatState = useLLMChat({\n    ...setupState,\n    loadedSuggestions,\n    dbs,\n    user,\n    connectionId,\n    workspaceId,\n    db,\n  });\n  const {\n    messages,\n    activeChat,\n    activeChatId,\n    latestChats,\n    llmMessages,\n    prompt,\n  } = chatState;\n  const { preferredPromptId, createNewChat } = chatState;\n  const { dbSchemaForPrompt } = useLLMSchemaStr({\n    tables,\n    db,\n    connection,\n    activeChat,\n  });\n  const isAdmin = user?.type === \"admin\";\n  const { chatIsLoading, onStopSending, sendMessage, sendQuery } =\n    useAskLLMChatSend({\n      askLLM,\n      stopAskLLM,\n      activeChatId,\n      activeChat,\n      dbSchemaForPrompt,\n    });\n\n  /* Prevents flickering when popup is opened */\n  if (!messages) return;\n  return (\n    <Popup\n      data-command=\"AskLLM.popup\"\n      showFullscreenToggle={{\n        getContentStyle: (isFullscreen) =>\n          isFullscreen && !window.isLowWidthScreen ?\n            { alignItems: \"center\" }\n          : {},\n        getStyle: (isFullscreen) =>\n          isFullscreen ?\n            {}\n          : {\n              width: `min(100vw, ${CHAT_WIDTH}px)`,\n              minWidth: \"0\",\n              maxWidth: `${CHAT_WIDTH}px`,\n            },\n      }}\n      title={(rootDiv) => (\n        <AskLLMChatHeader\n          {...setupState}\n          {...chatState}\n          chatRootDiv={rootDiv}\n        />\n      )}\n      positioning=\"right-panel\"\n      clickCatchStyle={{ opacity: 0.1 }}\n      onClickClose={false}\n      onClose={onClose}\n      anchorEl={anchorEl}\n      contentClassName=\"p-0 f-1\"\n      rootStyle={{\n        flex: 1,\n      }}\n      rootChildStyle={{\n        flex: 1,\n      }}\n      contentStyle={{\n        width: \"100%\",\n        overflow: \"unset\",\n      }}\n      rootChildClassname=\"AskLLMChat\"\n    >\n      {activeChat && (\n        <FlexCol\n          className=\"min-h-0 f-1\"\n          style={{\n            whiteSpace: \"pre-line\",\n            /**\n             * Expand to 800px but shrink on smaller screens\n             */\n            minWidth: \"min(100%, max(800px, 100%))\",\n            width: \"100%\",\n          }}\n        >\n          <Chat\n            style={chatStyle}\n            messages={messages}\n            disabledInfo={activeChat.disabled_message ?? undefined}\n            maxWidth={CHAT_WIDTH}\n            onSend={sendMessage}\n            currentlyTypedMessage={activeChat.currently_typed_message}\n            onCurrentlyTypedMessageChange={(currently_typed_message) => {\n              void dbs.llm_chats.update(\n                { id: activeChat.id },\n                { currently_typed_message },\n              );\n            }}\n            isLoading={chatIsLoading}\n            onStopSending={onStopSending}\n            actionBar={\n              isAdmin && (\n                <AskLLMChatActionBar\n                  prgl={prgl}\n                  activeChat={activeChat}\n                  setupState={setupState}\n                  prompt={prompt}\n                  dbSchemaForPrompt={dbSchemaForPrompt}\n                  llmMessages={llmMessages ?? []}\n                />\n              )\n            }\n          />\n          {prompt && (\n            <AskLLMToolApprover\n              connection={connection}\n              dbs={dbs}\n              activeChat={activeChat}\n              messages={llmMessages ?? []}\n              methods={methods}\n              sendQuery={sendQuery}\n              db={db}\n              prompt={prompt}\n            />\n          )}\n        </FlexCol>\n      )}\n      {latestChats && !activeChat && (\n        <Btn\n          onClickPromise={async () => createNewChat(preferredPromptId)}\n          className=\"m-2\"\n          color=\"action\"\n          variant=\"filled\"\n          data-command=\"AskLLMChat.NewChat\"\n        >\n          Start new chat\n        </Btn>\n      )}\n    </Popup>\n  );\n};\n\nconst chatStyle = {\n  minWidth: `min(${CHAT_WIDTH}px, 100%)`,\n  minHeight: \"0\",\n} satisfies React.CSSProperties;\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatHeader.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiPlus } from \"@mdi/js\";\nimport React from \"react\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport { getPGIntervalAsText } from \"../../W_SQL/customRenderers\";\nimport {\n  AskLLMChatOptions,\n  type LLMChatOptionsProps,\n} from \"./AskLLMChatOptions\";\nimport type { LLMChatState } from \"./useLLMChat\";\n\nexport const AskLLMChatHeader = (\n  props: LLMChatState & Pick<LLMChatOptionsProps, \"chatRootDiv\" | \"prompts\">,\n) => {\n  const {\n    activeChat,\n    credentials,\n    activeChatId,\n    latestChats,\n    createNewChat,\n    preferredPromptId,\n    setActiveChat,\n    chatRootDiv,\n    prompts,\n  } = props;\n\n  return (\n    <FlexRow className=\"AskLLMChatHeader\">\n      <FlexCol className=\"gap-p25\">\n        <div>{t.AskLLM[\"AI Assistant\"]}</div>\n      </FlexCol>\n      <FlexRow className=\"gap-p25 min-w-0\">\n        <AskLLMChatOptions\n          prompts={prompts}\n          activeChat={activeChat}\n          activeChatId={activeChatId}\n          credentials={credentials}\n          chatRootDiv={chatRootDiv}\n        />\n        <Select\n          title={t.AskLLMChatHeader.Chat}\n          data-command=\"LLMChat.select\"\n          fullOptions={\n            latestChats?.map((c) => ({\n              key: c.id,\n              label: c.name,\n              subLabel: getPGIntervalAsText(c.created_ago, true, true, true),\n            })) ?? []\n          }\n          value={activeChatId}\n          showSelectedSublabel={true}\n          style={{\n            flex: 1,\n            minWidth: \"80px\",\n            maxWidth: \"fit-content\",\n          }}\n          onChange={(v) => {\n            setActiveChat(v);\n          }}\n        />\n        <Btn\n          iconPath={mdiPlus}\n          title={t.AskLLMChatHeader[\"New chat\"]}\n          data-command=\"AskLLMChat.NewChat\"\n          variant=\"faded\"\n          color=\"action\"\n          disabledInfo={\n            !preferredPromptId ?\n              t.AskLLMChatHeader[\"No prompt found\"]\n            : undefined\n          }\n          onClickPromise={async () => {\n            if (!preferredPromptId)\n              throw new Error(t.AskLLMChatHeader[\"No prompt found\"]);\n            await createNewChat(preferredPromptId);\n          }}\n        />\n      </FlexRow>\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/LLMChatMessage/LLMChatMessage.tsx",
    "content": "import ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport { isEqual } from \"prostgles-types\";\nimport React, { memo } from \"react\";\nimport { Counter } from \"src/dashboard/W_SQL/W_SQL\";\nimport type { UseLLMChatProps } from \"../../useLLMChat\";\nimport type { LLMMessageItem } from \"../hooks/useLLMChatMessageGrouper\";\nimport { LLMGroupedToolCallsMessage } from \"./LLMGroupedToolCallsMessage\";\nimport { LLMSingleChatMessage } from \"./LLMSingleChatMessage\";\n\nexport type LLMChatMessageCommonProps = Pick<\n  UseLLMChatProps,\n  \"db\" | \"mcpServerIcons\" | \"workspaceId\" | \"loadedSuggestions\"\n>;\n\ntype P = LLMChatMessageCommonProps & {\n  messageItem: LLMMessageItem;\n  isLoadingSinceDate: Date | undefined;\n};\n\nexport const LLMChatMessage = memo(\n  (props: P) => {\n    const {\n      messageItem,\n      isLoadingSinceDate,\n      db,\n      mcpServerIcons,\n      loadedSuggestions,\n      workspaceId,\n    } = props;\n\n    const message =\n      messageItem.type === \"single_message\" ?\n        messageItem.message\n      : messageItem.firstMessage;\n    const { id, meta } = message;\n    return (\n      <FlexCol>\n        {messageItem.type === \"single_message\" ?\n          <LLMSingleChatMessage\n            key={`${id}-single_message `}\n            messageItem={messageItem}\n            mcpServerIcons={mcpServerIcons}\n            workspaceId={workspaceId}\n            db={db}\n            loadedSuggestions={loadedSuggestions}\n          />\n        : <LLMGroupedToolCallsMessage\n            messages={messageItem.messages}\n            messageContentItems={messageItem.messageContentItems}\n            onToggle={messageItem.onToggle}\n            mcpServerIcons={mcpServerIcons}\n            db={db}\n            loadedSuggestions={loadedSuggestions}\n          />\n        }\n        {isLoadingSinceDate && (\n          <>\n            <Loading />\n            <Counter from={isLoadingSinceDate} />\n          </>\n        )}\n        {(meta?.stop_reason as string | undefined)?.toLowerCase() ===\n          \"max_tokens\" && (\n          <ErrorComponent\n            error={`stop_reason: \"max_tokens\".\\n\\nThe response was cut off because it reached the maximum token limit`}\n          />\n        )}\n      </FlexCol>\n    );\n  },\n  (prev, next) => {\n    const areEqual = isEqual(prev, next);\n    return areEqual;\n  },\n);\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/LLMChatMessage/LLMChatMessageContent.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React from \"react\";\nimport type { LoadedSuggestions } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport { LLMChatMessageContentText } from \"./LLMChatMessageContentText\";\nimport {\n  ToolUseChatMessage,\n  type LLMMessageContent,\n} from \"../ToolUseChatMessage/ToolUseChatMessage\";\n\nexport const LLMChatMessageContent = ({\n  messageContent,\n  messageContentIndex,\n  message,\n  nextMessage,\n  loadedSuggestions,\n  db,\n  mcpServerIcons,\n  workspaceId,\n}: {\n  messageContent: Exclude<LLMMessageContent, { type: \"tool_result\" }>;\n  messageContentIndex: number;\n  message: DBSSchema[\"llm_messages\"];\n  nextMessage: DBSSchema[\"llm_messages\"] | undefined;\n  loadedSuggestions: LoadedSuggestions | undefined;\n  db: DBHandlerClient;\n  workspaceId: string | undefined;\n  mcpServerIcons: Map<string, string>;\n}) => {\n  const sqlHandler = db.sql;\n  if (messageContent.type === \"text\" && \"text\" in messageContent) {\n    return (\n      <LLMChatMessageContentText\n        messageContent={messageContent}\n        db={db}\n        loadedSuggestions={loadedSuggestions}\n      />\n    );\n  }\n  if (messageContent.type !== \"tool_use\") {\n    return (\n      <MediaViewer\n        url={messageContent.source.data}\n        style={{\n          maxHeight: \"200px\",\n          maxWidth: \"fit-content\",\n          // border: \"1px solid var(--b-color)\",\n        }}\n      />\n    );\n  }\n\n  return (\n    <ToolUseChatMessage\n      message={message}\n      nextMessage={nextMessage}\n      toolUseMessageContentIndex={messageContentIndex}\n      sqlHandler={sqlHandler}\n      loadedSuggestions={loadedSuggestions}\n      workspaceId={workspaceId}\n      mcpServerIcons={mcpServerIcons}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/LLMChatMessage/LLMChatMessageContentText.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { Marked } from \"@components/Chat/Marked\";\nimport Expander from \"@components/Expander\";\nimport { mdiBrain } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React from \"react\";\nimport type { LoadedSuggestions } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport { type LLMMessageContent } from \"../ToolUseChatMessage/ToolUseChatMessage\";\n\nexport const LLMChatMessageContentText = (props: {\n  messageContent: Extract<LLMMessageContent, { type: \"text\"; text: string }>;\n  loadedSuggestions: LoadedSuggestions | undefined;\n  db: DBHandlerClient;\n}) => {\n  const { messageContent, loadedSuggestions, db } = props;\n\n  const sqlHandler = db.sql;\n  return (\n    <React.Fragment>\n      {messageContent.reasoning && (\n        <Expander\n          getButton={() => (\n            <Btn title=\"Reasoning\" iconPath={mdiBrain} variant=\"faded\">\n              Reasoning...\n            </Btn>\n          )}\n        >\n          <Marked\n            codeHeader={undefined}\n            content={messageContent.reasoning}\n            sqlHandler={sqlHandler}\n            loadedSuggestions={loadedSuggestions}\n          />\n        </Expander>\n      )}\n      <Marked\n        codeHeader={undefined}\n        content={messageContent.text}\n        sqlHandler={sqlHandler}\n        loadedSuggestions={loadedSuggestions}\n      />\n    </React.Fragment>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/LLMChatMessage/LLMChatMessageHeader.tsx",
    "content": "import { filterArr } from \"@common/llmUtils\";\nimport type { AnyObject } from \"prostgles-types\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { CopyToClipboardBtn } from \"@components/CopyToClipboardBtn\";\nimport { FlexRow } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiChevronDown, mdiChevronUp, mdiDelete } from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport type { DBS } from \"src/dashboard/Dashboard/DBS\";\nimport { t } from \"src/i18n/i18nUtils\";\nimport type { LLMMessageItem } from \"../hooks/useLLMChatMessageGrouper\";\n\nexport const LLMChatMessageHeader = ({\n  item,\n  dbs,\n}: {\n  item: LLMMessageItem;\n  dbs: DBS;\n}) => {\n  const { cost, id, meta, textMessageToCopy, user_id, chat_id, created } =\n    useMemo(() => {\n      let textMessageToCopy: string | undefined;\n      let cost = 0;\n      if (item.type === \"single_message\") {\n        cost = item.message.cost ? parseFloat(item.message.cost) : 0;\n        const {\n          message: { message },\n        } = item;\n        const textMessages = filterArr(message, {\n          type: \"text\",\n        } as const);\n        textMessageToCopy =\n          textMessages.length && textMessages.length === message.length ?\n            textMessages.map((m) => m.text).join(\"\\n\")\n          : undefined;\n      } else {\n        cost = item.messages.reduce((acc, curr) => {\n          const currCost =\n            curr.message.cost ? parseFloat(curr.message.cost) : 0;\n          return acc + currCost;\n        }, 0);\n      }\n      const { id, user_id, chat_id, created } =\n        item.type === \"single_message\" ? item.message : item.firstMessage;\n\n      const meta =\n        item.type === \"single_message\" ?\n          (item.message.meta as AnyObject)\n        : undefined;\n\n      return {\n        id,\n        user_id,\n        textMessageToCopy,\n        cost,\n        meta,\n        chat_id,\n        created,\n      };\n    }, [item]);\n\n  const canCollapse = item.type === \"single_message\";\n  return (\n    <FlexRow className=\"show-on-parent-hover f-1 gap-p25\">\n      {!user_id && (\n        <Chip\n          className=\"ml-p5\"\n          title={JSON.stringify({ cost, ...meta }, null, 2)}\n        >\n          {`$${cost.toFixed(!cost ? 0 : 2)}`}\n        </Chip>\n      )}\n      <Select\n        title={t.common.Delete + \"...\"}\n        data-command=\"AskLLM.DeleteMessage\"\n        fullOptions={[\n          {\n            key: \"thisMessage\",\n            label: \"Delete this message\",\n          },\n          {\n            key: \"allToBottom\",\n            label: \"Delete this and all following messages\",\n          },\n        ]}\n        btnProps={{\n          size: \"micro\",\n          variant: \"icon\",\n          iconPath: mdiDelete,\n          children: \"\",\n        }}\n        onChange={async (option) => {\n          if (option === \"thisMessage\") {\n            await dbs.llm_messages.delete({ id });\n          } else {\n            await dbs.llm_messages.delete({\n              chat_id,\n              created: {\n                $gte: created,\n              },\n            });\n          }\n        }}\n        className=\"ml-auto\"\n      />\n      {textMessageToCopy && (\n        <CopyToClipboardBtn content={textMessageToCopy} size=\"micro\" />\n      )}\n      {item.onToggle && (\n        <Btn\n          title={canCollapse ? \"Collapse\" : \"Expand\"}\n          iconPath={canCollapse ? mdiChevronUp : mdiChevronDown}\n          onClick={item.onToggle}\n          size=\"micro\"\n        />\n      )}\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/LLMChatMessage/LLMGroupedToolCallsMessage.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport React, { useMemo } from \"react\";\nimport { isDefined } from \"src/utils/utils\";\nimport type { LLMMessageContent } from \"../ToolUseChatMessage/ToolUseChatMessage\";\nimport { getIconForToolUseMessage } from \"../ToolUseChatMessage/useToolUseChatMessage\";\nimport type { LLMChatMessageCommonProps } from \"./LLMChatMessage\";\nimport { LLMChatMessageContentText } from \"./LLMChatMessageContentText\";\nimport type { LLMMessageGroup } from \"../hooks/useLLMChatMessageGrouper\";\n\nexport const LLMGroupedToolCallsMessage = ({\n  messageContentItems,\n  mcpServerIcons,\n  db,\n  loadedSuggestions,\n  onToggle,\n  messages,\n}: {\n  messageContentItems: LLMMessageContent[];\n  messages: LLMMessageGroup[\"messages\"];\n  onToggle: VoidFunction;\n} & Pick<\n  LLMChatMessageCommonProps,\n  \"mcpServerIcons\" | \"db\" | \"loadedSuggestions\"\n>) => {\n  const { icons, toolCallCount } = useMemo(() => {\n    let toolCallCount = 0;\n    const iconPaths = messageContentItems\n      .map((m) => {\n        if (m.type === \"tool_use\") {\n          toolCallCount++;\n          return getIconForToolUseMessage(m, mcpServerIcons);\n        }\n      })\n      .filter(isDefined);\n    const icons = Array.from(new Set(iconPaths)).slice(0, 5);\n    return {\n      icons,\n      toolCallCount,\n    };\n  }, [messageContentItems, mcpServerIcons]);\n\n  const allMessagesAreErrored = useMemo(() => {\n    let totalToolResultMessages = 0;\n    let erroredToolResultMessages = 0;\n    messages.forEach(({ nextMessage }) => {\n      nextMessage?.message.forEach((m) => {\n        if (m.type === \"tool_result\") {\n          totalToolResultMessages++;\n          if (m.is_error) {\n            erroredToolResultMessages++;\n          }\n        }\n      });\n    });\n    return (\n      totalToolResultMessages > 0 &&\n      totalToolResultMessages === erroredToolResultMessages\n    );\n  }, [messages]);\n\n  const textMessages = useMemo(() => {\n    const textMessages = messageContentItems\n      .map((m) => {\n        if (m.type === \"text\" && \"text\" in m && m.text) {\n          return m;\n        }\n      })\n      .filter(isDefined);\n    return textMessages;\n  }, [messageContentItems]);\n  const firstTextMessage = textMessages[0];\n  const lastTextMessage = textMessages.at(-1);\n\n  return (\n    <>\n      {firstTextMessage && (\n        <LLMChatMessageContentText\n          messageContent={firstTextMessage}\n          db={db}\n          loadedSuggestions={loadedSuggestions}\n        />\n      )}\n      <Btn\n        variant=\"faded\"\n        size=\"small\"\n        color={allMessagesAreErrored ? \"danger\" : undefined}\n        onClick={onToggle}\n        data-command=\"ToolUseMessage.toggleGroup\"\n      >\n        {icons.map((iconPath) => {\n          return <SvgIcon key={iconPath} icon={iconPath} />;\n        })}\n        {toolCallCount} tool calls\n      </Btn>\n      {textMessages.length > 1 && lastTextMessage && (\n        <LLMChatMessageContentText\n          messageContent={lastTextMessage}\n          db={db}\n          loadedSuggestions={loadedSuggestions}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/LLMChatMessage/LLMSingleChatMessage.tsx",
    "content": "import { filterArrInverse } from \"@common/llmUtils\";\nimport type { LLMSingleMessage } from \"../hooks/useLLMChatMessageGrouper\";\nimport type { LLMChatMessageCommonProps } from \"./LLMChatMessage\";\nimport { LLMChatMessageContent } from \"./LLMChatMessageContent\";\nimport React from \"react\";\n\nexport const LLMSingleChatMessage = ({\n  messageItem,\n  db,\n  mcpServerIcons,\n  workspaceId,\n  loadedSuggestions,\n}: { messageItem: LLMSingleMessage } & LLMChatMessageCommonProps) => {\n  const { message: llmMessage, nextMessage } = messageItem;\n  const { id, message } = llmMessage;\n  const nonToolResultMessages = filterArrInverse(message, {\n    type: \"tool_result\",\n  } as const);\n  if (!nonToolResultMessages.length) {\n    return null;\n  }\n\n  return (\n    <>\n      {nonToolResultMessages.map((messageContent, idx) => {\n        return (\n          <LLMChatMessageContent\n            key={`${id}-${messageContent.type}-${idx}`}\n            messageContent={messageContent}\n            messageContentIndex={idx}\n            message={llmMessage}\n            nextMessage={nextMessage}\n            db={db}\n            workspaceId={workspaceId}\n            loadedSuggestions={loadedSuggestions}\n            mcpServerIcons={mcpServerIcons}\n          />\n        );\n      })}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/DockerSandboxCreateContainer.tsx",
    "content": "import { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport { getEntries, sliceText } from \"@common/utils\";\nimport { useAlert } from \"@components/AlertProvider\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { CopyToClipboardBtn } from \"@components/CopyToClipboardBtn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FILE_EXTENSION_TO_ICON_INFO } from \"@components/FileBrowser/FileBrowser\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { MenuList } from \"@components/MenuList\";\nimport {\n  MONACO_READONLY_DEFAULT_OPTIONS,\n  MonacoEditor,\n} from \"@components/MonacoEditor/MonacoEditor\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport {\n  mdiChevronDown,\n  mdiChevronUp,\n  mdiChip,\n  mdiLanConnect,\n  mdiMemory,\n  mdiReload,\n  mdiText,\n  mdiTimerLockOutline,\n} from \"@mdi/js\";\nimport { omitKeys, type JSONB } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\nimport { PopupSection } from \"../../ToolUseChatMessage/PopupSection\";\nimport type { ProstglesMCPToolsProps } from \"../ProstglesToolUseMessage\";\nimport { useTypedToolUseResultData } from \"./common/useTypedToolUseResultData\";\n\nexport type DockerSandboxCreateContainerData = JSONB.GetObjectType<\n  (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"docker-sandbox\"][\"create_container\"][\"schema\"][\"type\"]\n>;\n\nconst monacoOptions = {\n  ...MONACO_READONLY_DEFAULT_OPTIONS,\n  readOnly: false,\n  lineNumbers: \"on\",\n} as const;\n\nexport const DockerSandboxCreateContainer = ({\n  message,\n  toolUseResult: toolResult,\n  chatId,\n}: ProstglesMCPToolsProps) => {\n  const toolUseResult = toolResult?.toolUseResultMessage;\n  const { addAlert } = useAlert();\n  const initialData = message.input as DockerSandboxCreateContainerData;\n  const [editedFiles, setEditedFiles] = useState<Record<string, string>>();\n  const data = {\n    ...initialData,\n    files: {\n      ...initialData.files,\n      ...editedFiles,\n    },\n  };\n\n  const schema =\n    PROSTGLES_MCP_SERVERS_AND_TOOLS[\"docker-sandbox\"][\"create_container\"][\n      \"outputSchema\"\n    ];\n  const resultObj = useTypedToolUseResultData(toolUseResult, schema);\n  const [showLogs, setShowLogs] = useState(Boolean(resultObj?.log.length));\n  const [activeFilePath, setActiveFilePath] = useState(\n    Object.keys(data.files)[0],\n  );\n  const activeContent = data.files[activeFilePath ?? \"\"] ?? \"\";\n  const extension = activeFilePath?.toLowerCase().split(\".\").pop() ?? \"txt\";\n  const {\n    dbsMethods: { callMCPServerTool },\n    dbs,\n  } = usePrgl();\n\n  return (\n    <PopupSection\n      titleItems={\n        <>\n          <div\n            className=\"text-ellipsis min-w-0 ws-nowrap f-1 ta-start\"\n            title={`${resultObj?.command ?? \"\"}\\n\\n${JSON.stringify(omitKeys(data, [\"files\"]))}`}\n          >\n            {sliceText(resultObj?.command, 100) ??\n              \"Docker Sandbox Create Container\"}\n          </div>\n          <ScrollFade className=\"flex-row gap-1 oy-auto min-w-0 f-1 no-scroll-bar\">\n            {data.cpus && (\n              <FlexRow title={\"CPUs\"} className=\"gap-p25 pointer\">\n                <Icon path={mdiChip} />\n                <div>{data.cpus}</div>\n              </FlexRow>\n            )}\n            {data.memory && (\n              <FlexRow title={\"Memory\"} className=\"gap-p25 pointer\">\n                <Icon path={mdiMemory} />\n                <div>{data.memory}</div>\n              </FlexRow>\n            )}\n            <FlexRow title={\"Timeout\"} className=\"gap-p25 pointer\">\n              <Icon path={mdiTimerLockOutline} />\n              <div>\n                {getMillisecondsAsSingleInterval(data.timeout ?? 30_000)}\n              </div>\n            </FlexRow>\n            <FlexRow title={\"Network mode\"} className=\"gap-p25 pointer\">\n              <Icon path={mdiLanConnect} />\n              <div>{data.networkMode ?? \"none\"}</div>\n            </FlexRow>\n          </ScrollFade>\n          <CopyToClipboardBtn\n            size=\"small\"\n            content={JSON.stringify(message.input)}\n          />\n          {callMCPServerTool && toolResult && (\n            <Btn\n              variant=\"faded\"\n              color=\"action\"\n              iconPath={mdiReload}\n              size=\"small\"\n              onClickPromise={async () => {\n                const result = await callMCPServerTool(\n                  chatId,\n                  \"docker-sandbox\",\n                  \"create_container\",\n                  data,\n                );\n                console.log(\"Re-run result:\", result);\n                if (result.isError) {\n                  addAlert({\n                    title: \"Error re-running tool\",\n                    children: <ErrorComponent error={result.content} />,\n                  });\n                } else {\n                  await dbs.llm_messages.update(\n                    { id: toolResult.toolUseResult.id },\n                    {\n                      message: [\n                        {\n                          type: \"tool_result\",\n                          content: result.content,\n                          tool_name:\n                            toolUseResult?.tool_name ??\n                            \"docker-sandbox--create_container\",\n                          tool_use_id: toolUseResult!.tool_use_id,\n                        },\n                      ],\n                    },\n                  );\n                }\n              }}\n            >\n              Re-run\n            </Btn>\n          )}\n        </>\n      }\n    >\n      <FlexCol className=\"DockerSandboxCreateContainer b b-color ai-start gap-0 f-1\">\n        <FlexRow className=\"min-w-0 min-h-0 ai-start gap-0 w-full max-w-full f-1\">\n          <MenuList\n            activeKey={activeFilePath}\n            items={Object.keys(data.files).map((filePath) => {\n              const ext = filePath.toLowerCase().split(\".\").pop() ?? \"txt\";\n              return {\n                key: filePath,\n                label: filePath,\n                leftIconPath:\n                  FILE_EXTENSION_TO_ICON_INFO[ext]?.iconPath ?? mdiText,\n                iconStyle: { opacity: 0.7 },\n                onPress: () => setActiveFilePath(filePath),\n              };\n            })}\n            variant=\"vertical\"\n            className=\"pointer bg-color-1 rounded-none\"\n            style={{ alignSelf: \"stretch\", fontSize: 14 }}\n          />\n          <FlexRow className=\"o-auto f-1 w-full h-full\">\n            {activeFilePath && (\n              <MonacoEditor\n                className=\"f-1 h-full\"\n                language={\n                  FILE_EXTENSION_TO_ICON_INFO[extension]?.label ?? \"plaintext\"\n                }\n                loadedSuggestions={undefined}\n                value={activeContent}\n                style={{ width: \"min(600px, 100%)\", minHeight: 200 }}\n                onChange={(newValue) => {\n                  setEditedFiles((prev) => ({\n                    ...prev,\n                    [activeFilePath]: newValue,\n                  }));\n                }}\n                options={monacoOptions}\n              />\n            )}\n          </FlexRow>\n        </FlexRow>\n        <FlexRow className=\"bt b-color bg-color-2 w-full ta-start\">\n          <Btn\n            size=\"small\"\n            title=\"Toggle\"\n            iconPosition=\"right\"\n            iconPath={showLogs ? mdiChevronDown : mdiChevronUp}\n            onClick={() => setShowLogs(!showLogs)}\n          >\n            Logs\n          </Btn>\n          {resultObj && (\n            <Chip label=\"Duration\">\n              {getMillisecondsAsSingleInterval(\n                resultObj.buildDuration + resultObj.runDuration,\n              )}\n            </Chip>\n          )}\n        </FlexRow>\n        {showLogs && (\n          <MonacoEditor\n            key={\"logs\"}\n            language=\"text\"\n            className=\"f-p5\"\n            data-command=\"DockerSandboxCreateContainer.Logs\"\n            style={{ width: \"100%\", minHeight: 100 }}\n            value={resultObj?.log.map((l) => l.text).join(\"\") ?? \"\"}\n            loadedSuggestions={undefined}\n            options={MONACO_READONLY_DEFAULT_OPTIONS}\n          />\n        )}\n      </FlexCol>\n    </PopupSection>\n  );\n};\n\nconst getMillisecondsAsSingleInterval = (ms: number) => {\n  const seconds = ms / 1000;\n  const minutes = ms / 60_000;\n  const hours = ms / (60 * 60_000);\n  const result = {\n    s: seconds,\n    m: minutes,\n    h: hours,\n  };\n\n  const entries = getEntries(result);\n\n  return (\n    entries\n      .filter(([_n, v]) => v >= 1)\n      .map(([n, v]) => `${v}${n}`)\n      .at(-1) || `${seconds}s`\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/ExecuteSQL.tsx",
    "content": "import type { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport { MonacoCodeInMarkdown } from \"@components/Chat/MonacoCodeInMarkdown/MonacoCodeInMarkdown\";\nimport { FlexCol } from \"@components/Flex\";\nimport { type JSONB } from \"prostgles-types\";\nimport React from \"react\";\nimport { LANG } from \"src/dashboard/SQLEditor/W_SQLEditor\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\nimport type { ProstglesMCPToolsProps } from \"../ProstglesToolUseMessage\";\n\nexport type InputSchema = JSONB.GetObjectType<\n  (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"prostgles-db\"][\"execute_sql_with_commit\"][\"schema\"][\"type\"]\n>;\n\nexport const ExecuteSQL = ({ message }: ProstglesMCPToolsProps) => {\n  const initialData = message.input as InputSchema;\n  const { db } = usePrgl();\n  const codeString = initialData.sql;\n  return (\n    <FlexCol className=\"ExecuteSQL ai-start gap-0 f-1\">\n      <MonacoCodeInMarkdown\n        key={codeString}\n        codeHeader={undefined}\n        language={LANG}\n        codeString={codeString}\n        sqlHandler={db.sql}\n        loadedSuggestions={undefined}\n      />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/LoadSuggestedDashboards.tsx",
    "content": "import type { WorkspaceInsertModel } from \"@common/DashboardTypes\";\nimport type { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport { isObject } from \"@common/publishUtils\";\nimport { useAlert } from \"@components/AlertProvider\";\nimport Btn from \"@components/Btn\";\nimport { MonacoCodeInMarkdown } from \"@components/Chat/MonacoCodeInMarkdown/MonacoCodeInMarkdown\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { pageReload } from \"@components/Loader/Loading\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport { mdiAlert, mdiDelete, mdiOpenInNew, mdiViewCarousel } from \"@mdi/js\";\nimport { tryCatchV2, type JSONB } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { usePrgl } from \"../../../../../../pages/ProjectConnection/PrglContextProvider\";\nimport { isDefined } from \"../../../../../../utils/utils\";\nimport {\n  useSetActiveWorkspace,\n  useWorkspacesSync,\n} from \"../../../../../WorkspaceMenu/useWorkspaces\";\nimport { loadGeneratedWorkspaces } from \"../../../../Tools/loadGeneratedWorkspaces/loadGeneratedWorkspaces\";\nimport type { ProstglesMCPToolsProps } from \"../ProstglesToolUseMessage\";\n\nexport const LoadSuggestedDashboards = ({\n  workspaceId,\n  message,\n}: ProstglesMCPToolsProps) => {\n  const { setWorkspace } = useSetActiveWorkspace(workspaceId);\n  const { dbs, connectionId, tables } = usePrgl();\n\n  const workspaces = useWorkspacesSync(dbs, connectionId);\n  const alreadyLoadedWorkspaceIds = useMemo(() => {\n    return workspaces\n      .map((w) => (w.source?.tool_use_id === message.id ? w.id : undefined))\n      .filter(isDefined);\n  }, [message.id, workspaces]);\n  const json = message.input as\n    | JSONB.GetObjectType<\n        (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"prostgles-ui\"][\"suggest_dashboards\"][\"schema\"][\"type\"]\n      >\n    | undefined;\n  const { addAlert } = useAlert();\n\n  if (\n    !json ||\n    !Array.isArray(json.prostglesWorkspaces) ||\n    !json.prostglesWorkspaces.length\n  ) {\n    return (\n      <FlexCol>\n        <Chip color=\"red\" leftIcon={{ path: mdiAlert }}>\n          No suggested dashboards found in the code block.\n        </Chip>\n      </FlexCol>\n    );\n  }\n  const prostglesWorkspaces =\n    json.prostglesWorkspaces as WorkspaceInsertModel[];\n  return (\n    <FlexCol>\n      <FlexRowWrap>\n        {prostglesWorkspaces.map((w, i) => (\n          <PopupMenu\n            key={`${w.name}${i}-input`}\n            title={`Suggested Dashboard: ${w.name}`}\n            positioning=\"fullscreen\"\n            onClickClose={false}\n            button={\n              <Chip\n                key={i}\n                color=\"blue\"\n                leftIcon={w.icon ? undefined : { path: mdiViewCarousel }}\n                style={{ borderRadius: \"8px\", cursor: \"pointer\" }}\n              >\n                <FlexRow className=\"gap-p5 pr-p25\">\n                  {w.icon && <SvgIcon icon={w.icon} />}\n                  {w.name}\n                </FlexRow>\n              </Chip>\n            }\n          >\n            <MonacoCodeInMarkdown\n              codeString={\n                tryCatchV2(() => JSON.stringify(w, null, 2)).data ?? \"\"\n              }\n              className=\"f-1\"\n              language=\"json\"\n              codeHeader={undefined}\n              sqlHandler={undefined}\n              loadedSuggestions={undefined}\n            />\n          </PopupMenu>\n        ))}\n      </FlexRowWrap>\n      {!alreadyLoadedWorkspaceIds.length ?\n        <Btn\n          color=\"action\"\n          iconPath={mdiOpenInNew}\n          variant=\"filled\"\n          data-command=\"AskLLMChat.LoadSuggestedDashboards\"\n          disabledInfo={\n            !json.prostglesWorkspaces.length ?\n              \"No workspaces found in the code block.\"\n            : undefined\n          }\n          onClick={() => {\n            loadGeneratedWorkspaces(prostglesWorkspaces, message.id, {\n              dbs,\n              connectionId,\n              tables,\n            })\n              .then((insertedWorkspaces) => {\n                const [first] = insertedWorkspaces;\n                if (first) {\n                  setWorkspace(first);\n                }\n              })\n              .catch((error) => {\n                if (isObject(error) && error.code === \"23505\") {\n                  addAlert(\n                    `Workspace with this name already exists. Must delete or rename the clashing workspaces: \\n${prostglesWorkspaces.map((w) => w.name).join(\", \")}`,\n                  );\n                } else {\n                  addAlert(\n                    `Error loading workspaces: ${error.message || error}`,\n                  );\n                }\n              });\n          }}\n        >\n          Load suggested workspaces\n        </Btn>\n      : <Btn\n          iconPath={mdiDelete}\n          variant=\"faded\"\n          color=\"danger\"\n          title=\"Delete already loaded workspaces\"\n          data-command=\"AskLLMChat.UnloadSuggestedDashboards\"\n          onClickPromise={async () => {\n            await dbs.workspaces.update(\n              { id: { $in: alreadyLoadedWorkspaceIds } },\n              { deleted: true },\n            );\n            setWorkspace(undefined);\n            pageReload(\"Workspaces deleted\");\n          }}\n        >\n          Remove suggested workspaces\n        </Btn>\n      }\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/LoadSuggestedToolsAndPrompt/LoadSuggestedToolsAndPrompt.tsx",
    "content": "import { type PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport { sliceText } from \"@common/utils\";\nimport { Marked } from \"@components/Chat/Marked\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol } from \"@components/Flex\";\nimport { mdiLanguageTypescript, mdiScript, mdiTools } from \"@mdi/js\";\nimport type { JSONB } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { ProstglesMCPToolsProps } from \"../../ProstglesToolUseMessage\";\nimport { DatabaseAccessPermissions } from \"../common/DatabaseAccessPermissions\";\nimport { HeaderList } from \"../common/HeaderList\";\nimport { LoadSuggestedToolsAndPromptLoadBtn } from \"./LoadSuggestedToolsAndPromptLoadBtn\";\n\nexport const LoadSuggestedToolsAndPrompt = ({\n  chatId,\n  message,\n}: Pick<ProstglesMCPToolsProps, \"chatId\" | \"message\">) => {\n  const data = message.input as JSONB.GetObjectType<\n    (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"prostgles-ui\"][\"suggest_tools_and_prompt\"][\"schema\"][\"type\"]\n  >;\n\n  const [expandPrompt, setExpandPrompt] = useState(false);\n  const {\n    suggested_database_access,\n    suggested_mcp_tool_names,\n    suggested_prompt,\n    suggested_database_tool_names,\n  } = data;\n\n  return (\n    <FlexCol>\n      <Chip\n        title={\"Prompt\"}\n        color=\"blue\"\n        className=\"pointer\"\n        leftIcon={{ path: mdiScript }}\n        onClick={() => {\n          setExpandPrompt((prev) => !prev);\n        }}\n      >\n        {sliceText(suggested_prompt, 70)}\n      </Chip>\n      {expandPrompt && (\n        <Marked\n          codeHeader={undefined}\n          loadedSuggestions={undefined}\n          sqlHandler={undefined}\n          content={suggested_prompt}\n        />\n      )}\n\n      <DatabaseAccessPermissions {...suggested_database_access} />\n\n      <HeaderList\n        title=\"MCP Tools\"\n        items={suggested_mcp_tool_names}\n        iconPath={mdiTools}\n      />\n      {suggested_database_tool_names && (\n        <HeaderList\n          title=\"Database Functions\"\n          items={suggested_database_tool_names}\n          iconPath={mdiLanguageTypescript}\n        />\n      )}\n      <LoadSuggestedToolsAndPromptLoadBtn chatId={chatId} message={message} />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/LoadSuggestedToolsAndPrompt/LoadSuggestedToolsAndPromptLoadBtn.tsx",
    "content": "import {\n  getMCPToolNameParts,\n  type PROSTGLES_MCP_SERVERS_AND_TOOLS,\n} from \"@common/prostglesMcp\";\nimport { useAlert } from \"@components/AlertProvider\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport {\n  mdiLanguageTypescript,\n  mdiOpenInNew,\n  mdiTable,\n  mdiTools,\n} from \"@mdi/js\";\nimport type { JSONB } from \"prostgles-types\";\nimport React from \"react\";\nimport { usePrgl } from \"../../../../../../../pages/ProjectConnection/PrglContextProvider\";\nimport { isDefined } from \"../../../../../../../utils/utils\";\nimport type { ProstglesMCPToolsProps } from \"../../ProstglesToolUseMessage\";\n\nexport const LoadSuggestedToolsAndPromptLoadBtn = ({\n  chatId,\n  message,\n}: Pick<ProstglesMCPToolsProps, \"chatId\" | \"message\">) => {\n  const { addAlert } = useAlert();\n  const { dbs, connectionId } = usePrgl();\n  const data = message.input as JSONB.GetObjectType<\n    (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"prostgles-ui\"][\"suggest_tools_and_prompt\"][\"schema\"][\"type\"]\n  >;\n\n  return (\n    <Btn\n      color=\"action\"\n      iconPath={mdiOpenInNew}\n      variant=\"filled\"\n      data-command=\"AskLLMChat.LoadSuggestedToolsAndPrompt\"\n      onClickPromise={async () => {\n        if (data.suggested_mcp_tool_names.length) {\n          await dbs.llm_chats_allowed_mcp_tools.delete({\n            chat_id: chatId,\n          });\n          const allowedMcpTools = await dbs.mcp_server_tools.find({\n            $or: data.suggested_mcp_tool_names\n              .map((toolName) => {\n                const nameParts = getMCPToolNameParts(toolName);\n                if (!nameParts) return;\n                return {\n                  server_name: nameParts.serverName,\n                  name: nameParts.toolName,\n                };\n              })\n              .filter(isDefined),\n          });\n          await dbs.llm_chats_allowed_mcp_tools.insert(\n            allowedMcpTools.map((t) => {\n              return {\n                chat_id: chatId,\n                tool_id: t.id,\n                server_name: t.server_name,\n                auto_approve: true,\n              };\n            }),\n          );\n        }\n\n        if (data.suggested_database_tool_names?.length) {\n          await dbs.llm_chats_allowed_functions.delete({\n            chat_id: chatId,\n          });\n          const allowedFunctions = await dbs.published_methods.find({\n            $or: data.suggested_database_tool_names.map((toolName) => {\n              const name = getMCPToolNameParts(toolName)!.toolName;\n              return {\n                connection_id: connectionId,\n                name,\n              };\n            }),\n          });\n          if (allowedFunctions.length === 0) {\n            throw new Error(\n              `No database functions found for names: ${data.suggested_database_tool_names.join(\", \")}`,\n            );\n          }\n          await dbs.llm_chats_allowed_functions.insert(\n            allowedFunctions.map((t) => {\n              return {\n                connection_id: connectionId,\n                chat_id: chatId,\n                server_function_id: t.id,\n                auto_approve: true,\n              };\n            }),\n          );\n        }\n\n        const dbAccess = data.suggested_database_access;\n        await dbs.llm_chats.update(\n          { id: chatId },\n          {\n            db_data_permissions:\n              dbAccess.Mode === \"execute_sql_commit\" ?\n                { Mode: \"Run commited SQL\", auto_approve: true }\n              : dbAccess.Mode === \"execute_sql_rollback\" ?\n                { Mode: \"Run readonly SQL\", auto_approve: true }\n              : dbAccess.Mode === \"Custom\" ? { ...dbAccess, auto_approve: true }\n              : dbAccess,\n          },\n        );\n\n        addAlert({\n          title: \"Suggested tools and prompt loaded!\",\n          contentClassName: \"p-1\",\n          children: (\n            <FlexCol className=\"ta-start\">\n              {/* <div>The following were applied to this chat:</div> */}\n              {data.suggested_mcp_tool_names.length ?\n                <span>\n                  MCP Tools:{\" \"}\n                  <ul className=\"no-decor\">\n                    {data.suggested_mcp_tool_names.map((name) => (\n                      <li\n                        key={name}\n                        className=\"bold flex-row gap-p5 ai-center py-p5\"\n                      >\n                        <Icon path={mdiTools} />\n                        <div>{name}</div>\n                      </li>\n                    ))}\n                  </ul>\n                </span>\n              : null}\n              {data.suggested_database_tool_names?.length ?\n                <div>\n                  DB Functions:{\" \"}\n                  <ul className=\"no-decor\">\n                    {data.suggested_database_tool_names.map((name) => (\n                      <li\n                        key={name}\n                        className=\"bold flex-row gap-p5 ai-center py-p5\"\n                      >\n                        <Icon path={mdiLanguageTypescript} />\n                        <div>{name}</div>\n                      </li>\n                    ))}\n                  </ul>\n                </div>\n              : null}\n\n              {dbAccess.Mode !== \"Custom\" && (\n                <div>\n                  Database Access: <strong>{dbAccess.Mode}</strong>\n                </div>\n              )}\n              {dbAccess.Mode === \"Custom\" && (\n                <div>\n                  Table access:\n                  <ul className=\"no-decor\">\n                    {dbAccess.tables.map((t) => (\n                      <li\n                        key={t.tableName}\n                        className=\"bold flex-row gap-p5 ai-center py-p5\"\n                      >\n                        <Icon path={mdiTable} />\n                        <div>\n                          <strong>{t.tableName}</strong>:\n                        </div>\n                        {[\n                          t.select ? \"select\" : null,\n                          t.update ? \"update\" : null,\n                          t.insert ? \"insert\" : null,\n                          t.delete ? \"delete\" : null,\n                        ]\n                          .filter(isDefined)\n                          .join(\", \")}\n                      </li>\n                    ))}\n                  </ul>\n                </div>\n              )}\n            </FlexCol>\n          ),\n        });\n      }}\n    >\n      Load task chat\n    </Btn>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/LoadSuggestedWorkflow.tsx",
    "content": "import { type PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport Btn from \"@components/Btn\";\nimport { MonacoCodeInMarkdown } from \"@components/Chat/MonacoCodeInMarkdown/MonacoCodeInMarkdown\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { mdiDatabaseEdit, mdiLanguageTypescript, mdiTools } from \"@mdi/js\";\nimport type { JSONB } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { ProstglesMCPToolsProps } from \"../ProstglesToolUseMessage\";\nimport { HeaderList } from \"./common/HeaderList\";\nimport { DatabaseAccessPermissions } from \"./common/DatabaseAccessPermissions\";\n\nexport const LoadSuggestedWorkflow = ({\n  chatId,\n  message,\n}: Pick<ProstglesMCPToolsProps, \"chatId\" | \"message\">) => {\n  const data = message.input as JSONB.GetObjectType<\n    (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"prostgles-ui\"][\"suggest_agent_workflow\"][\"schema\"][\"type\"]\n  >;\n\n  const dbAccess = data.database_access;\n\n  return (\n    <FlexCol className=\"w-full\">\n      <FlexCol className=\"rounded b b-action o-auto p-1\">\n        <DatabaseAccessPermissions {...dbAccess} />\n        <HeaderList\n          title=\"MCP Tools\"\n          items={data.allowed_mcp_tool_names}\n          iconPath={mdiTools}\n        />\n        {/* {data.allowed_database_tool_names?.map((funcName, idx) => {\n          return (\n            <Chip\n              key={funcName + idx}\n              color=\"blue\"\n              title=\"Database Function\"\n              leftIcon={{ path: mdiLanguageTypescript }}\n            >\n              {funcName}\n            </Chip>\n          );\n        })} */}\n        <MonacoCodeInMarkdown\n          key={\"agent_definitions\"}\n          className=\"f-1 h-full\"\n          language={\"json\"}\n          sqlHandler={undefined}\n          codeHeader={() => (\n            <FlexRow>\n              <Icon path={mdiLanguageTypescript} className=\"mr-p5\" />\n              <div>Agent Definitions</div>\n            </FlexRow>\n          )}\n          loadedSuggestions={undefined}\n          codeString={JSON.stringify(data.agent_definitions, null, 2)}\n        />\n        <MonacoCodeInMarkdown\n          key={\"workflow_function_definition\"}\n          className=\"f-1 h-full\"\n          language={\"typescript\"}\n          sqlHandler={undefined}\n          codeHeader={() => (\n            <FlexRow>\n              <Icon path={mdiLanguageTypescript} className=\"mr-p5\" />\n              <div>Workflow Function Definition</div>\n            </FlexRow>\n          )}\n          loadedSuggestions={undefined}\n          codeString={data.workflow_function_definition}\n        />\n      </FlexCol>\n      <Btn\n        variant=\"filled\"\n        color=\"action\"\n        onClick={() => {\n          throw new Error(\"Not implemented yet\");\n        }}\n      >\n        Start workflow\n      </Btn>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/WebSearch/Favicon.tsx",
    "content": "import React, { useMemo } from \"react\";\nimport { tryCatchV2 } from \"prostgles-types\";\n\nexport const Favicon = ({ url }: { url: string }) => {\n  const faviconUrl = useMemo(\n    () =>\n      tryCatchV2(() => {\n        const _url = new URL(url);\n        const domain = _url.hostname;\n        // const mainUrl = `https://icons.duckduckgo.com/ip3/${domain}.ico`;\n        const otherUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;\n        return otherUrl;\n      }).data,\n    [url],\n  );\n\n  if (!faviconUrl) return null;\n\n  return (\n    <img src={faviconUrl} alt=\"Favicon\" style={{ width: 24, height: 24 }} />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/WebSearch/WebSearch.tsx",
    "content": "import { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport React from \"react\";\nimport type { ProstglesMCPToolsProps } from \"../../ProstglesToolUseMessage\";\nimport { useTypedToolUseResultData } from \"../common/useTypedToolUseResultData\";\nimport { Favicon } from \"./Favicon\";\nconst schema =\n  PROSTGLES_MCP_SERVERS_AND_TOOLS[\"websearch\"][\"websearch\"][\"outputSchema\"];\n\nexport const WebSearch = ({\n  toolUseResult: toolResult,\n}: ProstglesMCPToolsProps) => {\n  const toolUseResult = useTypedToolUseResultData(\n    toolResult?.toolUseResultMessage,\n    schema,\n  );\n\n  return (\n    <FlexCol>\n      {toolUseResult && toolUseResult.length === 0 && (\n        <div style={{ color: \"var(--gray)\" }}>No results found.</div>\n      )}\n      {toolUseResult?.map((result, index) => {\n        return (\n          <FlexCol\n            key={index}\n            style={{ lineHeight: \"1.2em\" }}\n            className=\"gap-p5\"\n          >\n            <FlexRow className=\"gap-p5\">\n              <Favicon url={result.url} />\n              <FlexCol className=\"gap-0\">\n                <a\n                  href={result.url}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  style={{\n                    fontSize: \"14px\",\n                    fontWeight: \"bold\",\n                    color: \"var(--blue)\",\n                  }}\n                >\n                  {result.title}\n                </a>\n                <div style={{ fontSize: \"12px\", color: \"var(--green)\" }}>\n                  {result.url}\n                </div>\n              </FlexCol>\n            </FlexRow>\n            <div style={{ color: \"var(--gray)\" }}>{result.content}</div>\n          </FlexCol>\n        );\n      })}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/common/DatabaseAccessPermissions.tsx",
    "content": "import React, { useMemo } from \"react\";\nimport type { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport type { JSONB } from \"prostgles-types\";\nimport { HeaderList } from \"./HeaderList\";\nimport { mdiDatabaseEdit } from \"@mdi/js\";\n\nexport const DatabaseAccessPermissions = (\n  dbAccess: JSONB.GetObjectType<\n    (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"prostgles-ui\"][\"suggest_agent_workflow\"][\"schema\"][\"type\"]\n  >[\"database_access\"],\n) => {\n  const databaseAccessList = useMemo(() => {\n    if (dbAccess.Mode === \"None\") return;\n    if (dbAccess.Mode === \"Custom\") {\n      return dbAccess.tables.map((t) => {\n        const tableMethods = [\"select\", \"update\", \"insert\", \"delete\"]\n          .filter((v) => t[v])\n          .join(\", \");\n        return (\n          <>\n            {t.tableName}:{\" \"}\n            <span style={{ fontWeight: \"normal\" }}>{tableMethods}</span>\n          </>\n        );\n      });\n    }\n\n    return dbAccess.Mode === \"execute_sql_rollback\" ?\n        [\"Execute SQL with rollback\"]\n      : [\"Execute SQL with commit\"];\n  }, [dbAccess]);\n\n  if (!databaseAccessList) return null;\n\n  return (\n    <HeaderList\n      title=\"Database access\"\n      iconPath={mdiDatabaseEdit}\n      items={databaseAccessList}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/common/HeaderList.tsx",
    "content": "import { FlexCol, FlexRow } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport React from \"react\";\n\ntype P = {\n  title: string;\n  iconPath: string;\n  items: React.ReactNode[];\n};\nexport const HeaderList = ({ title, iconPath, items }: P) => {\n  return (\n    <FlexCol className=\"gap-p5\">\n      <FlexRow className=\"gap-p5\">\n        {iconPath && <Icon path={iconPath} style={{ opacity: 0.8 }} />}\n        <div className=\" \">{title}</div>\n      </FlexRow>\n      <ul className=\"no-decor\" style={{ paddingLeft: \"2em\" }}>\n        {items.map((item, index) => (\n          <li key={index} className=\"bold flex-row gap-p5 ai-center m-0 p-0\">\n            {item}\n          </li>\n        ))}\n      </ul>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesMCPTools/common/useTypedToolUseResultData.ts",
    "content": "import { findArr } from \"@common/llmUtils\";\nimport { getJSONBSchemaValidationError, type JSONB } from \"prostgles-types\";\nimport type { ToolResultMessage } from \"../../../ToolUseChatMessage/ToolUseChatMessage\";\nimport { useMemo } from \"react\";\nexport const useTypedToolUseResultData = <S extends JSONB.FieldType>(\n  toolUseResult: ToolResultMessage | undefined,\n  schema: S,\n): JSONB.GetSchemaType<S> | undefined => {\n  //@ts-ignore\n  const resultObj = useMemo(() => {\n    try {\n      if (toolUseResult && !toolUseResult.is_error) {\n        const { content } = toolUseResult;\n        const stringContent =\n          typeof content === \"string\" ? content : (\n            findArr(content, { type: \"text\" } as const)?.text\n          );\n        if (!stringContent) return undefined;\n        const parseResult = getJSONBSchemaValidationError(\n          schema,\n          JSON.parse(stringContent),\n          {\n            allowExtraProperties: true,\n          },\n        );\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n        return parseResult.data;\n      }\n    } catch (error) {\n      console.error(\"Error parsing tool use result content:\", error);\n    }\n    return undefined;\n  }, [schema, toolUseResult]);\n\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n  return resultObj;\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesToolUseMessage.tsx",
    "content": "import { getProstglesMCPFullToolName } from \"@common/prostglesMcp\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { ToolResultMessage } from \"../ToolUseChatMessage/ToolUseChatMessage\";\nimport { DockerSandboxCreateContainer } from \"./ProstglesMCPTools/DockerSandboxCreateContainer\";\nimport { ExecuteSQL } from \"./ProstglesMCPTools/ExecuteSQL\";\nimport { LoadSuggestedDashboards } from \"./ProstglesMCPTools/LoadSuggestedDashboards\";\nimport { LoadSuggestedWorkflow } from \"./ProstglesMCPTools/LoadSuggestedWorkflow\";\nimport { LoadSuggestedToolsAndPrompt } from \"./ProstglesMCPTools/LoadSuggestedToolsAndPrompt/LoadSuggestedToolsAndPrompt\";\nimport { WebSearch } from \"./ProstglesMCPTools/WebSearch/WebSearch\";\n\nexport const ProstglesMCPToolsWithUI = {\n  [getProstglesMCPFullToolName(\"prostgles-ui\", \"suggest_dashboards\") as string]:\n    {\n      component: LoadSuggestedDashboards,\n      displayMode: \"full\",\n    },\n  [getProstglesMCPFullToolName(\n    \"prostgles-ui\",\n    \"suggest_tools_and_prompt\",\n  ) as string]: {\n    component: LoadSuggestedToolsAndPrompt,\n    displayMode: \"full\",\n  },\n  [getProstglesMCPFullToolName(\n    \"prostgles-ui\",\n    \"suggest_agent_workflow\",\n  ) as string]: {\n    component: LoadSuggestedWorkflow,\n    displayMode: \"full\",\n  },\n  \"docker-sandbox--create_container\": {\n    component: DockerSandboxCreateContainer,\n    displayMode: \"inline\",\n  },\n  [getProstglesMCPFullToolName(\n    \"prostgles-db\",\n    \"execute_sql_with_commit\",\n  ) as string]: {\n    component: ExecuteSQL,\n    displayMode: \"inline\",\n  },\n  [getProstglesMCPFullToolName(\n    \"prostgles-db\",\n    \"execute_sql_with_rollback\",\n  ) as string]: {\n    component: ExecuteSQL,\n    displayMode: \"inline\",\n  },\n  [getProstglesMCPFullToolName(\"websearch\", \"websearch\") as string]: {\n    component: WebSearch,\n    displayMode: \"inline\",\n  },\n} satisfies Record<\n  string,\n  {\n    component: React.ComponentType<ProstglesMCPToolsProps>;\n    /**\n     * How to display the tool UI\n     * - inline (default): Will show a summary button that opens an inline expanded component\n     * - full: will render component and a side button to show source JSON in popup\n     */\n    displayMode: \"full\" | \"inline\";\n  }\n>;\n\nexport type ProstglesMCPToolsProps = {\n  workspaceId: string | undefined;\n  // message: ToolUseMessage;\n  message: {\n    id: string;\n    input: any;\n  };\n  chatId: number;\n  toolUseResult:\n    | {\n        toolUseResult: DBSSchema[\"llm_messages\"];\n        toolUseResultMessage: ToolResultMessage;\n      }\n    | undefined;\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ToolUseChatMessage/PopupSection.tsx",
    "content": "import { classOverride, FlexCol, FlexRow } from \"@components/Flex\";\nimport React, { type ReactFragment } from \"react\";\n\nimport Btn from \"@components/Btn\";\nimport Popup, { type PopupProps } from \"@components/Popup/Popup\";\nimport { mdiFullscreen } from \"@mdi/js\";\n\nexport const PopupSection = (\n  props: Pick<PopupProps, \"children\"> & {\n    titleItems: React.ReactNode;\n    className?: string;\n    style?: React.CSSProperties;\n  },\n) => {\n  const { titleItems, children, className, style } = props;\n  const [anchorEl, setAnchorEl] = React.useState<HTMLElement>();\n  const titleNode = (\n    <FlexRow className=\"pl-p5 f-1\">\n      {titleItems}\n      {!anchorEl && (\n        <Btn\n          className=\"ml-auto\"\n          data-command=\"PopupSection.fullscreen\"\n          iconPath={mdiFullscreen}\n          onClick={(e) => setAnchorEl(e.currentTarget)}\n        />\n      )}\n    </FlexRow>\n  );\n  return (\n    <>\n      <FlexCol className=\"gap-0\">\n        {titleNode}\n        {children}\n      </FlexCol>\n      {anchorEl && (\n        <Popup\n          data-command=\"PopupSection.content\"\n          title={titleNode}\n          clickCatchStyle={{ opacity: 1 }}\n          anchorEl={anchorEl}\n          positioning=\"fullscreen\"\n          onClose={() => setAnchorEl(undefined)}\n          rootChildClassname={classOverride(\"f-1\", className)}\n          rootChildStyle={style}\n          contentClassName=\"p-1 flex-col gap-1 f-1\"\n        >\n          {children}\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ToolUseChatMessage/ToolUseChatMessage.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { mdiCodeJson } from \"@mdi/js\";\nimport React, { useCallback, useState } from \"react\";\n\nimport { ErrorTrap } from \"@components/ErrorComponent\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { ProstglesMCPToolsWithUI } from \"../ProstglesToolUseMessage/ProstglesToolUseMessage\";\nimport { ToolUseChatMessageBtn } from \"./ToolUseChatMessageBtn\";\nimport { ToolUseChatMessageJSONData } from \"./ToolUseChatMessageJSONData\";\nimport { ToolUseChatMessageResult } from \"./ToolUseChatMessageResult\";\nimport {\n  useToolUseChatMessage,\n  type ToolUseMessageProps,\n} from \"./useToolUseChatMessage\";\n\nexport const ToolUseChatMessage = (props: ToolUseMessageProps) => {\n  const [toolDataAnchorEl, setToolDataAnchorEl] = useState<HTMLButtonElement>();\n\n  const toolUseInfo = useToolUseChatMessage(props);\n  const onClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(\n    ({ currentTarget }) => {\n      setToolDataAnchorEl(toolDataAnchorEl ? undefined : currentTarget);\n    },\n    [toolDataAnchorEl],\n  );\n  if (typeof toolUseInfo === \"string\") {\n    return <>{toolUseInfo}</>;\n  }\n  const { toolUseMessageContent: m } = toolUseInfo;\n\n  const ToolUI = ProstglesMCPToolsWithUI[m.name];\n  const { displayMode } = ToolUI ?? {};\n\n  return (\n    <ErrorTrap>\n      <FlexCol\n        data-command=\"ToolUseMessage\"\n        className={\"ToolUseMessage gap-p5 trigger-hover\"}\n        style={\n          displayMode === \"full\" ?\n            { flexDirection: \"row-reverse\", justifyContent: \"start\" }\n          : undefined\n        }\n      >\n        <FlexRow className=\"ai-start\">\n          {(!ToolUI || displayMode !== \"full\") && (\n            <ToolUseChatMessageBtn\n              {...toolUseInfo}\n              displayMode={displayMode}\n              onClick={onClick}\n            />\n          )}\n          {ToolUI && (\n            <PopupMenu\n              positioning=\"fullscreen\"\n              title={m.name}\n              onClickClose={false}\n              button={\n                <Btn iconPath={mdiCodeJson} className=\"show-on-trigger-hover\" />\n              }\n              contentClassName=\"p-1 flex-col gap-1 f-1\"\n            >\n              <ToolUseChatMessageJSONData {...props} />\n            </PopupMenu>\n          )}\n        </FlexRow>\n\n        <ToolUseChatMessageResult\n          {...toolUseInfo}\n          {...props}\n          anchorEl={toolDataAnchorEl}\n          setAnchorEl={setToolDataAnchorEl}\n        />\n      </FlexCol>\n    </ErrorTrap>\n  );\n};\n\nexport type LLMMessageContent = DBSSchema[\"llm_messages\"][\"message\"][number];\nexport type ToolUseMessage = Extract<LLMMessageContent, { type: \"tool_use\" }>;\nexport type ToolResultMessage = Extract<\n  LLMMessageContent,\n  { type: \"tool_result\" }\n>;\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ToolUseChatMessage/ToolUseChatMessageBtn.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { mdiTools } from \"@mdi/js\";\nimport React, { type MouseEventHandler } from \"react\";\n\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport { ToolUseChatMessageBtnTextSummary } from \"./ToolUseChatMessageBtnTextSummary\";\nimport { type ToolUseChatMessageState } from \"./useToolUseChatMessage\";\nimport { FlexRow } from \"@components/Flex\";\n\nexport const ToolUseChatMessageBtn = ({\n  toolUseMessageContent: m,\n  iconName,\n  toolUseResult,\n  displayMode,\n  onClick,\n}: ToolUseChatMessageState & {\n  displayMode: \"full\" | \"inline\" | undefined;\n  onClick: MouseEventHandler<HTMLButtonElement>;\n}) => {\n  const needsResult = displayMode !== \"full\";\n  const isLoading = !toolUseResult && needsResult;\n  return (\n    <Btn\n      iconPath={!iconName ? mdiTools : undefined}\n      iconStyle={{ flex: \"none\" }}\n      iconNode={\n        !iconName ? undefined : (\n          <SvgIcon\n            icon={iconName}\n            style={{ margin: \"-4px\", flex: \"none\" }}\n            size={20}\n          />\n        )\n      }\n      onClick={onClick}\n      variant=\"faded\"\n      size=\"small\"\n      color={\n        toolUseResult?.toolUseResultMessage.is_error ? \"danger\" : undefined\n      }\n      title={\n        toolUseResult || !needsResult ?\n          `Tool use result for: ${m.name}`\n        : `Awaiting tool result: ${m.name}`\n      }\n      className=\"f-1 max-w-fit\"\n      data-command=\"ToolUseMessage.toggle\"\n      loading={isLoading}\n      children={\n        displayMode === \"full\" ? undefined : (\n          <FlexRow className=\"f-1 min-w-0\">\n            {\" \"}\n            {m.name}\n            <ToolUseChatMessageBtnTextSummary m={m} />\n          </FlexRow>\n        )\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ToolUseChatMessage/ToolUseChatMessageBtnTextSummary.tsx",
    "content": "import { sliceText } from \"@common/utils\";\nimport { isObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { ToolUseMessage } from \"./ToolUseChatMessage\";\n\nexport const ToolUseChatMessageBtnTextSummary = ({\n  m,\n}: {\n  m: ToolUseMessage;\n}) => {\n  const inputTextSummary = useMemo(() => {\n    const maxLength = 50;\n    if (isObject(m.input)) {\n      const keys = Object.keys(m.input);\n      const selectedKeys = keys.slice(0, 5);\n      const args = selectedKeys\n        .map((key) => {\n          const value = m.input[key];\n          const valueString =\n            Array.isArray(value) || isObject(value) ?\n              JSON.stringify(value)\n            : value.toString();\n\n          return `${key}: ${sliceText(valueString, Math.round(maxLength / selectedKeys.length), undefined, true)}`;\n        })\n        .join(\", \");\n      return ` ${args}`;\n    }\n    return sliceText(JSON.stringify(m.input), maxLength, undefined, true);\n  }, [m]);\n\n  return (\n    <>\n      {inputTextSummary && (\n        <span\n          className=\"text-ellipsis\"\n          style={{ fontWeight: \"normal\", opacity: 0.75 }}\n        >\n          {inputTextSummary}\n        </span>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ToolUseChatMessage/ToolUseChatMessageJSONData.tsx",
    "content": "import {\n  MonacoCodeInMarkdown,\n  type MonacoCodeInMarkdownProps,\n} from \"@components/Chat/MonacoCodeInMarkdown/MonacoCodeInMarkdown\";\nimport { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport { isEmpty, tryCatchV2 } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\n\nimport { ErrorTrap } from \"@components/ErrorComponent\";\nimport type { ToolUseMessageProps } from \"./useToolUseChatMessage\";\nimport { getToolUseResult } from \"./utils/getToolUseResult\";\n\nexport const ToolUseChatMessageJSONData = ({\n  message,\n  nextMessage,\n  toolUseMessageContentIndex: toolUseMessageIndex,\n  sqlHandler,\n  loadedSuggestions,\n}: ToolUseMessageProps) => {\n  const toolUseMessage = message;\n  const toolUseMessageContent = toolUseMessage.message[toolUseMessageIndex];\n\n  if (toolUseMessageContent?.type !== \"tool_use\") {\n    return <>Unexpected message tool use message</>;\n  }\n\n  const toolUseResult = getToolUseResult({\n    nextMessage,\n    toolUseMessage: toolUseMessage,\n    toolUseMessageContentIndex: toolUseMessageIndex,\n  });\n  return (\n    <ErrorTrap>\n      {toolUseMessageContent.input && !isEmpty(toolUseMessageContent.input) && (\n        <MonacoCodeInMarkdown\n          key={`${toolUseMessageContent.type}-input`}\n          title=\"Arguments:\"\n          codeString={\n            tryCatchV2(() =>\n              JSON.stringify(toolUseMessageContent.input, null, 2),\n            ).data ?? \"\"\n          }\n          className=\"f-1\"\n          language=\"json\"\n          codeHeader={undefined}\n          sqlHandler={undefined}\n          loadedSuggestions={undefined}\n        />\n      )}\n      {toolUseResult && (\n        <ContentRender\n          toolUseResult={toolUseResult}\n          sqlHandler={sqlHandler}\n          loadedSuggestions={loadedSuggestions}\n        />\n      )}\n    </ErrorTrap>\n  );\n};\n\nconst ContentRender = ({\n  toolUseResult,\n  sqlHandler,\n  loadedSuggestions,\n}: {\n  toolUseResult: ReturnType<typeof getToolUseResult>;\n} & Pick<MonacoCodeInMarkdownProps, \"sqlHandler\" | \"loadedSuggestions\">) => {\n  const content = useMemo(() => {\n    if (!toolUseResult) return undefined;\n    const { content: contentRaw } = toolUseResult.toolUseResultMessage;\n    if (typeof contentRaw === \"string\") {\n      return [\n        { type: \"text\", text: contentRaw } satisfies {\n          type: \"text\";\n          text: string;\n        },\n      ];\n    }\n    return contentRaw;\n  }, [toolUseResult]);\n\n  if (!content) return null;\n\n  return (\n    <>\n      {content.map((m, idx) => {\n        if (m.type === \"text\" || m.type === \"resource\") {\n          const value =\n            m.type === \"text\" ? m.text : JSON.stringify(m.resource, null, 2);\n          const JSON_START_CHARS = [\"{\", \"[\"];\n          const language =\n            JSON_START_CHARS.some((c) => value.trim().startsWith(c)) ? \"json\"\n            : \"text\";\n\n          const formatedValue =\n            tryCatchV2(() => {\n              if (language === \"json\") {\n                return JSON.stringify(JSON.parse(value), null, 2);\n              }\n              return value;\n            }).data ?? value;\n          return (\n            <MonacoCodeInMarkdown\n              key={`${m.type}${idx}`}\n              title={\n                toolUseResult?.toolUseResultMessage.is_error ?\n                  \"Error:\"\n                : \"Output:\"\n              }\n              className=\"f-1\"\n              codeString={formatedValue}\n              language={language}\n              codeHeader={undefined}\n              sqlHandler={sqlHandler}\n              loadedSuggestions={loadedSuggestions}\n            />\n          );\n        }\n\n        if (m.type !== \"image\") {\n          return <>Unsupported message type: {m.type}</>;\n        }\n\n        return (\n          <MediaViewer\n            key={`image-${idx}`}\n            content_type={\"image\"}\n            url={m.data}\n          />\n        );\n      })}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ToolUseChatMessage/ToolUseChatMessageResult.tsx",
    "content": "import { FlexCol } from \"@components/Flex\";\nimport React from \"react\";\n\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { ProstglesMCPToolsWithUI as MCPToolsWithUI } from \"../ProstglesToolUseMessage/ProstglesToolUseMessage\";\nimport { ToolUseChatMessageJSONData } from \"./ToolUseChatMessageJSONData\";\nimport type {\n  ToolUseChatMessageState,\n  ToolUseMessageProps,\n} from \"./useToolUseChatMessage\";\n\nexport const ToolUseChatMessageResult = (\n  props: ToolUseMessageProps & {\n    anchorEl: HTMLElement | undefined;\n    setAnchorEl: React.Dispatch<\n      React.SetStateAction<HTMLButtonElement | undefined>\n    >;\n  } & Pick<\n      ToolUseChatMessageState,\n      \"toolUseResult\" | \"toolUseMessage\" | \"toolUseMessageContent\"\n    >,\n) => {\n  const {\n    toolUseResult,\n    workspaceId,\n    anchorEl,\n    toolUseMessageContent,\n    toolUseMessage,\n  } = props;\n\n  const toolCallError =\n    toolUseResult?.toolUseResultMessage.is_error ?\n      toolUseResult.toolUseResultMessage.content\n    : undefined;\n\n  const ProstglesTool = MCPToolsWithUI[toolUseMessageContent.name];\n  const ProstglesToolComponent = ProstglesTool?.component;\n  const { displayMode } = ProstglesTool ?? {};\n\n  return (\n    <>\n      <FlexCol className=\"w-full\">\n        {(displayMode === \"full\" || anchorEl) && ProstglesToolComponent && (\n          <ProstglesToolComponent\n            workspaceId={workspaceId}\n            message={toolUseMessageContent}\n            chatId={toolUseMessage.chat_id}\n            toolUseResult={toolUseResult}\n          />\n        )}\n        {toolCallError && <ErrorComponent error={toolCallError} />}\n      </FlexCol>\n      {anchorEl && !displayMode && <ToolUseChatMessageJSONData {...props} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ToolUseChatMessage/useToolUseChatMessage.ts",
    "content": "import { getMCPToolNameParts } from \"@common/prostglesMcp\";\nimport { useMemo } from \"react\";\nimport type { MonacoCodeInMarkdownProps } from \"@components/Chat/MonacoCodeInMarkdown/MonacoCodeInMarkdown\";\nimport type { UseLLMChatProps } from \"../../useLLMChat\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { getToolUseResult } from \"./utils/getToolUseResult\";\nimport type { ToolUseMessage } from \"./ToolUseChatMessage\";\n\nexport type ToolUseMessageProps = Pick<UseLLMChatProps, \"mcpServerIcons\"> & {\n  message: DBSSchema[\"llm_messages\"];\n  nextMessage: DBSSchema[\"llm_messages\"] | undefined;\n  toolUseMessageContentIndex: number;\n  workspaceId: string | undefined;\n} & Pick<MonacoCodeInMarkdownProps, \"sqlHandler\" | \"loadedSuggestions\">;\n\nexport const useToolUseChatMessage = (props: ToolUseMessageProps) => {\n  const { message, nextMessage, toolUseMessageContentIndex, mcpServerIcons } =\n    props;\n\n  const toolUseMessage = message;\n  const toolUseMessageContent =\n    toolUseMessage.message[toolUseMessageContentIndex];\n\n  const iconName = useMemo(() => {\n    return toolUseMessageContent?.type === \"tool_use\" ?\n        getIconForToolUseMessage(toolUseMessageContent, mcpServerIcons)\n      : undefined;\n  }, [mcpServerIcons, toolUseMessageContent]);\n\n  if (toolUseMessageContent?.type !== \"tool_use\") {\n    return \"Unexpected message tool use message\";\n  }\n\n  const toolUseResult =\n    nextMessage &&\n    getToolUseResult({\n      nextMessage,\n      toolUseMessage,\n      toolUseMessageContentIndex: toolUseMessageContentIndex,\n    });\n\n  return {\n    toolUseResult,\n    iconName,\n    toolUseMessage,\n    toolUseMessageContent,\n  };\n};\n\nexport type ToolUseChatMessageState = Exclude<\n  ReturnType<typeof useToolUseChatMessage>,\n  string\n>;\n\nexport const getIconForToolUseMessage = (\n  { name }: ToolUseMessage,\n  mcpServerIcons: Map<string, string>,\n) => {\n  const serverName = getMCPToolNameParts(name)?.serverName;\n  return serverName && mcpServerIcons.get(serverName);\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/ToolUseChatMessage/utils/getToolUseResult.ts",
    "content": "import { filterArr } from \"@common/llmUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\n\nimport type { ToolResultMessage, ToolUseMessage } from \"../ToolUseChatMessage\";\n\nexport const getToolUseResult = ({\n  toolUseMessage,\n  nextMessage,\n  toolUseMessageContentIndex,\n}: {\n  toolUseMessageContentIndex: number;\n  toolUseMessage: DBSSchema[\"llm_messages\"];\n  nextMessage: DBSSchema[\"llm_messages\"] | undefined;\n}):\n  | undefined\n  | {\n      toolUseResult: DBSSchema[\"llm_messages\"];\n      toolUseResultMessage: ToolResultMessage;\n    } => {\n  const toolUseMessageRequest = toolUseMessage.message[\n    toolUseMessageContentIndex\n  ] as ToolUseMessage;\n  if (!nextMessage) return;\n\n  const toolResults = filterArr(nextMessage.message, {\n    type: \"tool_result\",\n  } as const);\n\n  const result = toolResults.find(\n    (tr) => tr.tool_use_id === toolUseMessageRequest.id,\n  );\n  if (!result) {\n    return;\n  }\n\n  return {\n    toolUseResult: nextMessage,\n    toolUseResultMessage: result,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/hooks/useLLMChatMessageGrouper.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { useMemo, useState } from \"react\";\nimport { ProstglesMCPToolsWithUI } from \"../ProstglesToolUseMessage/ProstglesToolUseMessage\";\nimport type { LLMMessageContent } from \"../ToolUseChatMessage/ToolUseChatMessage\";\nimport { quickClone } from \"src/utils/utils\";\n\ntype P = {\n  llmMessages: DBSSchema[\"llm_messages\"][] | undefined;\n};\n\n/**\n * Given a list of LLM messages, groups sequential tool use & tool result messages together\n * into a single message object for rendering in the chat UI.\n */\nexport const useLLMChatMessageGrouper = (props: P) => {\n  const { llmMessages } = props;\n\n  const [toggledSections, setToggledSections] = useState<Set<string>>(\n    new Set(),\n  );\n  const llmMessagesWithGroups = useMemo(() => {\n    if (!llmMessages) return;\n    const result: LLMMessageItem[] = [];\n    llmMessages.forEach((message, index) => {\n      const prevItem = result.at(-1);\n      const isToolResult = message.message.some(\n        (m) => m.type === \"tool_result\",\n      );\n      if (isToolResult) return; // Skip rendering tool result messages directly\n      const hasToolUseOrResult = message.message.some(\n        (m) => m.type === \"tool_use\" || m.type === \"tool_result\",\n      );\n      const nextMessage = llmMessages[index + 1]; //quickClone(llmMessages[index + 1]);\n\n      /** Start or continue group */\n      if (hasToolUseOrResult) {\n        /** Continue group */\n        if (prevItem?.type === \"tool_call_message_group\") {\n          prevItem.messages = [...prevItem.messages, { message, nextMessage }];\n          prevItem.messageContentItems = [\n            ...prevItem.messageContentItems,\n            ...message.message,\n          ];\n        } else {\n          /** Start new group */\n          result.push({\n            type: \"tool_call_message_group\",\n            messages: [{ message, nextMessage }],\n            messageContentItems: [...message.message],\n            firstMessage: message,\n            startId: message.id,\n            onToggle: () => {\n              setToggledSections((prev) => {\n                const newSet = new Set(prev);\n                newSet.add(message.id);\n                return newSet;\n              });\n            },\n          });\n        }\n      } else {\n        /** Regular single message */\n        result.push({\n          type: \"single_message\",\n          message,\n          nextMessage,\n          onToggle: undefined,\n        });\n      }\n    });\n\n    const resultWithExpands: LLMMessageItem[] = result\n      .map((item) => {\n        if (item.type === \"single_message\") {\n          return [item];\n        }\n\n        const toolCalls = item.messageContentItems.filter(\n          (m) => m.type === \"tool_use\",\n        );\n        const allowMinimise =\n          toolCalls.length >= 3 &&\n          !toolCalls.some((m) => ProstglesMCPToolsWithUI[m.name]);\n        const shouldExpand =\n          !allowMinimise || toggledSections.has(item.startId);\n\n        if (shouldExpand) {\n          return item.messages.map(\n            ({ message, nextMessage }) =>\n              ({\n                type: \"single_message\",\n                message,\n                nextMessage,\n                onToggle:\n                  toggledSections.has(message.id) ?\n                    () => {\n                      setToggledSections((prev) => {\n                        const newSet = new Set(prev);\n                        newSet.delete(message.id);\n                        return newSet;\n                      });\n                    }\n                  : undefined,\n              }) satisfies LLMMessageItem,\n          );\n        }\n\n        return [item];\n      })\n      .flat();\n\n    return resultWithExpands;\n  }, [llmMessages, toggledSections]);\n\n  return {\n    llmMessagesWithGroups,\n  };\n};\n\nexport type LLMSingleMessage = {\n  type: \"single_message\";\n  message: DBSSchema[\"llm_messages\"];\n  nextMessage: DBSSchema[\"llm_messages\"] | undefined;\n  onToggle: undefined | (() => void);\n};\nexport type LLMMessageGroup = {\n  type: \"tool_call_message_group\";\n  messages: {\n    message: DBSSchema[\"llm_messages\"];\n    nextMessage: DBSSchema[\"llm_messages\"] | undefined;\n  }[];\n  messageContentItems: LLMMessageContent[];\n  firstMessage: DBSSchema[\"llm_messages\"];\n  startId: string;\n  onToggle: () => void;\n};\nexport type LLMMessageItem = LLMSingleMessage | LLMMessageGroup;\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatMessages/hooks/useLLMChatMessages.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport type { Message } from \"@components/Chat/Chat\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport React, { useMemo } from \"react\";\nimport { isDefined } from \"../../../../../utils/utils\";\nimport type { UseLLMChatProps } from \"../../useLLMChat\";\nimport { LLMChatMessage } from \"../LLMChatMessage/LLMChatMessage\";\nimport { LLMChatMessageHeader } from \"../LLMChatMessage/LLMChatMessageHeader\";\nimport { useLLMChatMessageGrouper } from \"./useLLMChatMessageGrouper\";\n\ntype P = UseLLMChatProps & {\n  activeChat: DBSSchema[\"llm_chats\"] | undefined;\n};\n\nexport const useLLMChatMessages = (props: P) => {\n  const {\n    dbs,\n    user,\n    activeChat,\n    db,\n    loadedSuggestions,\n    workspaceId,\n    mcpServerIcons,\n  } = props;\n  const { status } = activeChat ?? {};\n  // TODO: llmMessages is changed on each render\n  const { data: llmMessages } = dbs.llm_messages.useSubscribe(\n    { chat_id: activeChat?.id },\n    { orderBy: { created: 1 } },\n    { skip: !activeChat?.id },\n  );\n\n  const isLoadingSince = status?.state === \"loading\" ? status.since : null;\n  const { llmMessagesWithGroups } = useLLMChatMessageGrouper({\n    llmMessages,\n  });\n  const actualMessages: Message[] | undefined = useMemo(\n    () =>\n      !llmMessages ? undefined : (\n        llmMessagesWithGroups\n          ?.map((messageItem, messageOrGroupIndex) => {\n            const isLastMessage =\n              llmMessagesWithGroups.length - 1 === messageOrGroupIndex;\n            const { id, user_id } =\n              messageItem.type === \"single_message\" ?\n                messageItem.message\n              : messageItem.firstMessage;\n            const isLoadingSinceDate =\n              !isLastMessage || !isLoadingSince ?\n                undefined\n              : new Date(isLoadingSince);\n            return {\n              id,\n              incoming: user_id !== user?.id,\n              messageTopContent: (\n                <LLMChatMessageHeader dbs={dbs} item={messageItem} />\n              ),\n              message: (\n                <LLMChatMessage\n                  isLoadingSinceDate={isLoadingSinceDate}\n                  messageItem={messageItem}\n                  db={db}\n                  mcpServerIcons={mcpServerIcons}\n                  workspaceId={workspaceId}\n                  loadedSuggestions={loadedSuggestions}\n                />\n              ),\n              sender_id: user_id || \"ai\",\n            };\n          })\n          .filter(isDefined)\n      ),\n    [\n      llmMessages,\n      llmMessagesWithGroups,\n      isLoadingSince,\n      user?.id,\n      dbs,\n      db,\n      workspaceId,\n      loadedSuggestions,\n      mcpServerIcons,\n    ],\n  );\n\n  const lastMessage = llmMessages?.at(-1);\n  const disabled_message =\n    (\n      activeChat?.disabled_until &&\n      new Date(activeChat.disabled_until) > new Date() &&\n      activeChat.disabled_message\n    ) ?\n      activeChat.disabled_message\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n    : lastMessage?.meta?.finish_reason === \"length\" ?\n      <FlexCol>\n        <ErrorComponent\n          error={\"finish_reason = 'length'. Increase max_tokens and try again\"}\n        />\n        <FormFieldDebounced\n          label={\"Max tokens\"}\n          value={\n            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n            activeChat?.extra_body?.max_tokens ||\n            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n            lastMessage.meta?.max_tokens ||\n            6000\n          }\n          onChange={(max_tokens) => {\n            void dbs.llm_chats.update(\n              { id: activeChat!.id },\n              { extra_body: { max_tokens: Number(max_tokens) } },\n            );\n          }}\n        />\n      </FlexCol>\n    : undefined;\n\n  const messages: Message[] = (\n    actualMessages?.length ? actualMessages : (\n      [\n        {\n          id: \"first\",\n          message: \"Hello, I am the AI assistant. How can I help you?\",\n          incoming: true,\n          sender_id: \"ai\",\n        } as const,\n      ].map((m) => {\n        const incoming = m.sender_id !== user?.id;\n        return {\n          ...m,\n          incoming,\n          message: m.message,\n        };\n      })\n    )).concat(\n    disabled_message ?\n      [\n        {\n          id: \"disabled-last\",\n          incoming: true,\n          message: disabled_message,\n          sender_id: \"ai\",\n        },\n      ]\n    : [],\n  );\n\n  return {\n    llmMessages,\n    messages: activeChat && actualMessages ? messages : undefined,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/AskLLMChatOptions.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport Popup from \"@components/Popup/Popup\";\nimport { mdiCogOutline } from \"@mdi/js\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useMemo, useState } from \"react\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport { SmartForm, type SmartFormProps } from \"../../SmartForm/SmartForm\";\n\nexport type LLMChatOptionsProps = {\n  prompts: DBSSchema[\"llm_prompts\"][] | undefined;\n  activeChat: DBSSchema[\"llm_chats\"] | undefined;\n  credentials: DBSSchema[\"llm_credentials\"][] | undefined;\n  activeChatId: number | undefined;\n  chatRootDiv: HTMLDivElement;\n};\n\nexport const AskLLMChatOptions = (props: LLMChatOptionsProps) => {\n  const { chatRootDiv, activeChatId } = props;\n  const { dbs, dbsTables } = usePrgl();\n  const [anchorEl, setAnchorEl] = useState<HTMLDivElement>();\n\n  const formProps = useMemo(() => {\n    return {\n      columns: {\n        name: 1,\n        llm_prompt_id: 1,\n        model: 1,\n        db_schema_permissions: 1,\n        db_data_permissions: 1,\n        maximum_consecutive_tool_fails: 1,\n        max_total_cost_usd: 1,\n        extra_body: 1,\n        extra_headers: 1,\n        created: 1,\n        id: 1,\n      } as const satisfies Partial<Record<keyof DBSSchema[\"llm_chats\"], 1>>,\n      methods: {},\n      rowFilter: [{ fieldName: \"id\", value: activeChatId }],\n      jsonbSchemaWithControls: {\n        // noLabels: true,\n      },\n    } satisfies Pick<\n      SmartFormProps,\n      \"jsonbSchemaWithControls\" | \"columns\" | \"methods\" | \"rowFilter\"\n    >;\n  }, [activeChatId]);\n\n  return (\n    <>\n      <Btn\n        title={t.AskLLMChatHeader[\"Chat settings\"]}\n        variant=\"icon\"\n        iconPath={mdiCogOutline}\n        onClick={() => setAnchorEl(anchorEl ? undefined : chatRootDiv)}\n        data-command=\"LLMChatOptions.toggle\"\n      />\n      {anchorEl && (\n        <Popup\n          contentStyle={{ padding: 0, maxWidth: \"min(100vw, 700px)\" }}\n          onClickClose={false}\n          onClose={() => setAnchorEl(undefined)}\n          anchorEl={chatRootDiv}\n          positioning=\"right-panel\"\n          clickCatchStyle={{ opacity: 1 }}\n          title={t.AskLLMChatHeader[\"Chat settings\"]}\n          content={\n            <FlexCol className=\"f-1 min-s-0\">\n              <SmartForm\n                label=\"\"\n                tableName=\"llm_chats\"\n                contentClassname=\"p-1 pt-1\"\n                {...formProps}\n                db={dbs as DBHandlerClient}\n                tables={dbsTables}\n              />\n            </FlexCol>\n          }\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/useAskLLMChatSend.ts",
    "content": "import type { LLMMessage } from \"@common/llmUtils\";\nimport { MINUTE } from \"@common/utils\";\nimport { useAlert } from \"@components/AlertProvider\";\nimport { type ChatProps } from \"@components/Chat/Chat\";\nimport { useCallback, useMemo } from \"react\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { AskLLMChatProps } from \"./AskLLMChat\";\nimport type { LLMChatState } from \"./useLLMChat\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport type { LLMMessageContent } from \"./AskLLMChatMessages/ToolUseChatMessage/ToolUseChatMessage\";\n\ntype P = Pick<AskLLMChatProps, \"askLLM\" | \"stopAskLLM\"> &\n  Pick<LLMChatState, \"activeChatId\" | \"activeChat\"> & {\n    dbSchemaForPrompt: string;\n  };\n\nexport const useAskLLMChatSend = ({\n  askLLM,\n  stopAskLLM,\n  activeChat,\n  activeChatId,\n  dbSchemaForPrompt,\n}: P) => {\n  const { connectionId } = usePrgl();\n  const { addAlert } = useAlert();\n  const sendQuery = useCallback(\n    (msg: LLMMessage[\"message\"] | undefined, isToolApproval: boolean) => {\n      if (!msg || !activeChatId) return;\n      /** TODO: move dbSchemaForPrompt to server-side */\n      void askLLM(\n        connectionId,\n        msg,\n        dbSchemaForPrompt,\n        activeChatId,\n        isToolApproval ? \"approve-tool-use\" : \"new-message\",\n      ).catch((error) => {\n        const errorText = error?.message || error;\n        const errorTextMessage =\n          typeof errorText === \"string\" ? errorText : JSON.stringify(errorText);\n\n        addAlert(\n          \"Error when when sending AI Assistant query: \" + errorTextMessage,\n        );\n      });\n    },\n    [activeChatId, askLLM, connectionId, dbSchemaForPrompt, addAlert],\n  );\n\n  const sendMessage: ChatProps[\"onSend\"] = useCallback(\n    async (text: string | undefined, files) => {\n      const fileMessages = await Promise.all(\n        (files ?? []).map(async (file) => toMediaMessage(file)),\n      );\n      return sendQuery(\n        [\n          text ? ({ type: \"text\", text } as const) : undefined,\n          ...fileMessages,\n        ].filter(isDefined),\n        false,\n      );\n    },\n    [sendQuery],\n  );\n\n  const status = activeChat?.status;\n  const isLoading = status?.state === \"loading\";\n  const chatIsLoading =\n    isLoading && new Date(status.since) > new Date(Date.now() - 1 * MINUTE);\n\n  const onStopSending = useMemo(() => {\n    if (!isLoading || activeChatId === undefined) {\n      return;\n    }\n    return () => stopAskLLM(activeChatId);\n  }, [activeChatId, isLoading, stopAskLLM]);\n\n  return {\n    sendMessage,\n    chatIsLoading,\n    onStopSending,\n    sendQuery,\n  };\n};\n\nconst toBase64 = (file: File) =>\n  new Promise<string>((resolve, reject) => {\n    const reader = new FileReader();\n    reader.readAsDataURL(file);\n    reader.onload = () => resolve(reader.result as string);\n    reader.onerror = reject;\n  });\n\nconst toMediaMessage = async (\n  file: File,\n): Promise<\n  Extract<\n    LLMMessageContent,\n    {\n      source: {\n        type: \"base64\";\n      };\n    }\n  >\n> => {\n  const base64 = await toBase64(file);\n  const type = file.type.split(\"/\")[0] as \"image\";\n  return {\n    type,\n    source: {\n      type: \"base64\",\n      media_type: file.type,\n      data: base64,\n    },\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/useLLMChat.tsx",
    "content": "import { useEffectDeep } from \"prostgles-client/dist/prostgles\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport type { LoadedSuggestions } from \"../../Dashboard/dashboardUtils\";\nimport type { LLMSetupStateReady } from \"../Setup/useLLMSetupState\";\nimport { useLLMChatMessages } from \"./AskLLMChatMessages/hooks/useLLMChatMessages\";\n\nexport type UseLLMChatProps = LLMSetupStateReady &\n  Pick<Prgl, \"dbs\" | \"user\" | \"connectionId\" | \"db\"> & {\n    workspaceId: string | undefined;\n    loadedSuggestions: LoadedSuggestions | undefined;\n  };\n\nexport type LLMChatState = ReturnType<typeof useLLMChat>;\nexport const useLLMChat = (props: UseLLMChatProps) => {\n  const { dbs, credentials, firstPromptId, defaultCredential, prompts } = props;\n  const chatsFilter = useMemo(() => {\n    return {\n      /** TODO: fix $in: [string, null] types */\n      connection_id: { $in: [props.connectionId, null as any] },\n    };\n  }, [props.connectionId]);\n  const [selectedChatId, setSelectedChat] = useState<number>();\n  const { data: latestChats } = dbs.llm_chats.useSubscribe(chatsFilter, {\n    select: { \"*\": 1, created_ago: { $ageNow: [\"created\"] } },\n    orderBy: { created: -1 },\n  });\n\n  const latestChat = latestChats?.[0];\n  /**\n   * Always show the selected chat if it exists otherwise show latest\n   * If no chats exist, new chat will be created\n   */\n  const activeChat =\n    latestChats?.find((c) => c.id === selectedChatId) ?? latestChat;\n  const activeChatId = activeChat?.id;\n\n  const preferredPromptId = activeChat?.llm_prompt_id ?? firstPromptId;\n  const lastModelId = activeChat?.model;\n  const createNewChat = useCallback(\n    async (promptId: number, ifNoOtherChatsExist = false) => {\n      if (ifNoOtherChatsExist) {\n        const chat = await dbs.llm_chats.findOne(chatsFilter);\n        if (chat) {\n          console.warn(\"Chat already exists\", chat);\n          return;\n        }\n      }\n      if (!preferredPromptId) {\n        console.warn(\"No prompt found\", { prompts });\n        return;\n      }\n      await dbs.llm_chats.insert(\n        {\n          name: \"New chat\",\n          //@ts-ignore\n          user_id: undefined,\n          connection_id: props.connectionId,\n          llm_prompt_id: promptId,\n          model: lastModelId,\n        },\n        { returning: \"*\" },\n      );\n      setSelectedChat(undefined);\n    },\n    [\n      chatsFilter,\n      dbs.llm_chats,\n      lastModelId,\n      preferredPromptId,\n      prompts,\n      props.connectionId,\n    ],\n  );\n\n  useEffectDeep(() => {\n    if (latestChats && !latestChats.length && preferredPromptId) {\n      void createNewChat(preferredPromptId, true);\n    }\n  }, [latestChats, preferredPromptId, defaultCredential]);\n\n  const { llmMessages, messages } = useLLMChatMessages({\n    ...props,\n    activeChat,\n  });\n\n  const prompt = useMemo(() => {\n    return prompts.find((p) => p.id === activeChat?.llm_prompt_id);\n  }, [activeChat?.llm_prompt_id, prompts]);\n\n  return {\n    activeChatId,\n    createNewChat,\n    preferredPromptId,\n    llmMessages,\n    messages,\n    latestChats,\n    setActiveChat: setSelectedChat,\n    credentials,\n    defaultCredential,\n    activeChat,\n    prompt,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Chat/useLLMSchemaStr.ts",
    "content": "import { useMemoDeep, usePromise } from \"prostgles-client\";\nimport { useMemo } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Prgl } from \"../../../App\";\n\ntype P = Pick<Prgl, \"connection\" | \"db\" | \"tables\"> & {\n  activeChat: DBSSchema[\"llm_chats\"] | undefined;\n};\nexport const useLLMSchemaStr = ({ db, connection, tables, activeChat }: P) => {\n  const { db_schema_permissions } = activeChat ?? {};\n  const cachedSchemaPermissions = useMemoDeep(\n    () => db_schema_permissions || undefined,\n    [db_schema_permissions],\n  );\n\n  const tableConstraints = usePromise(async () => {\n    if (!db.sql) return;\n\n    const schemas = Object.entries(connection.db_schema_filter || { public: 1 })\n      .filter(([k, v]) => v)\n      .map(([k, v]) => k);\n    if (!schemas.includes(\"public\")) schemas.push(\"public\");\n    const query = `SELECT  \n      rel.oid as table_oid, \n      conname,\n      quote_ident(conname) as escaped_conname,\n      conkey ,  \n      pg_get_constraintdef(c.oid) as definition, \n      contype, \n      rel.relname as table_name ,\n      format('%I', rel.relname) as escaped_table_name,\n      nspname as schema\n      FROM pg_catalog.pg_constraint c\n      INNER JOIN pg_catalog.pg_class rel\n        ON rel.oid = c.conrelid\n      LEFT JOIN pg_catalog.pg_class frel\n        ON frel.oid = c.confrelid\n      INNER JOIN pg_catalog.pg_namespace nsp\n        ON nsp.oid = connamespace\n      WHERE nspname IN (\\${schemas:csv})\n    `;\n\n    const res = (await db.sql(query, { schemas }, { returnType: \"rows\" })) as {\n      table_oid: number;\n      conname: string;\n      escaped_conname: string;\n      conkey: number[];\n      definition: string;\n      contype: \"c\" | \"f\" | \"p\" | \"u\" | \"e\";\n      table_name: string;\n      escaped_table_name: string;\n      schema: string;\n    }[];\n\n    return res;\n  }, [db, connection.db_schema_filter]);\n\n  const dbSchemaForPrompt = useMemo(() => {\n    if (\n      !tableConstraints ||\n      !cachedSchemaPermissions ||\n      cachedSchemaPermissions.type === \"None\"\n    )\n      return \"\";\n    const allowedTables =\n      cachedSchemaPermissions.type === \"Full\" ?\n        tables\n      : tables.filter((t) => {\n          return cachedSchemaPermissions.tables.some(\n            (allowedTableName) => allowedTableName === t.name,\n          );\n        });\n    const res = allowedTables\n      .map((t) => {\n        const constraints = tableConstraints.filter(\n          (c) => c.table_oid === t.info.oid,\n        );\n\n        const singlePkeyConstraints = new Set<string>();\n        const singlePkeyColPositions = new Set<number>();\n        constraints\n          .filter((c) => c.contype === \"p\" && c.conkey.length === 1)\n          .forEach((c) => {\n            singlePkeyConstraints.add(c.conname);\n            singlePkeyColPositions.add(c.conkey[0]!);\n          });\n\n        const colDefs = t.columns\n          .sort((a, b) => a.ordinal_position - b.ordinal_position)\n          .map((c) => {\n            const dataTypePrecisionInfo =\n              c.udt_name.startsWith(\"int\") ? \"\"\n              : c.character_maximum_length ? `(${c.character_maximum_length})`\n              : c.numeric_precision ?\n                `(${c.numeric_precision}${c.numeric_scale ? `, ${c.numeric_scale}` : \"\"})`\n              : \"\";\n            return [\n              `  ${addDoubleQuotesIfNeeded(c.name)} ${c.udt_name}${dataTypePrecisionInfo}`,\n              c.is_pkey && singlePkeyColPositions.has(c.ordinal_position) ?\n                \"PRIMARY KEY\"\n              : \"\",\n              !c.is_pkey && !c.is_nullable ? \"NOT NULL\" : \"\",\n              !c.is_pkey && c.has_default ? `DEFAULT ${c.column_default}` : \"\",\n            ]\n              .filter((v) => v)\n              .join(\" \");\n          })\n          .concat(\n            constraints\n              .filter((c) => !singlePkeyConstraints.has(c.conname))\n              .map((c) => `CONSTRAINT ${c.escaped_conname} ${c.definition}`),\n          )\n          .join(\",\\n \");\n        const query = `CREATE TABLE ${t.name} (\\n${colDefs}\\n)`;\n        return {\n          query,\n          constraints,\n        };\n      })\n      /** Tables will least fkeys first */\n      .sort((a, b) => {\n        const aFkeys = a.constraints.filter((c) => c.contype === \"f\");\n        const bFkeys = b.constraints.filter((c) => c.contype === \"f\");\n        return aFkeys.length - bFkeys.length;\n      })\n      .map((t) => t.query)\n      .join(\";\\n\");\n\n    return res;\n  }, [tables, tableConstraints, cachedSchemaPermissions]);\n\n  return { dbSchemaForPrompt };\n};\n\nconst addDoubleQuotesIfNeeded = (name: string) => {\n  const identifierRegex = /^[a-z_][a-z0-9_]*$/;\n  const needsDoubleQuotes = !identifierRegex.test(name);\n  return needsDoubleQuotes ? JSON.stringify(name) : name;\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/ChatActionBar/AskLLMChatActionBar.tsx",
    "content": "import React from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { FlexRow } from \"@components/Flex\";\nimport type { AskLLMChatProps } from \"../Chat/AskLLMChat\";\nimport { AskLLMChatActionBarDatabaseAccess } from \"./AskLLMChatActionBarDatabaseAccess\";\nimport { AskLLMChatActionBarMCPTools } from \"./AskLLMChatActionBarMCPTools\";\nimport { AskLLMChatActionBarModelSelector } from \"./AskLLMChatActionBarModelSelector\";\nimport { AskLLMChatActionBarPromptSelector } from \"./AskLLMChatActionBarPromptSelector\";\n\nexport const AskLLMChatActionBar = (\n  props: Pick<AskLLMChatProps, \"prgl\" | \"setupState\"> & {\n    activeChat: DBSSchema[\"llm_chats\"];\n    dbSchemaForPrompt: string;\n    llmMessages: DBSSchema[\"llm_messages\"][];\n    prompt: DBSSchema[\"llm_prompts\"] | undefined;\n  },\n) => {\n  return (\n    <FlexRow\n      className={`AskLLMChatActionBar pr-1 ${window.isMobile ? \"gap-0\" : \"gap-p5\"}`}\n    >\n      <AskLLMChatActionBarMCPTools {...props} />\n      <AskLLMChatActionBarDatabaseAccess {...props} />\n      <AskLLMChatActionBarPromptSelector {...props} />\n      <AskLLMChatActionBarModelSelector {...props} />\n    </FlexRow>\n  );\n};\n\nexport const ChatActionBarBtnStyleProps = {\n  variant: \"icon\",\n  size: \"small\",\n  style: { opacity: 0.75, flex: 1, maxWidth: \"fit-content\", minWidth: \"0\" },\n} as const;\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/ChatActionBar/AskLLMChatActionBarDatabaseAccess.tsx",
    "content": "import {\n  mdiDatabase,\n  mdiDatabaseEdit,\n  mdiDatabaseSearch,\n  mdiTable,\n  mdiTableSearch,\n} from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useMemo } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SmartForm } from \"../../SmartForm/SmartForm\";\nimport type { AskLLMChatProps } from \"../Chat/AskLLMChat\";\nimport { ChatActionBarBtnStyleProps } from \"./AskLLMChatActionBar\";\nimport { LLM_PROMPT_VARIABLES } from \"@common/llmUtils\";\n\nexport const AskLLMChatActionBarDatabaseAccess = (\n  props: Pick<AskLLMChatProps, \"prgl\" | \"setupState\"> & {\n    activeChat: DBSSchema[\"llm_chats\"];\n    dbSchemaForPrompt: string;\n    prompt: DBSSchema[\"llm_prompts\"] | undefined;\n  },\n) => {\n  const { prgl, activeChat } = props;\n  const prompt = props.prompt?.prompt;\n  const activeChatId = activeChat.id;\n  const { dbs, dbsMethods, dbsTables } = prgl;\n\n  const { data: llm_chats_allowed_functions } =\n    dbs.llm_chats_allowed_functions.useSubscribe({\n      chat_id: activeChatId,\n      connection_id: prgl.connectionId,\n    });\n\n  const allowedFunctions = llm_chats_allowed_functions?.length;\n  const dataPermission = activeChat.db_data_permissions;\n  const tablePermissionInfo =\n    dataPermission?.Mode === \"Custom\" ?\n      dataPermission.tables.map(\n        (t) =>\n          `${t.tableName}: ${[\"select\", \"update\", \"insert\", \"delete\"].filter((v) => t[v]).join(\", \")}`,\n      )\n    : undefined;\n\n  const schemaPermission = activeChat.db_schema_permissions;\n  const schemaReadAccess = useMemo(() => {\n    if (!schemaPermission || schemaPermission.type === \"None\") {\n      return;\n    }\n    /**\n     * db_schema_permissions simply allows the backend to replace the schema variable from the prompt with actual schema before sending.\n     * If the prompt doesn't use the schema variable then no point showing the icon\n     */\n    if (!prompt?.includes(LLM_PROMPT_VARIABLES.SCHEMA)) {\n      return;\n    }\n    return { icon: mdiDatabase, type: schemaPermission.type };\n  }, [prompt, schemaPermission]);\n  const databaseAccess = useMemo(() => {\n    if (!dataPermission || dataPermission.Mode === \"None\") {\n      return;\n    }\n\n    const { Mode } = dataPermission;\n    const canEditData =\n      dataPermission.Mode === \"Custom\" &&\n      dataPermission.tables.some((t) => t.update || t.insert || t.delete);\n    const icon = {\n      \"Run readonly SQL\": mdiDatabaseSearch,\n      Custom: canEditData ? mdiTable : mdiTableSearch,\n      \"Run commited SQL\": mdiDatabaseEdit,\n    }[Mode];\n    return { icon, Mode };\n  }, [dataPermission]);\n\n  return (\n    <PopupMenu\n      data-command=\"LLMChatOptions.DatabaseAccess\"\n      contentClassName=\"p-0 max-w-700\"\n      positioning=\"above-center\"\n      title=\"Database access\"\n      button={\n        <Btn\n          {...ChatActionBarBtnStyleProps}\n          iconPath={\n            databaseAccess?.icon ?? schemaReadAccess?.icon ?? mdiDatabase\n          }\n          title={[\n            `Database access for this chat:\\n`,\n            `Schema read access: ${schemaReadAccess?.type ?? \"None\"}`,\n            `Data: \\n ${(tablePermissionInfo?.join(\", \") || dataPermission?.Mode) ?? \"None\"}`,\n            allowedFunctions ? `Allowed Functions: ${allowedFunctions}` : \"\",\n          ].join(\"\\n\")}\n          color={\n            schemaReadAccess?.icon || llm_chats_allowed_functions?.length ?\n              \"action\"\n            : undefined\n          }\n        />\n      }\n      onClickClose={false}\n    >\n      <SmartForm\n        db={dbs as DBHandlerClient}\n        label=\"\"\n        tableName=\"llm_chats\"\n        rowFilter={[{ fieldName: \"id\", value: activeChatId }]}\n        tables={dbsTables}\n        methods={dbsMethods}\n        columns={{\n          db_schema_permissions: 1,\n          db_data_permissions: 1,\n        }}\n        confirmUpdates={false}\n        disabledActions={[\"delete\", \"clone\", \"update\"]}\n        showJoinedTables={{\n          llm_chats_allowed_functions: {},\n        }}\n        contentClassname=\"p-1\"\n        // contentClassname=\"p-0 pb-1\"\n        jsonbSchemaWithControls={{\n          tables: props.prgl.tables,\n          schemaStyles: [\n            {\n              path: [\"3\", \"tables\"],\n              style: {\n                width: \"100%\",\n              },\n            },\n          ],\n        }}\n      />\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/ChatActionBar/AskLLMChatActionBarMCPTools.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport React, { useState } from \"react\";\nimport { MCPServers } from \"../../../pages/ServerSettings/MCPServers/MCPServers\";\nimport type { AskLLMChatProps } from \"../Chat/AskLLMChat\";\nimport { AskLLMChatActionBarMCPToolsBtn } from \"./AskLLMChatActionBarMCPToolsBtn\";\n\nexport const AskLLMChatActionBarMCPTools = (\n  props: Pick<AskLLMChatProps, \"prgl\" | \"setupState\"> & {\n    activeChat: DBSSchema[\"llm_chats\"];\n    dbSchemaForPrompt: string;\n  },\n) => {\n  const { prgl, activeChat } = props;\n  const { dbs } = prgl;\n\n  const [loading, setLoading] = useState(false);\n\n  return (\n    <PopupMenu\n      title=\"Allowed MCP Tools\"\n      contentClassName=\"py-1\"\n      clickCatchStyle={{ opacity: 1 }}\n      onClickClose={false}\n      data-command=\"LLMChatOptions.MCPTools\"\n      style={loading ? { visibility: \"hidden\" } : undefined}\n      onContentFinishedResizing={() => setLoading(false)}\n      button={\n        <AskLLMChatActionBarMCPToolsBtn\n          activeChat={activeChat}\n          dbs={dbs}\n          loading={loading}\n          dbsMethods={prgl.dbsMethods}\n        />\n      }\n    >\n      <MCPServers {...props.prgl} chatId={activeChat.id} />\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/ChatActionBar/AskLLMChatActionBarMCPToolsBtn.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport Popup from \"@components/Popup/Popup\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { mdiClose, mdiTools } from \"@mdi/js\";\nimport type { FilterItem } from \"prostgles-types\";\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport { MCPServerConfig } from \"../../../pages/ServerSettings/MCPServers/MCPServerConfig/MCPServerConfig\";\nimport { ChatActionBarBtnStyleProps } from \"./AskLLMChatActionBar\";\n\nexport const AskLLMChatActionBarMCPToolsBtn = ({\n  dbs,\n  activeChat,\n  loading,\n  dbsMethods,\n}: Pick<Prgl, \"dbs\" | \"dbsMethods\"> & {\n  loading: boolean;\n  activeChat: DBSSchema[\"llm_chats\"];\n}) => {\n  const { data: allowedTools } = dbs.mcp_server_tools.useSubscribe(\n    {\n      $existsJoined: {\n        \"llm_chats_allowed_mcp_tools.llm_chats\": {\n          id: activeChat.id,\n        },\n      },\n    } as FilterItem,\n    {\n      select: {\n        name: 1,\n        server_name: 1,\n      },\n    },\n  );\n\n  const allowedMcpServerNames = useMemo(\n    () =>\n      Array.from(new Set(allowedTools?.map((tool) => tool.server_name) ?? [])),\n    [allowedTools],\n  );\n\n  const [serverToConfigure, setServerToConfigure] =\n    useState<DBSSchema[\"mcp_servers\"]>();\n\n  const { data: mcpServersThatNeedEnabling } = dbs.mcp_servers.useFind(\n    {\n      enabled: false,\n      name: { $in: allowedMcpServerNames },\n    },\n    {},\n    { deps: [serverToConfigure] },\n  );\n\n  useEffect(() => {\n    const canBeEnabledServers = mcpServersThatNeedEnabling?.filter(\n      (server) => !server.config_schema,\n    );\n    if (!canBeEnabledServers?.length) {\n      return;\n    }\n    void dbs.mcp_servers.update(\n      {\n        name: { $in: canBeEnabledServers.map((s) => s.name) },\n      },\n      { enabled: true },\n    );\n  }, [dbs.mcp_servers, mcpServersThatNeedEnabling]);\n\n  const mcpServersThatConfiguring = mcpServersThatNeedEnabling?.filter(\n    (server) => server.config_schema,\n  );\n\n  const btnRef = React.useRef<HTMLButtonElement>(null);\n\n  return (\n    <>\n      <Btn\n        _ref={btnRef}\n        title={`MCP Tools Allowed in this chat${\n          allowedTools?.length ?\n            `: \\n\\n${allowedTools.map((t) => t.name).join(\"\\n\")}`\n          : \"\"\n        }`}\n        {...ChatActionBarBtnStyleProps}\n        color={\n          mcpServersThatConfiguring?.length ? \"danger\"\n          : allowedTools?.length ?\n            \"action\"\n          : undefined\n        }\n        iconPath={mdiTools}\n        loading={loading}\n        disabledInfo={!dbsMethods.getMcpHostInfo ? \"Must be admin\" : undefined}\n        children={allowedTools?.length || null}\n      />\n      {!!mcpServersThatConfiguring?.length &&\n        !serverToConfigure &&\n        btnRef.current && (\n          <Popup\n            title=\"MCP Servers Not Configured\"\n            contentClassName=\"p-2 gap-2 flex-col\"\n            onClose={() => {\n              setServerToConfigure(undefined);\n            }}\n            positioning=\"center\"\n            anchorEl={btnRef.current}\n            clickCatchStyle={{ opacity: 1 }}\n          >\n            <div className=\"ta-start font-18\">\n              Some of the MCP servers allowed for this chat are not configured.\n            </div>\n            <ScrollFade className=\"flex-col gap-1 o-auto\">\n              {mcpServersThatConfiguring.map((server) => (\n                <FlexRow\n                  key={server.name}\n                  className=\"pl-1 pr-p5 py-p5 b b-color rounded\"\n                >\n                  <div className=\"ta-start font-20 bold mr-2 f-1\">\n                    {server.name}\n                  </div>\n                  <Btn\n                    title=\"Configure and enable MCP server\"\n                    variant=\"filled\"\n                    color=\"action\"\n                    iconPath={mdiTools}\n                    onClick={() => {\n                      setServerToConfigure(server);\n                    }}\n                  >\n                    Configure\n                  </Btn>\n                  <Btn\n                    iconPath={mdiClose}\n                    title=\"Remove MCP server from chat\"\n                    onClickPromise={async () => {\n                      await dbs.llm_chats_allowed_mcp_tools.delete({\n                        chat_id: activeChat.id,\n                        $existsJoined: {\n                          \"mcp_server_tools.mcp_servers\": {\n                            name: server.name,\n                          },\n                        },\n                      } as FilterItem);\n                    }}\n                  />\n                </FlexRow>\n              ))}\n            </ScrollFade>\n          </Popup>\n        )}\n      {serverToConfigure && (\n        <MCPServerConfig\n          chatId={activeChat.id}\n          existingConfig={undefined}\n          dbs={dbs}\n          onDone={() => {\n            setServerToConfigure(undefined);\n          }}\n          serverName={serverToConfigure.name}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/ChatActionBar/AskLLMChatActionBarModelSelector.tsx",
    "content": "import { mdiAccountKey, mdiPencil, mdiPlus, mdiRefresh } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { DetailedJoinSelect } from \"prostgles-types\";\nimport React, { useMemo, useState } from \"react\";\nimport type { DetailedFilterBase } from \"@common/filterUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { Select, type FullOption } from \"@components/Select/Select\";\nimport { SvgIconFromURL } from \"@components/SvgIcon\";\nimport { SmartForm, SmartFormPopup } from \"../../SmartForm/SmartForm\";\nimport type { AskLLMChatProps } from \"../Chat/AskLLMChat\";\nimport { ChatActionBarBtnStyleProps } from \"./AskLLMChatActionBar\";\n\nexport const AskLLMChatActionBarModelSelector = (\n  props: Pick<AskLLMChatProps, \"prgl\" | \"setupState\"> & {\n    activeChat: DBSSchema[\"llm_chats\"];\n    dbSchemaForPrompt: string;\n    llmMessages: DBSSchema[\"llm_messages\"][];\n  },\n) => {\n  const { prgl, activeChat, llmMessages } = props;\n  const activeChatId = activeChat.id;\n  const { dbs, dbsMethods } = prgl;\n\n  const { data: models } = dbs.llm_models.useSubscribe(\n    {},\n    {\n      select: {\n        \"*\": 1,\n        llm_providers: {\n          logo_url: 1,\n        },\n        llm_credentials: {\n          $leftJoin: [\"llm_providers\", \"llm_credentials\"],\n          select: \"*\",\n          limit: 1,\n        } satisfies DetailedJoinSelect,\n      },\n    },\n  );\n\n  const [addProviderCredentials, setAddProviderCredentials] = useState(\"\");\n  const [viewModelForm, setViewModelForm] = useState<DetailedFilterBase>();\n  const totalCost = useMemo(() => {\n    return llmMessages.reduce((acc, msg) => {\n      const cost = parseFloat(msg.cost);\n      return acc + cost;\n    }, 0);\n  }, [llmMessages]);\n  return (\n    <>\n      {viewModelForm && (\n        <SmartForm\n          asPopup={true}\n          db={dbs as DBHandlerClient}\n          tableName=\"llm_models\"\n          rowFilter={[viewModelForm]}\n          tables={prgl.dbsTables}\n          methods={prgl.dbsMethods}\n          onClose={() => setViewModelForm(undefined)}\n        />\n      )}\n      {addProviderCredentials && (\n        <SmartForm\n          label={\"Add LLM credentials for \" + addProviderCredentials}\n          asPopup={true}\n          tableName=\"llm_credentials\"\n          db={dbs as DBHandlerClient}\n          methods={prgl.dbsMethods}\n          defaultData={{\n            provider_id: addProviderCredentials,\n          }}\n          onClose={() => setAddProviderCredentials(\"\")}\n          tables={prgl.dbsTables}\n          showJoinedTables={false}\n        />\n      )}\n      <Select\n        data-command=\"LLMChatOptions.Model\"\n        fullOptions={\n          models\n            ?.map(\n              ({\n                id,\n                name,\n                provider_id,\n                llm_credentials,\n                llm_providers,\n                pricing_info,\n              }) => {\n                const noCredentials = !llm_credentials.length;\n                const iconUrl = llm_providers[0]?.logo_url;\n                const isFree = Object.values(pricing_info ?? {}).every(\n                  (v) => v === 0,\n                );\n                return {\n                  key: id,\n                  label: name + (isFree ? \" (free)\" : \"\"),\n                  subLabel: provider_id,\n                  leftContent:\n                    !iconUrl ? undefined : (\n                      <SvgIconFromURL\n                        url={iconUrl}\n                        className=\"mr-p5 text-0\"\n                        style={{\n                          width: \"24px\",\n                          height: \"24px\",\n                        }}\n                      />\n                    ),\n                  rightContent:\n                    noCredentials ?\n                      <Btn\n                        title=\"Add provider API Key\"\n                        onClick={() => setAddProviderCredentials(provider_id)}\n                        color=\"action\"\n                        data-command=\"LLMChatOptions.Model.AddCredentials\"\n                        iconPath={mdiAccountKey}\n                      />\n                    : <Btn\n                        title=\"View info\"\n                        className=\"show-on-parent-hover\"\n                        onClick={() =>\n                          setViewModelForm({ fieldName: \"id\", value: id })\n                        }\n                        color=\"action\"\n                        iconPath={mdiPencil}\n                      />,\n                  disabledInfo: noCredentials ? \"No credentials\" : undefined,\n                } satisfies FullOption<number>;\n              },\n            )\n            .slice()\n            .sort(\n              (a, b) =>\n                (a.disabledInfo?.length ?? 0) - (b.disabledInfo?.length ?? 0) ||\n                a.label.localeCompare(b.label),\n            ) ?? []\n        }\n        size=\"small\"\n        btnProps={{\n          ...ChatActionBarBtnStyleProps,\n          iconPath: \"\",\n        }}\n        title=\"Model\"\n        emptyLabel=\"Select model...\"\n        className=\"ml-auto text-2\"\n        multiSelect={false}\n        value={activeChat.model}\n        onChange={(model) => {\n          if (!activeChatId) return;\n          void dbs.llm_chats.update(\n            { id: activeChatId },\n            {\n              model,\n            },\n          );\n        }}\n        endOfResultsContent={\n          <FlexCol className=\"p-1\">\n            <div className=\"text-1\">End of results.</div>\n            <FlexRowWrap>\n              <Btn\n                title=\"Refresh models\"\n                iconPath={mdiRefresh}\n                onClickPromise={async () => await dbsMethods.refreshModels?.()}\n                color=\"action\"\n                variant=\"faded\"\n              >\n                Refresh models\n              </Btn>\n              <SmartFormPopup\n                asPopup={true}\n                label=\"Add model\"\n                db={dbs as DBHandlerClient}\n                tableName=\"llm_models\"\n                methods={prgl.dbsMethods}\n                tables={prgl.dbsTables}\n                triggerButton={{\n                  iconPath: mdiPlus,\n                  title: \"Add model\",\n                  color: \"action\",\n                  children: \"Add model\",\n                  variant: \"faded\",\n                }}\n              />\n            </FlexRowWrap>\n          </FlexCol>\n        }\n      />\n      {!!totalCost && (\n        <Chip\n          title={\"Total cost: \" + totalCost}\n          style={{ fontSize: \"12px\", background: \"transparent\", opacity: 0.75 }}\n          className=\"pointer\"\n        >\n          ${totalCost.toFixed(2)}\n        </Chip>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/ChatActionBar/AskLLMChatActionBarPromptSelector.tsx",
    "content": "import { dashboardTypesContent } from \"@common/dashboardTypesContent\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { Marked } from \"@components/Chat/Marked\";\nimport { FlexCol } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport {\n  mdiCheck,\n  mdiCircleOutline,\n  mdiFileEyeOutline,\n  mdiViewCarousel,\n} from \"@mdi/js\";\nimport { usePromise, type DBHandlerClient } from \"prostgles-client\";\nimport React, { useMemo } from \"react\";\nimport { CodeEditorWithSaveButton } from \"../../CodeEditor/CodeEditorWithSaveButton\";\nimport { SmartCardList } from \"../../SmartCardList/SmartCardList\";\nimport type { AskLLMChatProps } from \"../Chat/AskLLMChat\";\nimport { ChatActionBarBtnStyleProps } from \"./AskLLMChatActionBar\";\n\nexport const AskLLMChatActionBarPromptSelector = (\n  props: Pick<AskLLMChatProps, \"prgl\" | \"setupState\"> & {\n    activeChat: DBSSchema[\"llm_chats\"];\n    dbSchemaForPrompt: string;\n  },\n) => {\n  const { prgl, setupState, activeChat, dbSchemaForPrompt } = props;\n  const activeChatId = activeChat.id;\n  const { prompts } = setupState;\n  const { dbs, dbsMethods } = prgl;\n  const prompt = useMemo(\n    () => prompts.find(({ id }) => id === activeChat.llm_prompt_id),\n    [prompts, activeChat.llm_prompt_id],\n  );\n  const promptContent = usePromise(async () => {\n    if (!prompt) return \"\";\n    return (\n      dbsMethods.getFullPrompt?.({\n        prompt: prompt.prompt,\n        schema: dbSchemaForPrompt,\n        dashboardTypesContent,\n      }) || prompt.prompt\n    );\n  }, [dbSchemaForPrompt, dbsMethods, prompt]);\n  return (\n    <PopupMenu\n      title=\"Prompt Selector\"\n      positioning=\"above-center\"\n      data-command=\"LLMChatOptions.Prompt\"\n      showFullscreenToggle={{}}\n      clickCatchStyle={{ opacity: 1 }}\n      onClickClose={false}\n      contentClassName=\"p-2 flex-col gap-1 f-1\"\n      rootChildClassname=\"f-1\"\n      button={\n        <Btn\n          title=\"Prompt\"\n          {...ChatActionBarBtnStyleProps}\n          iconPath={\n            prompt?.options?.prompt_type === \"dashboards\" ?\n              mdiViewCarousel\n            : undefined\n          }\n        >\n          {prompt?.name}\n        </Btn>\n      }\n    >\n      <SmartCardList\n        style={{\n          maxWidth: \"min(600px, 100vw)\",\n        }}\n        showTopBar={{ insert: true }}\n        rowProps={{\n          className: \"pointer hover-bg\",\n        }}\n        showEdit={true}\n        fieldConfigs={[\n          {\n            name: \"name\",\n            renderMode: \"full\",\n            render: (name, { id, description }) => {\n              const isActive = activeChat.llm_prompt_id === id;\n              return (\n                <Btn\n                  className={\"p-0 text-0 ta-start max-w-full ws-pre-wrap\"}\n                  style={{ padding: 0 }}\n                  variant=\"text\"\n                  iconPath={isActive ? mdiCheck : mdiCircleOutline}\n                  iconStyle={isActive ? { opacity: 1 } : { opacity: 0 }}\n                  onClick={() => {\n                    if (!activeChatId) return;\n                    void dbs.llm_chats.update(\n                      { id: activeChatId },\n                      {\n                        llm_prompt_id: id,\n                      },\n                    );\n                  }}\n                >\n                  <FlexCol className=\"gap-p25\">\n                    <div style={{ fontWeight: \"bold\" }}>{name}</div>\n                    <div style={{ fontWeight: \"normal\" }}>{description}</div>\n                  </FlexCol>\n                </Btn>\n              );\n            },\n          },\n          {\n            name: \"id\",\n            hide: true,\n          },\n          {\n            name: \"description\",\n            hide: true,\n          },\n        ]}\n        tableName={\"llm_prompts\"}\n        db={dbs as DBHandlerClient}\n        methods={prgl.dbsMethods}\n        tables={prgl.dbsTables}\n      />\n      {prompt && (\n        <CodeEditorWithSaveButton\n          key={prompt.id}\n          value={prompt.prompt}\n          label={<div className=\"ml-1\">Prompt template</div>}\n          headerButtons={\n            <PopupMenu\n              title=\"Prompt preview\"\n              subTitle=\"Preview of the prompt with context variables filled in\"\n              positioning=\"fullscreen\"\n              button={<Btn iconPath={mdiFileEyeOutline} title=\"Preview\" />}\n              data-command=\"LLMChatOptions.Prompt.Preview\"\n              contentClassName=\"p-2\"\n              showFullscreenToggle={{}}\n              rootChildClassname=\"f-1\"\n              onClickClose={false}\n            >\n              <Marked\n                className=\"f-1 m-auto\"\n                content={promptContent || \"\"}\n                loadedSuggestions={undefined}\n                codeHeader={undefined}\n                sqlHandler={undefined}\n              />\n            </PopupMenu>\n          }\n          language={\"text\"}\n          onSave={async (v) => {\n            await dbs.llm_prompts.update(\n              { id: prompt.id },\n              {\n                prompt: v,\n              },\n            );\n          }}\n        />\n      )}\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Setup/AddLLMPromptForm.tsx",
    "content": "import React from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport Btn from \"@components/Btn\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SmartForm } from \"../../SmartForm/SmartForm\";\nimport { mdiPlus } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\n\nexport const AddLLMPromptForm = ({\n  dbs,\n  dbsTables,\n}: Pick<Prgl, \"dbs\" | \"dbsTables\">) => {\n  return (\n    <PopupMenu\n      button={\n        <Btn iconPath={mdiPlus} color=\"action\" variant=\"filled\">\n          Create new prompt\n        </Btn>\n      }\n      render={(pClose) => (\n        <SmartForm\n          db={dbs as DBHandlerClient}\n          tableName=\"llm_prompts\"\n          tables={dbsTables}\n          methods={{}}\n          onSuccess={pClose}\n        />\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Setup/AskLLMAccessControl.tsx",
    "content": "import { mdiAssistant, mdiClose, mdiPlus } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { SectionHeader } from \"../../AccessControl/AccessControlRuleEditor\";\nimport type { ValidEditedAccessRuleState } from \"../../AccessControl/useEditedAccessRule\";\nimport { SmartForm } from \"../../SmartForm/SmartForm\";\nimport { SetupLLMCredentials } from \"./SetupLLMCredentials\";\nimport { useLLMSetupState } from \"./useLLMSetupState\";\n\ntype P = Prgl & {\n  accessRuleId: number | undefined;\n  editedRule: ValidEditedAccessRuleState | undefined;\n  className?: string;\n  style?: React.CSSProperties;\n};\nexport const AskLLMAccessControl = ({\n  dbs,\n  connectionId,\n  accessRuleId,\n  className,\n  style,\n  editedRule,\n  ...prgl\n}: P) => {\n  const { dbsTables } = prgl;\n  const [localPromptId, setLocalPromptId] = useState<number>();\n  const [localCredentialId, setLocalCredentialId] = useState<number>();\n  const rule = editedRule?.newRule ?? editedRule?.rule;\n  const isAllowed = !!rule?.access_control_allowed_llm?.length;\n  const { data: creds } = dbs.llm_credentials.useSubscribe({});\n  const { data: prompts } = dbs.llm_prompts.useSubscribe({});\n  const allowedItems = rule?.access_control_allowed_llm?.map((d) => ({\n    ...d,\n    llm_prompt: prompts?.find((p) => p.id === d.llm_prompt_id),\n    llm_credential: creds?.find((c) => c.id === d.llm_credential_id),\n  }));\n\n  /** We need to reset form after both values are undefined */\n  const [addFormKey, setAddFormKey] = useState(0);\n  const state = useLLMSetupState({ dbs, user: prgl.user });\n\n  return (\n    <FlexCol className={className} style={style}>\n      <SectionHeader icon={mdiAssistant}>AI Assistant</SectionHeader>\n      <FlexCol className={\"pl-2\"}>\n        <PopupMenu\n          positioning=\"center\"\n          title=\"Allow chatting with AI assistant\"\n          onClickClose={false}\n          clickCatchStyle={{ opacity: 1 }}\n          contentClassName=\"flex-col p-2 gap-1\"\n          data-command=\"AskLLMAccessControl\"\n          button={\n            <SwitchToggle\n              label={\"Allow chatting with AI assistant\"}\n              checked={isAllowed}\n              onChange={(_checked) => {}}\n            />\n          }\n          footerButtons={\n            !creds?.length ?\n              undefined\n            : [\n                {\n                  label: \"Close\",\n                  onClickClose: true,\n                },\n                {\n                  label: \"Disable\",\n                  variant: \"faded\",\n                  color: \"danger\",\n                  className: \"ml-auto\",\n                  onClick: () => {\n                    editedRule?.onChange({\n                      access_control_allowed_llm: [],\n                    });\n                  },\n                },\n                {\n                  label: \"Done\",\n                  variant: \"filled\",\n                  color: \"action\",\n                  onClickClose: true,\n                },\n              ]\n          }\n        >\n          {state.state !== \"ready\" ?\n            <SetupLLMCredentials\n              {...prgl}\n              asPopup={false}\n              dbs={dbs}\n              setupState={state}\n            />\n          : <>\n              <div className=\"ta-left\" style={{ maxWidth: \"500px\" }}>\n                To allow chatting with the AI assistant, allowed prompts and\n                credentials must be specified\n              </div>\n              <FlexCol className=\"gap-0 ta-left bold my-1\">\n                <div className=\"mb-1\">\n                  Allowed Prompts and APIs ({allowedItems?.length ?? 0})\n                </div>\n\n                {!allowedItems?.length && (\n                  <Btn\n                    variant={\"filled\"}\n                    color={\"action\"}\n                    className=\"mt-1\"\n                    data-command=\"AskLLMAccessControl.AllowAll\"\n                    onClick={() => {\n                      editedRule?.onChange({\n                        access_control_allowed_llm: state.credentials.flatMap(\n                          (c) => {\n                            return state.prompts.map((p) => {\n                              return {\n                                llm_prompt_id: p.id,\n                                llm_credential_id: c.id,\n                              };\n                            });\n                          },\n                        ),\n                      });\n                    }}\n                  >\n                    Allow all\n                  </Btn>\n                )}\n\n                {allowedItems?.map((a, i) => {\n                  return (\n                    <FlexRow key={i}>\n                      <Chip label=\"Key name\" variant=\"header\">\n                        {a.llm_credential?.name}\n                      </Chip>\n                      <Chip label=\"Provider\" variant=\"header\">\n                        {a.llm_credential?.provider_id}\n                      </Chip>\n                      <Chip label=\"Prompt\" variant=\"header\">\n                        {a.llm_prompt?.name}\n                      </Chip>\n                      <Btn\n                        className=\"ml-auto\"\n                        iconPath={mdiClose}\n                        onClick={() => {\n                          editedRule?.onChange({\n                            access_control_allowed_llm: (\n                              editedRule.newRule?.access_control_allowed_llm ??\n                              []\n                            ).filter((_, j) => j !== i),\n                          });\n                        }}\n                      />\n                    </FlexRow>\n                  );\n                })}\n                {!!allowedItems?.length && (\n                  <FormField\n                    label={\"Max requests per day\"}\n                    className=\"mx-p25 mt-1\"\n                    value={rule?.llm_daily_limit}\n                    type=\"integer\"\n                    hint=\"0 for unlimited\"\n                    data-command=\"AskLLMAccessControl.llm_daily_limit\"\n                    onChange={(v) => {\n                      editedRule?.onChange({ llm_daily_limit: +v });\n                    }}\n                  />\n                )}\n              </FlexCol>\n              <FlexCol className=\"gap-1\">\n                <div className=\"bold ta-start\">Add custom pair</div>\n                <SmartForm\n                  label=\"\"\n                  key={addFormKey}\n                  contentClassname=\"flex-row px-0 p-p25\"\n                  tableName=\"access_control_allowed_llm\"\n                  db={dbs as DBHandlerClient}\n                  methods={{}}\n                  tables={dbsTables}\n                  columnFilter={(c) =>\n                    [\"llm_prompt_id\", \"llm_credential_id\"].includes(c.name)\n                  }\n                  jsonbSchemaWithControls={{ noLabels: true }}\n                  onChange={(row) => {\n                    if (\"llm_credential_id\" in row) {\n                      setLocalCredentialId(row.llm_credential_id);\n                    }\n                    if (\"llm_prompt_id\" in row) {\n                      setLocalPromptId(row.llm_prompt_id);\n                    }\n                  }}\n                />\n                <Btn\n                  variant={\"filled\"}\n                  color={\"action\"}\n                  iconPath={mdiPlus}\n                  className=\"as-end\"\n                  style={{ marginBottom: \"1.5em\" }}\n                  disabledInfo={\n                    (\n                      allowedItems?.some(\n                        (d) =>\n                          d.llm_prompt_id === localPromptId &&\n                          d.llm_credential_id === localCredentialId,\n                      )\n                    ) ?\n                      \"Already allowed\"\n                    : !localPromptId ?\n                      \"Please select a prompt\"\n                    : !localCredentialId ?\n                      \"Please select a credential\"\n                    : undefined\n                  }\n                  onClick={() => {\n                    if (!localPromptId || !localCredentialId) return;\n                    editedRule?.onChange({\n                      access_control_allowed_llm: [\n                        ...(editedRule.newRule?.access_control_allowed_llm ??\n                          []),\n                        {\n                          llm_prompt_id: localPromptId,\n                          llm_credential_id: localCredentialId,\n                        },\n                      ],\n                    });\n                    setLocalPromptId(undefined);\n                    setLocalCredentialId(undefined);\n                    setAddFormKey(addFormKey + 1);\n                  }}\n                >\n                  Add\n                </Btn>\n              </FlexCol>\n            </>\n          }\n        </PopupMenu>\n      </FlexCol>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Setup/LLMProviderSetup.tsx",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useMemo } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Prgl } from \"../../../App\";\nimport Chip from \"@components/Chip\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport {\n  SmartCardList,\n  type SmartCardListProps,\n} from \"../../SmartCardList/SmartCardList\";\n\nexport const LLMProviderSetup = ({\n  dbs,\n  dbsMethods,\n  dbsTables,\n}: Pick<Prgl, \"dbs\" | \"dbsMethods\" | \"dbsTables\">) => {\n  const listProps = useMemo(() => {\n    return {\n      showTopBar: {\n        insert: true,\n      },\n      fieldConfigs: [\n        {\n          name: \"llm_providers\" as \"name\",\n          select: { logo_url: 1 },\n          label: \"\",\n        },\n        {\n          name: \"provider_id\",\n          hide: true,\n        },\n        {\n          name: \"name\",\n          label: \"\",\n          render: (name: string | null, row) => name || row.provider_id,\n        },\n        {\n          name: \"is_default\",\n          className: \"o-visible\",\n          renderMode: \"full\",\n          render: (is_default) =>\n            is_default ? <Chip color=\"blue\">default</Chip> : \" \",\n        },\n      ],\n    } satisfies Pick<\n      SmartCardListProps<DBSSchema[\"llm_credentials\"]>,\n      \"fieldConfigs\" | \"showTopBar\"\n    >;\n  }, []);\n\n  return (\n    <>\n      <SmartCardList\n        className=\"mb-1 w-fit min-w-0\"\n        db={dbs as DBHandlerClient}\n        tableName={\"llm_credentials\"}\n        methods={dbsMethods}\n        tables={dbsTables}\n        noDataComponent={\n          <InfoRow color=\"info\" variant=\"filled\">\n            No LLM providers\n          </InfoRow>\n        }\n        excludeNulls={true}\n        realtime={true}\n        {...listProps}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Setup/ProstglesSignup.tsx",
    "content": "import React from \"react\";\nimport { SuccessMessage } from \"@components/Animations\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport type { SetupLLMCredentialsProps } from \"./SetupLLMCredentials\";\nimport { isObject } from \"@common/publishUtils\";\nimport { ERR_CODE_MESSAGES } from \"../../../pages/Login/useLoginState\";\n\nexport const ProstglesSignup = ({\n  setupState,\n  dbsMethods,\n  dbs,\n}: Pick<SetupLLMCredentialsProps, \"setupState\" | \"dbs\" | \"dbsMethods\">) => {\n  const [email, setEmail] = React.useState(\n    setupState.globalSettings?.data?.prostgles_registration?.email || \"\",\n  );\n  const [didSendCode, setDidSendCode] = React.useState(false);\n  const [otpCode, setOtpCode] = React.useState(\"\");\n  const [error, setError] = React.useState<any>();\n  return (\n    <FlexCol className=\"ProstglesSignup\">\n      <div>\n        Provide an email address to get started.\n        <br />\n        Registering will get you free credits for OpenAI and Anthropic.\n      </div>\n      <FormField\n        id=\"email\"\n        type=\"email\"\n        label=\"Email\"\n        value={email}\n        required={true}\n        onChange={(email) => {\n          setEmail(email);\n        }}\n      />\n      {didSendCode && (\n        <>\n          <SuccessMessage\n            variant=\"text-sized\"\n            message=\"A code was sent to your email. Enter the code to complete registration\"\n            style={{ flex: 1, minWidth: 0 }}\n          />\n          <FormField\n            id=\"otp-code\"\n            type=\"text\"\n            label=\"OTP Code\"\n            value={otpCode}\n            required={true}\n            onChange={(otpCode) => {\n              setOtpCode(otpCode);\n            }}\n          />\n        </>\n      )}\n      {error && <ErrorComponent error={error} />}\n      <Btn\n        variant=\"filled\"\n        color=\"action\"\n        className=\"mt-1\"\n        data-command=\"ProstglesSignup.continue\"\n        onClickPromise={async () => {\n          setError(undefined);\n          try {\n            const { token, host, error, hasError } =\n              await dbsMethods.prostglesSignup!(email, otpCode);\n            if (hasError) {\n              throw error;\n            }\n            setDidSendCode(true);\n            if (!token) return;\n            const api_key = btoa(token);\n            await dbs.llm_providers.update(\n              {\n                id: \"Prostgles\",\n              },\n              {\n                api_url: `${host}/rest-api/cloud/methods/askLLM`,\n              },\n            );\n            await dbs.llm_credentials.insert({\n              provider_id: \"Prostgles\",\n              api_key,\n              //@ts-ignore\n              user_id: undefined,\n            });\n            /** This will trigger a page reload. Keep it last to ensure any errors during token validation are shown */\n            await dbs.global_settings.update(\n              {},\n              { prostgles_registration: { email, token, enabled: true } },\n            );\n          } catch (err) {\n            if (isObject(err) && \"code\" in err) {\n              setError(ERR_CODE_MESSAGES[err.code] ?? err);\n            } else {\n              setError(err);\n            }\n          }\n        }}\n      >\n        Continue\n      </Btn>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Setup/SetupLLMCredentials.tsx",
    "content": "import { mdiKey, mdiLogin } from \"@mdi/js\";\nimport React from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Loading from \"@components/Loader/Loading\";\nimport Popup from \"@components/Popup/Popup\";\nimport { AddLLMPromptForm } from \"./AddLLMPromptForm\";\nimport { LLMProviderSetup } from \"./LLMProviderSetup\";\nimport { ProstglesSignup } from \"./ProstglesSignup\";\nimport type { LLMSetupState } from \"./useLLMSetupState\";\nimport { isPlaywrightTest } from \"../../../i18n/i18nUtils\";\n\nexport type SetupLLMCredentialsProps = Pick<\n  Prgl,\n  \"theme\" | \"dbs\" | \"dbsTables\" | \"dbsMethods\"\n> & {\n  setupState: Exclude<LLMSetupState, { state: \"ready\" }>;\n} & (\n    | {\n        asPopup: true;\n        onClose: VoidFunction;\n      }\n    | {\n        asPopup?: false;\n        onClose?: undefined;\n      }\n  );\nexport const SetupLLMCredentials = (props: SetupLLMCredentialsProps) => {\n  const { dbs, dbsTables, dbsMethods, asPopup, onClose, setupState } = props;\n  const [setupType, setSetupType] = React.useState<\"free\" | \"api\" | undefined>(\n    isPlaywrightTest ? undefined : \"api\",\n  );\n  const { state, prompts } = setupState;\n  const content =\n    state === \"loading\" ? <Loading delay={1000} />\n    : state === \"cannotSetupOrNotAllowed\" ?\n      <div>Contact the admin to setup the AI assistant</div>\n    : <FlexCol data-command=\"SetupLLMCredentials\">\n        <FlexCol className=\"ai-center mb-2\">\n          {!setupType && (\n            <div className={\"font-18 bold my-2\"}>\n              To to use the AI assistant you need to either:\n            </div>\n          )}\n          <FlexRowWrap>\n            <Btn\n              data-command=\"SetupLLMCredentials.free\"\n              variant={setupType === \"free\" ? \"filled\" : \"faded\"}\n              color=\"action\"\n              onClick={() => setSetupType(\"free\")}\n              iconPath={mdiLogin}\n              disabledInfo={isPlaywrightTest ? undefined : \"Coming soon\"}\n            >\n              Signup (free)\n            </Btn>\n            <strong>Or</strong>\n            <Btn\n              data-command=\"SetupLLMCredentials.api\"\n              variant={setupType === \"api\" ? \"filled\" : \"faded\"}\n              color=\"action\"\n              onClick={() => setSetupType(\"api\")}\n              iconPath={mdiKey}\n            >\n              Provide API Keys\n            </Btn>\n          </FlexRowWrap>\n        </FlexCol>\n        {setupType === \"free\" && (\n          <ProstglesSignup\n            setupState={setupState}\n            dbs={dbs}\n            dbsMethods={dbsMethods}\n          />\n        )}\n        {setupType === \"api\" && <LLMProviderSetup {...props} />}\n        {setupType && !prompts.length && (\n          <FlexCol className=\"mt-2\">\n            <InfoRow color=\"info\" variant=\"filled\">\n              No existing prompts\n            </InfoRow>\n            <AddLLMPromptForm dbs={dbs} dbsTables={dbsTables} />\n          </FlexCol>\n        )}\n      </FlexCol>;\n\n  if (!asPopup) {\n    return content;\n  }\n  return (\n    <Popup\n      title=\"Setup AI assistant\"\n      positioning=\"top-center\"\n      data-command=\"AskLLM.popup\"\n      contentClassName=\"p-2\"\n      onClose={onClose}\n      clickCatchStyle={{ opacity: 1 }}\n    >\n      {content}\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Setup/useLLMSetupState.ts",
    "content": "import { useMemo } from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport type { DbsByUserType } from \"../../Dashboard/DBS\";\n\nexport type LLMSetupState = ReturnType<typeof useLLMSetupState>;\nexport type LLMSetupStateReady = Extract<LLMSetupState, { state: \"ready\" }>;\n\nexport const useLLMSetupState = (props: Pick<Prgl, \"dbs\" | \"user\">) => {\n  const dbs = props.dbs as DbsByUserType;\n  const { user } = props;\n  const { data: credentials } = dbs.llm_credentials.useSubscribe();\n  const isAdmin = user?.type === \"admin\";\n  const globalSettings = dbs.global_settings?.useSubscribeOne?.();\n\n  const mcpServers = dbs.mcp_servers.useFind(\n    {},\n    { select: { name: 1, icon_path: 1 } },\n  );\n  const mcpServerIcons = useMemo(() => {\n    const iconMap = new Map<string, string>();\n    mcpServers.data?.forEach((s) => {\n      if (s.icon_path) {\n        iconMap.set(s.name, s.icon_path);\n      }\n    });\n    iconMap.set(\"prostgles-db\", \"Database\");\n    return iconMap;\n  }, [mcpServers]);\n\n  /** For backward compatibility pick last credential as default */\n  const defaultCredential =\n    credentials?.find((c) => c.is_default) ?? credentials?.at(-1);\n\n  /** Order by Id to ensure the first prompt is the default chat */\n  const { data: prompts } = dbs.llm_prompts.useSubscribe(\n    {},\n    { orderBy: { id: 1 } },\n  );\n  const firstPromptId = prompts?.[0]?.id;\n\n  if (isAdmin) {\n    if (!globalSettings?.data || !credentials || !prompts) {\n      return {\n        state: \"loading\" as const,\n        prompts,\n      };\n    }\n\n    if (!defaultCredential || !firstPromptId) {\n      return {\n        state: \"mustSetup\" as const,\n        prompts,\n        globalSettings,\n      };\n    }\n\n    const {\n      data: { prostgles_registration },\n    } = globalSettings;\n    if (prostgles_registration) {\n      const { enabled, email, token } = prostgles_registration;\n      // const quota = await POST(\"/api/llm/quota\", { token });\n      console.error(\"Finish this\");\n    }\n  } else if (!defaultCredential || !credentials || !prompts || !firstPromptId) {\n    return {\n      state: \"cannotSetupOrNotAllowed\" as const,\n      prompts,\n    };\n  }\n\n  const result = {\n    state: \"ready\" as const,\n    defaultCredential,\n    globalSettings,\n    credentials,\n    prompts,\n    firstPromptId,\n    mcpServerIcons,\n  };\n\n  return result;\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Tools/AskLLMToolApprover.tsx",
    "content": "import type { DBHandlerClient } from \"prostgles-client\";\nimport React, { useCallback, useMemo } from \"react\";\n\nimport { getMCPToolNameParts } from \"@common/prostglesMcp\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { Marked } from \"@components/Chat/Marked\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport Popup from \"@components/Popup/Popup\";\nimport { CodeEditorWithSaveButton } from \"src/dashboard/CodeEditor/CodeEditorWithSaveButton\";\nimport type { Prgl } from \"../../../App\";\nimport { isEmpty } from \"../../../utils/utils\";\nimport type { DBS } from \"../../Dashboard/DBS\";\nimport { ProstglesMCPToolsWithUI } from \"../Chat/AskLLMChatMessages/ProstglesToolUseMessage/ProstglesToolUseMessage\";\nimport type { ToolUseMessage } from \"../Chat/AskLLMChatMessages/ToolUseChatMessage/ToolUseChatMessage\";\nimport {\n  useLLMToolsApprover,\n  type ApproveRequest,\n  type ToolApproval,\n} from \"./useLLMToolsApprover\";\n\nexport type AskLLMToolsProps = {\n  dbs: DBS;\n  db: DBHandlerClient;\n  activeChat: DBSSchema[\"llm_chats\"];\n  prompt: DBSSchema[\"llm_prompts\"];\n  messages: DBSSchema[\"llm_messages\"][];\n  sendQuery: (\n    msg: DBSSchema[\"llm_messages\"][\"message\"] | undefined,\n    isToolApproval: boolean,\n  ) => void;\n} & Pick<Prgl, \"methods\" | \"connection\">;\n\nexport const AskLLMToolApprover = (props: AskLLMToolsProps) => {\n  const { dbs, activeChat } = props;\n  const activeChatId = activeChat.id;\n  const [mustApprove, setMustApprove] = React.useState<\n    {\n      onResponse: (mode: \"once\" | \"for-chat\" | \"deny\") => void;\n      toolUseMessage: ToolUseMessage;\n    } & ApproveRequest\n  >();\n\n  const { db_data_permissions } = activeChat;\n  const requestApproval = useCallback(\n    async (req: ApproveRequest, toolUseMessage: ToolUseMessage) => {\n      return new Promise<ToolApproval>((resolve) => {\n        setMustApprove({\n          toolUseMessage,\n          ...req,\n          onResponse: async (toolUseResponse) => {\n            if (toolUseResponse !== \"deny\") {\n              const auto_approve = toolUseResponse === \"for-chat\";\n              if (req.type === \"mcp\") {\n                const { tool_id } = req;\n                if (typeof tool_id !== \"number\") {\n                  throw new Error(\"Unexpected. tool_id missing\");\n                }\n                await dbs.llm_chats_allowed_mcp_tools.upsert(\n                  { chat_id: activeChatId, tool_id },\n                  {\n                    chat_id: activeChatId,\n                    tool_id,\n                    auto_approve,\n                  },\n                );\n              } else if (req.type === \"prostgles-db-methods\") {\n                const { server_function_id } = req;\n                await dbs.llm_chats_allowed_functions.upsert(\n                  { chat_id: activeChatId, server_function_id },\n                  {\n                    chat_id: activeChatId,\n                    server_function_id,\n                    auto_approve,\n                  },\n                );\n              } else if (req.type === \"prostgles-db\") {\n                await dbs.llm_chats.update(\n                  {\n                    id: activeChatId,\n                  },\n                  {\n                    db_data_permissions:\n                      req.tool_name === \"execute_sql_with_commit\" ?\n                        {\n                          Mode: \"Run commited SQL\",\n                          auto_approve,\n                        }\n                      : req.tool_name === \"execute_sql_with_rollback\" ?\n                        {\n                          Mode: \"Run readonly SQL\",\n                          auto_approve,\n                        }\n                      : {\n                          ...(db_data_permissions as Extract<\n                            typeof db_data_permissions,\n                            { Mode: \"Custom\" }\n                          >),\n                          auto_approve,\n                        },\n                  },\n                );\n              } else {\n                throw new Error(\n                  `Unexpected tool use request ${JSON.stringify(req)}`,\n                );\n              }\n            }\n            resolve({\n              approved: toolUseResponse !== \"deny\",\n              mode: toolUseResponse,\n            });\n          },\n        });\n      });\n    },\n    [\n      dbs.llm_chats_allowed_mcp_tools,\n      dbs.llm_chats_allowed_functions,\n      dbs.llm_chats,\n      activeChatId,\n      db_data_permissions,\n    ],\n  );\n  const nameParts = useMemo(\n    () => (mustApprove ? getMCPToolNameParts(mustApprove.name) : undefined),\n    [mustApprove],\n  );\n\n  useLLMToolsApprover({ ...props, requestApproval });\n\n  if (!mustApprove) return null;\n\n  const { toolUseMessage, description, name } = mustApprove;\n\n  const ToolUI = ProstglesMCPToolsWithUI[name];\n  return (\n    <Popup\n      title={\n        mustApprove.type === \"mcp\" ?\n          `Allow tool from ${nameParts?.serverName} to run?`\n        : `Allow function to run?`\n      }\n      showFullscreenToggle={{}}\n      onClose={() => {\n        mustApprove.onResponse(\"deny\");\n        setMustApprove(undefined);\n      }}\n      clickCatchStyle={{ opacity: 1 }}\n      contentStyle={{\n        maxWidth: \"min(800px, 100vw)\",\n        width: \"100%\",\n      }}\n      contentClassName=\"p-1 f-1 as-center\"\n      footerButtons={[\n        {\n          label: \"Deny\",\n          color: \"danger\",\n          variant: \"faded\",\n          \"data-command\": \"AskLLMToolApprover.Deny\",\n          onClick: () => {\n            mustApprove.onResponse(\"deny\");\n            setMustApprove(undefined);\n          },\n        },\n        {\n          className: \"ml-auto\",\n          label: \"Allow once\",\n          color: \"action\",\n          variant: \"filled\",\n          \"data-command\": \"AskLLMToolApprover.AllowOnce\",\n          onClick: () => {\n            mustApprove.onResponse(\"once\");\n            setMustApprove(undefined);\n          },\n        },\n        {\n          label: \"Allow always\",\n          color: \"action\",\n          variant: \"filled\",\n          \"data-command\": \"AskLLMToolApprover.AllowAlways\",\n          onClick: () => {\n            mustApprove.onResponse(\"for-chat\");\n            setMustApprove(undefined);\n          },\n        },\n      ]}\n    >\n      <FlexCol className=\"f-1\">\n        <FlexRow>\n          Run <strong>{nameParts?.toolName}</strong>\n          {mustApprove.type === \"mcp\" && (\n            <FlexRow>\n              from <strong>{nameParts?.serverName}</strong>\n            </FlexRow>\n          )}\n        </FlexRow>\n        <Marked\n          style={{ maxHeight: \"200px\" }}\n          className=\"ta-start\"\n          content={description}\n          codeHeader={undefined}\n          loadedSuggestions={undefined}\n          sqlHandler={undefined}\n        />\n        {!isEmpty(toolUseMessage.input) && (\n          <>\n            {ToolUI ?\n              <ToolUI.component\n                chatId={activeChat.id}\n                message={toolUseMessage}\n                toolUseResult={undefined}\n                workspaceId={undefined}\n              />\n            : <CodeEditorWithSaveButton\n                label=\"Input\"\n                value={JSON.stringify(toolUseMessage.input, null, 2)}\n                language=\"json\"\n              />\n            }\n          </>\n        )}\n      </FlexCol>\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Tools/loadGeneratedWorkspaces/loadGeneratedBarchart.ts",
    "content": "import type { BarchartWindowInsertModel } from \"@common/DashboardTypes\";\nimport type { WindowInsertModel } from \"./loadGeneratedWorkspaces\";\nimport type {\n  DBSchemaTableWJoins,\n  WindowData,\n} from \"src/dashboard/Dashboard/dashboardUtils\";\nimport { aggFunctions } from \"src/dashboard/W_Table/ColumnMenu/FunctionSelector/functions\";\nimport { pickKeys } from \"prostgles-types\";\n\nexport const loadGeneratedBarchart = (\n  generatedWindow: BarchartWindowInsertModel,\n  tables: DBSchemaTableWJoins[],\n): WindowInsertModel => {\n  const { xAxis, yAxisColumn, title } = generatedWindow;\n\n  const funcDef = aggFunctions.find(\n    (f) =>\n      f.key ===\n      (xAxis.aggregation === \"count(*)\" ?\n        \"$countAll\"\n      : \"$\" + xAxis.aggregation),\n  )!;\n  const xColName =\n    xAxis.aggregation === \"count(*)\" ?\n      \"Count\"\n    : `${funcDef.name}(${xAxis.column})`;\n  const table =\n    \"table_name\" in generatedWindow ?\n      tables.find((t) => t.name === generatedWindow.table_name)\n    : undefined;\n  const { joinPath } = xAxis;\n  const xAxisTable =\n    !joinPath ? table : tables.find((t) => t.name === joinPath.at(-1)?.table);\n  const xAxisColumnInfo =\n    xAxis.aggregation === \"count(*)\" ?\n      undefined\n    : xAxisTable?.columns.find((c) => c.name === xAxis.column);\n\n  const xAxisColumn: NonNullable<WindowData[\"columns\"]>[number] = {\n    name: xColName,\n    width: 250,\n    show: true,\n    computedConfig: {\n      column: xAxis.column === \"count(*)\" ? undefined : xAxis.column,\n      ...(funcDef.outType === \"sameAsInput\" ?\n        pickKeys(xAxisColumnInfo!, [\"tsDataType\", \"udt_name\"])\n      : funcDef.outType),\n      funcDef: {\n        ...funcDef,\n        subLabel: \"\",\n      },\n    },\n    style: {\n      type: \"Barchart\",\n      barColor: \"#0081A7\",\n      textColor: \"\",\n    },\n  };\n\n  const columns: WindowData[\"columns\"] = [\n    {\n      name: yAxisColumn,\n      width: 150,\n      show: true,\n    },\n    joinPath ?\n      {\n        name: xColName,\n        nested: {\n          path: joinPath,\n          columns: [\n            xAxisColumn,\n            ...xAxisTable!.columns.map((col) => ({\n              name: col.name,\n              show: false,\n            })),\n          ],\n        },\n      }\n    : xAxisColumn,\n  ];\n  if (\"sql\" in generatedWindow) {\n    return {\n      type: \"sql\",\n      name: generatedWindow.title ?? \"Barchart SQL\",\n      sql: generatedWindow.sql,\n      columns,\n    };\n  }\n\n  const { table_name, filter, filterOperand, quickFilterGroups } =\n    generatedWindow;\n  table?.columns.forEach((col) => {\n    if (col.name !== yAxisColumn) {\n      columns.push({\n        name: col.name,\n        show: false,\n      });\n    }\n  });\n  return {\n    type: \"table\",\n    title,\n    table_name,\n    columns,\n    filter,\n    options: {\n      filterOperand,\n      quickFilterGroups,\n      hideEditRow: true,\n      hideInsertButton: true,\n    } satisfies WindowData<\"table\">[\"options\"],\n    sort: [{ key: xColName, asc: false, nulls: \"last\" }] satisfies NonNullable<\n      WindowData[\"sort\"]\n    >,\n  } satisfies WindowInsertModel;\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Tools/loadGeneratedWorkspaces/loadGeneratedMap.ts",
    "content": "import type { MapWindowInsertModel } from \"@common/DashboardTypes\";\nimport type { LinkOption, WindowInsertModel } from \"./loadGeneratedWorkspaces\";\nimport { getPaletteRGBColor } from \"src/dashboard/W_Table/ColumnMenu/ColorPicker\";\n\nexport const loadGeneratedMap = (\n  generatedWindow: MapWindowInsertModel,\n): { window: WindowInsertModel; linkOptions: LinkOption[] } => {\n  const { title, layers } = generatedWindow;\n\n  const tableLayer = layers.find(\n    (l): l is Exclude<typeof l, { sql: string }> => \"table_name\" in l,\n  );\n  const window: WindowInsertModel = {\n    type: \"map\",\n    title,\n    table_name: tableLayer?.table_name || \"\",\n  };\n\n  const linkOptions: LinkOption[] = layers.map((l, i) => {\n    const columns = [\n      {\n        name: l.geoColumn,\n        colorArr: [...getPaletteRGBColor(i), 200],\n      },\n    ];\n    if (\"table_name\" in l) {\n      const { filter, table_name, filterOperand, quickFilterGroups } = l;\n      const smartGroupFilter =\n        filter ?\n          filterOperand === \"OR\" ?\n            { $or: filter }\n          : { $and: filter }\n        : undefined;\n      if (quickFilterGroups) {\n        console.warn(\"Map with quickFilterGroups is not supported yet\");\n      }\n      return {\n        type: \"map\",\n        columns: columns,\n        dataSource: {\n          type: \"local-table\",\n          localTableName: table_name,\n          smartGroupFilter,\n        },\n      } satisfies LinkOption;\n    }\n\n    return {\n      type: \"map\",\n      columns,\n      dataSource: { type: \"sql\", sql: l.sql, withStatement: \"\" },\n    } satisfies LinkOption;\n  });\n  return {\n    window,\n    linkOptions,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Tools/loadGeneratedWorkspaces/loadGeneratedTimechart.ts",
    "content": "import type { TimechartWindowInsertModel } from \"@common/DashboardTypes\";\nimport { getPaletteRGBColor } from \"src/dashboard/W_Table/ColumnMenu/ColorPicker\";\nimport type { LinkOption, WindowInsertModel } from \"./loadGeneratedWorkspaces\";\n\nexport const loadGeneratedTimechart = (\n  generatedWindow: TimechartWindowInsertModel,\n): { window: WindowInsertModel; linkOptions: LinkOption[] } => {\n  const { title, layers } = generatedWindow;\n\n  const tableLayer = layers.find(\n    (l): l is Exclude<typeof l, { sql: string }> => \"table_name\" in l,\n  );\n  const window: WindowInsertModel = {\n    type: \"timechart\",\n    title,\n    table_name: tableLayer?.table_name || \"\",\n  };\n\n  const linkOptions: LinkOption[] = layers.map((l, i) => {\n    const columns = [\n      {\n        name: l.dateColumn,\n        statType:\n          l.yAxis === \"count(*)\" ?\n            undefined\n          : {\n              funcName: `$${l.yAxis.aggregation}` as const,\n              numericColumn: l.yAxis.column,\n            },\n\n        colorArr: [...getPaletteRGBColor(i), 255],\n      },\n    ];\n    if (\"table_name\" in l) {\n      const { filter, table_name, filterOperand, quickFilterGroups } = l;\n      const smartGroupFilter =\n        filter ?\n          filterOperand === \"OR\" ?\n            { $or: filter }\n          : { $and: filter }\n        : undefined;\n      if (quickFilterGroups) {\n        console.warn(\"quickFilterGroups is not supported yet\");\n      }\n      return {\n        type: \"timechart\",\n        columns,\n        title: l.title,\n        groupByColumn: l.groupByColumn,\n        dataSource: {\n          type: \"local-table\",\n          localTableName: table_name,\n          smartGroupFilter,\n        },\n      } satisfies LinkOption;\n    }\n\n    return {\n      type: \"timechart\",\n      columns,\n      title: l.title,\n      groupByColumn: l.groupByColumn,\n      dataSource: { type: \"sql\", sql: l.sql, withStatement: \"\" },\n    } satisfies LinkOption;\n  });\n  return {\n    window,\n    linkOptions,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Tools/loadGeneratedWorkspaces/loadGeneratedWorkspaces.ts",
    "content": "import type { WorkspaceInsertModel } from \"@common/DashboardTypes\";\nimport {\n  isObject,\n  type DBSSchema,\n  type DBSSchemaForInsert,\n} from \"@common/publishUtils\";\nimport { isDefined, omitKeys } from \"prostgles-types\";\nimport type { WindowData } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport type { Prgl } from \"../../../../App\";\nimport { CHIP_COLOR_NAMES } from \"../../../W_Table/ColumnMenu/ColumnDisplayFormat/ChipStylePalette\";\nimport { loadGeneratedBarchart } from \"./loadGeneratedBarchart\";\nimport { loadGeneratedMap } from \"./loadGeneratedMap\";\nimport { loadGeneratedTimechart } from \"./loadGeneratedTimechart\";\n\nexport const loadGeneratedWorkspaces = async (\n  generatedWorkspaces: WorkspaceInsertModel[],\n  tool_use_id: string,\n  { dbs, connectionId, tables }: Pick<Prgl, \"dbs\" | \"connectionId\" | \"tables\">,\n) => {\n  const workspaces = generatedWorkspaces.map((wsp, i) => {\n    const windows: WindowInsertModel[] = wsp.windows.map((generatedWindow) => {\n      if (generatedWindow.type === \"barchart\") {\n        return loadGeneratedBarchart(generatedWindow, tables);\n      } else if (generatedWindow.type === \"map\") {\n        const { window } = loadGeneratedMap(generatedWindow);\n        return window;\n      } else if (generatedWindow.type === \"timechart\") {\n        const { window } = loadGeneratedTimechart(generatedWindow);\n        return window;\n      } else if (generatedWindow.type === \"table\") {\n        const columns = generatedWindow.columns?.map((c) => {\n          return {\n            ...c,\n            show: true,\n            style: c.styling && {\n              type: \"Conditional\",\n              conditions: c.styling.conditions.map((cond) => {\n                // \"textColor\": \"#ffffff\",\n                // \"textColorDarkMode\": \"#2386d5\",\n                // \"chipColor\": \"#673AB7\"\n                const style =\n                  Object.entries(CHIP_COLOR_NAMES).find(\n                    ([k]) => k === cond.chipColor,\n                  )?.[1] ?? CHIP_COLOR_NAMES.blue!;\n                return {\n                  condition: cond.value,\n                  operator: cond.operator,\n                  textColor: style.textColor,\n                  chipColor: style.color,\n                  textColorDarkMode: style.textColorDarkMode,\n                };\n              }),\n            },\n          };\n        });\n        const {\n          sort,\n          filter,\n          filterOperand,\n          quickFilterGroups,\n          cardLayout,\n          table_name,\n          title,\n        } = generatedWindow;\n        return {\n          type: \"table\",\n          title,\n          columns,\n          filter,\n          options: {\n            filterOperand,\n            quickFilterGroups,\n            cardLayout,\n          } satisfies WindowData<\"table\">[\"options\"],\n          sort: sort\n            ?.map((s) => {\n              const nestedCol = columns?.find(\n                (c) => c.name === s.key && c.nested,\n              );\n              if (nestedCol) {\n                return {\n                  ...s,\n                  key: `${s.key}.value`,\n                };\n              }\n              return s;\n            })\n            .filter(isDefined),\n          table_name,\n        } satisfies Omit<\n          DBSSchemaForInsert[\"windows\"],\n          \"last_updated\" | \"user_id\"\n        >;\n      }\n      return omitKeys(\n        {\n          ...generatedWindow,\n          name: generatedWindow.name || \"Query\",\n        },\n        [\"id\"],\n      );\n    });\n    return {\n      ...wsp,\n      options: {\n        pinnedMenu: false,\n      },\n      user_id: undefined as any,\n      last_updated: undefined as any,\n      connection_id: connectionId,\n      windows,\n      source: {\n        tool_use_id,\n      },\n      layout_mode: \"fixed\",\n    } satisfies DBSSchemaForInsert[\"workspaces\"] & {\n      windows: Omit<\n        DBSSchemaForInsert[\"windows\"],\n        \"last_updated\" | \"user_id\"\n      >[];\n    };\n  });\n\n  const insertedWorkspaces = await dbs.workspaces.insert(workspaces, {\n    returning: \"*\",\n  });\n\n  const wspToWindow: {\n    wspIndex: number;\n    wIndex: number;\n    insertedWindowId: string;\n  }[] = [];\n\n  /** Add links for charts */\n  await Promise.all(\n    generatedWorkspaces.map(\n      async (generatedWorkspace, generatedWorkspaceIndex) => {\n        await Promise.all(\n          generatedWorkspace.windows.map(\n            async (generatedWindow, generatedWindowIndex) => {\n              const insertedWorkspace =\n                insertedWorkspaces[generatedWorkspaceIndex];\n              const insertedWindows = (insertedWorkspace as any).windows as\n                | DBSSchema[\"windows\"][]\n                | DBSSchema[\"windows\"];\n              // TODO fix bug where a single inserted window is not an array but an object\n              const insertedWindow =\n                isObject(insertedWindows) && !generatedWindowIndex ?\n                  insertedWindows\n                : insertedWindows[generatedWindowIndex];\n\n              const generatedWindowChartOptions =\n                generatedWindow.type === \"map\" ?\n                  loadGeneratedMap(generatedWindow)\n                : generatedWindow.type === \"timechart\" ?\n                  loadGeneratedTimechart(generatedWindow)\n                : undefined;\n\n              wspToWindow.push({\n                wspIndex: generatedWorkspaceIndex,\n                wIndex: generatedWindowIndex,\n                insertedWindowId: insertedWindow.id,\n              });\n\n              if (generatedWindowChartOptions && insertedWorkspace) {\n                const insertedChart: DBSSchema[\"windows\"] | undefined =\n                  insertedWindow;\n                if (insertedChart) {\n                  await dbs.links.insert(\n                    generatedWindowChartOptions.linkOptions.map((options) => {\n                      return {\n                        w1_id: insertedChart.id,\n                        w2_id: insertedChart.id,\n                        workspace_id: insertedWorkspace.id,\n                        options,\n                        last_updated: undefined as any,\n                        user_id: undefined as any,\n                      };\n                    }),\n                  );\n                }\n              }\n            },\n          ),\n        );\n      },\n    ),\n  );\n\n  /** Update layouts with correct view id */\n  await Promise.all(\n    insertedWorkspaces.map(async (wsp, i) => {\n      const layout = { ...(wsp.layout || {}) };\n      const fixIds = (layout: any) => {\n        if (\"items\" in layout) {\n          layout.items.forEach((item: any) => {\n            fixIds(item);\n          });\n        } else {\n          const viewIndex = generatedWorkspaces[i]?.windows.findIndex(\n            (w) => w.id === layout.id,\n          );\n          const insertedWindowId = wspToWindow.find(\n            (w) => w.wspIndex === i && w.wIndex === viewIndex,\n          )?.insertedWindowId;\n          layout.id =\n            isDefined(insertedWindowId) ? insertedWindowId : layout.id;\n        }\n      };\n      fixIds(layout);\n      await dbs.workspaces.update({ id: wsp.id }, { layout });\n    }),\n  );\n\n  console.log(generatedWorkspaces);\n\n  return insertedWorkspaces;\n};\n\nexport type WindowInsertModel = Omit<\n  DBSSchemaForInsert[\"windows\"],\n  \"last_updated\" | \"user_id\"\n>;\n\nexport type LinkOption = DBSSchema[\"links\"][\"options\"];\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/Tools/useLLMToolsApprover.tsx",
    "content": "import {\n  getLLMMessageToolUse,\n  isAssistantMessageRequestingToolUse,\n} from \"@common/llmUtils\";\nimport type { AllowedChatTool } from \"@common/prostglesMcp\";\nimport { usePromise } from \"prostgles-client\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { ToolUseMessage } from \"../Chat/AskLLMChatMessages/ToolUseChatMessage/ToolUseChatMessage\";\nimport type { AskLLMToolsProps } from \"./AskLLMToolApprover\";\n\nlet approvingMessageId = \"\";\n\nexport type ToolApproval = {\n  approved: boolean;\n  mode: \"once\" | \"for-chat\" | \"deny\";\n};\n\n/**\n * https://docs.anthropic.com/en/docs/build-with-claude/tool-use\n */\nexport const useLLMToolsApprover = ({\n  activeChat,\n  messages,\n  sendQuery,\n  requestApproval,\n}: AskLLMToolsProps & {\n  requestApproval: (\n    tool: ApproveRequest,\n    toolUseMessage: ToolUseMessage,\n  ) => Promise<ToolApproval>;\n}) => {\n  const { dbsMethods } = usePrgl();\n\n  usePromise(async () => {\n    const lastToolUseMessage = messages\n      .slice(-1)\n      .toReversed()\n      .find(isAssistantMessageRequestingToolUse);\n    if (!lastToolUseMessage) {\n      return;\n    }\n    if (approvingMessageId && approvingMessageId === lastToolUseMessage.id) {\n      return;\n    }\n\n    const toolUseRequests = getLLMMessageToolUse(lastToolUseMessage);\n    const allowedTools = await dbsMethods.getLLMAllowedChatTools?.(\n      activeChat.id,\n    );\n    const toolUseRequestsThatNeedApproval = toolUseRequests\n      .map((toolUseRequest) => {\n        const matchedTool = allowedTools?.find((tool) => {\n          return toolUseRequest.name === tool.name;\n        });\n\n        if (!matchedTool || matchedTool.auto_approve) {\n          // Handled by the backend\n          return;\n        }\n        return {\n          toolUseRequest,\n          matchedTool,\n        };\n      })\n      .filter(isDefined);\n    approvingMessageId = lastToolUseMessage.id;\n    const toolApprovalReponses: (ToolUseMessage | undefined)[] = [];\n    /** Must ensure parallel tool request permissions behave as expected */\n    const toolsNamesThatHaveJustBeenAutoApproved = new Set<string>();\n    for (const {\n      matchedTool,\n      toolUseRequest,\n    } of toolUseRequestsThatNeedApproval) {\n      const isAllowedWithoutApproval =\n        matchedTool.auto_approve ||\n        toolsNamesThatHaveJustBeenAutoApproved.has(matchedTool.name);\n      if (!isAllowedWithoutApproval) {\n        const { approved, mode } = await requestApproval(\n          matchedTool,\n          toolUseRequest,\n        );\n        if (approved && mode === \"for-chat\") {\n          toolsNamesThatHaveJustBeenAutoApproved.add(matchedTool.name);\n        }\n        toolApprovalReponses.push(approved ? toolUseRequest : undefined);\n      }\n    }\n    if (toolUseRequestsThatNeedApproval.length) {\n      sendQuery(toolApprovalReponses.filter(isDefined), true);\n    }\n  }, [messages, dbsMethods, activeChat.id, requestApproval, sendQuery]);\n};\n\nexport type ApproveRequest = AllowedChatTool;\n// | (Pick<\n//     DBSSchema[\"mcp_server_tools\"],\n//     \"name\" | \"description\" | \"server_name\"\n//   > & {\n//     type: \"mcp\";\n//     auto_approve: boolean;\n//     tool_id: number;\n//   })\n// | (ProstglesMcpTool & {\n//     tool_id?: undefined;\n//     name: string;\n//     description: string;\n//     auto_approve: boolean;\n//   });\n"
  },
  {
    "path": "client/src/dashboard/AskLLM/useLocalLLM.ts",
    "content": "// import { useEffect, useState } from \"react\";\n\n// import type {\n//   TextClassificationPipeline,\n//   TaskType,\n// } from \"@xenova/transformers\";\n\n// type P = {};\n\n// const MODELS = [{ key: \"Xenova/Qwen1.5-0.5B-Chat\" }];\n\n// const TASKS = [\n//   {\n//     key: \"text-classification\",\n//   },\n//   {\n//     key: \"text-classification\",\n//   },\n// ] as const;\n\n// export const useLocalLLM = (props: P) => {\n//   const [enabled, setEnabled] = useState(true);\n//   //       \"text-generation\",\n//   //       \"Qwen/Qwen2.5-Coder-7B-Instruct\",\n//   const [model, setModel] = useState(\"Qwen/Qwen2.5-Coder-7B-Instruct\");\n//   const [task, setTask] = useState<TaskType | undefined>(\"text-classification\");\n//   const [pipeObj, setPipe] = useState<\n//     { pipe: TextClassificationPipeline } | undefined\n//   >();\n\n//   useEffect(() => {\n//     const setupPipe = async () => {\n//       if (!enabled || !model || !task) return;\n//       const { pipeline, env } = await import(\n//         /* webpackChunkName: \"@xenova/transformers\" */ \"@xenova/transformers\"\n//       );\n//       env.allowLocalModels = false;\n//       env.useBrowserCache = false;\n//       const pipe = (await pipeline(task, model, {\n//         progress_callback: console.log,\n//       })) as TextClassificationPipeline;\n//       setPipe({ pipe });\n//     };\n//     setupPipe();\n//   }, [enabled, model, task]);\n\n//   useEffect(() => {\n//     const getResponse = async () => {\n//       if (!pipeObj) return;\n//       const out = await pipeObj.pipe(\"I love transformers!\");\n//       console.log(out);\n//     };\n//     getResponse();\n//   }, [pipeObj]);\n// };\n\n// // async function runQwen() {\n// //   try {\n// //     // Allocate a pipeline for sentiment-analysis\n// //     const pipe = await pipeline(\"sentiment-analysis\");\n\n// //     const out = await pipe(\"I love transformers!\");\n// //     console.log(out);\n// //     // Initialize the model\n// //     const generator = await pipeline(\n// //       \"text-generation\",\n// //       \"Qwen/Qwen2.5-Coder-7B-Instruct\",\n// //     );\n\n// //     // Generate text\n// //     const result = await generator(\n// //       \"Write a function to calculate fibonacci numbers:\",\n// //       {\n// //         max_new_tokens: 128,\n// //         temperature: 0.7,\n// //       },\n// //     );\n\n// //     console.log(result[0]);\n// //   } catch (error) {\n// //     console.error(\"Error:\", error);\n// //   }\n// // }\n// // runQwen();\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/AutomaticBackups.tsx",
    "content": "import { mdiRefreshAuto } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport type { ExtraProps, Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\n\nconst DESTINATIONS = [\n  { key: \"Local\", subLabel: \"Saved locally (server in address bar)\" },\n  { key: \"Cloud\", subLabel: \"Saved to Amazon S3\" },\n] as const;\n\nconst BACKUP_FREQUENCIES = [\n  { key: \"hourly\", label: \"Hourly\" },\n  { key: \"daily\", label: \"Daily\" },\n  { key: \"weekly\", label: \"Weekly\" },\n  { key: \"monthly\", label: \"Monthly\" },\n] as const;\nconst DAYS_OF_WEEK = [\n  { key: 1, label: \"Monday\" },\n  { key: 2, label: \"Tuesday\" },\n  { key: 3, label: \"Wednesday\" },\n  { key: 4, label: \"Thursday\" },\n  { key: 5, label: \"Friday\" },\n  { key: 6, label: \"Saturday\" },\n  { key: 7, label: \"Sunday\" },\n] as const;\n\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { PGDumpParams } from \"@common/utils\";\nimport FormField from \"@components/FormField/FormField\";\nimport { CloudStorageCredentialSelector } from \"./CloudStorageCredentialSelector\";\nimport { DEFAULT_DUMP_OPTS, PGDumpOptions } from \"./PGDumpOptions\";\n\ntype P = Pick<Prgl, \"db\" | \"dbs\" | \"dbsMethods\" | \"dbsTables\"> & {\n  connectionId: string;\n};\nexport const AutomaticBackups = ({\n  dbs,\n  dbsTables,\n  dbsMethods,\n  connectionId: connection_id,\n  db,\n}: P) => {\n  const [dumpOpts, setDumpOpts] = useState<PGDumpParams>(DEFAULT_DUMP_OPTS);\n\n  const { data: database_config, isLoading } =\n    dbs.database_configs.useSubscribeOne(\n      { $existsJoined: { connections: { id: connection_id } } },\n      {},\n    );\n\n  const setBackupConf = (\n    newBackupConfig: Partial<DBSSchema[\"database_configs\"][\"backups_config\"]>,\n    merge = true,\n  ) => {\n    if (!database_config) throw \"database_config missing\";\n    const { backups_config } = database_config;\n    return dbs.database_configs.update(\n      { id: database_config.id },\n      {\n        backups_config: {\n          frequency: newBackupConfig?.frequency ?? \"daily\",\n          dump_options: DEFAULT_DUMP_OPTS.options,\n          ...(!merge ? {} : (backups_config ?? {})),\n          ...newBackupConfig,\n          cloudConfig: newBackupConfig?.cloudConfig || null,\n        },\n      },\n    );\n  };\n\n  const bkpConf = database_config?.backups_config;\n  const noSpaceForAutomaticLocalBackups =\n    bkpConf?.err && !bkpConf.cloudConfig?.credential_id && bkpConf.enabled;\n\n  return (\n    <PopupMenu\n      title=\"Automatic backups\"\n      button={\n        <Btn\n          iconPath={mdiRefreshAuto}\n          variant={bkpConf?.enabled ? \"filled\" : \"outline\"}\n          color={noSpaceForAutomaticLocalBackups ? \"danger\" : \"action\"}\n          className=\"mr-1\"\n          data-command=\"config.bkp.AutomaticBackups\"\n          loading={isLoading}\n        >\n          {bkpConf?.enabled ?\n            `Automatic backups: ${bkpConf.frequency}`\n          : `Enable automatic backups`}\n        </Btn>\n      }\n      clickCatchStyle={{ opacity: 1 }}\n      positioning=\"beneath-left\"\n      footerButtons={(_popupClose) => [\n        { label: \"Close\", onClickClose: true },\n        {\n          label: bkpConf?.enabled ? \"Disable\" : \"Enable\",\n          color: bkpConf?.enabled ? \"warn\" : \"action\",\n          variant: \"filled\",\n          \"data-command\": \"config.bkp.AutomaticBackups.toggle\",\n          onClickPromise: async (e) => {\n            await setBackupConf({\n              enabled: !bkpConf?.enabled,\n              dump_options: bkpConf?.dump_options ?? DEFAULT_DUMP_OPTS.options,\n            });\n            _popupClose?.(e);\n          },\n        },\n      ]}\n      render={() => (\n        <div className=\"flex-col gap-1 p-1 bg-inherit\">\n          {noSpaceForAutomaticLocalBackups && (\n            <InfoRow color=\"danger\">{bkpConf.err}</InfoRow>\n          )}\n          <Select\n            className=\"mr-1\"\n            label=\"Destination\"\n            data-command=\"AutomaticBackups.destination\"\n            fullOptions={DESTINATIONS}\n            value={\n              database_config?.backups_config?.cloudConfig ? \"S3\" : \"Local\"\n            }\n            onChange={(o) => {\n              setBackupConf({ cloudConfig: o === \"Cloud\" ? {} : null });\n            }}\n          />\n          {!!database_config?.backups_config?.cloudConfig && (\n            <CloudStorageCredentialSelector\n              dbs={dbs}\n              dbsTables={dbsTables}\n              dbsMethods={dbsMethods}\n              selectedId={\n                database_config.backups_config.cloudConfig.credential_id ||\n                undefined\n              }\n              onChange={(credential_id) => {\n                if (credential_id)\n                  setBackupConf({ cloudConfig: { credential_id } });\n              }}\n            />\n          )}\n          <Select\n            label=\"Frequency\"\n            data-command=\"AutomaticBackups.frequency\"\n            fullOptions={BACKUP_FREQUENCIES}\n            value={database_config?.backups_config?.frequency}\n            onChange={(frequency) => {\n              setBackupConf({ frequency });\n            }}\n          />\n          {bkpConf?.frequency && (\n            <>\n              <FormField\n                type=\"number\"\n                inputProps={{ min: 1, max: 200 }}\n                label=\"Keep last\"\n                hint=\"Will delete older automatic backups except top N. 0 means nothing will get deleted\"\n                value={bkpConf.keepLast}\n                onChange={(keepLast) => {\n                  setBackupConf({ keepLast: +keepLast });\n                }}\n              />\n              {bkpConf.frequency !== \"hourly\" && (\n                <Select\n                  label=\"Hour of day for backup\"\n                  data-command=\"AutomaticBackups.hourOfDay\"\n                  options={new Array(24).fill(1).map((_, i) => i)}\n                  value={bkpConf.hour}\n                  onChange={(hour) => {\n                    setBackupConf({ hour });\n                  }}\n                />\n              )}\n              {bkpConf.frequency === \"weekly\" && (\n                <Select\n                  label=\"Day of week for backup\"\n                  fullOptions={DAYS_OF_WEEK}\n                  value={bkpConf.dayOfWeek}\n                  onChange={(dayOfWeek) => {\n                    setBackupConf({ dayOfWeek });\n                  }}\n                />\n              )}\n              {bkpConf.frequency === \"monthly\" && (\n                <Select\n                  label=\"Day of month for backup\"\n                  options={new Array(31).fill(1).map((_, i) => i + 1)}\n                  value={bkpConf.dayOfMonth}\n                  onChange={(dayOfMonth) => {\n                    setBackupConf({ dayOfMonth });\n                  }}\n                />\n              )}\n              <PGDumpOptions\n                connectionId={connection_id}\n                dbsMethods={dbsMethods}\n                dbs={dbs}\n                dbProject={db}\n                dbsTables={dbsTables}\n                opts={dumpOpts}\n                onChange={setDumpOpts}\n                hideDestination={true}\n              />\n            </>\n          )}\n        </div>\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/BackupsControls.tsx",
    "content": "import type { PGDumpParams } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport {\n  mdiDatabasePlusOutline,\n  mdiDelete,\n  mdiFileUploadOutline,\n  mdiStop,\n} from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { usePromise } from \"prostgles-client\";\nimport { type AnyObject } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport { dataCommand } from \"../../Testing\";\nimport type { DBS, DBSMethods } from \"../Dashboard/DBS\";\nimport type { Backups } from \"../Dashboard/dashboardUtils\";\nimport type { FieldConfig } from \"../SmartCard/SmartCard\";\nimport { SmartCardList } from \"../SmartCardList/SmartCardList\";\nimport { StyledInterval } from \"../W_SQL/customRenderers\";\nimport { AutomaticBackups } from \"./AutomaticBackups\";\nimport { BackupsInProgress } from \"./BackupsInProgress\";\nimport { CodeConfirmation } from \"./CodeConfirmation\";\nimport { CompletedBackups } from \"./CompletedBackups\";\nimport { DEFAULT_DUMP_OPTS, PGDumpOptions } from \"./PGDumpOptions\";\nimport { RenderBackupLogs } from \"./RenderBackupLogs\";\nimport { RenderBackupStatus } from \"./RenderBackupStatus\";\nimport { Restore } from \"./Restore/Restore\";\nimport { useBackupsControlsState } from \"./useBackupsControlsState\";\n\nexport const orderByCreated = {\n  key: \"created\",\n  asc: false,\n  // created: false,\n} as const;\n\nexport const BackupsControls = ({ prgl }: { prgl: Prgl }) => {\n  const { connectionId, serverState, dbs, dbsTables, dbsMethods, db } = prgl;\n  const { getInstalledPsqlVersions, getDBSize, pgDump } = dbsMethods;\n  const connection_id = connectionId;\n\n  const {\n    backupFilter,\n    backupsFilterType,\n    completedBackupsFilter,\n    hasBackups,\n    setBackupsFilterType,\n    setHasBackups,\n  } = useBackupsControlsState(connection_id);\n  const [dumpOpts, setDumpOpts] = useState<PGDumpParams>(DEFAULT_DUMP_OPTS);\n\n  const dbSize = usePromise(\n    async () => getDBSize?.(connection_id),\n    [getDBSize, connection_id],\n  );\n\n  const installedPrograms = usePromise(async () => {\n    return (await getInstalledPsqlVersions?.()) ?? \"none\";\n  }, [getInstalledPsqlVersions]);\n\n  const restoreLogsFConf: FieldConfig<Backups> = {\n    name: \"restore_logs\",\n    hideIf: (logs) => !logs,\n    render: (logs, row) => (\n      <RenderBackupLogs\n        logs={logs}\n        completed={!(row.restore_status as any)?.loading}\n      />\n    ),\n  };\n\n  if (!installedPrograms) {\n    return null;\n  }\n\n  if (installedPrograms === \"none\") {\n    return (\n      <div className=\"flex-col p-1 gap-1 f-1 min-h-0 w-fit\">\n        <InfoRow>\n          This feature is not available. The following commands are not\n          available to the server pg_dump, pg_restore and psql{\" \"}\n        </InfoRow>\n      </div>\n    );\n  }\n\n  const restoreStoppedError = \"Stopped by user\";\n\n  return (\n    <div className=\"flex-col gap-2 f-1 min-h-0 w-fit\">\n      <InfoRow color=\"info\" iconPath={\"\"} variant=\"naked\">\n        Current database size: <strong>{dbSize ?? \"??\"}</strong>\n      </InfoRow>\n      <div className=\"flex-row-wrap ai-center gap-p5\">\n        <PopupMenu\n          data-command=\"config.bkp.create\"\n          button={\n            <Btn\n              variant=\"filled\"\n              color=\"action\"\n              iconPath={mdiDatabasePlusOutline}\n            >\n              Create backup\n            </Btn>\n          }\n          title=\"Create backup\"\n          positioning=\"center\"\n          clickCatchStyle={{ opacity: 1 }}\n          footerButtons={(popupClose) => [\n            {\n              label: \"Cancel\",\n              onClickClose: true,\n            },\n            {\n              label: \"Start backup\",\n              variant: \"filled\",\n              color: \"action\",\n              className: \"ml-auto\",\n              ...dataCommand(\"config.bkp.create.start\"),\n              onClickPromise: async (e) => {\n                try {\n                  await pgDump!(\n                    connection_id,\n                    dumpOpts.destination === \"Cloud\" ?\n                      dumpOpts.credentialID\n                    : null,\n                    dumpOpts,\n                  );\n                } catch (err) {\n                  console.error(err);\n                  throw err;\n                }\n                popupClose?.(e);\n              },\n            },\n          ]}\n          render={() => (\n            <div className=\"flex-col gap-1 f-1 min-s-0 bg-inherit\">\n              <FormField\n                label={\"Name\"}\n                type=\"text\"\n                hint=\"Optional, will be used to identify the backup\"\n                value={dumpOpts.name}\n                inputProps={{\n                  \"data-command\": \"config.bkp.create.name\",\n                }}\n                onChange={(name) => {\n                  setDumpOpts((o) => ({ ...o, name }));\n                }}\n              />\n\n              <PGDumpOptions\n                connectionId={connection_id}\n                dbsMethods={dbsMethods}\n                dbs={dbs}\n                dbProject={db}\n                dbsTables={dbsTables}\n                opts={dumpOpts}\n                onChange={setDumpOpts}\n                hideDestination={false}\n              />\n            </div>\n          )}\n        />\n\n        {serverState.isElectron ?\n          <></>\n        : <AutomaticBackups\n            dbs={dbs}\n            db={db}\n            dbsTables={dbsTables}\n            connectionId={connection_id}\n            dbsMethods={dbsMethods}\n          />\n        }\n\n        <Restore\n          db={db}\n          dbs={dbs}\n          connectionId={connection_id}\n          dbsMethods={dbsMethods}\n          fromFile={true}\n          button={\n            <Btn\n              color=\"action\"\n              iconPath={mdiFileUploadOutline}\n              data-command=\"BackupsControls.restoreFromFile\"\n            >\n              Restore from file...\n            </Btn>\n          }\n        />\n      </div>\n      <SmartCardList\n        db={dbs as DBHandlerClient}\n        methods={dbsMethods}\n        tableName=\"backups\"\n        btnColor=\"gray\"\n        style={{ minHeight: \"250px\" }}\n        data-command=\"BackupControls.backupsInProgress\"\n        title=\"Restore in progress:\"\n        tables={dbsTables}\n        filter={{\n          $and: [\n            backupFilter,\n            { \"restore_status.<>\": null },\n            { \"restore_status->loading.<>\": null },\n            {\n              $or: [\n                { \"restore_status->err\": null },\n                { \"restore_status->>err.<>\": restoreStoppedError },\n              ],\n            },\n          ],\n        }}\n        realtime={true}\n        className=\"mt-2\"\n        orderBy={orderByCreated}\n        excludeNulls={true}\n        fieldConfigs={\n          [\n            { name: \"id\", hide: true },\n            {\n              name: \"restore_status\",\n              render: (val, row) => (\n                <RenderBackupStatus row={row} status={val} />\n              ),\n            },\n            {\n              name: \"restore_start\",\n              label: \"Started\",\n              select: { $ageNow: [\"restore_start\", null, \"second\"] },\n              render: (value) => <StyledInterval value={value} />,\n            },\n            restoreLogsFConf,\n          ] satisfies FieldConfig<Backups>[]\n        }\n        getRowFooter={(row) => (\n          <div className=\"flex-row-wrap gap-1 jc-end ai-center\">\n            <Btn\n              iconPath={mdiStop}\n              variant=\"outline\"\n              onClickPromise={async () => {\n                await dbs.backups.update(\n                  { id: row.id },\n                  { restore_status: { err: restoreStoppedError } },\n                );\n              }}\n            >\n              Stop\n            </Btn>\n          </div>\n        )}\n        noDataComponent={<></>}\n        noDataComponentMode=\"hide-all\"\n      />\n\n      <BackupsInProgress {...prgl} backupFilter={backupFilter} />\n\n      <CompletedBackups\n        setHasBackups={setHasBackups}\n        backupsFilterType={backupsFilterType}\n        completedBackupsFilter={completedBackupsFilter}\n        setBackupsFilterType={setBackupsFilterType}\n      />\n\n      {hasBackups && (\n        <DeleteAllBackups\n          dbs={dbs}\n          dbsMethods={dbsMethods}\n          filter={completedBackupsFilter}\n          filterName={backupsFilterType}\n          data-command=\"BackupsControls.Completed.deleteAll\"\n        />\n      )}\n    </div>\n  );\n};\n\ntype DeleteAllBackupsProps = {\n  dbs: DBS;\n  dbsMethods: DBSMethods;\n  filter: AnyObject;\n  filterName: string;\n};\n\nconst DeleteAllBackups = ({\n  dbs,\n  filter,\n  dbsMethods,\n  filterName,\n}: DeleteAllBackupsProps) => {\n  const onDeleteAll = async (popupClose: VoidFunction) => {\n    let bkp;\n    do {\n      bkp = await dbs.backups.findOne(filter);\n      if (bkp) {\n        await dbsMethods.bkpDelete!(bkp.id, true);\n      }\n    } while (bkp);\n\n    popupClose();\n  };\n\n  return (\n    <CodeConfirmation\n      className=\"ml-p25\"\n      positioning=\"center\"\n      data-command=\"BackupControls.DeleteAll\"\n      button={\n        <Btn iconPath={mdiDelete} color=\"danger\" title=\"Will need to confirm\">\n          Delete all...\n        </Btn>\n      }\n      message={\n        <InfoRow style={{ alignItems: \"center\" }} color=\"danger\">\n          Will delete ALL backup files from storage for{\" \"}\n          <strong>{filterName}</strong>. This action is not reversible!\n        </InfoRow>\n      }\n      confirmButton={(popupClose) => (\n        <>\n          <Btn\n            iconPath={mdiDelete}\n            variant=\"outline\"\n            color=\"danger\"\n            data-command=\"BackupControls.DeleteAll.Confirm\"\n            onClickPromise={() => onDeleteAll(popupClose)}\n          >\n            Force delete backups\n          </Btn>\n        </>\n      )}\n    />\n  );\n};\n\nexport const bytesToSize = (bytes, _precision = 2) => {\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  if (bytes == 0) return \"0 Byte\";\n  const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)) + \"\");\n  const unit = sizes[i];\n  const value = bytes / Math.pow(1024, i);\n  const precision = i > 0 ? _precision : 0;\n  const valueStr = value.toFixed(precision);\n  return `${valueStr} ${unit}`;\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/BackupsInProgress.tsx",
    "content": "import { mdiStop } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport {\n  SmartCardList,\n  type SmartCardListProps,\n} from \"../SmartCardList/SmartCardList\";\nimport { StyledInterval, type PG_Interval } from \"../W_SQL/customRenderers\";\nimport { orderByCreated } from \"./BackupsControls\";\nimport { RenderBackupLogs } from \"./RenderBackupLogs\";\nimport { RenderBackupStatus } from \"./RenderBackupStatus\";\n\nexport const BackupsInProgress = ({\n  dbs,\n  dbsMethods,\n  dbsTables,\n  backupFilter,\n}: Prgl & {\n  backupFilter: AnyObject;\n}) => {\n  const props = useMemo(() => {\n    return {\n      style: { minHeight: \"250px\" },\n      filter: { $and: [backupFilter, { \"status->ok\": null }] },\n      fieldConfigs: [\n        { name: \"id\", hide: true },\n        { name: \"sizeInBytes\", hide: true },\n        { name: \"dbSizeInBytes\", hide: true },\n        { name: \"name\" },\n        {\n          name: \"created_ago\" as \"created\",\n          label: \"Started\",\n          select: { $ageNow: [\"created\", null, \"second\"] },\n          render: (value: PG_Interval) => <StyledInterval value={value} />,\n        },\n        {\n          name: \"status\",\n          className: \"gap-p25\",\n          label: \"Dump status\",\n          render: (val, row) => <RenderBackupStatus row={row} status={val} />,\n        },\n        {\n          name: \"dump_logs\",\n          render: (logs: string, row) => (\n            <RenderBackupLogs\n              logs={logs}\n              completed={!(row.status as any)?.loading}\n            />\n          ),\n        },\n      ],\n      getRowFooter: (row) => (\n        <div className=\"flex-row-wrap gap-1 jc-end ai-center\">\n          <Btn\n            iconPath={mdiStop}\n            variant=\"outline\"\n            color=\"danger\"\n            onClickPromise={async () => {\n              await dbsMethods.bkpDelete!(row.id, true);\n            }}\n          >\n            Stop & delete\n          </Btn>\n        </div>\n      ),\n      noDataComponent: <></>,\n    } satisfies Pick<\n      SmartCardListProps<DBSSchema[\"backups\"]>,\n      \"style\" | \"filter\" | \"fieldConfigs\" | \"getRowFooter\" | \"noDataComponent\"\n    >;\n  }, [backupFilter, dbsMethods.bkpDelete]);\n\n  return (\n    <SmartCardList<DBSSchema[\"backups\"]>\n      db={dbs as DBHandlerClient}\n      methods={dbsMethods}\n      tableName=\"backups\"\n      btnColor=\"gray\"\n      title=\"Backup in progress:\"\n      showTopBar={false}\n      tables={dbsTables}\n      realtime={true}\n      className=\"mt-2\"\n      orderBy={orderByCreated}\n      excludeNulls={true}\n      {...props}\n      noDataComponentMode=\"hide-all\"\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/CloudStorageCredentialSelector.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiInformationOutline, mdiPlus } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport type { DBS, DBSMethods } from \"../Dashboard/DBS\";\nimport type { DBSchemaTablesWJoins } from \"../Dashboard/dashboardUtils\";\nimport { getMonaco } from \"../SQLEditor/W_SQLEditor\";\nimport { SmartForm } from \"../SmartForm/SmartForm\";\nimport { ViewMoreSmartCardList } from \"../SmartForm/SmartFormField/ViewMoreSmartCardList\";\n\ntype P = {\n  pickFirst?: boolean;\n  pickFirstIfNoOthers?: boolean;\n  dbsTables: DBSchemaTablesWJoins;\n  dbs: DBS;\n  dbsMethods: DBSMethods;\n  selectedId?: number | null;\n  onChange: (credentialId: number) => void;\n  style?: React.CSSProperties;\n};\n\nexport function CloudStorageCredentialSelector({\n  selectedId,\n  onChange,\n  dbs,\n  dbsTables,\n  pickFirst,\n  dbsMethods,\n  pickFirstIfNoOthers,\n  style,\n}: P) {\n  const { data: credentials } = dbs.credentials.useSubscribe(\n    {},\n    { select: { id: 1, name: 1, type: 1, key_id: 1 } },\n  );\n\n  const credentialsTable = useMemo(\n    () => dbsTables.find((t) => t.name === \"credentials\"),\n    [dbsTables],\n  );\n\n  useEffect(() => {\n    const [firstCredential] = credentials ?? [];\n    if (selectedId) return;\n    if (\n      (pickFirst && firstCredential) ||\n      (pickFirstIfNoOthers && firstCredential && credentials?.length === 1)\n    ) {\n      onChange(firstCredential.id);\n    }\n  }, [credentials, pickFirst, onChange, selectedId, pickFirstIfNoOthers]);\n\n  const MarkerSeverity = usePromise(\n    async () => (await getMonaco()).MarkerSeverity,\n  );\n\n  if (!MarkerSeverity) return null;\n\n  return (\n    <div className=\"flex-row-wrap ai-end gap-1\" style={style}>\n      <Select\n        className=\" \"\n        label=\"Cloud credential\"\n        data-command=\"CloudStorageCredentialSelector.selectCredential\"\n        fullOptions={(credentials ?? []).map((c) => ({\n          key: c.id,\n          label: `${c.name || `${c.type} - ${c.id}`}`,\n          subLabel: `${c.key_id}`,\n        }))}\n        value={selectedId}\n        onChange={(o) => {\n          onChange(o);\n        }}\n      />\n      {credentialsTable && Boolean(credentials?.length) && (\n        <ViewMoreSmartCardList\n          db={dbs as DBHandlerClient}\n          methods={dbsMethods}\n          ftable={credentialsTable}\n          tables={dbsTables}\n          getActions={undefined}\n          searchFilter={[]}\n        />\n      )}\n      <PopupMenu\n        button={\n          <Btn\n            title=\"Add credential\"\n            className=\"mt-1\"\n            color=\"action\"\n            variant=\"filled\"\n            iconPath={mdiPlus}\n          />\n        }\n        title=\"Add cloud storage credential\"\n        positioning=\"center\"\n        clickCatchStyle={{ opacity: 1 }}\n        contentStyle={{ padding: 0 }}\n        render={(popupClose) => (\n          <SmartForm\n            label=\"\"\n            contentClassname=\"p-1\"\n            methods={dbsMethods}\n            db={dbs as DBHandlerClient}\n            tableName=\"credentials\"\n            tables={dbsTables}\n            showJoinedTables={false}\n            onClose={popupClose}\n          />\n        )}\n      />\n      <PopupMenu\n        className=\"\"\n        button={\n          <Btn iconPath={mdiInformationOutline} variant=\"outline\">\n            Example bucket policy\n          </Btn>\n        }\n        positioning=\"top-center\"\n        title={\n          <div className=\"flex-row ai-center p-p5  w-full gap-1\">\n            <Icon path={mdiInformationOutline} />\n            <h4 className=\"m-0 ta-center \">Suggested bucket policy</h4>\n          </div>\n        }\n        contentStyle={{\n          width: \"650px\",\n          minWidth: 0,\n          maxWidth: \"100vw\",\n          height: \"450px\",\n          minHeight: 0,\n          maxHeight: \"100vh\",\n          flex: \"none\",\n        }}\n        render={(pClose) => (\n          <div className=\"flex-col gap-p1 f-1 b b-color rounded o-hidden\">\n            <CodeEditor\n              value={SAMPLE_BUCKET_POLICY}\n              language=\"json\"\n              style={{ minHeight: \"200px\" }}\n              className=\"p-p5 o-hidden\"\n              options={{\n                tabSize: 2,\n                minimap: {\n                  enabled: false,\n                },\n                lineNumbers: \"off\",\n              }}\n              // onChange={() => {}}\n              markers={[\n                {\n                  startColumn: 12,\n                  endColumn: 66,\n                  startLineNumber: 8,\n                  endLineNumber: 8,\n                  severity: MarkerSeverity[\"Error\"],\n                  message: \"Replace with your AWS IAM user\",\n                },\n                {\n                  startColumn: 30,\n                  endColumn: 46,\n                  startLineNumber: 17,\n                  endLineNumber: 17,\n                  severity: MarkerSeverity[\"Error\"],\n                  message: \"Replace with your AWS Bucket name\",\n                },\n\n                {\n                  startColumn: 6,\n                  endColumn: 23,\n                  startLineNumber: 14,\n                  endLineNumber: 14,\n                  severity: MarkerSeverity[\"Error\"],\n                  message:\n                    \"May only keep this for testing. Not recommended for production\",\n                },\n                {\n                  startColumn: 6,\n                  endColumn: 29,\n                  startLineNumber: 15,\n                  endLineNumber: 15,\n                  severity: MarkerSeverity[\"Error\"],\n                  message:\n                    \"May only keep this for testing. Not recommended for production\",\n                },\n              ]}\n            />\n          </div>\n        )}\n      />\n    </div>\n  );\n}\n\nconst SAMPLE_BUCKET_POLICY = `\n{\n\t\"Version\": \"2012-10-17\",\n\t\"Statement\": [\n\t\t{\n\t\t\t\"Effect\": \"Allow\",\n\t\t\t\"Principal\": {\n\t\t\t\t\"AWS\": \"arn:aws:iam::123456789012:user/your_iam_user\"\n\t\t\t},\n\t\t\t\"Action\": [\n\t\t\t\t\"s3:PutObject\",\n\t\t\t\t\"s3:GetObject\",\n\t\t\t\t\"s3:GetObjectVersion\",\n\t\t\t\t\"s3:DeleteObject\",\n\t\t\t\t\"s3:DeleteObjectVersion\"\n\t\t\t],\n\t\t\t\"Resource\": \"arn:aws:s3:::your_bucket_name/*\"\n\t\t}\n\t]\n}`;\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/CodeConfirmation.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport Loading from \"@components/Loader/Loading\";\nimport type { PopupProps } from \"@components/Popup/Popup\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport type { TestSelectors } from \"../../Testing\";\nimport { useIsMounted } from \"prostgles-client\";\n\ntype CodeConfirmationProps = TestSelectors & {\n  button: React.ReactNode;\n  message: React.ReactNode | (() => Promise<React.ReactNode>);\n  confirmButton: (popupClose) => React.ReactNode;\n  topContent?: (popupClose) => React.ReactNode;\n  title?: React.ReactNode;\n  show?: \"button\" | \"confirmButton\";\n  className?: string;\n  style?: React.CSSProperties;\n  contentClassName?: string;\n  contentStyle?: React.CSSProperties;\n  hideConfirm?: boolean;\n  positioning?: PopupProps[\"positioning\"];\n  fixedCode?: string;\n};\nconst getCode = () => Math.random().toFixed(3).slice(2, 5);\nexport const CodeConfirmation = ({\n  button,\n  confirmButton,\n  message: rawMessage,\n  show,\n  topContent,\n  className,\n  style,\n  contentClassName = \"\",\n  contentStyle,\n  hideConfirm = false,\n  title,\n  positioning = \"beneath-left\",\n  fixedCode,\n  ...testSelectors\n}: CodeConfirmationProps) => {\n  const [message, setMessage] = useState<React.ReactNode>();\n\n  const getIsMounted = useIsMounted();\n  useEffect(() => {\n    if (typeof rawMessage === \"function\") {\n      (async () => {\n        const message = await rawMessage();\n        if (!getIsMounted()) return;\n        setMessage(message);\n      })();\n    } else {\n      setMessage(rawMessage);\n    }\n  }, [rawMessage, getIsMounted]);\n\n  const isMounted = useIsMounted();\n  const [key, setKey] = useState(getCode());\n  const [hasConfirmed, setHasConfirmed] = useState(false);\n\n  if (show) {\n    return show === \"button\" ? <>{button} </> : <>{confirmButton(() => {})} </>;\n  }\n\n  return (\n    <PopupMenu\n      key={key}\n      title={title}\n      className={className}\n      style={style}\n      button={button}\n      {...testSelectors}\n      initialState={{ ok: false, code: key, confirmCode: \"\" }}\n      positioning={positioning}\n      onClose={() => {\n        setKey(getCode());\n      }}\n      clickCatchStyle={{ opacity: 1 }}\n      contentClassName=\"p-1\"\n      render={(_popupClose) => {\n        const popupClose = () => {\n          if (!isMounted()) return;\n          setKey(getCode());\n          setHasConfirmed(false);\n        };\n        return (\n          <div\n            className={\n              \"flex-col gap-1 ai-start o-auto p-p25 \" + contentClassName\n            }\n            style={contentStyle}\n          >\n            {topContent?.(popupClose)}\n\n            {!hideConfirm && (\n              <>\n                {message ?? <Loading />}\n                <CodeChecker\n                  key={key}\n                  fixedCode={fixedCode}\n                  onChange={setHasConfirmed}\n                />\n                <div className=\"flex-row gap-1 ai-center mt-1  w-full\">\n                  <Btn onClick={popupClose} variant=\"outline\">\n                    Close\n                  </Btn>\n                  {hasConfirmed && confirmButton(popupClose)}\n                </div>\n              </>\n            )}\n          </div>\n        );\n      }}\n    />\n  );\n};\n\ntype CodeCheckerProps = Pick<\n  CodeConfirmationProps,\n  \"style\" | \"className\" | \"fixedCode\"\n> & {\n  onChange: (hasConfirmed: boolean) => void;\n};\nexport function CodeChecker({\n  className,\n  style,\n  onChange,\n  fixedCode,\n}: CodeCheckerProps): JSX.Element {\n  const [code] = useState(fixedCode ?? getTextCode());\n  const [confirmCode, setConfirmCode] = useState(\"\");\n\n  return (\n    <div className={\"flex-col \" + (className ?? \"\")} style={style}>\n      <p>\n        <span className=\"noselect\">Confirm by typing this: </span>\n        <strong title=\"confirmation-code\">{code}</strong>\n      </p>\n      <FormField\n        name=\"confirmation\"\n        value={confirmCode}\n        onChange={(val) => {\n          setConfirmCode(val);\n          onChange(val === code);\n        }}\n      />\n    </div>\n  );\n}\n\nconst getTextCode = () => {\n  const alphabet = \"abcdefghijklmnopqrstuvwxyz\";\n  return [Math.random(), Math.random(), Math.random()]\n    .map((rand) => {\n      const randomCharacter = alphabet[Math.floor(rand * alphabet.length)];\n      return randomCharacter;\n    })\n    .join(\"\");\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/CompletedBackups.tsx",
    "content": "import { ROUTES, sliceText } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport ButtonGroup from \"@components/ButtonGroup\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport {\n  mdiBackupRestore,\n  mdiDelete,\n  mdiDownload,\n  mdiGestureTapButton,\n  mdiRefreshAuto,\n} from \"@mdi/js\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { type AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type { Backups } from \"../Dashboard/dashboardUtils\";\nimport type { FieldConfig } from \"../SmartCard/SmartCard\";\nimport { SmartCardList } from \"../SmartCardList/SmartCardList\";\nimport { StyledInterval } from \"../W_SQL/customRenderers\";\nimport { bytesToSize } from \"./BackupsControls\";\nimport { CodeConfirmation } from \"./CodeConfirmation\";\nimport { RenderBackupLogs } from \"./RenderBackupLogs\";\nimport { RenderBackupStatus } from \"./RenderBackupStatus\";\nimport { Restore } from \"./Restore/Restore\";\nimport {\n  BACKUP_FILTER_OPTS,\n  type BackupsControlsState,\n} from \"./useBackupsControlsState\";\n\nexport const orderByCreated = {\n  key: \"created\",\n  asc: false,\n  // created: false,\n} as const;\n\nexport const CompletedBackups = ({\n  setHasBackups,\n  backupsFilterType,\n  setBackupsFilterType,\n  completedBackupsFilter,\n}: {\n  setHasBackups: (has: boolean) => void;\n} & Pick<\n  BackupsControlsState,\n  \"backupsFilterType\" | \"setBackupsFilterType\" | \"completedBackupsFilter\"\n>) => {\n  const { connectionId, dbs, dbsTables, dbsMethods, db } = usePrgl();\n  const { pgRestore, bkpDelete } = dbsMethods;\n  const connection_id = connectionId;\n\n  // const [backupsFilterType, setBackupsFilterType] = useState<\n  //   (typeof BACKUP_FILTER_OPTS)[number][\"key\"]\n  // >(BACKUP_FILTER_OPTS[0].key);\n\n  const restoreLogsFConf: FieldConfig<Backups> = {\n    name: \"restore_logs\",\n    hideIf: (logs) => !logs,\n    render: (logs, row) => (\n      <RenderBackupLogs\n        logs={logs}\n        completed={!(row.restore_status as any)?.loading}\n      />\n    ),\n  };\n  const dumpLogsFConf: FieldConfig<Backups> = {\n    name: \"dump_logs\",\n    render: (logs, row) => (\n      <RenderBackupLogs logs={logs} completed={!(row.status as any)?.loading} />\n    ),\n  };\n\n  return (\n    <SmartCardList\n      data-command=\"BackupsControls.Completed\"\n      btnColor=\"gray\"\n      showTopBar={false}\n      title={\n        <FlexCol className=\"mt-1 flex-col gap-p5\">\n          <label className=\"font-16 bold\">Completed backups</label>\n          <div className=\"flex-row ai-center gap-p5\">\n            {/* <Btn iconPath={mdiFilter} variant=\"text\" size=\"small\" color=\"action\" />  */}\n            <ButtonGroup\n              variant=\"select\"\n              options={BACKUP_FILTER_OPTS.map((v) => v.key)}\n              value={backupsFilterType}\n              onChange={(v) => setBackupsFilterType(v)}\n            />\n          </div>\n        </FlexCol>\n      }\n      onSetData={(items) => setHasBackups(!!items.length)}\n      db={dbs as DBHandlerClient}\n      methods={dbsMethods}\n      tableName=\"backups\"\n      tables={dbsTables}\n      filter={completedBackupsFilter}\n      realtime={true}\n      // className=\"mt-2\"\n      orderBy={orderByCreated}\n      excludeNulls={true}\n      fieldConfigs={[\n        { name: \"id\", hide: true },\n        {\n          name: \"initiator\",\n          label: \" \",\n          render: (v) => (\n            <div title={v}>\n              {v === \"automatic_backups\" ?\n                <Icon path={mdiRefreshAuto} />\n              : v === \"manual_backup\" ?\n                <Icon path={mdiGestureTapButton} />\n              : v}\n            </div>\n          ),\n        },\n        {\n          name: \"name\",\n          hideIf: (name) => !name,\n          label: \"Backup name\",\n          render: (v, row) => (\n            <span title={v} className=\"text-ellipsis\">\n              {sliceText(v, 30)}\n            </span>\n          ),\n        },\n        {\n          name: \"created\",\n          label: \"Created\",\n          select: { $ageNow: [\"created\", null, \"second\"] },\n          render: (value: AnyObject) => <StyledInterval value={value} />,\n        },\n        {\n          name: \"dbSizeInBytes\",\n          label: \"DB size\",\n          render: (val) => bytesToSize(val),\n        },\n        {\n          name: \"dump_command\",\n          label: \"Dump command\",\n          hide: true,\n          render: (val) => (\n            <span title={val} className=\"text-ellipsis\">\n              {sliceText(val, 50)}\n            </span>\n          ),\n        },\n        // \"connection_id\",\n        // \"credential_id\",\n        \"destination\",\n        // \"created\",\n        { name: \"uploaded\", hide: true },\n        // \"dbSizeInBytes\",\n        // {\n        //   name: \"Upload Duration\", label: \"Upload Duration\",\n        //   select: { $age: [\"uploaded\", \"created\"] },\n        //   render: renderInterval\n        // },\n        {\n          name: \"sizeInBytes\",\n          render: (val) => (\n            <span title={(+val || 0).toLocaleString() + \" Bytes\"}>\n              {bytesToSize(+val || 0)}\n            </span>\n          ),\n        },\n        // \"dump_command\",\n        {\n          name: \"status\",\n          label: \"Dump status\",\n          render: (val, row) => <RenderBackupStatus row={row} status={val} />,\n        },\n        dumpLogsFConf,\n        // \"restore_command\",\n        // {\n        //   name: \"restore_status\",\n        //   render: renderStatus\n        // },\n        {\n          name: \"restored_\" as \"restore_end\",\n          label: \"Last restored\",\n          hideIf: (v) => !v,\n          select: { $ageNow: [\"restore_end\", null, \"second\"] },\n          render: (value: AnyObject) => <StyledInterval value={value} />,\n        },\n        restoreLogsFConf,\n      ]}\n      getRowFooter={(row: Backups) => (\n        <div className=\"flex-row-wrap gap-1 jc-end ai-center show-on-parent-hoverdd\">\n          <CodeConfirmation\n            title={\"Delete the backup file from storage\"}\n            data-command=\"BackupsControls.Completed.delete\"\n            show={!row.uploaded ? \"confirmButton\" : undefined}\n            button={\n              <Btn iconPath={mdiDelete} title=\"Will need to confirm\">\n                Delete\n              </Btn>\n            }\n            message={\n              <InfoRow color=\"warning\">This action is not reversible!</InfoRow>\n            }\n            confirmButton={(popupClose) => (\n              <>\n                <Btn\n                  iconPath={mdiDelete}\n                  variant=\"outline\"\n                  color=\"danger\"\n                  onClickPromise={() => bkpDelete!(row.id).then(popupClose)}\n                >\n                  Delete\n                </Btn>\n                <Btn\n                  iconPath={mdiDelete}\n                  variant=\"outline\"\n                  color=\"danger\"\n                  onClickPromise={() =>\n                    bkpDelete!(row.id, true).then(popupClose)\n                  }\n                >\n                  Force delete\n                </Btn>\n              </>\n            )}\n          />\n\n          <Btn\n            iconPath={mdiDownload}\n            href={ROUTES.BACKUPS + \"/\" + row.id}\n            color=\"action\"\n            title=\"Right click and 'Save link as...' to download\"\n            data-command=\"BackupsControls.Completed.download\"\n            download\n          >\n            Download\n          </Btn>\n\n          <Restore\n            dbs={dbs}\n            db={db}\n            backupId={row.id}\n            connectionId={connection_id}\n            dbsMethods={dbsMethods}\n            data-command=\"BackupsControls.Completed.restore\"\n            button={\n              <Btn\n                iconPath={mdiBackupRestore}\n                title=\"Will need to confirm\"\n                variant=\"filled\"\n                color=\"action\"\n                data-command=\"BackupControls.Restore\"\n              >\n                Restore...\n              </Btn>\n            }\n            onReadyButton={(restoreOpts, popupClose) => (\n              <Btn\n                iconPath={mdiBackupRestore}\n                variant=\"filled\"\n                color=\"action\"\n                onClickPromise={() =>\n                  pgRestore!(\n                    { bkpId: row.id, connId: connectionId },\n                    restoreOpts,\n                  ).then(popupClose)\n                }\n              >\n                Restore\n              </Btn>\n            )}\n          />\n        </div>\n      )}\n      noDataComponent={\n        <InfoRow\n          className=\"\"\n          variant=\"filled\"\n          color=\"info\"\n          iconPath=\"\"\n          style={{ padding: \"2em 2em\" }}\n        >\n          No completed backups\n        </InfoRow>\n      }\n      // noDataComponentMode=\"hide-all\"\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/DumpLocationOptions.tsx",
    "content": "import type { PGDumpParams } from \"@common/utils\";\nimport { DESTINATIONS } from \"@common/utils\";\nimport { Select } from \"@components/Select/Select\";\nimport React from \"react\";\nimport { CloudStorageCredentialSelector } from \"./CloudStorageCredentialSelector\";\nimport type { DumpOptionsProps } from \"./PGDumpOptions\";\n\ntype P = Pick<DumpOptionsProps, \"dbs\" | \"dbsTables\" | \"dbsMethods\"> & {\n  currOpts: PGDumpParams;\n  onChangeCurrOpts: (\n    newLocation: Pick<PGDumpParams, \"credentialID\" | \"destination\">,\n  ) => void;\n};\n\nexport const DumpLocationOptions = ({\n  dbs,\n  dbsTables,\n  dbsMethods,\n  currOpts,\n  onChangeCurrOpts,\n}: P) => {\n  const { destination, credentialID } = currOpts;\n  return (\n    <>\n      <Select\n        className=\"mr-1\"\n        label=\"Destination\"\n        fullOptions={DESTINATIONS}\n        data-command=\"PGDumpOptions.destination\"\n        value={destination}\n        variant=\"button-group\"\n        onChange={(destination) => {\n          onChangeCurrOpts({ destination, credentialID });\n        }}\n      />\n      {destination === \"Cloud\" && (\n        <CloudStorageCredentialSelector\n          dbs={dbs}\n          dbsTables={dbsTables}\n          dbsMethods={dbsMethods}\n          selectedId={credentialID}\n          onChange={(id) => {\n            onChangeCurrOpts({ destination, credentialID: id });\n          }}\n          pickFirstIfNoOthers={true}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/DumpRestoreAlerts.tsx",
    "content": "import { usePromise } from \"prostgles-client\";\nimport React from \"react\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport type { FullExtraProps } from \"../../pages/ProjectConnection/ProjectConnection\";\n\nexport const DumpRestoreAlerts = ({\n  dbsMethods,\n  connectionId,\n  dbProject,\n}: Pick<FullExtraProps, \"dbsMethods\" | \"dbProject\"> & {\n  connectionId: string;\n}) => {\n  const versionMismatch = usePromise(async () => {\n    try {\n      const versions = await dbsMethods.getInstalledPsqlVersions?.();\n      if (!versions) {\n        return;\n      }\n      const prglVersion = versions.psql?.split(\")\")[1]?.split(\"(\")[0]?.trim();\n      if (!prglVersion) {\n        return;\n      }\n      let serverVersion = (await dbProject.sql?.(\n        `show server_version;`,\n        {},\n        { returnType: \"value\" },\n      )) as string;\n      if (!serverVersion) return;\n      if (serverVersion.includes(\"(\")) {\n        serverVersion = serverVersion.split(\"(\")[0]!.trim();\n      }\n      const serverHasHigherVersion = serverVersion.localeCompare(\n        prglVersion,\n        undefined,\n        { numeric: true, sensitivity: \"base\" },\n      );\n      if (serverHasHigherVersion <= 0) return;\n\n      const majorVersion = serverVersion.split(\".\")[0];\n      return {\n        prglVersion,\n        serverVersion,\n        message: (\n          <>\n            Server version ({serverVersion}) is higher than the Prostgles UI\n            host version of psql ({prglVersion})<br></br>\n            {versions.os === \"Linux\" ?\n              <>\n                On linux this command may be used to update Prostgles UI host to\n                the same version:\n                <br></br>\n                <strong className=\"m-1\">\n                  sudo apt install postgresql-client-{majorVersion}\n                </strong>\n              </>\n            : <>This might cause issues.</>}\n          </>\n        ),\n      };\n    } catch {}\n  }, [dbsMethods, dbProject]);\n\n  const isSuperUser = usePromise(async () => {\n    if (dbsMethods.getIsSuperUser && connectionId) {\n      return dbsMethods.getIsSuperUser(connectionId);\n    }\n    return false;\n  }, [dbsMethods, connectionId]);\n\n  return (\n    <>\n      {isSuperUser === false && (\n        <InfoRow>\n          Using non superuser postgres account. This action might not be fully\n          successful\n        </InfoRow>\n      )}\n      {versionMismatch && (\n        <InfoRow color=\"danger\">{versionMismatch.message}</InfoRow>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/PGDumpOptions.tsx",
    "content": "import React, { useState } from \"react\";\nimport type { PGDumpParams } from \"@common/utils\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Section } from \"@components/Section\";\nimport { Select } from \"@components/Select/Select\";\nimport type { FullExtraProps } from \"../../pages/ProjectConnection/ProjectConnection\";\nimport { DumpLocationOptions } from \"./DumpLocationOptions\";\nimport { DumpRestoreAlerts } from \"./DumpRestoreAlerts\";\n\nexport const FORMATS = [\n  { key: \"p\", label: \"Plain\", subLabel: \"Output a plain-text SQL script file\" },\n  {\n    key: \"c\",\n    label: \"Custom\",\n    subLabel:\n      \"(Preferred) Output a custom-format archive suitable for input into pg_restore. This format is also compressed by default\",\n  },\n  {\n    key: \"t\",\n    label: \"Tar\",\n    subLabel: \"Output a tar-format archive suitable for input into pg_restore\",\n  },\n] as const;\n\nconst DUMP_COMMANDS = [\n  {\n    key: \"pg_dump\",\n    label: \"This database\",\n    subLabel: \"Backup this database only - pg_dump\",\n  },\n  {\n    key: \"pg_dumpall\",\n    label: \"This server\",\n    subLabel: \"Backup this server (all databases and global data) - pg_dumpall\",\n  },\n] as const;\n\nexport const DEFAULT_DUMP_OPTS: PGDumpParams = {\n  options: {\n    command: \"pg_dump\",\n    excludeSchema: \"prostgles\",\n    format: \"c\",\n    clean: true,\n    ifExists: true,\n    keepLogs: true,\n  },\n  destination: \"Local\",\n};\n\nconst DEFAULT_DUMP_ALL_OPTS: PGDumpParams = {\n  options: {\n    command: \"pg_dumpall\",\n    clean: true,\n    ifExists: true,\n    keepLogs: true,\n  },\n  destination: \"Local\",\n};\n\nexport type DumpOptionsProps = Pick<\n  FullExtraProps,\n  \"dbProject\" | \"dbs\" | \"dbsTables\" | \"dbsMethods\"\n> & {\n  onReadyButton?: (dumpOpts: PGDumpParams) => React.ReactNode;\n  opts?: PGDumpParams;\n  onChange?: (newOpts: PGDumpParams) => void;\n  connectionId: string;\n  hideDestination: boolean;\n};\nexport const PGDumpOptions = (props: DumpOptionsProps) => {\n  const {\n    opts,\n    onReadyButton,\n    onChange,\n    dbsMethods,\n    dbProject,\n    connectionId,\n    hideDestination = false,\n  } = props;\n\n  const [currOpts, setCurrOpts] = useState(opts ?? DEFAULT_DUMP_OPTS);\n  const { options: o, destination, credentialID } = currOpts;\n  const _onChange = (_newOpts: Partial<PGDumpParams>) => {\n    if (_newOpts.options?.clean) {\n      _newOpts.options.dataOnly = false;\n    } else if (_newOpts.options?.dataOnly) {\n      _newOpts.options.clean = false;\n      _newOpts.options.ifExists = false;\n    }\n    const newOpts: PGDumpParams = { ...currOpts, ..._newOpts };\n    setCurrOpts(newOpts);\n    onChange?.(newOpts);\n  };\n\n  const onChangeOptions = (\n    newOpts: Partial<PGDumpParams[\"options\"]>,\n    overwrite = false,\n  ) =>\n    _onChange({ options: overwrite ? newOpts : { ...o, ...newOpts } } as any);\n\n  const dumpAll = o.command === \"pg_dumpall\";\n  const err =\n    destination === \"Cloud\" && !credentialID ?\n      \"Must select/provide a cloud credential first\"\n    : null;\n\n  return (\n    <div\n      className=\"DumpOptions flex-col gap-1 min-s-0 o-auto bg-inherit\"\n      style={{ maxHeight: \"800px\", minWidth: \"min(500px, 99vw)\" }}\n    >\n      <DumpRestoreAlerts {...{ dbsMethods, connectionId, dbProject }} />\n      <Select\n        className=\"mr-1\"\n        label=\"Data from\"\n        fullOptions={DUMP_COMMANDS}\n        value={o.command}\n        onChange={(command) => {\n          const opts =\n            command === \"pg_dumpall\" ?\n              DEFAULT_DUMP_ALL_OPTS.options\n            : DEFAULT_DUMP_OPTS.options;\n          onChangeOptions({ ...opts, command }, true);\n        }}\n      />\n\n      {!hideDestination && (\n        <DumpLocationOptions\n          {...props}\n          currOpts={currOpts}\n          onChangeCurrOpts={_onChange}\n        />\n      )}\n\n      <Section\n        title=\"More options...\"\n        titleIconPath=\"\"\n        contentClassName=\"DumpOptionsMoreOptions flex-col py-1 gap-1 f-1 min-s-0 bg-inherit\"\n      >\n        {o.command === \"pg_dump\" ?\n          <>\n            <FlexRowWrap className=\"ai-start\">\n              <Select\n                label=\"File format\"\n                value={o.format}\n                data-command=\"PGDumpOptions.format\"\n                fullOptions={FORMATS}\n                onChange={(format) => {\n                  onChangeOptions({ format });\n                }}\n              />\n              <Select\n                label=\"Number of jobs\"\n                data-command=\"PGDumpOptions.numberOfJobs\"\n                value={o.numberOfJobs}\n                options={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}\n                onChange={(numberOfJobs) => {\n                  onChangeOptions({ numberOfJobs });\n                }}\n              />\n              <Select\n                label=\"Compression level\"\n                value={o.compressionLevel}\n                data-command=\"PGDumpOptions.compressionLevel\"\n                options={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}\n                onChange={(compressionLevel) => {\n                  onChangeOptions({ compressionLevel });\n                }}\n              />\n              <FormField\n                value={o.excludeSchema}\n                label=\"Exclude schema\"\n                data-command=\"PGDumpOptions.excludeSchema\"\n                hint=\"Do not restore objects that are in the named schema\"\n                type=\"text\"\n                onChange={(excludeSchema) => {\n                  onChangeOptions({ excludeSchema });\n                }}\n              />\n            </FlexRowWrap>\n            <FormField\n              value={o.noOwner}\n              type=\"checkbox\"\n              label=\"No owner\"\n              data-command=\"PGDumpOptions.noOwner\"\n              onChange={(noOwner) => {\n                onChangeOptions({ noOwner });\n              }}\n              hint=\"Do not output commands to set ownership of objects to match the original database\"\n            />\n            <FormField\n              value={o.create}\n              type=\"checkbox\"\n              label=\"Create\"\n              data-command=\"PGDumpOptions.create\"\n              onChange={(create) => {\n                onChangeOptions({ create });\n              }}\n              hint=\"Create the database before restoring into it. If --clean is also specified, drop and recreate the target database before connecting to it.\"\n            />\n          </>\n        : <>\n            <FormField\n              value={o.globalsOnly}\n              type=\"checkbox\"\n              label=\"Globals only\"\n              data-command=\"PGDumpOptions.globalsOnly\"\n              onChange={(globalsOnly) => {\n                onChangeOptions({ globalsOnly });\n              }}\n              hint=\"\"\n            />\n            <FormField\n              value={o.rolesOnly}\n              type=\"checkbox\"\n              label=\"Roles only\"\n              data-command=\"PGDumpOptions.rolesOnly\"\n              onChange={(rolesOnly) => {\n                onChangeOptions({ rolesOnly });\n              }}\n              hint=\"\"\n            />\n          </>\n        }\n        <FormField\n          value={o.schemaOnly}\n          type=\"checkbox\"\n          label=\"Schema only\"\n          data-command=\"PGDumpOptions.schemaOnly\"\n          onChange={(schemaOnly) => {\n            onChangeOptions({ schemaOnly });\n          }}\n          hint=\"\"\n        />\n        <FormField\n          value={o.encoding}\n          type=\"text\"\n          label=\"Encoding\"\n          data-command=\"PGDumpOptions.encoding\"\n          onChange={(encoding) => {\n            onChangeOptions({ encoding });\n          }}\n          hint=\"\"\n        />\n        <FormField\n          value={o.clean}\n          type=\"checkbox\"\n          label=\"Clean\"\n          data-command=\"PGDumpOptions.clean\"\n          onChange={(clean) => {\n            onChangeOptions({ clean });\n          }}\n          hint=\"Drop and Create commands for database objects. Will not delete objects that don't exist in the dump file\"\n        />\n\n        <FormField\n          value={o.dataOnly}\n          type=\"checkbox\"\n          label=\"Data only\"\n          data-command=\"PGDumpOptions.dataOnly\"\n          onChange={(dataOnly) => {\n            onChangeOptions({ dataOnly });\n          }}\n          hint=\"\"\n        />\n        <FormField\n          value={o.ifExists}\n          type=\"checkbox\"\n          label=\"If exists\"\n          data-command=\"PGDumpOptions.ifExists\"\n          onChange={(ifExists) => {\n            onChangeOptions({ ifExists });\n          }}\n          hint=\"Add an IF EXISTS clause to clean commands to reduce errors. Will not work for extensions\"\n        />\n\n        <FormField\n          value={o.keepLogs}\n          type=\"checkbox\"\n          label=\"Keep logs\"\n          data-command=\"PGDumpOptions.keepLogs\"\n          onChange={(keepLogs) => {\n            onChangeOptions({ keepLogs });\n          }}\n          hint=\"Save dump logs to the backup record. Useful for debugging\"\n        />\n\n        <InfoRow color=\"info\" className=\"noselect\">\n          For more info on options visit{\" \"}\n          <a\n            target=\"_blank\"\n            href={\n              \"https://www.postgresql.org/docs/current/\" +\n              (dumpAll ? \"app-pg-dumpall.html\" : \"app-pgdump.html\")\n            }\n            rel=\"noreferrer\"\n          >\n            the official site\n          </a>\n        </InfoRow>\n      </Section>\n      {err ?\n        <ErrorComponent error={err} />\n      : onReadyButton?.({ options: o, destination, credentialID })}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/RenderBackupLogs.tsx",
    "content": "import React from \"react\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport Btn from \"@components/Btn\";\nimport { sliceText } from \"@common/utils\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\n\nexport const RenderBackupLogs = ({\n  logs,\n  completed,\n  // type,\n}: {\n  logs: string;\n  completed: boolean;\n  // type: \"restore\" | \"dump\";\n}) => (\n  <PopupMenu\n    showFullscreenToggle={{}}\n    clickCatchStyle={{ opacity: 1 }}\n    title=\"Logs\"\n    data-command=\"BackupLogs\"\n    button={\n      !logs ?\n        <div></div>\n      : <Btn className=\"w-full\" size=\"small\">\n          {completed ?\n            \"...\"\n          : sliceText(\n              (logs || \"\").split(\"\\n\").at(-1)!.slice(15).split(\":\")[1] ?? \"\",\n              40,\n              \"...\",\n            )\n          }\n        </Btn>\n    }\n    onClickClose={false}\n    positioning=\"top-center\"\n  >\n    <CodeEditor\n      style={{ minWidth: \"80vw\", minHeight: \"80vh\" }}\n      value={logs}\n      options={{\n        minimap: { enabled: false },\n        lineNumbers: \"off\",\n      }}\n      language=\"bash\"\n    />\n  </PopupMenu>\n);\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/RenderBackupStatus.tsx",
    "content": "import React from \"react\";\nimport type { Backups } from \"../Dashboard/dashboardUtils\";\nimport Chip from \"@components/Chip\";\nimport { parsedError } from \"@components/ErrorComponent\";\nimport { ProgressBar } from \"@components/ProgressBar\";\nimport { bytesToSize } from \"./BackupsControls\";\n\nexport const RenderBackupStatus = ({\n  row,\n  status,\n}: {\n  status: Backups[\"status\"] | undefined;\n  row: Backups;\n}) => {\n  const commonChipStyle: React.CSSProperties = {\n    padding: 0,\n    background: \"unset\",\n    border: \"unset\",\n  };\n  const total = +(\n    (status as any)?.loading?.total ||\n    row.sizeInBytes ||\n    +row.dbSizeInBytes ||\n    0\n  );\n  return (\n    !status ? null\n    : \"ok\" in status ?\n      <Chip\n        style={commonChipStyle}\n        className=\"font-12\"\n        color=\"green\"\n        value={\"Completed\"}\n      />\n    : \"err\" in status ?\n      <Chip\n        style={commonChipStyle}\n        color=\"red\"\n        value={parsedError(status.err)}\n      />\n    : status.loading ?\n      <div className=\"text-1p5\">\n        <ProgressBar\n          message={\n            !status.loading.loaded || status.loading.loaded < 0 ?\n              \"Preparing...\"\n            : `Processed ${bytesToSize(status.loading.loaded || 0)}/${total ? bytesToSize(total) : \"unknown\"}`\n          }\n          style={{\n            minWidth: \"150px\",\n          }}\n          value={status.loading.loaded || 0}\n          totalValue={total || 0}\n        />\n      </div>\n    : null\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/Restore/Restore.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { sliceText } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Section } from \"@components/Section\";\nimport { mdiBackupRestore } from \"@mdi/js\";\nimport { useEffectDeep, usePromise } from \"prostgles-client\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport type { DBS } from \"../../Dashboard/DBS\";\nimport { CodeConfirmation } from \"../CodeConfirmation\";\nimport { DumpRestoreAlerts } from \"../DumpRestoreAlerts\";\nimport { RestoreOptions } from \"./RestoreOptions\";\n\nexport type RestoreOpts = DBSSchema[\"backups\"][\"restore_options\"];\n\nconst DEFAULT_RESTORE_OPTS: RestoreOpts = {\n  clean: true,\n  create: false,\n  dataOnly: false,\n  noOwner: false,\n  command: \"pg_restore\",\n  excludeSchema: \"prostgles\",\n  ifExists: true,\n  format: \"c\",\n  keepLogs: false,\n};\n\nexport type RestoreProps = Pick<Prgl, \"dbsMethods\" | \"db\"> & {\n  button: React.ReactNode;\n  defaultOpts?: RestoreOpts;\n  dbs: DBS;\n  backupId?: string;\n  connectionId: string;\n} & (\n    | {\n        fromFile?: true;\n        onReadyButton?: undefined;\n      }\n    | {\n        fromFile?: undefined;\n        onReadyButton: (\n          opts: RestoreOpts,\n          popupClose: () => void,\n        ) => React.ReactNode;\n      }\n  );\nexport const Restore = (props: RestoreProps) => {\n  const {\n    defaultOpts,\n    button,\n    dbs,\n    db,\n    backupId,\n    connectionId,\n    dbsMethods,\n    fromFile,\n    onReadyButton,\n  } = props;\n\n  type FileOrMaybeItsNothing = File | null | undefined;\n  const [file, setFile] = useState<FileOrMaybeItsNothing | void>();\n  const [restoreOpts, setRestoreOpts] = useState(\n    defaultOpts ?? DEFAULT_RESTORE_OPTS,\n  );\n  const { format } = restoreOpts;\n\n  useEffectDeep(() => {\n    if (file) {\n      setRestoreOpts({\n        ...restoreOpts,\n        format: file.name.toLowerCase().endsWith(\".sql\") ? \"p\" : \"c\",\n      });\n    }\n  }, [file, restoreOpts]);\n\n  const restoreFile = async (popupClose: () => void) => {\n    if (!dbsMethods.streamBackupFile) return;\n    const { streamBackupFile } = dbsMethods;\n\n    const f = file;\n    if (!f) return;\n\n    const stream = f.stream();\n    const streamId = await streamBackupFile(\n      \"start\",\n      f.name,\n      connectionId,\n      undefined,\n      f.size,\n      restoreOpts,\n    );\n\n    const writableStream = new WritableStream({\n      start(controller) {},\n      async write(chunk, controller) {\n        try {\n          await streamBackupFile(\"chunk\", streamId, null, chunk, undefined);\n        } catch (err) {\n          console.error(err);\n          controller.error(err);\n          writableStream.abort(err);\n        }\n      },\n      async close() {\n        await (async () => {\n          await streamBackupFile(\"end\", streamId, null, undefined, undefined);\n        })();\n      },\n      abort(reason) {\n        console.error(\"[abort]\", reason);\n      },\n    });\n    stream.pipeTo(writableStream);\n    popupClose();\n  };\n\n  const backup = usePromise(async () => {\n    const bkp = await dbs.backups.findOne({ id: backupId });\n    if (bkp) {\n      const bkpFormat =\n        bkp.options.command === \"pg_dumpall\" ? \"p\" : bkp.options.format;\n      if (format !== bkpFormat) {\n        setRestoreOpts({ ...restoreOpts, format: bkpFormat });\n      }\n    }\n\n    return bkp;\n  }, [backupId, dbs.backups, format, restoreOpts]);\n\n  const plainFormatAlert = format === \"p\" && (\n    <InfoRow color=\"warning\">\n      Data from this entire server (including user data) may be affected because\n      this is a restore from a plain SQL file.\n    </InfoRow>\n  );\n\n  return (\n    <CodeConfirmation\n      contentStyle={{\n        maxWidth: \"700px\",\n      }}\n      positioning=\"center\"\n      message={\n        <InfoRow color=\"warning\">\n          If you continue the current database and/or server information might\n          be lost. This process is not reversible\n        </InfoRow>\n      }\n      button={button}\n      confirmButton={(popupClose) =>\n        onReadyButton ?\n          onReadyButton(restoreOpts, popupClose)\n        : <Btn\n            iconPath={mdiBackupRestore}\n            color=\"action\"\n            variant=\"filled\"\n            onClick={() => restoreFile(popupClose)}\n          >\n            Restore\n          </Btn>\n      }\n      hideConfirm={!!(fromFile && !file)}\n      topContent={(popupClose) => (\n        <>\n          <InfoRow\n            variant=\"naked\"\n            iconPath={mdiBackupRestore}\n            color=\"action\"\n            className=\"text-medium font-20 \"\n            style={{ fontWeight: 400 }}\n          >\n            {!fromFile ?\n              <>\n                Restore{\" \"}\n                {backup?.options.command === \"pg_dumpall\" ?\n                  \"server \"\n                : \"database \"}\n                using backup from{\" \"}\n                <strong>\n                  {backup ? new Date(backup.created).toLocaleString() : \"???\"}\n                </strong>\n              </>\n            : <>\n                Restore database from{\" \"}\n                <strong>\n                  {!file ?\n                    \"local file\"\n                  : sliceText(file.name, 44, undefined, true)}{\" \"}\n                </strong>\n              </>\n            }\n          </InfoRow>\n          <DumpRestoreAlerts {...{ dbsMethods, connectionId, dbProject: db }} />\n          {plainFormatAlert}\n          {fromFile && (\n            <FormField\n              type=\"file\"\n              label=\"File\"\n              onChange={(files: FileList) => {\n                const f: FileOrMaybeItsNothing = files[0];\n                setFile(f);\n              }}\n            />\n          )}\n          {Boolean(!fromFile || file) && (\n            <Section\n              title={t.common.Options}\n              className=\"w-full\"\n              contentClassName=\"p-1\"\n              buttonStyle={{ background: \"var(--bg-color-0)\" }}\n            >\n              <RestoreOptions\n                fromFile={fromFile}\n                restoreOpts={restoreOpts}\n                setRestoreOpts={setRestoreOpts}\n              />\n            </Section>\n          )}\n        </>\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/Restore/RestoreOptions.tsx",
    "content": "import React from \"react\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Select } from \"@components/Select/Select\";\nimport { FORMATS } from \"../PGDumpOptions\";\nimport type { RestoreOpts } from \"./Restore\";\n\ntype P = {\n  fromFile: boolean | undefined;\n  restoreOpts: RestoreOpts;\n  setRestoreOpts: (opts: RestoreOpts) => void;\n};\n\nexport const RestoreOptions = (props: P) => {\n  const { restoreOpts, setRestoreOpts, fromFile } = props;\n\n  const {\n    numberOfJobs,\n    format,\n    clean,\n    create,\n    dataOnly,\n    noOwner,\n    ifExists,\n    keepLogs,\n    excludeSchema,\n  } = restoreOpts;\n\n  const formats = FORMATS.slice(0).map((_f) => {\n    const f = { ..._f };\n    if (!fromFile && f.key !== format) {\n      (f as any).disabledInfo = \"Can only use the same format as the dump file\";\n    }\n    return f;\n  });\n\n  return (\n    <>\n      <Select\n        label=\"File format\"\n        value={format}\n        fullOptions={formats}\n        onChange={(format) => {\n          setRestoreOpts({ ...restoreOpts, format });\n        }}\n      />\n      {format !== \"p\" && (\n        <>\n          <Select\n            label=\"Number of jobs\"\n            value={numberOfJobs}\n            options={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}\n            onChange={(numberOfJobs) => {\n              setRestoreOpts({ ...restoreOpts, numberOfJobs });\n            }}\n          />\n          <FormField\n            value={excludeSchema}\n            label=\"Exclude schema\"\n            hint=\"Do not restore objects that are in the named schema\"\n            type=\"text\"\n            onChange={(excludeSchema) => {\n              setRestoreOpts({ ...restoreOpts, excludeSchema });\n            }}\n          />\n          <FormField\n            value={clean}\n            label=\"Clean\"\n            hint=\"Drop and Create commands for database objects. Will not delete objects that don't exist in the dump file\"\n            type=\"checkbox\"\n            onChange={(clean) => {\n              setRestoreOpts({ ...restoreOpts, clean });\n            }}\n          />\n          <FormField\n            value={create}\n            label=\"Create\"\n            hint=\"Create the database before restoring into it. If --clean is also specified, drop and recreate the target database before connecting to it. Set to false if restoring into a different database.\"\n            type=\"checkbox\"\n            onChange={(create) => {\n              setRestoreOpts({ ...restoreOpts, create });\n            }}\n          />\n          <FormField\n            value={dataOnly}\n            label=\"Data only\"\n            hint=\"Restore only the data, not the schema (data definitions). Table data, large objects, and sequence values are restored, if present in the archive.\"\n            type=\"checkbox\"\n            onChange={(dataOnly) => {\n              setRestoreOpts({ ...restoreOpts, dataOnly });\n            }}\n          />\n          <FormField\n            value={noOwner}\n            label=\"No owner\"\n            hint=\"Do not output commands to set ownership of objects to match the original database\"\n            type=\"checkbox\"\n            onChange={(noOwner) => {\n              setRestoreOpts({ ...restoreOpts, noOwner });\n            }}\n          />\n          <FormField\n            value={ifExists}\n            label=\"If exists\"\n            hint=\"Use conditional commands (i.e., add an IF EXISTS clause) to drop database objects. This option is not valid unless --clean is also specified.\"\n            type=\"checkbox\"\n            onChange={(ifExists) => {\n              setRestoreOpts({ ...restoreOpts, ifExists });\n            }}\n          />\n        </>\n      )}\n      <FormField\n        value={keepLogs}\n        type=\"checkbox\"\n        label=\"Keep logs\"\n        onChange={(keepLogs) => {\n          setRestoreOpts({ ...restoreOpts, keepLogs });\n        }}\n        hint=\"Save restore logs to the backup record. Useful for debugging\"\n      />\n      {format !== \"p\" && (\n        <InfoRow color=\"info\" className=\"noselect\">\n          For more info on options visit{\" \"}\n          <a\n            target=\"_blank\"\n            href=\"https://www.postgresql.org/docs/current/app-pgrestore.html\"\n            rel=\"noreferrer\"\n          >\n            this official site\n          </a>\n        </InfoRow>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/BackupAndRestore/useBackupsControlsState.ts",
    "content": "import { useMemo, useState } from \"react\";\n\nexport type BackupsControlsState = ReturnType<typeof useBackupsControlsState>;\nexport const useBackupsControlsState = (connection_id: string) => {\n  const [backupsFilterType, setBackupsFilterType] = useState<\n    (typeof BACKUP_FILTER_OPTS)[number][\"key\"]\n  >(BACKUP_FILTER_OPTS[0].key);\n  const [hasBackups, setHasBackups] = useState(false);\n\n  const { backupFilter, completedBackupsFilter } = useMemo(() => {\n    const backupFilter =\n      backupsFilterType === \"This connection\" ? { connection_id }\n      : backupsFilterType === \"Deleted connections\" ? { connection_id: null }\n      : {};\n    const completedBackupsFilter = {\n      $and: [backupFilter, { \"status->ok\": { \"<>\": null } }],\n    };\n    return { backupFilter, completedBackupsFilter };\n  }, [backupsFilterType, connection_id]);\n  return {\n    completedBackupsFilter,\n    backupsFilterType,\n    backupFilter,\n    setBackupsFilterType,\n    hasBackups,\n    setHasBackups,\n  };\n};\n\nexport const BACKUP_FILTER_OPTS = [\n  { key: \"This connection\" },\n  { key: \"Deleted connections\" },\n  { key: \"All connections\" },\n] as const;\n"
  },
  {
    "path": "client/src/dashboard/Charts/CanvasChart.ts",
    "content": "import { asRGB } from \"src/utils/colorUtils\";\nimport type { Coords, Point } from \"../Charts\";\nimport type { PanListeners } from \"../setPan\";\nimport { setPan } from \"../setPan\";\nimport { createHiPPICanvas } from \"./createHiPPICanvas\";\nimport { drawMonotoneXCurve } from \"./drawMonotoneXCurve\";\nimport { type ShapeV2 } from \"./drawShapes/drawShapes\";\nimport { roundRect } from \"./roundRect\";\nimport type { XYFunc } from \"./TimeChart/TimeChart\";\nimport { isDefined } from \"@common/filterUtils\";\nimport { getTimechartGradientPeakSections } from \"./drawShapes/getTimechartGradientPeakSections\";\n\nexport type StrokeProps = {\n  lineWidth: number;\n  strokeStyle: string;\n};\nexport type FillProps = {\n  fillStyle: CanvasGradient | string | CanvasPattern;\n};\nexport type ShapeBase<T = void> = {\n  id: string | number;\n  elevation?: number;\n  opacity?: number;\n} & (T extends void ? { data?: T } : { data: T });\n\nexport type Circle<T = any> = ShapeBase<T> &\n  StrokeProps &\n  FillProps & {\n    type: \"circle\";\n    r: number;\n    coords: Point;\n  };\n\nexport type Rectangle<T = any, C = void> = ShapeBase<T> &\n  StrokeProps &\n  FillProps & {\n    type: \"rectangle\";\n    w: number;\n    h: number;\n    borderRadius?: number;\n    coords: Point;\n    children?: Exclude<ShapeV2<C>, LinkLine>[];\n  };\n\nexport type ChartedText<T = any> = ShapeBase<T> &\n  FillProps & {\n    type: \"text\";\n    text: string;\n    font?: string;\n    textAlign?: CanvasTextAlign;\n    textBaseline?: CanvasTextBaseline;\n    coords: Point;\n    background?: Partial<StrokeProps> &\n      Partial<FillProps> & {\n        padding?: number;\n        borderRadius?: number;\n      };\n  };\n\nexport type MultiLine<T = any> = ShapeBase<T> &\n  StrokeProps & {\n    type: \"multiline\";\n    coords: Point[];\n    withGradient?: boolean;\n    variant?: \"smooth\";\n  };\nexport type LinkLine<T = any> = ShapeBase<T> &\n  StrokeProps & {\n    type: \"linkline\";\n    sourceId: string | number;\n    targetId: string | number;\n    sourceYOffset: number;\n    targetYOffset: number;\n    variant?: \"smooth\";\n  };\nexport type Image<T = any> = ShapeBase<T> & {\n  type: \"image\";\n  coords: Point;\n  w: number;\n  h: number;\n  image: CanvasImageSource;\n};\n\nexport type Polygon<T = any> = ShapeBase<T> &\n  StrokeProps &\n  FillProps & {\n    type: \"polygon\";\n    coords: Point[];\n  };\n\nexport type Shape<T = any> =\n  | Rectangle<T>\n  | Circle<T>\n  | ChartedText<T>\n  | MultiLine<T>\n  | Polygon<T>;\n\nexport type TextMeasurement = {\n  width: number;\n  fontHeight: number;\n  actualHeight: number;\n};\n\ntype ChartView = {\n  xScale: number;\n  yScale: number;\n\n  /** topLeft scaled offset used for panning  */\n  xO: number;\n  yO: number;\n};\n\nexport type CanvasChartViewDataExtent = {\n  /**\n   * Left most visible X value\n   * To get canvas position use getScreenXY\n   */\n  leftX: number;\n  topY: number;\n  /**\n   * Right most visible X value\n   * To get canvas position use getScreenXY\n   */\n  rightX: number;\n  bottomY: number;\n  xScale: number;\n  yScale: number;\n};\nexport type GetShapes = (opts: CanvasChartViewDataExtent) => Shape[];\n\ntype TimeChartZoomPanEvents = Partial<PanListeners> & {\n  zoomPanDisabled?: boolean;\n  onExtentChange?: (ext: CanvasChartViewDataExtent) => void;\n  onExtentChanged?: (ext: CanvasChartViewDataExtent) => void;\n};\n\ntype ChartOptions = {\n  node: HTMLDivElement;\n  canvas: HTMLCanvasElement;\n  yScaleLocked?: boolean;\n  yPanLocked?: boolean;\n  minXScale?: number;\n  maxXScale?: number;\n  onResize: undefined | VoidFunction;\n  events?:\n    | {\n        disabled: true;\n      }\n    | (TimeChartZoomPanEvents & { disabled?: undefined });\n};\n\n/**\n * Canvas draw component that allows zoom in and panning\n */\nexport class CanvasChart {\n  node?: HTMLDivElement;\n  ctx?: CanvasRenderingContext2D;\n  opts?: ChartOptions;\n  deckLayers?: any[];\n  currViewState?: any;\n  currViewStateValid = false;\n  shapes?: Shape[] = [];\n\n  /**\n   * Used for zoom in and panning\n   */\n  view: ChartView = {\n    xScale: 1,\n    yScale: 1,\n    xO: 0,\n    yO: 0,\n  };\n\n  static clampScale(newScale: number): number {\n    const MIN_SCALE = 0.000001;\n    const MAX_SCALE = 12200000;\n\n    return Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale));\n  }\n\n  get events() {\n    return this.opts?.events && this.opts.events.disabled !== true ?\n        this.opts.events\n      : undefined;\n  }\n\n  constructor(opts: ChartOptions) {\n    this.opts = opts;\n    const { node, canvas, events } = opts;\n\n    this.init();\n    const resizeObserver = new ResizeObserver(() => {\n      if (this.ctx) {\n        const { offsetHeight, offsetWidth } = node;\n\n        createHiPPICanvas(canvas, offsetWidth, offsetHeight);\n\n        /**\n         * This cancels createHiPPICanvas. Why was it needed?\n         */\n        // this.ctx.canvas.width  = offsetWidth;\n        // this.ctx.canvas.height = offsetHeight;\n        setTimeout(() => {\n          this.opts?.onResize?.();\n        }, 50);\n        this.render();\n      }\n    });\n    resizeObserver.observe(node);\n\n    if (this.opts.events?.disabled) return;\n    const getEvents = () => this.events;\n    let initialView: ChartView | null = null;\n    setPan(node, {\n      ...events,\n      onPanStart: (ev, e) => {\n        initialView = { ...this.view };\n        const events = getEvents();\n        if (!events) return;\n\n        events.onPanStart?.(ev, e);\n      },\n      onPan: (ev, e) => {\n        if (!initialView) return;\n        this.setView({\n          ...this.view,\n          xO: initialView.xO + ev.xDiff,\n          yO: initialView.yO + ev.yDiff,\n        });\n        canvas.style.cursor = \"move\";\n        const events = getEvents();\n        if (!events) return;\n\n        events.onPan?.(ev, e);\n      },\n      onPanEnd: (ev, e) => {\n        canvas.style.cursor = \"default\";\n        const events = getEvents();\n        if (!events) return;\n\n        events.onPanEnd?.(ev, e);\n\n        this.onExtentChange();\n      },\n      onDoubleTap: ({ x, y }) => {\n        const delta = 1;\n        const xScaleDelta = this.view.xScale * Math.sign(delta) * 2,\n          yScaleDelta = this.view.yScale * Math.sign(delta) * 2,\n          xScale = CanvasChart.clampScale(this.view.xScale + xScaleDelta),\n          yScale = CanvasChart.clampScale(this.view.yScale + yScaleDelta),\n          xSF = xScale / this.view.xScale,\n          ySF = yScale / this.view.yScale;\n\n        const xO = x - xSF * (x - this.view.xO),\n          yO = y - ySF * (y - this.view.yO);\n\n        this.setView({\n          ...this.view,\n          xScale,\n          yScale,\n          xO,\n          yO,\n        });\n      },\n      onSinglePinch: ({ delta, x, y, w, h }) => {\n        const xScaleDelta = this.view.xScale * Math.sign(delta) * 0.1,\n          yScaleDelta = this.view.yScale * Math.sign(delta) * 0.1,\n          xScale = CanvasChart.clampScale(this.view.xScale + xScaleDelta),\n          yScale = CanvasChart.clampScale(this.view.yScale + yScaleDelta),\n          xSF = xScale / this.view.xScale,\n          ySF = yScale / this.view.yScale;\n\n        const xO = x - xSF * (x - this.view.xO),\n          yO = y - ySF * (y - this.view.yO);\n\n        this.setView({\n          ...this.view,\n          xScale,\n          yScale,\n          xO,\n          yO,\n        });\n      },\n    });\n  }\n\n  onExtentChangeTimeout?: NodeJS.Timeout;\n  onExtentChange = () => {\n    if (!this.events) return;\n    this.events.onExtentChange?.({ ...this.getExtent() });\n\n    if (this.onExtentChangeTimeout) {\n      clearTimeout(this.onExtentChangeTimeout);\n    }\n\n    if (!this.events.onExtentChanged) return;\n    this.onExtentChangeTimeout = setTimeout(() => {\n      const newExtent = { ...this.getExtent() };\n\n      this.events?.onExtentChanged?.(newExtent);\n    }, 200);\n  };\n\n  setView(view: Partial<ChartView>) {\n    if (!this.opts) throw \"No opts\";\n\n    const {\n      yScaleLocked = false,\n      yPanLocked = false,\n      minXScale,\n      maxXScale,\n    } = this.opts;\n    const oldView = { ...this.view };\n    this.view = {\n      ...this.view,\n      ...view,\n    };\n\n    if (yScaleLocked) {\n      this.view.yScale = 1;\n    }\n    if (yPanLocked) {\n      this.view.yO = oldView.yO;\n    }\n    if (\n      (typeof minXScale === \"number\" && this.view.xScale < minXScale) ||\n      (typeof maxXScale === \"number\" && this.view.xScale > maxXScale)\n    ) {\n      this.view.xO = oldView.xO;\n      if (typeof minXScale === \"number\" && this.view.xScale < minXScale) {\n        this.view.xScale = minXScale;\n        this.view.xO = Math.max(0, oldView.xO / 10);\n      }\n      if (typeof maxXScale === \"number\" && this.view.xScale > maxXScale) {\n        this.view.xScale = oldView.xScale;\n      }\n    }\n\n    if (JSON.stringify(this.view) !== JSON.stringify(oldView)) {\n      this.onExtentChange();\n      this.render(this.rawShapes);\n    }\n  }\n\n  init() {\n    if (!this.opts) return;\n    const { node, canvas, events } = this.opts,\n      { w, h } = this.getWH();\n\n    this.ctx = canvas.getContext(\"2d\")!;\n    this.ctx.canvas.width = w;\n    this.ctx.canvas.height = h;\n\n    createHiPPICanvas(canvas, node.offsetWidth, node.offsetHeight);\n\n    this.ctx.imageSmoothingEnabled = true;\n\n    if (events?.disabled) return;\n    node.onwheel = (e) => {\n      e.preventDefault();\n      e.preventDefault();\n      const { xScale, yScale } = this.view;\n      let { xO, yO } = this.view;\n\n      const r = node.getBoundingClientRect(),\n        xNode = e.pageX - r.left,\n        yNode = e.pageY - r.top,\n        // { deltaY } = e;\n        getFactor = (deltaV) => Math.abs(deltaV) * (0.2 / 50),\n        ne = normalizeWheel(e),\n        deltaY = ne.pixelY, // (-1 * (e as any).wheelDeltaY) || e.deltaY,\n        deltaX = ne.pixelX, //(-1 * (e as any).wheelDeltaX) || e.deltaX;\n        factor = Math.max(0.1, getFactor(deltaY));\n\n      // console.log(factor)\n      let newYScale = yScale * Math.exp(-Math.sign(deltaY) * factor),\n        newXScale = xScale * Math.exp(-Math.sign(deltaY) * factor);\n\n      newXScale = CanvasChart.clampScale(newXScale);\n      newYScale = CanvasChart.clampScale(newYScale);\n\n      const xSF = newXScale / xScale;\n      const ySF = newYScale / yScale;\n      xO = -deltaX + xNode - xSF * (xNode - xO);\n      yO = yNode - ySF * (yNode - yO);\n\n      this.setView({ xO, yO, yScale: newYScale, xScale: newXScale });\n    };\n  }\n\n  getWH() {\n    const { canvas } = this.opts!;\n    return {\n      w: canvas.offsetWidth,\n      h: canvas.offsetHeight,\n    };\n  }\n\n  /** Used to get initial XY of data after panning and zooming is reversed. Used for tooltips */\n  getDataXY = (xCanvas: number, yCanvas: number): [number, number] => {\n    const {\n      view: { xScale, yScale, xO, yO },\n    } = this;\n    return [(xCanvas - xO) / xScale, (yCanvas - yO) / yScale];\n  };\n\n  /** Used to get final drawing XY that takes into account panning and zooming. Used for drawing data on canvas */\n  getScreenXY: XYFunc = <X extends number, Y extends number>(\n    xData: X,\n    yData?: Y,\n  ) => {\n    const {\n      view: { xScale, yScale, xO, yO },\n    } = this;\n    const x = xData * xScale + xO;\n    let y: number | undefined = undefined;\n    if (yData !== undefined) {\n      y = yData * yScale + yO;\n    }\n    return [x, y] as [X, Y];\n  };\n\n  getExtent = (): CanvasChartViewDataExtent => {\n    const { w, h } = this.getWH();\n    const [leftX, topY] = this.getDataXY(0, 0);\n    const [rightX, bottomY] = this.getDataXY(w, h);\n\n    return {\n      leftX,\n      topY,\n      rightX,\n      bottomY,\n      ...this.view,\n    };\n  };\n\n  measureTextCache: Map<string, TextMeasurement> = new Map();\n  measureText = (s: ChartedText): TextMeasurement => {\n    const { ctx } = this;\n    if (!ctx) throw \"No ctx\";\n    const { textAlign = \"\", text = \"\", font = \"\" } = s;\n    const key = [textAlign, text.length, font].join(\";\");\n    const cached = this.measureTextCache.get(key);\n    if (cached) return cached;\n    ctx.fillStyle = s.fillStyle;\n    ctx.textAlign = s.textAlign ?? ctx.textAlign;\n    ctx.textBaseline = s.textBaseline ?? ctx.textBaseline;\n    ctx.font = s.font || ctx.font;\n    const metrics = ctx.measureText(s.text);\n\n    const fontHeight =\n      metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;\n    const actualHeight =\n      metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;\n    const result = {\n      width: metrics.width,\n      fontHeight,\n      actualHeight,\n    };\n    this.measureTextCache.set(key, result);\n    return result;\n  };\n\n  private parseData(_shapes: (Shape | GetShapes)[]): Shape[] {\n    let shapes: Shape[] = [];\n    const ext = this.getExtent();\n    _shapes.map((s) => {\n      if (typeof s === \"function\") {\n        shapes = shapes.concat(s(ext));\n      } else {\n        shapes.push(s);\n      }\n    });\n    return window.structuredClone(shapes);\n  }\n\n  rawShapes: (Shape | GetShapes)[] = [];\n  render(_shapes?: (Shape | GetShapes)[]) {\n    let shapes: Shape[] = [];\n\n    if (_shapes) {\n      this.rawShapes = [..._shapes];\n      shapes = this.parseData(_shapes);\n      this.shapes = shapes;\n    } else if (this.shapes) {\n      shapes = [...this.shapes];\n    }\n\n    const { w, h } = this.getWH();\n    const { ctx } = this;\n    if (!ctx) return;\n    ctx.clearRect(0, 0, w, h);\n\n    const getScreenCoords = <C extends Coords>(coords: C): C => {\n      if (Array.isArray(coords[0])) {\n        return coords.map((p) => getScreenCoords(p as Point)) as C;\n      }\n      const point = coords as Point;\n      const [x, y] = this.getScreenXY(point[0], point[1]);\n      return [x, y] as C;\n    };\n    shapes.map((s, i) => {\n      ctx.lineJoin = \"bevel\";\n      if (s.type === \"rectangle\") {\n        const coords = getScreenCoords(s.coords);\n        ctx.fillStyle = s.fillStyle;\n        ctx.lineWidth = s.lineWidth;\n        ctx.strokeStyle = s.strokeStyle;\n        const [x1] = this.getScreenXY(coords[0]);\n        const [x2] = this.getScreenXY(s.w + coords[0]);\n        const w = x2 - x1;\n        if (s.borderRadius) {\n          roundRect(ctx, coords[0], coords[1], w, s.h, s.borderRadius);\n        } else {\n          ctx.beginPath();\n          ctx.rect(coords[0], coords[1], w, s.h);\n        }\n        ctx.fill();\n        ctx.stroke();\n      } else if (s.type === \"circle\") {\n        const coords = getScreenCoords(s.coords);\n        ctx.fillStyle = s.fillStyle;\n        ctx.lineWidth = s.lineWidth;\n        ctx.strokeStyle = s.strokeStyle;\n        ctx.beginPath();\n        ctx.arc(coords[0], coords[1], s.r, 0, 2 * Math.PI);\n        ctx.fill();\n        ctx.stroke();\n      } else if (s.type === \"multiline\") {\n        const coords = getScreenCoords(s.coords);\n\n        if (s.withGradient && coords.length > 2) {\n          ctx.save();\n\n          const { peakSections, minY, stops } =\n            getTimechartGradientPeakSections(coords);\n\n          peakSections.forEach((sectionCoords) => {\n            if (sectionCoords.length < 2) return;\n            const gradient = ctx.createLinearGradient(0, minY, 0, h);\n            const rgba = asRGB(s.strokeStyle);\n            const rgb = rgba.slice(0, 3).join(\", \");\n            stops.forEach(({ offset, opacity }) => {\n              gradient.addColorStop(offset, `rgba(${rgb}, ${opacity})`);\n            });\n            const firstPoint = sectionCoords[0]!;\n            const lastPoint = sectionCoords.at(-1)!;\n\n            ctx.beginPath();\n            ctx.moveTo(firstPoint.x, h);\n            ctx.lineTo(firstPoint.x, firstPoint.y);\n            if (s.variant === \"smooth\" && sectionCoords.length > 2) {\n              drawMonotoneXCurve(\n                ctx,\n                sectionCoords.map(({ x, y }) => [x, y]),\n                true,\n              );\n            } else {\n              sectionCoords.forEach(({ x, y }) => {\n                ctx.lineTo(x, y);\n              });\n            }\n            ctx.lineTo(lastPoint.x, h);\n            ctx.closePath();\n            ctx.fillStyle = gradient;\n            ctx.fill();\n          });\n          ctx.restore();\n        }\n\n        ctx.lineCap = \"round\";\n        ctx.lineWidth = s.lineWidth;\n        ctx.strokeStyle = s.strokeStyle;\n\n        ctx.beginPath();\n        if (s.variant === \"smooth\" && coords.length > 2) {\n          drawMonotoneXCurve(ctx, coords);\n        } else {\n          coords.forEach(([x, y], i) => {\n            if (!i) {\n              ctx.moveTo(x, y);\n            } else {\n              ctx.lineTo(x, y);\n            }\n          });\n        }\n        ctx.stroke();\n      } else if (s.type === \"polygon\") {\n        const coords = getScreenCoords(s.coords);\n        ctx.fillStyle = s.fillStyle;\n        ctx.lineWidth = s.lineWidth;\n        ctx.strokeStyle = s.strokeStyle;\n        ctx.beginPath();\n        coords.map(([x, y], i) => {\n          if (!i) {\n            ctx.beginPath();\n            ctx.moveTo(x, y);\n          } else {\n            ctx.lineTo(x, y);\n          }\n        });\n        ctx.closePath();\n        ctx.stroke();\n        ctx.fill();\n        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n      } else if (s.type === \"text\") {\n        const coords = getScreenCoords(s.coords);\n        const { textAlign = \"start\" } = s;\n\n        ctx.fillStyle = s.fillStyle;\n        ctx.textAlign = textAlign;\n        ctx.textBaseline = s.textBaseline ?? ctx.textBaseline;\n        ctx.font = s.font || ctx.font;\n        ctx.save();\n        if (s.background) {\n          const txtSize = this.measureText(s);\n          const txtPadding = s.background.padding || 6;\n          ctx.fillStyle = s.background.fillStyle ?? ctx.fillStyle;\n          if (s.background.strokeStyle) {\n            ctx.strokeStyle = s.background.strokeStyle;\n          }\n          ctx.lineWidth = s.background.lineWidth ?? ctx.lineWidth;\n          let x = coords[0] - txtSize.width / 2 - txtPadding;\n          const y = coords[1] - txtSize.actualHeight / 1.4 - txtPadding;\n\n          if ([\"left\", \"start\"].includes(textAlign)) {\n            x = coords[0] - txtPadding;\n          } else if ([\"right\", \"end\"].includes(textAlign)) {\n            x = coords[0] - txtSize.width - txtPadding;\n          }\n\n          roundRect(\n            ctx,\n            x,\n            y,\n            txtSize.width + 2 * txtPadding,\n            txtSize.actualHeight + 2 * txtPadding,\n            s.background.borderRadius || 0,\n          );\n          ctx.closePath();\n          if (s.background.strokeStyle) {\n            ctx.stroke();\n          }\n          ctx.fill();\n        }\n        ctx.restore();\n\n        const topOffsetToCenterItVertically = 2;\n        ctx.fillText(\n          s.text,\n          coords[0],\n          coords[1] - topOffsetToCenterItVertically,\n        );\n        // s.text.split(\"\\n\").map(text => {\n        //   ctx.fillText(text, coords[0], coords[1]);\n        // })\n      } else throw \"Unexpected shape type: \" + (s as any).type;\n    });\n\n    const { canvas } = this.opts ?? {};\n    if (canvas) {\n      canvas._drawn = {\n        shapes,\n        scale: 1,\n        translate: { x: 0, y: 0 },\n      };\n    }\n  }\n}\n\nconst PIXEL_STEP = 10;\nconst LINE_HEIGHT = 40;\nconst PAGE_HEIGHT = 800;\nfunction normalizeWheel(event: WheelEvent): {\n  spinX: number;\n  spinY: number;\n  pixelX: number;\n  pixelY: number;\n} {\n  let sX = 0,\n    sY = 0, // spinX, spinY\n    pX = 0,\n    pY = 0; // pixelX, pixelY\n\n  // side scrolling on FF with DOMMouseScroll\n  //@ts-ignore\n  if (\"axis\" in event && event.axis === event.HORIZONTAL_AXIS) {\n    sX = sY;\n    sY = 0;\n  }\n\n  pX = sX * PIXEL_STEP;\n  pY = sY * PIXEL_STEP;\n\n  if (\"deltaY\" in event) {\n    pY = event.deltaY;\n  }\n  if (\"deltaX\" in event) {\n    pX = event.deltaX;\n  }\n\n  if ((pX || pY) && event.deltaMode) {\n    if (event.deltaMode == 1) {\n      // delta in LINE units\n      pX *= LINE_HEIGHT;\n      pY *= LINE_HEIGHT;\n    } else {\n      // delta in PAGE units\n      pX *= PAGE_HEIGHT;\n      pY *= PAGE_HEIGHT;\n    }\n  }\n\n  // Fall-back if spin cannot be determined\n  if (pX && !sX) {\n    sX = pX < 1 ? -1 : 1;\n  }\n  if (pY && !sY) {\n    sY = pY < 1 ? -1 : 1;\n  }\n\n  return {\n    spinX: sX,\n    spinY: sY,\n    pixelX: pX,\n    pixelY: pY,\n  };\n}\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/TimeChart.tsx",
    "content": "type ChartDate = { v: number; x: number; date: Date };\n\nimport type { ScaleLinear } from \"d3\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\n\nimport { classOverride } from \"@components/Flex\";\nimport React from \"react\";\nimport type { ChartOptions } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport type { Point } from \"../../Charts\";\nimport RTComp from \"../../RTComp\";\nimport type { CanvasChart, Shape } from \"../CanvasChart\";\nimport type { DateExtent } from \"./getTimechartBinSize\";\nimport { getTimechartTooltipIntersections } from \"./getTimechartTooltipIntersections\";\nimport { onDeltaTimechart } from \"./onDeltaTimechart\";\nimport { prepareTimechartData } from \"./prepareTimechartData\";\nimport type { ColumnValue } from \"src/dashboard/W_Table/ColumnMenu/ColumnStyleControls/ColumnStyleControls\";\n\nexport type DataItem = {\n  date: number | string;\n  value: number;\n};\nexport type TimeChartLayer = {\n  label: string | undefined;\n  color: string;\n  getYLabel: (opts: {\n    value: number;\n    min: number;\n    max: number;\n    prev: number;\n    next: number;\n  }) => string;\n  /** Raw data */\n  data: DataItem[];\n  sortedParsedData?: {\n    date: number;\n    value: number;\n    x: number;\n    y: number;\n  }[];\n  fullExtent: [Date, Date];\n  variant?: \"smooth\";\n  cols: Pick<\n    ValidatedColumnInfo,\n    \"name\" | \"label\" | \"tsDataType\" | \"udt_name\"\n  >[];\n  groupByValue?: ColumnValue;\n  minVal?: number;\n  maxVal?: number;\n};\n\nexport type TimeChartProps = Pick<\n  ChartOptions<\"timechart\">,\n  | \"renderStyle\"\n  | \"showBinLabels\"\n  | \"showGradient\"\n  | \"tooltipPosition\"\n  | \"binValueLabelMaxDecimals\"\n> & {\n  layers: TimeChartLayer[];\n  className?: string;\n  onExtentChange?: (visibleData: DateExtent, viewPort: DateExtent) => void;\n  onExtentChanged?: (\n    visibleData: DateExtent,\n    viewPort: DateExtent,\n    opts: { resetExtent: boolean },\n  ) => void;\n  chartRef?: (ref: TimeChart) => void;\n  style?: React.CSSProperties;\n  binSize: number | undefined;\n  showXAxis?: boolean;\n  zoomPanDisabled?: boolean;\n  padding?: {\n    /**\n     * Defaults to 60px\n     */\n    top?: number;\n\n    /**\n     * Defaults to 60px\n     */\n    bottom?: number;\n  };\n  yAxisVariant?: \"compact\";\n  yAxisScaleMode?: \"single\" | \"multiple\";\n  onClick?: (ev: { dateMillis: number; isMinDate: boolean }) => void;\n};\n\nexport type XYFunc = {\n  (xData: number, yData?: undefined): [number, undefined];\n  (xData: number, yData: number): [number, number];\n};\n\nexport type TimeChartD = {\n  xCursor?: number;\n  yCursor?: number;\n  extent?: {};\n};\n\nexport class TimeChart extends RTComp<\n  TimeChartProps,\n  Record<string, never>,\n  TimeChartD\n> {\n  ref?: HTMLDivElement;\n  canv?: HTMLCanvasElement;\n  chart?: CanvasChart;\n  lineLayers?: (TimeChartLayer & {\n    coords: Point[];\n  })[];\n  style?: React.CSSProperties;\n\n  d: TimeChartD = {};\n\n  data?: {\n    layers: TimeChartLayer[];\n    dates: ChartDate[];\n    xScale: ScaleLinear<number, number, never>;\n    xScaleYLabels: ScaleLinear<number, number, never>;\n    getYScale: (layerIndex: number) => ScaleLinear<number, number, never>;\n\n    minDate: number;\n    maxDate: number;\n    minVal: number;\n    maxVal: number;\n  };\n\n  lastExtentChange = 0;\n  getVisibleExtent = () => {\n    if (!this.chart || !this.data) return undefined;\n\n    const { xMin, xMax } = this.getMargins();\n    const [leftX] = this.chart.getDataXY(xMin, 0);\n    const [rightX] = this.chart.getDataXY(xMax, 0);\n    const xScale = this.data.xScale.copy().clamp(false);\n    const res = {\n      minDate: new Date(xScale.invert(leftX)),\n      maxDate: new Date(xScale.invert(rightX)),\n    };\n    return res;\n  };\n\n  /**\n   * Margins used to leave space for bottom axis\n   * @returns the limits of the linechart area\n   */\n  getMargins = () => {\n    const { w, h } = this.chart!.getWH();\n    const { padding } = this.props;\n    const xForYLabels = 6;\n    const xMin = 50 + 20;\n    const xMax = w - 20;\n    const yMin = padding?.top ?? 60;\n    const yMax = h - (padding?.bottom ?? 50);\n    return { xMin, xMax, yMin, yMax, xForYLabels };\n  };\n  parseData = prepareTimechartData.bind(this);\n\n  getIntersections = getTimechartTooltipIntersections.bind(this);\n\n  mainShapes: Shape[] = [];\n  tooltips?: { shapes: Shape[]; snapped_x: number | undefined };\n  panning = false;\n\n  getPointXY = (point: { date: Date; value: number }) => {\n    if (!this.data || !this.chart) return undefined;\n    const x = this.data.xScale(+point.date);\n    const y = this.data.xScale(point.value);\n    return this.chart.getScreenXY(x, y);\n  };\n  onDelta = onDeltaTimechart.bind(this);\n\n  render() {\n    const { className = \"\", style = {}, onClick } = this.props;\n\n    const onHover = (e?: React.PointerEvent<HTMLDivElement>) => {\n      if (!e) {\n        this.setData({ xCursor: undefined, yCursor: undefined });\n      } else {\n        const r = e.currentTarget.getBoundingClientRect();\n\n        this.setData({\n          xCursor: e.clientX - r.x,\n          yCursor: e.clientY - r.y,\n        });\n      }\n    };\n    return (\n      <div\n        ref={(e) => {\n          if (e) this.ref = e;\n        }}\n        style={{\n          ...style,\n          /** Prevent resize-render recursion due to scrollbars */\n          overflow: \"hidden\",\n        }}\n        className={classOverride(\n          \"TimeChart flex-col f-1 h-fit min-h-0 min-w-0 relative noselect \",\n          className,\n        )}\n        onPointerMove={onHover}\n        onPointerUp={(e) => {\n          if (\n            this.tooltips?.snapped_x !== undefined &&\n            this.data &&\n            Date.now() - this.lastExtentChange > 500\n          ) {\n            onClick?.({\n              dateMillis: this.data.xScale\n                .copy()\n                .invert(this.tooltips.snapped_x),\n              isMinDate:\n                this.data.xScale.range()[0] === this.tooltips.snapped_x,\n            });\n          }\n          onHover(e);\n        }}\n        onPointerCancel={() => {\n          onHover();\n        }}\n        onPointerLeave={() => onHover()}\n      >\n        <canvas\n          ref={(e) => {\n            if (e) this.canv = e;\n          }}\n          className=\"f-1 noselect\"\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/getBinValueLabels.ts",
    "content": "import { nFormatter } from \"src/utils/utils\";\nimport type { Point } from \"../../Charts\";\nimport type { ChartedText, Circle } from \"../CanvasChart\";\nimport { getYOnMonotoneXCurve } from \"../drawMonotoneXCurve\";\nimport type { DataItem, TimeChartProps } from \"./TimeChart\";\n\ntype GetBinValueLabelArgs = Pick<\n  TimeChartProps,\n  \"renderStyle\" | \"binValueLabelMaxDecimals\" | \"showBinLabels\"\n> & {\n  circles: Circle<DataItem>[];\n  showCircles: boolean;\n};\nexport const getBinValueLabels = ({\n  circles,\n  renderStyle,\n  binValueLabelMaxDecimals,\n  showCircles,\n  showBinLabels,\n}: GetBinValueLabelArgs) => {\n  const layerCoords = circles.map((c) => c.coords);\n  const getLabel = (\n    c: (typeof circles)[number],\n    prevP: Point | undefined,\n    nextP: Point | undefined,\n  ) => {\n    const [x, y] = c.coords;\n\n    const prevAngle = prevP ? getAngle(prevP, c.coords) : undefined;\n    const nextAngle = nextP ? getAngle(c.coords, nextP) : undefined;\n    const angles = {\n      p: prevAngle !== undefined && prevAngle > 1,\n      n: nextAngle !== undefined && nextAngle < -1,\n    };\n    const textSize = 16;\n    const textMargin = showCircles ? 8 : 5;\n\n    let xOffset = 0;\n    if (prevP && nextP) {\n      const a1 = getAngle(prevP, c.coords);\n      const a2 = getAngle(c.coords, nextP);\n      const lineIsVertical =\n        [a1, a2].every((v) => v > -120 && v < -70) ||\n        [a1, a2].every((v) => v > 70 && v < 120);\n      if (lineIsVertical) xOffset = -textSize;\n    }\n\n    const showBelow =\n      (renderStyle === \"line\" || renderStyle === \"smooth\") &&\n      !showCircles &&\n      (angles.p || angles.n);\n    const yOffset = showBelow ? textMargin + textSize : -textMargin * 2;\n    const text =\n      Number.isFinite(c.data.value) ?\n        nFormatter(c.data.value, binValueLabelMaxDecimals || 1)\n      : \"\";\n\n    const finalX = x + xOffset;\n    const finalY = y + yOffset;\n    const smoothY =\n      renderStyle === \"smooth\" ?\n        getYOnMonotoneXCurve(layerCoords, finalX)\n      : undefined;\n    const screenY = smoothY !== undefined ? smoothY + yOffset : finalY;\n    const label: ChartedText = {\n      ...c,\n      type: \"text\",\n      text,\n      coords: [finalX, screenY],\n      textAlign: showBinLabels === \"latest point\" ? \"start\" : \"center\",\n    };\n    return label;\n  };\n\n  // TODO: when rendering multiple layers (barchart with groupBy), avoid overlapping labels\n  if (showBinLabels === \"latest point\") {\n    const lastPoint = circles.at(-1);\n    return (lastPoint && [getLabel(lastPoint, undefined, undefined)]) ?? [];\n  }\n\n  if (showBinLabels === \"all points\" || showBinLabels === \"peaks and troughs\") {\n    const labels: ChartedText[] = [];\n    circles.forEach((c, i) => {\n      const prevP = circles[i - 1];\n      const nextP = circles[i + 1];\n      let isPeak: boolean | undefined = false;\n      let isTrough: boolean | undefined = false;\n      if (showBinLabels === \"peaks and troughs\") {\n        isPeak =\n          prevP &&\n          nextP &&\n          c.coords[1] >= prevP.coords[1] &&\n          c.coords[1] >= nextP.coords[1];\n        isTrough =\n          prevP &&\n          nextP &&\n          c.coords[1] <= prevP.coords[1] &&\n          c.coords[1] <= nextP.coords[1];\n        if (!isPeak && !isTrough) {\n          return;\n        }\n      }\n      const label = getLabel(c, prevP?.coords, nextP?.coords);\n      const minWidth = 50;\n      const prevLabel = labels.at(-1);\n      if (prevLabel && prevLabel.coords[0] > label.coords[0] - minWidth) {\n      } else {\n        labels.push(label);\n      }\n    });\n\n    return labels;\n  }\n\n  return [];\n};\n\nconst getAngle = ([cx, cy]: Point, [ex, ey]: Point) => {\n  const dy = ey - cy;\n  const dx = ex - cx;\n  let theta = Math.atan2(dy, dx); // range (-PI, PI]\n  theta *= 180 / Math.PI; // rads to degs, range (-180, 180]\n  //if (theta < 0) theta = 360 + theta; // range [0, 360)\n  return theta;\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/getTimeAxisTicks.ts",
    "content": "import { getAge } from \"@common/utils\";\nimport { isDefined } from \"prostgles-types\";\nimport {\n  dateAsYMD,\n  DAY,\n  HOUR,\n  MINUTE,\n  MONTH,\n  roundToNearest,\n  SECOND,\n  toDateStr,\n} from \"../../Charts\";\nimport type { ChartedText, TextMeasurement } from \"./../CanvasChart\";\nimport type { XYFunc } from \"./TimeChart\";\nimport type { DateExtent } from \"./getTimechartBinSize\";\nimport { getCssVariableValue } from \"./onRenderTimechart\";\n\ntype GetTimeTicksOpts = DateExtent & {\n  leftX: number;\n  rightX: number;\n  height: number;\n  getX: (dateVal: number) => number;\n  getScreenXY: XYFunc;\n  measureText: (txt: ChartedText) => TextMeasurement;\n  /** If provided then render these values */\n  values: undefined | Date[];\n};\n\ntype DateIncrementer = {\n  id: string;\n  getStart: (d: Date) => Date;\n  getLabel: (d: Date) => string;\n  // getNext: (d: Date) => Date;\n  dateDelta: number;\n};\n\nexport function getTimeAxisTicks(args: GetTimeTicksOpts): ChartedText[] {\n  const {\n    minDate: lDate,\n    maxDate: rDate,\n    getX,\n    leftX,\n    rightX,\n    height,\n    measureText,\n    getScreenXY,\n    values,\n  } = args;\n\n  const age = getAge(+lDate, +rDate, true);\n  const color0 = getCssVariableValue(\"--color-ticks-0\");\n  const color1 = getCssVariableValue(\"--color-ticks-1\");\n\n  const getTickText = (args: { text: string } & Partial<ChartedText>) => {\n    return {\n      type: \"text\",\n      font: \"12px Arial\",\n      fillStyle: color0,\n      textAlign: \"center\",\n      ...args,\n    } as ChartedText;\n  };\n\n  const IDEAL_SPACING = 30;\n  const MIN_SPACING = 20;\n\n  const getLabelHHMM = (d: Date) =>\n    toDateStr(d, { hour: \"2-digit\", minute: \"2-digit\" });\n  const getStartMINUTES = (d: Date) =>\n    new Date(\n      d.getFullYear(),\n      d.getMonth(),\n      d.getDate(),\n      d.getHours(),\n      d.getMinutes(),\n    );\n\n  const incrementers: DateIncrementer[] = [\n    {\n      id: \"100 Year\",\n      getStart: (d: Date) =>\n        new Date(roundToNearest(d.getFullYear(), 100, 3000), 0, 1),\n      dateDelta: 100 * MONTH * 12,\n      getLabel: (d: Date) => toDateStr(d, { year: \"numeric\" }),\n    },\n    {\n      id: \"10 Year\",\n      getStart: (d: Date) =>\n        new Date(roundToNearest(d.getFullYear(), 10, 3000), 0, 1),\n      dateDelta: 10 * MONTH * 12,\n      getLabel: (d: Date) => toDateStr(d, { year: \"numeric\" }),\n    },\n    {\n      id: \"Yearly\",\n      getStart: (d: Date) => new Date(d.getFullYear(), 0, 1),\n      dateDelta: MONTH * 12,\n      getLabel: (d: Date) => toDateStr(d, { year: \"numeric\" }),\n    },\n    {\n      id: \"Quarter Yearly\",\n      getStart: (d: Date) => new Date(d.getFullYear(), 0, 1),\n      dateDelta: MONTH * 3,\n      getLabel: (d: Date) => toDateStr(d, { month: \"short\" }),\n    },\n    {\n      id: \"Monthly\",\n      getStart: (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1),\n      dateDelta: MONTH,\n      getLabel: (d: Date) => toDateStr(d, { month: \"short\" }),\n    },\n    {\n      id: \"Weekly\",\n      dateDelta: DAY * 7,\n      getStart: (d: Date) =>\n        new Date(d.getFullYear(), d.getMonth(), d.getDate()),\n      getLabel: (d: Date) => toDateStr(d, { day: \"numeric\" }),\n    },\n    {\n      id: \"Daily\",\n      dateDelta: DAY,\n      getStart: (d: Date) =>\n        new Date(d.getFullYear(), d.getMonth(), d.getDate()),\n      getLabel: (d: Date) => toDateStr(d, { day: \"numeric\" }),\n    },\n    ...[12, 6, 3, 2, 1].map((multiplier) => ({\n      id: multiplier + \" Hour\",\n      dateDelta: HOUR * multiplier,\n      getStart: (d: Date) =>\n        new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()),\n      getLabel: getLabelHHMM,\n    })),\n    ...[30, 20, 15, 10, 5, 2, 1].map((multiplier) => ({\n      id: multiplier + \" Minute\",\n      dateDelta: MINUTE * multiplier,\n      getStart: getStartMINUTES,\n      getLabel: getLabelHHMM,\n    })),\n    ...[30, 20, 15, 10, 5, 2, 1].map((multiplier) => ({\n      id: multiplier + \" Second\",\n      dateDelta: SECOND * multiplier,\n      getStart: (d: Date) => {\n        d = new Date(+d);\n        d.setSeconds(0);\n        return d;\n      },\n      getLabel: (d: Date) =>\n        toDateStr(d, { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" }),\n    })),\n    ...[500, 250, 100, 50, 25, 15, 10, 5, 2, 1].map((multiplier) => ({\n      id: multiplier + \" Millisecond\",\n      dateDelta: multiplier,\n      getStart: (d: Date) => {\n        d = new Date(+d);\n        const val = d.getMilliseconds();\n        d.setMilliseconds(roundToNearest(val, multiplier, 999));\n        return new Date(+d);\n      },\n      getLabel: (d: Date) =>\n        toDateStr(d, {\n          hour: \"2-digit\",\n          minute: \"2-digit\",\n          second: \"2-digit\",\n        }) +\n        \".\" +\n        d.toISOString().slice(20, -1),\n    })),\n  ];\n\n  const MIDTICK_STYLE: Pick<\n    ChartedText,\n    \"type\" | \"font\" | \"fillStyle\" | \"textAlign\"\n  > = {\n    type: \"text\",\n    font: \"12px Arial\",\n    fillStyle: color0,\n    textAlign: \"center\",\n  };\n\n  if (Number.isFinite(+lDate) && +lDate === +rDate) {\n    const row1 = toDateStr(lDate, {\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      second: \"2-digit\",\n    });\n    const row2 = dateAsYMD(lDate); // toDateStr(lDate, { year: \"numeric\", month: \"2-digit\", day: \"2-digit\" });\n    const t1: ChartedText = {\n      ...MIDTICK_STYLE,\n      font: \"16px Arial\",\n      id: \"t1\",\n      text: row1,\n      coords: [getX(+lDate), height - 28],\n    };\n    const t2: ChartedText = {\n      ...MIDTICK_STYLE,\n      font: \"600 14px Arial\",\n      id: \"t2\",\n      text: row2,\n      coords: [getX(+lDate), height - 10],\n    };\n\n    return [t1, t2];\n  }\n\n  let incs = incrementers.map((inc) => {\n    /* Get start point v, then two consecutive v1, v2 to ensure we have a full spacing between v1 and v2 */\n    const v = inc.getStart(new Date(+lDate)),\n      v1 = new Date(+v + inc.dateDelta),\n      v2 = new Date(+v1 + inc.dateDelta),\n      tickWidth = measureText(\n        getTickText({ text: inc.getLabel(new Date(2001, 1, 1)) }),\n      ).width,\n      x1 = getScreenXY(getX(+v1), 0)[0],\n      x2 = getScreenXY(getX(+v2), 0)[0],\n      spacing = x2 - x1 - tickWidth;\n\n    return {\n      inc,\n      spacing,\n      diffFromIdeal: Math.abs(spacing - IDEAL_SPACING),\n    };\n  });\n\n  incs = incs.filter((inc) => inc.spacing > MIN_SPACING);\n\n  if (!incs.length) return [];\n  const theChosenInc = incs.sort(\n    (a, b) => a.diffFromIdeal - b.diffFromIdeal,\n  )[0]!.inc;\n\n  let midTicks: { x: number; v: number; label: string }[] = [];\n  const getTickChartedText = (\n    tick: (typeof midTicks)[number],\n    i: number,\n  ): ChartedText & { xRange: [number, number] } => {\n    const ct: ChartedText = {\n      id: \"midTicks\" + i,\n      ...MIDTICK_STYLE,\n      text: tick.label,\n      coords: [tick.x, height - 10],\n    };\n\n    const x = getScreenXY(ct.coords[0], 0)[0];\n    const w = measureText(ct).width;\n\n    return {\n      ...ct,\n      xRange: [x - w / 2, x + w / 2],\n    };\n  };\n\n  const getProvidedTicks = (): undefined | typeof midTicks => {\n    const valueTicks = values?.map((date) => {\n      const v = +date;\n      return {\n        v,\n        x: getX(v),\n        label: theChosenInc.getLabel(date),\n      };\n    });\n\n    const chartedTicks = valueTicks?.map(getTickChartedText);\n\n    const overlapsOrDuplicates = chartedTicks?.slice(1, -1).some((t, i) => {\n      const prevT = chartedTicks[i - 1];\n      const nextT = chartedTicks[i + 1];\n      const ticks = [prevT, nextT].filter(isDefined);\n      return ticks.some((st) => {\n        const overlap =\n          t.xRange[0] < st.xRange[1] && t.xRange[1] > st.xRange[0];\n        return overlap || t.text === st.text;\n      });\n    });\n\n    return !overlapsOrDuplicates ? valueTicks : undefined;\n  };\n\n  const providedTicks = getProvidedTicks();\n  if (providedTicks) {\n    midTicks = providedTicks;\n  } else {\n    let d = theChosenInc.getStart(lDate);\n    do {\n      const v = +d;\n      midTicks.push({\n        v,\n        x: getX(v),\n        label: theChosenInc.getLabel(d),\n      });\n      d = new Date(+d + theChosenInc.dateDelta);\n    } while (+d < +rDate);\n  }\n\n  const y_top = height - 20,\n    y_bottom = height - 5;\n\n  /**\n   * Edge time tick labels\n   */\n  const lt: ChartedText = getTickText({\n      id: \"lt\",\n      textAlign: \"start\",\n      text: toDateStr(lDate, { month: \"short\" }),\n      coords: [leftX, y_top],\n    }),\n    lb: ChartedText = getTickText({\n      id: \"lb\",\n      textAlign: \"start\",\n      text: toDateStr(lDate, { year: \"numeric\" }),\n      font: \"600 14px Arial\",\n      coords: [leftX, y_bottom],\n    }),\n    rt: ChartedText = getTickText({\n      id: \"lt\",\n      textAlign: \"end\",\n      text: toDateStr(rDate, { month: \"short\" }),\n      coords: [rightX, y_top],\n    }),\n    rb: ChartedText = getTickText({\n      id: \"lb\",\n      textAlign: \"end\",\n      text: toDateStr(rDate, { year: \"numeric\" }),\n      font: \"600 14px Arial\",\n      coords: [rightX, y_bottom],\n    });\n\n  if (age.seconds < 10) {\n    lt.text =\n      toDateStr(lDate, {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        hourCycle: \"h23\",\n      }) +\n      \".\" +\n      lDate.getMilliseconds();\n    rt.text =\n      toDateStr(rDate, {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        hourCycle: \"h23\",\n      }) +\n      \".\" +\n      rDate.getMilliseconds();\n\n    lb.text = `${lDate.getDate()} ${toDateStr(lDate, { month: \"short\" })} ${lDate.getFullYear()}`;\n    rb.text = `${rDate.getDate()} ${toDateStr(rDate, { month: \"short\" })} ${rDate.getFullYear()}`;\n  } else if (age.days < 3) {\n    lt.text = `${lDate.getDate()} ${lt.text}`;\n    rt.text = `${rDate.getDate()} ${rt.text}`;\n  }\n\n  const leftTickMaxWidth =\n    Math.max(measureText(lt).width, measureText(lb).width) + 10;\n  const rightTickMaxWidth =\n    Math.max(measureText(rt).width, measureText(rb).width) + 10;\n  const leftTickMaxX = Math.max(getScreenXY(leftX, 0)[0] + leftTickMaxWidth);\n  const rightTickMinX = Math.max(getScreenXY(rightX, 0)[0] - rightTickMaxWidth);\n\n  return [\n    lt,\n    lb,\n    ...midTicks\n      .map(\n        (t, i) =>\n          ({\n            id: \"midTicks\" + i,\n            ...MIDTICK_STYLE,\n            text: t.label,\n            coords: [t.x, height - 10],\n          }) as ChartedText,\n      )\n      .filter((t) => {\n        /* Remove mid ticks that overlap with the end ticks */\n        const x = getScreenXY(t.coords[0], 0)[0],\n          w = measureText(t).width;\n        return x - w / 2 > leftTickMaxX && x + w / 2 < rightTickMinX;\n      }),\n    rt,\n    rb,\n  ];\n}\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/getTimechartBinSize.ts",
    "content": "import { DAY, HOUR, MILLISECOND, MINUTE, SECOND, YEAR } from \"../../Charts\";\nimport type { TimeChartBinSize } from \"../../W_TimeChart/W_TimeChartMenu\";\n\ntype Bin = {\n  name: Exclude<TimeChartBinSize, \"auto\">;\n  value: number;\n};\n\nconst hourIncrements = [8, 4, 2] as const;\nconst smIncrements = [30, 15, 5] as const;\nconst millisecondIncrements = [500, 250, 100, 10, 5] as const;\ntype SizePart = {\n  size: number;\n  increment: number;\n  unit:\n    | \"year\"\n    | \"month\"\n    | \"week\"\n    | \"day\"\n    | \"hour\"\n    | \"minute\"\n    | \"second\"\n    | \"millisecond\";\n};\n\nif (!HOUR) {\n  throw new Error(\"HOUR is not defined. Circular import error?!\");\n}\n\nexport const getMainTimeBinSizes = () =>\n  ({\n    year: { size: YEAR, increment: 1, unit: \"year\" },\n    month: { size: YEAR / 12, increment: 1, unit: \"month\" },\n    week: { size: DAY * 7, increment: 1, unit: \"week\" },\n    day: { size: DAY, increment: 1, unit: \"day\" },\n    ...(Object.fromEntries(\n      hourIncrements.map((val) => [\n        `${val}hour`,\n        { size: HOUR * val, increment: val, unit: \"hour\" },\n      ]),\n    ) as Record<`${(typeof hourIncrements)[number]}hour`, SizePart>),\n    hour: { size: HOUR, increment: 1, unit: \"hour\" },\n    ...(Object.fromEntries(\n      smIncrements.map((val) => [\n        `${val}minute`,\n        { size: MINUTE * val, increment: val, unit: \"minute\" },\n      ]),\n    ) as Record<`${(typeof smIncrements)[number]}minute`, SizePart>),\n    minute: { size: MINUTE, increment: 1, unit: \"minute\" },\n    ...(Object.fromEntries(\n      smIncrements.map((val) => [\n        `${val}second`,\n        { size: SECOND * val, increment: val, unit: \"second\" },\n      ]),\n    ) as Record<`${(typeof smIncrements)[number]}second`, SizePart>),\n    second: { size: SECOND, increment: 1, unit: \"second\" },\n    ...(Object.fromEntries(\n      millisecondIncrements.map((val) => [\n        `${val}millisecond`,\n        { size: MILLISECOND * val, increment: val, unit: \"millisecond\" },\n      ]),\n    ) as Record<\n      `${(typeof millisecondIncrements)[number]}millisecond`,\n      SizePart\n    >),\n    millisecond: { size: MILLISECOND, increment: 1, unit: \"millisecond\" },\n  }) as const satisfies Record<string, SizePart>;\n\nconst getBinDiffs = (extent: DateExtent) => {\n  const { minDate, maxDate } = extent;\n  /**\n   * Proportions of each unit\n   */\n\n  const millisDelta = +new Date(maxDate) - +new Date(minDate);\n  const MainTimeBinSizes = getMainTimeBinSizes();\n  return Object.fromEntries(\n    Object.entries(MainTimeBinSizes).map(([key, { size }]) => [\n      key,\n      {\n        binCount: millisDelta / size,\n        size,\n      },\n    ]),\n  ) as Record<\n    keyof typeof MainTimeBinSizes,\n    { binCount: number; size: number }\n  >;\n};\n\nexport type DateExtent = {\n  minDate: Date;\n  maxDate: Date;\n};\n\ntype GetTimechartBinSizeArgs = {\n  data: DateExtent;\n  viewPort: DateExtent | undefined;\n  bin_count: number;\n};\nexport const getTimechartBinSize = ({\n  data,\n  viewPort,\n  bin_count,\n}: GetTimechartBinSizeArgs): { key: Bin[\"name\"]; size: number } => {\n  const diffs = getBinDiffs(viewPort ?? data);\n  const diffSorted = Object.entries(diffs)\n    .map(([key, value]) => ({\n      key,\n      size: value.size,\n      binCount: value.binCount,\n      delta: value.binCount - bin_count,\n      absDelta: Math.abs(value.binCount - bin_count),\n    }))\n    .filter((d) => d.delta < 0)\n    .sort((a, b) => a.absDelta - b.absDelta);\n\n  const [firstBin] = diffSorted as any;\n  const MainTimeBinSizes = getMainTimeBinSizes();\n  return firstBin ?? { key: \"hour\" as const, size: MainTimeBinSizes.hour.size };\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/getTimechartTooltipIntersections.ts",
    "content": "import { getYOnMonotoneXCurve } from \"../drawMonotoneXCurve\";\nimport type { DataItem, TimeChart, TimeChartLayer } from \"./TimeChart\";\n\ntype IntersectedLayers = {\n  layers: (TimeChartLayer & { y: number; snapped_data?: DataItem })[];\n  snapped_x?: number;\n};\n\nexport const getTimechartTooltipIntersections = function (\n  this: TimeChart,\n  xCanvas: number,\n): IntersectedLayers | undefined {\n  let snappedX: number | undefined;\n\n  if (!this.chart) return undefined;\n\n  const [xPoint] = this.chart.getDataXY(xCanvas, 0);\n\n  const layers: IntersectedLayers[\"layers\"] = [];\n  const { renderStyle = \"line\" } = this.props;\n  const SNAP_DISTANCE =\n    this.lineLayers?.some((layer) => layer.coords.length < 3) ? 10 : 5;\n  this.lineLayers?.forEach((layer) => {\n    const getSmoothY =\n      renderStyle !== \"smooth\" && layer.variant !== \"smooth\" ?\n        undefined\n      : (xPoint: number) => {\n          return getYOnMonotoneXCurve(layer.coords, xPoint);\n        };\n    layer.coords.find((coord, i) => {\n      const x = coord[0];\n\n      /**\n       * Y value starts with 0 at the top\n       */\n      const y = coord[1];\n\n      if (layer.coords.length === 1 || i < layer.coords.length - 1) {\n        const nextPoint = layer.coords[i + 1];\n\n        if (!this.chart) return undefined;\n\n        const [xS] = this.chart.getScreenXY(x);\n        const xDist = Math.abs(xCanvas - xS);\n\n        /* Snapped point to line joints */\n        if (xDist <= SNAP_DISTANCE) {\n          snappedX = x;\n\n          layers.push({\n            ...layer,\n            y: getSmoothY?.(x) ?? y,\n            snapped_data: layer.data[i],\n          });\n          return true;\n        } else if (nextPoint !== undefined) {\n          const [xNext, yNext] = nextPoint;\n          const [xNS] = this.chart.getScreenXY(xNext);\n          const xNextDist = Math.abs(xCanvas - xNS);\n\n          if (xNextDist <= SNAP_DISTANCE) {\n            snappedX = xNext;\n            const layerSnapData = {\n              ...layer,\n              // y: yNext,\n              y: getSmoothY?.(xNext) ?? yNext,\n              snapped_data: layer.data[i + 1],\n            };\n            layers.push(layerSnapData);\n            return true;\n\n            /* Point along line */\n          } else if (\n            renderStyle === \"line\" &&\n            ((xPoint >= x && xPoint < xNext) || (xPoint >= xNext && xPoint < x))\n          ) {\n            const xDiff = xNext - x,\n              yDiff = yNext - y,\n              perc = (xPoint - x) / xDiff;\n            const layerIntersectionData = {\n              ...layer,\n              y: y + perc * yDiff,\n            };\n            layers.push(layerIntersectionData);\n            return true;\n          }\n        }\n      }\n      return false;\n    });\n  });\n\n  return { layers, snapped_x: snappedX };\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/getTimechartTooltipShapes.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\nimport type { Point } from \"../../Charts\";\nimport { DAY, HOUR, MINUTE, MONTH, SECOND, toDateStr } from \"../../Charts\";\nimport type { ChartedText, Circle, MultiLine } from \"../../Charts/CanvasChart\";\nimport type { TimeChart } from \"./TimeChart\";\n\nconst HoursMinutes: Intl.DateTimeFormatOptions = {\n  hour: \"2-digit\",\n  minute: \"2-digit\",\n};\nconst HoursMinutesSeconds: Intl.DateTimeFormatOptions = {\n  ...HoursMinutes,\n  second: \"2-digit\",\n};\n\nexport const getTimechartTooltipShapes = function (this: TimeChart) {\n  const { tooltipPosition = \"auto\", binSize } = this.props;\n  if (!this.chart || tooltipPosition === \"hidden\") return undefined;\n\n  const getCssVarValue = (name: string) => {\n    return getComputedStyle(window.document.documentElement).getPropertyValue(\n      name,\n    );\n  };\n  const { yMin, yMax } = this.getMargins();\n  const { h } = this.chart.getWH();\n  const { xCursor, yCursor } = this.d;\n  if (!xCursor || this.panning || (yCursor ?? 0) > yMax - 14) return undefined;\n  else {\n    let [x] = this.chart.getDataXY(xCursor, yCursor ?? 0);\n    const { layers = [], snapped_x } = this.getIntersections(xCursor) ?? {};\n    if (!layers.length || !this.data) {\n      //  && !(\"layers\" in delta)\n      return undefined;\n    }\n\n    if (snapped_x) {\n      x = snapped_x;\n    }\n\n    const tooltipDate = new Date(this.data.xScale.invert(x));\n    let tooltipBottomDateText: string;\n    // const { leftX, rightX } = this.chart.getExtent();\n    const getMinOffset = () => {\n      const minOffset = this.data?.dates.reduce(\n        (a, v, i, arr) => {\n          if (i) {\n            const diff = Math.abs(+v.date - +arr[i - 1]!.date);\n            a ??= diff;\n            a = Math.min(a, diff);\n          }\n          return a;\n        },\n        undefined as number | undefined,\n      );\n      return minOffset;\n    };\n    const dateDelta = binSize ?? getMinOffset() ?? HOUR; //(this.data.xScale.invert(rightX) - this.data.xScale.invert(leftX));\n\n    const dateText = [\n      tooltipDate.getFullYear(),\n      tooltipDate.getMonth() + 1,\n      tooltipDate.getDate(),\n    ].join(\"-\");\n\n    if (\n      dateDelta <= SECOND ||\n      (tooltipDate.getMilliseconds() !== 0 && snapped_x !== undefined)\n    ) {\n      tooltipBottomDateText =\n        toDateStr(tooltipDate, HoursMinutesSeconds) +\n        \".\" +\n        tooltipDate.getMilliseconds() +\n        \" \" +\n        dateText;\n    } else if (dateDelta <= MINUTE) {\n      tooltipBottomDateText =\n        toDateStr(tooltipDate, HoursMinutesSeconds) + \" \" + dateText;\n    } else if (dateDelta <= DAY) {\n      tooltipBottomDateText =\n        toDateStr(tooltipDate, HoursMinutes) + \" \" + dateText;\n    } else if (dateDelta <= MONTH) {\n      tooltipBottomDateText =\n        toDateStr(tooltipDate, {\n          ...HoursMinutes,\n        }) +\n        \" \" +\n        dateText;\n    } else {\n      tooltipBottomDateText = dateText;\n    }\n\n    const tooltipBottomDateLabel: ChartedText = {\n      id: \"tooltipBottomDateLabel\",\n      type: \"text\",\n      // font: \"20px \" + getComputedStyle(this.canv || document.body).fontFamily,\n      fillStyle: getCssVarValue(\"--text-0\"),\n      textAlign: \"center\",\n      text: tooltipBottomDateText,\n      background: {\n        fillStyle: getCssVarValue(\"--bg-color-1\"),\n        strokeStyle: getCssVarValue(\"--b-color\"),\n        lineWidth: 1,\n        padding: 6,\n        borderRadius: 3,\n      },\n      elevation: 8,\n      coords: [x, h - 10],\n    };\n\n    const btmW = this.chart.measureText(tooltipBottomDateLabel);\n    const { xMin, xMax } = this.getMargins();\n    if (xCursor - btmW.width / 2 < xMin + 5) {\n      tooltipBottomDateLabel.textAlign = \"start\";\n    } else if (xCursor + btmW.width / 2 > xMax - 5) {\n      tooltipBottomDateLabel.textAlign = \"end\";\n    }\n\n    let moveToLeft = false;\n    let minLabelY: number | undefined;\n    let maxLabelY: number | undefined;\n    const labelTickCanvasX = xCursor + 14;\n    let textLabels = layers\n      .map((l, layerIndex) => {\n        if (!this.data || l.snapped_data?.value === undefined) {\n          return undefined;\n        }\n        const yScale = this.data.getYScale(layerIndex);\n        const text =\n          l.getYLabel({\n            // value: +(l.snapped_data?.value ?? this.data.yScale.invert(l.y)), // Showing yScale.invert might not be useful and needs rounding/max length in some cases\n            value: l.snapped_data.value,\n            min: yScale.invert(yMin),\n            max: yScale.invert(yMax),\n\n            prev: yScale.invert(l.y),\n            next: yScale.invert(l.y),\n          }) + (this.props.layers.length > 1 && l.label ? ` ${l.label}` : \"\");\n\n        const coords: Point = [\n          this.chart!.getDataXY(labelTickCanvasX, 0)[0],\n          l.y + 5,\n        ];\n\n        minLabelY ??= coords[1];\n        maxLabelY ??= coords[1];\n        minLabelY = Math.min(minLabelY, coords[1]);\n        maxLabelY = Math.max(maxLabelY, coords[1]);\n\n        const res: ChartedText = {\n          id: \"tooltip-text-\" + Date.now(),\n          type: \"text\",\n          fillStyle: getCssVarValue(\"--text-0\"),\n          text,\n          textAlign: \"left\",\n          background: {\n            fillStyle: getCssVarValue(\"--bg-color-1\"),\n            strokeStyle: l.color,\n            lineWidth: 2,\n            padding: 6,\n            borderRadius: 3,\n          },\n          coords,\n        };\n\n        if (!moveToLeft) {\n          const labelWidth = this.chart!.measureText(res);\n          moveToLeft = labelTickCanvasX + labelWidth.width > xMax - 5;\n        }\n\n        return res;\n      })\n      .filter(isDefined);\n\n    // const closerToTop = isDefined(minLabelY) && isDefined(maxLabelY) && minLabelY < h - maxLabelY;\n\n    /** Highest value on top */\n    textLabels = textLabels.toSorted((a, b) => a.coords[1] - b.coords[1]);\n\n    const labelHeight = 28;\n    const freeHeight = yMax - Math.min(yMax, textLabels.length * labelHeight);\n\n    /* Adjust text y */\n    textLabels = textLabels.map((l, i) => {\n      const x = l.coords[0];\n      const offset =\n        tooltipPosition === \"top\" ? 0\n        : tooltipPosition === \"bottom\" ? freeHeight\n        : tooltipPosition === \"middle\" ? freeHeight * 0.5\n        : undefined;\n      let y =\n        offset !== undefined ?\n          offset + labelHeight * 0.75 + i * labelHeight\n        : l.coords[1];\n      if (i && offset === undefined) {\n        const prevY = textLabels[i - 1]!.coords[1];\n        y = Math.max(y, prevY + labelHeight);\n        // if(closerToTop){\n        // } else {\n        //   y = Math.min(y, prevY - labelHeight);\n        // }\n      }\n\n      return {\n        ...l,\n        coords: [x, y],\n        ...(!moveToLeft ?\n          {}\n        : {\n            coords: [this.chart!.getDataXY(labelTickCanvasX - 28, 0)[0], y],\n            textAlign: \"end\",\n          }),\n      };\n    });\n\n    if (tooltipPosition === \"auto\") {\n      const lowestOnTop = textLabels.slice(0).reverse();\n      textLabels = [];\n      lowestOnTop.forEach((l, i) => {\n        const y = l.coords[1];\n        if (!i) {\n          l.coords[1] = Math.min(y, yMax);\n        } else {\n          const prevY = textLabels.at(-1)!.coords[1];\n          l.coords[1] = Math.min(prevY - labelHeight, y);\n        }\n        textLabels.push(l);\n      });\n    }\n\n    const pointCircles: Circle[] = layers.flatMap((l) => {\n      const commonOpts: Pick<Circle, \"id\" | \"type\" | \"coords\" | \"lineWidth\"> = {\n        id: `tooltip-${Date.now()}`,\n        type: \"circle\",\n        coords: [x, l.y],\n        lineWidth: 0,\n      };\n\n      return [\n        {\n          ...commonOpts,\n          fillStyle: \"white\",\n          strokeStyle: \"white\",\n          r: 5,\n        },\n        {\n          ...commonOpts,\n          coords: [x, l.y],\n          fillStyle: l.color,\n          strokeStyle: l.color,\n          r: 3,\n        },\n      ];\n    });\n\n    const tooltipVertLine: MultiLine = {\n      id: \"tooltipVertLine\",\n      type: \"multiline\",\n      lineWidth: 1,\n      strokeStyle: \"#cecece\",\n      coords: [\n        [x, 0],\n        [x, h - 30],\n      ],\n    };\n\n    const shapes = [\n      tooltipVertLine,\n      ...pointCircles,\n      ...textLabels,\n      tooltipBottomDateLabel,\n    ];\n\n    return {\n      shapes,\n      snapped_x,\n    };\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/measureText.ts",
    "content": "import type { ChartedText, TextMeasurement } from \"./../CanvasChart\";\n\nconst measureTextCache: Map<string, TextMeasurement> = new Map();\nexport const measureText = (\n  s: ChartedText,\n  ctx: CanvasRenderingContext2D,\n  /**\n   * TODO: disable caching because it produces bad results\n   */\n  useCache = true,\n): TextMeasurement => {\n  const { textAlign = \"\", text = \"\", font = \"\" } = s;\n  const key = [textAlign, text.length, font].join(\";\");\n  const cached = measureTextCache.get(key);\n  if (cached && useCache) {\n    return cached;\n  }\n  ctx.fillStyle = s.fillStyle;\n  ctx.textAlign = s.textAlign ?? ctx.textAlign;\n  ctx.font = s.font || ctx.font;\n  const metrics = ctx.measureText(s.text);\n\n  const fontHeight =\n    metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;\n  const actualHeight =\n    metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;\n  const result = {\n    width: metrics.width,\n    fontHeight,\n    actualHeight,\n  };\n  measureTextCache.set(key, result);\n  return result;\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/onDeltaTimechart.ts",
    "content": "import { CanvasChart } from \"../CanvasChart\";\nimport type { DeepPartial } from \"../../RTComp\";\nimport type { TimeChart, TimeChartD, TimeChartProps } from \"./TimeChart\";\nimport { onRenderTimechart } from \"./onRenderTimechart\";\n\nexport const onDeltaTimechart = function (\n  this: TimeChart,\n  dp?: Partial<TimeChartProps>,\n  ds?: never,\n  dd?: DeepPartial<TimeChartD>,\n) {\n  const delta = { ...dp, ...dd };\n  /** Chart cannot be reinstantiated because old handlers are not destroyed */\n  if (this.ref && this.canv && !this.chart) {\n    const { onExtentChanged, chartRef, zoomPanDisabled } = this.props;\n\n    this.chart = new CanvasChart({\n      node: this.ref,\n      canvas: this.canv,\n      yScaleLocked: true,\n      yPanLocked: true,\n      minXScale: 1,\n      onResize: () => {\n        this.parseData();\n        this.onDelta({ layers: this.props.layers });\n      },\n      events:\n        zoomPanDisabled ?\n          { disabled: true }\n        : {\n            onExtentChange: () => {\n              this.lastExtentChange = Date.now();\n            },\n            onExtentChanged: (extent) => {\n              if (onExtentChanged && this.data && this.chart) {\n                const resetExtent =\n                  [extent.xScale, extent.yScale].every((v) => v < 1.0001) &&\n                  [extent.leftX, extent.topY].every(\n                    (v) => Math.abs(v) < 0.0000001,\n                  );\n                const viewPortExtent = this.getVisibleExtent();\n                if (viewPortExtent) {\n                  const visibleData = {\n                    minDate: new Date(this.data.xScale.invert(extent.leftX)),\n                    maxDate: new Date(this.data.xScale.invert(extent.rightX)),\n                  };\n                  onExtentChanged(visibleData, viewPortExtent, { resetExtent });\n                }\n              }\n              this.setData({ extent });\n            },\n            onPan: () => {\n              this.panning = true;\n            },\n            onPanEnd: () => {\n              this.panning = false;\n            },\n          },\n    });\n\n    chartRef?.(this);\n  }\n\n  onRenderTimechart.bind(this)(delta, dd);\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/onRenderTimechart.ts",
    "content": "import { isDefined, pickKeys } from \"prostgles-types\";\nimport { nFormatter } from \"../../../utils/utils\";\nimport type { Point } from \"../../Charts\";\nimport type {\n  ChartedText,\n  Circle,\n  GetShapes,\n  MultiLine,\n  Rectangle,\n} from \"../../Charts/CanvasChart\";\nimport type { DeepPartial } from \"../../RTComp\";\nimport type {\n  DataItem,\n  TimeChart,\n  TimeChartD,\n  TimeChartProps,\n} from \"./TimeChart\";\nimport { getTimeAxisTicks } from \"./getTimeAxisTicks\";\nimport { getTimechartTooltipShapes } from \"./getTimechartTooltipShapes\";\nimport { getBinValueLabels } from \"./getBinValueLabels\";\n\nexport function onRenderTimechart(\n  this: TimeChart,\n  delta: Partial<TimeChartProps & TimeChartD>,\n  dd: DeepPartial<TimeChartD> | undefined,\n) {\n  if (!this.chart) return;\n\n  let reRender = false;\n  const { h } = this.chart.getWH();\n  const { yMax, xMax, xMin } = this.getMargins();\n  let lineTooShort = false;\n\n  if (delta.layers) {\n    reRender = true;\n\n    this.mainShapes = [];\n    this.lineLayers = [];\n\n    this.parseData();\n\n    if (!this.data) {\n      return;\n    }\n    const { layers } = this.data;\n    const { renderStyle = \"line\", binValueLabelMaxDecimals } = this.props;\n    layers.map((layer, layerIndex) => {\n      const { sortedParsedData: _data } = layer;\n      const firstDataItem = _data?.[0];\n      if (firstDataItem) {\n        const circlesXY = _data\n          .map((c, idx) => {\n            const [screenX, screenY]: Point = this.chart!.getScreenXY(c.x, c.y);\n            return {\n              ...c,\n              idx,\n              screenX,\n              screenY,\n            };\n          })\n          .sort((a, b) => a.screenX - b.screenX);\n\n        const displayedCircles = circlesXY.slice(0);\n\n        const circles: Circle<DataItem>[] = displayedCircles.map((d, i) => ({\n          id: i,\n          type: \"circle\",\n          coords: [d.x, d.y],\n          fillStyle: layer.color,\n          strokeStyle: layer.color,\n          lineWidth: 0,\n          r: 3,\n          data: d,\n        }));\n\n        const line: MultiLine = {\n          id: 1,\n          type: \"multiline\",\n          variant: renderStyle === \"smooth\" ? \"smooth\" : layer.variant,\n          strokeStyle: layer.color,\n          lineWidth: 2,\n          withGradient: this.props.showGradient,\n          coords: circles.map((c) => c.coords),\n          data: circles.map((c) => c.data),\n        };\n\n        const lines = [line];\n\n        this.lineLayers?.push({\n          ...layer,\n          ...pickKeys(line, [\"coords\", \"data\"]),\n        });\n\n        let lineLength = 0;\n        line.coords.map((c, i) => {\n          if (i) {\n            lineLength += Math.hypot(\n              c[0] - line.coords[i - 1]![0],\n              c[1] - line.coords[i - 1]![1],\n            );\n          }\n        });\n\n        lineTooShort = lineLength < 4;\n        const showCircles =\n          renderStyle === \"scatter plot\" ||\n          (renderStyle === \"line\" && lineTooShort);\n\n        const chartWidth = xMax - xMin;\n        const numberOfBars = circles.length & layers.length;\n        const spacingBetweenBars = 1;\n        let barWidth =\n          chartWidth / numberOfBars - numberOfBars * spacingBetweenBars;\n        const xScale = this.data?.xScale.copy().clamp(false);\n        // TODO: Fix bars too wide at high zoom\n        const [leftDomain, rightDomain] = xScale?.domain() ?? [];\n        if (\n          this.props.binSize &&\n          xScale &&\n          isDefined(leftDomain) &&\n          isDefined(rightDomain)\n        ) {\n          const actualBinSize = this.props.binSize;\n          barWidth =\n            (xScale(leftDomain + actualBinSize) - xScale(leftDomain)) /\n            layers.length;\n        }\n        const barWidthRounded = 0.9 * barWidth;\n\n        const showBinLabels =\n          this.props.showBinLabels && this.props.showBinLabels !== \"off\" ?\n            this.props.showBinLabels\n          : undefined;\n\n        const getBars = () => {\n          const halfGroupWidth = (barWidthRounded * layers.length) / 2;\n          const bars = circles.flatMap((c) => {\n            /** Must ensure the tooltip appears on bar center */\n            const x =\n              c.coords[0] - halfGroupWidth + barWidthRounded * layerIndex;\n            const r: Rectangle = {\n              ...c,\n              coords: [x, c.coords[1]],\n              type: \"rectangle\",\n              w: barWidthRounded,\n              h: yMax - c.coords[1],\n            };\n\n            return r;\n          });\n          return bars;\n        };\n\n        const labels = getBinValueLabels({\n          circles,\n          showCircles,\n          binValueLabelMaxDecimals,\n          renderStyle,\n          showBinLabels,\n        });\n        const finalShapes =\n          renderStyle === \"bars\" ? getBars()\n          : showCircles ? circles\n          : lines;\n\n        this.mainShapes = this.mainShapes.concat([...finalShapes, ...labels]);\n      }\n    });\n  }\n\n  if (dd || \"layers\" in delta) {\n    reRender = true;\n    this.tooltips = getTimechartTooltipShapes.bind(this)();\n  }\n\n  if (reRender) {\n    if (!this.data) return;\n\n    const getShapes: GetShapes = () => {\n      if (!this.data || !this.chart) {\n        throw \"No data\";\n      }\n      const { xMin, xMax, xForYLabels } = this.getMargins();\n      let [leftX] = this.chart.getDataXY(xMin, 0);\n      let [rightX] = this.chart.getDataXY(xMax, 0);\n      const [xForYTicks] = this.chart.getDataXY(xForYLabels, 0);\n\n      const {\n        minDate,\n        maxDate,\n        dates,\n        xScale,\n        minVal,\n        maxVal,\n        layers,\n        getYScale,\n      } = this.data;\n\n      if (!dates.length) return [];\n\n      /** Ensure edge ticks are not outside data extent */\n      if (xScale(minDate) > leftX) {\n        leftX = xScale(minDate);\n      }\n      if (xScale(maxDate) < rightX) {\n        rightX = xScale(maxDate);\n      }\n\n      const timeAxisTicks =\n        this.props.showXAxis === false ?\n          []\n        : getTimeAxisTicks({\n            minDate: new Date(xScale.invert(leftX)),\n            maxDate: new Date(xScale.invert(rightX)),\n            leftX,\n            rightX,\n            getX: xScale.copy().clamp(false),\n            height: h,\n            values: layers\n              .flatMap((l) => l.sortedParsedData?.map((d) => new Date(d.date)))\n              .filter(isDefined),\n            measureText: this.chart.measureText,\n            getScreenXY: this.chart.getScreenXY,\n          });\n\n      const compactYAxis =\n        this.props.yAxisVariant === \"compact\" ||\n        (this.canv?.height && this.canv.height < 200);\n      const yTickValues =\n        compactYAxis ?\n          [minVal, maxVal]\n        : getYTickValues({ min: minVal, max: maxVal, steps: 6 });\n      const cannotShorten =\n        compactYAxis ? false : (\n          new Set(yTickValues.map((v) => nFormatter(v, 1))).size <\n          yTickValues.length\n        );\n      const yTicks: ChartedText[] = [];\n      const yPercTicks: ChartedText[] = [];\n      const { yAxisScaleMode = \"multiple\" } = this.props;\n      const [xForPercYTicks] = this.chart.getDataXY(xMax, 0);\n      yTickValues.forEach((v, i) => {\n        if (yAxisScaleMode === \"multiple\" && layers.length > 1) {\n          return;\n        }\n        const yScale = getYScale(0);\n        const yTick: ChartedText = {\n          id: `y${i}`,\n          text: `${cannotShorten ? v.toLocaleString() : nFormatter(v, 1)}`,\n          type: \"text\",\n          coords: [xForYTicks, Math.round(yScale(v))],\n          textBaseline: \"middle\",\n          /** Alternate top and bottom ticks for better comprehension */\n          fillStyle:\n            compactYAxis && !i ?\n              getCssVariableValue(\"--color-ticks-1\")\n            : getCssVariableValue(\"--color-ticks-0\"),\n          // background: {\n          //   fillStyle: \"var(--color-ticks-1)\",\n          // },\n          font: \"14px Arial \",\n        };\n        const yTicksHeight = 14;\n        const lastTick = yTicks.at(-1);\n        if (\n          !lastTick ||\n          lastTick.coords[1] - yTick.coords[1] > yTicksHeight * 1.2\n        ) {\n          yTicks.push(yTick);\n        }\n        /** TODO: add percentage bar */\n        // const lastPercTick = yPercTicks.at(-1);\n        // if (\n        //   !lastPercTick ||\n        //   lastPercTick.coords[1] - yTick.coords[1] > yTicksHeight * 1.2\n        // ) {\n        //   const yPerc = ((v - minVal) / (maxVal - minVal)) * 100;\n        //   yPercTicks.push({\n        //     ...yTick,\n        //     text: `${yPerc.toFixed(0)}%`,\n        //     textAlign: \"right\",\n        //     coords: [xForPercYTicks, Math.round(yScale(v))],\n        //   });\n        // }\n      });\n\n      return [...yTicks, ...timeAxisTicks, ...yPercTicks];\n    };\n    this.chart.render([\n      ...this.mainShapes,\n      getShapes,\n      ...(this.tooltips?.shapes ?? []),\n    ]);\n  }\n}\n\nfunction getYTickValues(args: {\n  min: number;\n  max: number;\n  steps: number;\n}): number[] {\n  const { min, max, steps } = args;\n  let res: number[] = [];\n\n  /** 1. Get nicest val in range */\n  const nicestVal = getNicestVal(min, max);\n\n  /** 2. Get nicest step size */\n  const stepSize = (max - min) / steps;\n  const stepDelta = (1 / steps) * stepSize;\n  const nicestStep = getNicestVal(stepSize - stepDelta, stepSize + stepDelta);\n\n  res = [nicestVal];\n  while (res[0]! - nicestStep > min) {\n    res = [res[0]! - nicestStep, ...res];\n  }\n  while (res[res.length - 1]! + nicestStep < max) {\n    res = [...res, res[res.length - 1]! + nicestStep];\n  }\n\n  return res;\n}\n\nfunction getNicestVal(min: number, max: number): number {\n  /** 1. Get nicest val in range */\n\n  const nicestValStr = max + \"\";\n  let nicestVal = max;\n\n  const roundVal = (val: string): number | undefined => {\n    const nv0 = Number(val.split(\"\").concat(\"0\").join(\"\"));\n    const nv5 = Number(val.split(\"\").concat(\"5\").join(\"\"));\n    const preLastDigit = Number(val.split(\"\").slice(0, -1).pop());\n    const nv0Next = Number(\n      val\n        .split(\"\")\n        .slice(0, -1)\n        .concat([`${preLastDigit - 1}`])\n        .join(\"\") + \"0\",\n    );\n\n    return [nv0, nv5, nv0Next].find(\n      (v) => Number.isFinite(v) && v >= min && v <= max,\n    );\n  };\n\n  for (let i = nicestValStr.length; i >= 0; i--) {\n    const curVal = nicestValStr.slice(0, i);\n    const val = roundVal(curVal);\n    if (val !== undefined) {\n      nicestVal = val;\n    }\n  }\n\n  return nicestVal;\n}\n\nexport const getCssVariableValue = (\n  varName: string,\n  node: HTMLElement = document.body,\n) => {\n  return getComputedStyle(node).getPropertyValue(varName);\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/TimeChart/prepareTimechartData.ts",
    "content": "import { scaleLinear } from \"d3\";\nimport type { TimeChart, TimeChartLayer } from \"./TimeChart\";\nimport { getCssVariableValue } from \"./onRenderTimechart\";\n\nexport const prepareTimechartData = function (this: TimeChart) {\n  const renderMessage = (text: string) => {\n    this.chart?.render([\n      {\n        text,\n        coords: [this.ref!.offsetWidth / 2, this.ref!.offsetHeight / 2],\n        id: \"2111r\",\n        type: \"text\",\n        fillStyle: getCssVariableValue(\"--text-1\"),\n        textAlign: \"center\",\n        textBaseline: \"middle\",\n        font: \"18px arial\",\n        background: {\n          fillStyle: getCssVariableValue(\"--bg-color-0\"),\n          lineWidth: 1,\n          padding: 6,\n          borderRadius: 3,\n        },\n      },\n    ]);\n  };\n\n  const { w } = this.chart!.getWH();\n  const { layers, yAxisScaleMode = \"multiple\" } = this.props;\n  if (!this.ref) {\n    return;\n  }\n  if (!layers.length) {\n    this.data = undefined;\n    renderMessage(\"No data\");\n    return;\n  }\n  let minDate = null as number | null;\n  let maxDate = null as number | null;\n  let minVal = null as number | null;\n  let maxVal = null as number | null;\n\n  const dates: { x: number; v: number; date: Date }[] = [];\n\n  const { xMin, xMax, yMin, yMax, xForYLabels } = this.getMargins();\n  let tcLayers: TimeChartLayer[] = layers.map((l) => {\n    const maxDataCount = 1e4;\n    const data = l.data.slice(0, maxDataCount);\n    if (l.data.length > data.length) {\n      console.error(\"Too much data. slicing to \" + maxDataCount);\n    }\n\n    let layerMinVal: number | undefined;\n    let layerMaxVal: number | undefined;\n\n    minDate ??= +l.fullExtent[0];\n    maxDate ??= +l.fullExtent[1];\n    minDate = Math.min(minDate, +l.fullExtent[0]);\n    maxDate = Math.max(maxDate, +l.fullExtent[1]);\n\n    const sortedParsedData = data\n      .map((d) => {\n        const date = +new Date(d.date);\n        const value = +d.value;\n        if (isNaN(date)) {\n          console.warn(\"timechart date is NaN\");\n        }\n        return {\n          ...d,\n          date,\n          value,\n          x: 0,\n          y: 0,\n        };\n      })\n      .sort((a, b) => +a.date - +b.date);\n\n    const filledData = sortedParsedData; //.slice(0, 0);\n    // if(binSize){\n    //   sortedParsedData.forEach((d, i)=> {\n\n    //     let lastItem = filledData.at(-1);\n    //     if(!lastItem){\n    //       filledData.push(d);\n    //     } else {\n    //       do {\n    //         filledData.push({\n    //           date: lastItem!.date + binSize,\n    //           value: 0,\n    //           x: 0,\n    //           y: 0,\n    //         });\n    //         lastItem = filledData.at(-1);\n    //       } while(d.date - lastItem!.date > binSize);\n    //       filledData.push(d);\n    //     }\n    //   })\n    // }\n\n    filledData.forEach((d, i) => {\n      ((minVal ??= d.value), (maxVal ??= d.value));\n      minVal = Math.min(minVal, d.value);\n      maxVal = Math.max(maxVal, d.value);\n\n      layerMinVal ??= d.value;\n      layerMaxVal ??= d.value;\n      layerMinVal = Math.min(layerMinVal, d.value);\n      layerMaxVal = Math.max(layerMaxVal, d.value);\n    });\n\n    return {\n      ...l,\n      sortedParsedData: filledData,\n      minVal: layerMinVal,\n      maxVal: layerMaxVal,\n    };\n  });\n\n  if (\n    minDate === null ||\n    maxDate === null ||\n    maxVal === null ||\n    minVal === null\n  ) {\n    renderMessage(\"No Data\");\n\n    return;\n  }\n\n  const xScale = scaleLinear()\n    .domain([minDate, maxDate])\n    .range([xMin, xMax])\n    .clamp(false);\n  const xScaleYLabels = scaleLinear()\n    .domain([minDate, maxDate])\n    .range([xForYLabels, xMax])\n    .clamp(true);\n  const getYScale = ({ maxVal, minVal }: { maxVal: number; minVal: number }) =>\n    scaleLinear().domain([maxVal, minVal]).range([yMin, yMax]).clamp(false);\n\n  tcLayers = tcLayers.map((l) => {\n    l.sortedParsedData = l.sortedParsedData?.map((d) => {\n      const x = xScale(d.date);\n      if (!dates.find((d) => d.v === +d.date)) {\n        dates.push({ x, v: d.date, date: new Date(d.date) });\n      }\n      const yScale =\n        yAxisScaleMode === \"single\" ?\n          getYScale({ maxVal: maxVal!, minVal: minVal! })\n        : getYScale({ maxVal: l.maxVal!, minVal: l.minVal! });\n      return {\n        ...d,\n        x,\n        y: yScale(d.value),\n      };\n    });\n    return l;\n  });\n\n  if (this.chart?.opts) {\n    const deltaSecs = (+maxDate - +minDate) / 1000;\n\n    /* Max zoom as the width of a second in pixels */\n    const MAX_PX_PER_SECOND = 70;\n    this.chart.opts.maxXScale = MAX_PX_PER_SECOND / (w / deltaSecs);\n  }\n\n  this.data = {\n    layers: tcLayers,\n    dates,\n    xScale,\n    getYScale: (layerIndex: number) => {\n      if (yAxisScaleMode === \"multiple\") {\n        return getYScale({\n          maxVal: tcLayers[layerIndex]!.maxVal!,\n          minVal: tcLayers[layerIndex]!.minVal!,\n        });\n      }\n      return getYScale({ maxVal: maxVal!, minVal: minVal! });\n    },\n    xScaleYLabels,\n\n    minDate,\n    maxDate,\n    minVal,\n    maxVal,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/createHiPPICanvas.ts",
    "content": "/** Used in making canvas less blurry on mobile */\nexport const createHiPPICanvas = (\n  canvas: HTMLCanvasElement,\n  _w: number,\n  _h: number,\n) => {\n  const ratio = window.devicePixelRatio;\n\n  const w = Math.max(30, _w);\n  const h = Math.max(30, _h);\n  const width = w * ratio;\n  const height = h * ratio;\n  canvas.width = width;\n  canvas.height = height;\n  canvas.style.width = w + \"px\";\n  canvas.style.height = h + \"px\";\n  const overflowThatWillNotCauseScrollbars = [\"hidden\", \"visible\", \"clip\"];\n  const parentStyle = getComputedStyle(canvas.parentElement!);\n  if (\n    canvas.isConnected &&\n    !overflowThatWillNotCauseScrollbars.includes(parentStyle.overflow)\n  ) {\n    throw new Error(\n      `Canvas parent should have overflow:${overflowThatWillNotCauseScrollbars.join(\" | \")} to prevent resize-render recursion due to scrollbars`,\n    );\n  }\n  if (ratio > 1) {\n    canvas.getContext(\"2d\")?.scale(ratio, ratio);\n  }\n  return { cv: canvas, width, height };\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/drawMonotoneXCurve.ts",
    "content": "import type { Point } from \"../Charts\";\n\nexport function drawMonotoneXCurve(\n  ctx: CanvasRenderingContext2D,\n  line: Point[],\n  skipFirstMove?: boolean,\n) {\n  if (line.length < 2) return;\n\n  // Move to first point\n  if (!skipFirstMove) {\n    ctx.moveTo(line[0]![0], line[0]![1]);\n  }\n\n  if (line.length === 2) {\n    ctx.lineTo(line[1]![0], line[1]![1]);\n    return;\n  }\n\n  // Draw curves through all intermediate points\n  for (let i = 0; i < line.length - 1; i++) {\n    const point = line[i]!;\n    const nextPoint = line[i + 1]!;\n\n    const xc = (point[0] + nextPoint[0]) / 2;\n    const yc = (point[1] + nextPoint[1]) / 2;\n\n    ctx.quadraticCurveTo(point[0], point[1], xc, yc);\n  }\n\n  // Complete the curve to the last point\n  const lastPoint = line[line.length - 1]!;\n  ctx.lineTo(lastPoint[0], lastPoint[1]);\n}\n\n/**\n * Get the Y coordinate on a monotone X curve at a given X position\n * This matches the curve drawn by drawMonotoneXCurve\n */\nexport const getYOnMonotoneXCurve = (\n  line: Point[],\n  targetX: number,\n): number | undefined => {\n  if (line.length === 0) return undefined;\n  if (line.length === 1) {\n    return line[0]![0] === targetX ? line[0]![1] : undefined;\n  }\n\n  // Handle the first segment (quadratic curve from first point to first midpoint)\n  const firstPoint = line[0]!;\n  const secondPoint = line[1]!;\n  const firstMidX = (firstPoint[0] + secondPoint[0]) / 2;\n  const firstMidY = (firstPoint[1] + secondPoint[1]) / 2;\n\n  const minX = Math.min(firstPoint[0], firstMidX);\n  const maxX = Math.max(firstPoint[0], firstMidX);\n\n  if (targetX >= minX && targetX <= maxX) {\n    // This is a quadratic Bézier from firstPoint to firstMid with control at firstPoint\n    // Start: firstPoint, Control: firstPoint, End: firstMid\n    // Since start and control are the same, this degenerates to a line, BUT\n    // let's match the actual curve drawn\n\n    // Actually from drawMonotoneXCurve: ctx.quadraticCurveTo(point[0], point[1], xc, yc);\n    // where point is firstPoint and (xc, yc) is the midpoint\n    // This means: from current position (firstPoint) to (xc, yc) with control at (point[0], point[1])\n    // So: Start = firstPoint, Control = firstPoint, End = firstMid\n\n    // Find t using Newton-Raphson\n    let t = 0.5;\n    for (let iter = 0; iter < 20; iter++) {\n      const oneMinusT = 1 - t;\n      const x =\n        oneMinusT * oneMinusT * firstPoint[0] +\n        2 * oneMinusT * t * firstPoint[0] +\n        t * t * firstMidX;\n\n      const dx =\n        -2 * oneMinusT * firstPoint[0] +\n        2 * (oneMinusT - t) * firstPoint[0] +\n        2 * t * firstMidX;\n\n      if (Math.abs(dx) < 1e-10) break;\n\n      const error = x - targetX;\n      if (Math.abs(error) < 0.001) break;\n\n      t = t - error / dx;\n      t = Math.max(0, Math.min(1, t));\n    }\n\n    const oneMinusT = 1 - t;\n    const y =\n      oneMinusT * oneMinusT * firstPoint[1] +\n      2 * oneMinusT * t * firstPoint[1] +\n      t * t * firstMidY;\n\n    return y;\n  }\n\n  // Handle the curved segments (from i=1 to line.length-3)\n  for (let i = 1; i < line.length - 2; i++) {\n    const prevPoint = line[i - 1]!;\n    const point = line[i]!;\n    const nextPoint = line[i + 1]!;\n\n    const startX = (prevPoint[0] + point[0]) / 2;\n    const startY = (prevPoint[1] + point[1]) / 2;\n\n    const endX = (point[0] + nextPoint[0]) / 2;\n    const endY = (point[1] + nextPoint[1]) / 2;\n\n    const controlX = point[0];\n    const controlY = point[1];\n\n    const minX = Math.min(startX, endX);\n    const maxX = Math.max(startX, endX);\n\n    if (targetX >= minX && targetX <= maxX) {\n      let t = 0.5;\n\n      for (let iter = 0; iter < 20; iter++) {\n        const oneMinusT = 1 - t;\n        const x =\n          oneMinusT * oneMinusT * startX +\n          2 * oneMinusT * t * controlX +\n          t * t * endX;\n\n        const dx =\n          -2 * oneMinusT * startX +\n          2 * (oneMinusT - t) * controlX +\n          2 * t * endX;\n\n        if (Math.abs(dx) < 1e-10) break;\n\n        const error = x - targetX;\n        if (Math.abs(error) < 0.001) break;\n\n        t = t - error / dx;\n        t = Math.max(0, Math.min(1, t));\n      }\n\n      const oneMinusT = 1 - t;\n      const y =\n        oneMinusT * oneMinusT * startY +\n        2 * oneMinusT * t * controlY +\n        t * t * endY;\n\n      return y;\n    }\n  }\n\n  // Handle the last segment (straight line from last midpoint to last point)\n  if (line.length >= 2) {\n    const secondLastPoint = line[line.length - 2]!;\n    const lastPoint = line[line.length - 1]!;\n\n    const lastMidX = (secondLastPoint[0] + lastPoint[0]) / 2;\n    const lastMidY = (secondLastPoint[1] + lastPoint[1]) / 2;\n\n    if (\n      (targetX >= lastMidX && targetX <= lastPoint[0]) ||\n      (targetX <= lastMidX && targetX >= lastPoint[0])\n    ) {\n      const t = (targetX - lastMidX) / (lastPoint[0] - lastMidX);\n      return lastMidY + t * (lastPoint[1] - lastMidY);\n    }\n  }\n\n  return undefined;\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/drawShapes/drawLinkLine.ts",
    "content": "import type { LinkLine, Rectangle } from \"../CanvasChart\";\nimport { type ShapeV2 } from \"./drawShapes\";\n\nexport const getCtx = (canvas: HTMLCanvasElement) => {\n  return canvas.getContext(\"2d\");\n};\n\n/**\n * How much horizontal offset for control points (adjust for more/less curve)\n *  */\nconst controlPointFactor = 0.4;\n\nexport const drawLinkLine = (\n  shapes: (ShapeV2 | LinkLine)[],\n  ctx: CanvasRenderingContext2D,\n  linkLine: LinkLine,\n) => {\n  const { sourceId, targetId, sourceYOffset, targetYOffset } = linkLine;\n  const r1 = shapes.find(\n    (r): r is Rectangle => r.type === \"rectangle\" && r.id === sourceId,\n  );\n  const r2 = shapes.find(\n    (r): r is Rectangle => r.type === \"rectangle\" && r.id === targetId,\n  );\n  if (!r1 || !r2) return;\n  const [x1, y1] = r1.coords;\n  const [x2, y2] = r2.coords;\n\n  const startP = [x1 + r1.w, y1 + sourceYOffset] as const;\n  const endP = [x2, y2 + targetYOffset] as const;\n  const startPoint = {\n    x: startP[0],\n    y: startP[1],\n  };\n  const endPoint = {\n    x: endP[0],\n    y: endP[1],\n  };\n\n  const dx = endPoint.x - startPoint.x;\n  const horizontalOffset = Math.abs(dx) * controlPointFactor;\n  const controlPoint1 = { x: startPoint.x + horizontalOffset, y: startPoint.y };\n  const controlPoint2 = { x: endPoint.x - horizontalOffset, y: endPoint.y };\n\n  ctx.beginPath();\n  ctx.moveTo(startPoint.x, startPoint.y);\n  ctx.bezierCurveTo(\n    controlPoint1.x,\n    controlPoint1.y,\n    controlPoint2.x,\n    controlPoint2.y,\n    endPoint.x,\n    endPoint.y,\n  );\n  ctx.stroke();\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/drawShapes/drawShapes.ts",
    "content": "import type { Point } from \"../../Charts\";\nimport type { Image, LinkLine, Shape } from \"../CanvasChart\";\nimport { drawMonotoneXCurve } from \"../drawMonotoneXCurve\";\nimport { measureText } from \"../TimeChart/measureText\";\nimport { roundRect } from \"../roundRect\";\nimport { drawLinkLine } from \"./drawLinkLine\";\n// import { drawLinkLine } from \"./shortestLinkLineV2\";\nexport type ShapeV2<T = void> = Shape<T> | LinkLine<T> | Image<T>;\nconst getWH = (canvas: HTMLCanvasElement) => {\n  return {\n    w: canvas.offsetWidth,\n    h: canvas.offsetHeight,\n  };\n};\nexport const getCtx = (canvas: HTMLCanvasElement) => {\n  return canvas.getContext(\"2d\");\n};\n\nexport const drawShapes = (\n  shapes: ShapeV2<any>[],\n  canvas: HTMLCanvasElement,\n  opts?: {\n    scale?: number;\n    translate?: { x: number; y: number };\n    isChild?: boolean;\n  },\n) => {\n  const { w, h } = getWH(canvas);\n  const ctx = getCtx(canvas);\n  if (!ctx) return;\n\n  if (!opts?.isChild) {\n    ctx.clearRect(0, 0, w, h);\n  }\n\n  if (opts?.scale || opts?.translate) {\n    const { scale, translate: position } = opts;\n    ctx.save();\n    if (position) {\n      ctx.translate(position.x, position.y);\n    }\n    if (scale) {\n      ctx.scale(scale, scale);\n    }\n  }\n\n  shapes.forEach((s) => {\n    if (\"fillStyle\" in s) {\n      ctx.fillStyle = s.fillStyle;\n    }\n    if (\"strokeStyle\" in s) {\n      ctx.strokeStyle = s.strokeStyle;\n    }\n    if (\"lineWidth\" in s) {\n      ctx.lineWidth = s.lineWidth;\n    }\n\n    ctx.globalAlpha = s.opacity ?? 1;\n    ctx.lineJoin = \"bevel\";\n    if (s.type === \"image\") {\n      ctx.drawImage(s.image, ...s.coords, s.w, s.h);\n    } else if (s.type === \"linkline\") {\n      drawLinkLine(shapes, ctx, s);\n    } else if (s.type === \"rectangle\") {\n      const {\n        coords: [x1, y1],\n      } = s;\n      const x2 = s.w + x1;\n      const w = x2 - x1;\n      if (s.borderRadius) {\n        ctx.lineJoin = \"round\";\n        roundRect(ctx, x1, y1, w, s.h, s.borderRadius);\n      } else {\n        ctx.beginPath();\n        ctx.rect(x1, y1, w, s.h);\n      }\n      ctx.fill();\n      ctx.stroke();\n      if (s.children?.length) {\n        drawShapes(\n          s.children.map((cs) => {\n            /** Child shape coords start from parent rect x,y */\n            if (cs.type === \"multiline\" || cs.type === \"polygon\") {\n              return {\n                ...cs,\n                coords: cs.coords.map(\n                  ([x, y]) => [x + x1, y + y1] satisfies Point,\n                ),\n              };\n            }\n            return {\n              ...cs,\n              opacity: (cs.opacity ?? 1) * (s.opacity ?? 1),\n              coords: [cs.coords[0] + x1, cs.coords[1] + y1],\n            };\n          }),\n          canvas,\n          {\n            isChild: true,\n          },\n        );\n      }\n    } else if (s.type === \"circle\") {\n      const {\n        coords: [x1, y1],\n      } = s;\n      ctx.beginPath();\n      ctx.arc(x1, y1, s.r, 0, 2 * Math.PI);\n      ctx.fill();\n      ctx.stroke();\n    } else if (s.type === \"multiline\") {\n      const { coords } = s;\n      ctx.lineCap = \"round\";\n\n      if (s.variant === \"smooth\" && coords.length > 2) {\n        ctx.beginPath();\n        drawMonotoneXCurve(ctx, coords);\n        ctx.stroke();\n      } else {\n        coords.forEach(([x, y], i) => {\n          if (!i) {\n            ctx.beginPath();\n            ctx.moveTo(x, y);\n          } else {\n            ctx.lineTo(x, y);\n          }\n        });\n        ctx.stroke();\n      }\n    } else if (s.type === \"polygon\") {\n      const { coords } = s;\n      ctx.beginPath();\n      coords.map(([x, y], i) => {\n        if (!i) {\n          ctx.beginPath();\n          ctx.moveTo(x, y);\n        } else {\n          ctx.lineTo(x, y);\n        }\n      });\n      ctx.closePath();\n      ctx.stroke();\n      ctx.fill();\n    } else if ((s.type as any) === \"text\") {\n      const { textAlign = \"start\", coords } = s;\n\n      if (s.background) {\n        const txtSize = measureText(s, ctx, opts?.isChild ? false : true);\n        const txtPadding = s.background.padding || 6;\n        ctx.fillStyle = s.background.fillStyle ?? ctx.fillStyle;\n        if (s.background.strokeStyle) {\n          ctx.strokeStyle = s.background.strokeStyle;\n        }\n        ctx.lineWidth = s.background.lineWidth ?? ctx.lineWidth;\n        let x = coords[0] - txtSize.width / 2 - txtPadding;\n        const y =\n          coords[1] -\n          (allLowerCase(s.text) ? 1 : 1) * txtSize.actualHeight -\n          txtPadding;\n\n        if ([\"left\", \"start\"].includes(textAlign)) {\n          x = coords[0] - txtPadding;\n        } else if ([\"right\", \"end\"].includes(textAlign)) {\n          x = coords[0] - txtSize.width - txtPadding;\n        }\n\n        roundRect(\n          ctx,\n          x,\n          y,\n          txtSize.width + 2 * txtPadding,\n          txtSize.actualHeight + 2 * txtPadding,\n          s.background.borderRadius || 0,\n        );\n        ctx.closePath();\n        if (s.background.strokeStyle) {\n          ctx.stroke();\n        }\n        ctx.fill();\n      }\n\n      ctx.fillStyle = s.fillStyle;\n      ctx.textAlign = textAlign;\n      ctx.font = s.font || ctx.font;\n      ctx.fillText(s.text, coords[0], coords[1]);\n      // s.text.split(\"\\n\").map(text => {\n      //   ctx.fillText(text, coords[0], coords[1]);\n      // })\n    } else throw \"Unexpected shape type: \" + (s as any).type;\n  });\n  if (opts?.scale || opts?.translate) {\n    ctx.restore();\n  }\n};\n\n/** Big lower case text appears lower than needed */\nexport function allLowerCase(str) {\n  return !!(str && str.toLowerCase() === str);\n}\n"
  },
  {
    "path": "client/src/dashboard/Charts/drawShapes/drawShapesOnSVG.ts",
    "content": "import { toFixed } from \"src/app/domToSVG/utils/toFixed\";\nimport { asRGB } from \"src/utils/colorUtils\";\nimport { hashCode } from \"src/utils/hashCode\";\nimport type { SVGContext } from \"../../../app/domToSVG/containers/elementToSVG\";\nimport { addImageFromDataURL } from \"../../../app/domToSVG/graphics/imgToSVG\";\nimport type { Point } from \"../../Charts\";\nimport type { LinkLine, Rectangle } from \"../CanvasChart\";\nimport { DEFAULT_SHADOW } from \"../roundRect\";\nimport type { ShapeV2 } from \"./drawShapes\";\nimport { getTimechartGradientPeakSections } from \"./getTimechartGradientPeakSections\";\n\nexport const drawShapesOnSVG = (\n  shapes: ShapeV2<any>[],\n  context: SVGContext,\n  g: SVGGElement,\n  opts:\n    | undefined\n    | {\n        scale?: number;\n        translate?: { x: number; y: number };\n        isChild?: boolean;\n      },\n  {\n    width,\n    height,\n  }: {\n    width: number;\n    height: number;\n  },\n) => {\n  let transform = \"\";\n  if (opts?.translate) {\n    transform += `translate(${opts.translate.x}, ${opts.translate.y})`;\n  }\n  if (opts?.scale) {\n    transform += ` scale(${opts.scale})`;\n  }\n  g.setAttribute(\"transform\", transform);\n\n  shapes.forEach((s) => {\n    const opacity = s.opacity !== undefined ? s.opacity : 1;\n\n    if (s.type === \"image\") {\n      const [x, y] = s.coords;\n      const localCanvas = document.createElement(\"canvas\");\n      localCanvas.width = s.w;\n      localCanvas.height = s.h;\n      const ctx = localCanvas.getContext(\"2d\");\n      if (!ctx) throw new Error(\"Failed to get canvas context\");\n      ctx.canvas.width = s.w;\n      ctx.canvas.height = s.h;\n      ctx.drawImage(s.image, 0, 0, s.w, s.h);\n      const dataURL = localCanvas.toDataURL();\n      addImageFromDataURL(g, dataURL, context, {\n        style: {} as CSSStyleDeclaration,\n        height: s.h,\n        width: s.w,\n        x,\n        y,\n      });\n    } else if (s.type === \"linkline\") {\n      drawSvgLinkLine(shapes, g as SVGElement, s);\n    } else if (s.type === \"rectangle\") {\n      const [x, y] = s.coords.map((v) => toFixed(v)) as typeof s.coords;\n      const width = toFixed(s.w);\n      const height = toFixed(s.h);\n\n      const rectElement = createRoundedRect(\n        x,\n        y,\n        width,\n        height,\n        s.borderRadius,\n      );\n\n      if (s.elevation !== 0) {\n        rectElement.setAttribute(\n          \"filter\",\n          `drop-shadow(${DEFAULT_SHADOW.offsetX}px ${DEFAULT_SHADOW.offsetY}px ${DEFAULT_SHADOW.blur} ${DEFAULT_SHADOW.color})`,\n        );\n      }\n\n      if (s.fillStyle) {\n        rectElement.setAttribute(\"fill\", s.fillStyle);\n      }\n      if (s.strokeStyle) {\n        rectElement.setAttribute(\"stroke\", s.strokeStyle);\n      }\n      if (s.lineWidth) {\n        rectElement.setAttribute(\"stroke-width\", s.lineWidth.toString());\n      }\n      rectElement.setAttribute(\"opacity\", opacity.toString());\n      rectElement.setAttribute(\"stroke-linejoin\", \"bevel\");\n\n      g.appendChild(rectElement);\n\n      // Handle children\n      if (s.children?.length) {\n        const childGroup = createSvgElement(\"g\", {\n          transform: `translate(${x}, ${y})`,\n        });\n\n        drawShapesOnSVG(\n          s.children.map((cs) => {\n            if (cs.type === \"multiline\" || cs.type === \"polygon\") {\n              return {\n                ...cs,\n                coords: cs.coords.map(([cx, cy]) => [cx, cy] as Point),\n              };\n            }\n            return {\n              ...cs,\n              opacity: (cs.opacity ?? 1) * opacity,\n              coords: [cs.coords[0] + x, cs.coords[1] + y],\n            };\n          }),\n          context,\n          childGroup,\n          { isChild: true },\n          { width, height },\n        );\n\n        g.appendChild(childGroup);\n      }\n    } else if (s.type === \"circle\") {\n      const [cx, cy] = s.coords;\n      const circleElement = createSvgElement(\"circle\", {\n        cx: toFixed(cx),\n        cy: toFixed(cy),\n        r: toFixed(s.r),\n      });\n\n      if (s.fillStyle) {\n        circleElement.setAttribute(\"fill\", s.fillStyle);\n      }\n      if (s.strokeStyle) {\n        circleElement.setAttribute(\"stroke\", s.strokeStyle);\n      }\n      if (s.lineWidth) {\n        circleElement.setAttribute(\"stroke-width\", s.lineWidth.toString());\n      }\n      circleElement.setAttribute(\"opacity\", opacity.toString());\n\n      g.appendChild(circleElement);\n    } else if (s.type === \"multiline\") {\n      if (s.withGradient && s.coords.length > 2) {\n        const { peakSections, minY, stops } = getTimechartGradientPeakSections(\n          s.coords,\n        );\n        peakSections.forEach((sectionCoords) => {\n          if (sectionCoords.length < 2) return;\n\n          const uniqueShapeIdentifier = hashCode(\n            sectionCoords\n              .map((s) => {\n                return [Math.round(s.x), Math.round(s.y)];\n              })\n              .join(\"\") + s.strokeStyle,\n          );\n          const gradientId = `gradient-${uniqueShapeIdentifier}`;\n          const defsElement = context.defs;\n\n          const gradientElement = createSvgElement(\"linearGradient\");\n          gradientElement.setAttribute(\"id\", gradientId);\n          gradientElement.setAttribute(\"x1\", \"0\");\n          gradientElement.setAttribute(\"y1\", toFixed(minY));\n          gradientElement.setAttribute(\"x2\", \"0\");\n          gradientElement.setAttribute(\"y2\", height);\n          gradientElement.setAttribute(\"gradientUnits\", \"userSpaceOnUse\");\n\n          const rgba = asRGB(s.strokeStyle);\n          const rgb = rgba.slice(0, 3).join(\", \");\n\n          stops.forEach(({ offset, opacity: stopOpacity }) => {\n            const stopElement = createSvgElement(\"stop\");\n            stopElement.setAttribute(\"offset\", offset);\n            stopElement.setAttribute(\"stop-color\", `rgb(${rgb})`);\n            stopElement.setAttribute(\"stop-opacity\", stopOpacity);\n            gradientElement.appendChild(stopElement);\n          });\n\n          defsElement.appendChild(gradientElement);\n\n          // Create gradient path\n          const firstPoint = sectionCoords[0]!;\n          const lastPoint = sectionCoords.at(-1)!;\n\n          let pathData = `M ${toFixed(firstPoint.x)},${height}`;\n          pathData += ` L ${toFixed(firstPoint.x)},${toFixed(firstPoint.y)}`;\n\n          if (s.variant === \"smooth\" && sectionCoords.length > 2) {\n            pathData +=\n              \" \" +\n              drawSvgMonotoneXCurve(\n                sectionCoords.map(({ x, y }) => [x, y]),\n                // true,\n              ).substring(2); // Remove the initial \"M x,y\"\n          } else {\n            sectionCoords.forEach(({ x, y }) => {\n              pathData += ` L ${toFixed(x)},${toFixed(y)}`;\n            });\n          }\n\n          pathData += ` L ${toFixed(lastPoint.x)},${height}`;\n          pathData += \" Z\";\n\n          const gradientPath = createSvgElement(\"path\");\n          gradientPath.setAttribute(\"d\", pathData);\n          gradientPath.setAttribute(\"fill\", `url(#${gradientId})`);\n          gradientPath.setAttribute(\"opacity\", opacity.toString());\n\n          g.appendChild(gradientPath);\n        });\n      }\n\n      // Draw the main line\n      const pathElement = createSvgElement(\"path\");\n\n      if (s.variant === \"smooth\" && s.coords.length > 2) {\n        pathElement.setAttribute(\"d\", drawSvgMonotoneXCurve(s.coords));\n      } else {\n        let pathData = \"\";\n        s.coords.forEach((point, i) => {\n          const [x, y] = point.map((v) => toFixed(v)) as typeof point;\n          if (i === 0) {\n            pathData = `M ${x},${y}`;\n          } else {\n            pathData += ` L ${x},${y}`;\n          }\n        });\n        pathElement.setAttribute(\"d\", pathData);\n      }\n\n      pathElement.setAttribute(\"fill\", \"none\");\n      if (s.strokeStyle) {\n        pathElement.setAttribute(\"stroke\", s.strokeStyle);\n      }\n      if (s.lineWidth) {\n        pathElement.setAttribute(\"stroke-width\", s.lineWidth.toString());\n      }\n      pathElement.setAttribute(\"opacity\", opacity.toString());\n      pathElement.setAttribute(\"stroke-linecap\", \"round\");\n\n      g.appendChild(pathElement);\n    } else if (s.type === \"polygon\") {\n      const pathElement = createSvgElement(\"path\");\n\n      let pathData = \"\";\n      s.coords.forEach((point, i) => {\n        const [x, y] = point.map((v) => toFixed(v)) as typeof point;\n        if (i === 0) {\n          pathData = `M ${x},${y}`;\n        } else {\n          pathData += ` L ${x},${y}`;\n        }\n      });\n      pathData += \" Z\"; // Close the path\n\n      pathElement.setAttribute(\"d\", pathData);\n\n      if (s.fillStyle) {\n        pathElement.setAttribute(\"fill\", s.fillStyle);\n      }\n      if (s.strokeStyle) {\n        pathElement.setAttribute(\"stroke\", s.strokeStyle);\n      }\n      if (s.lineWidth) {\n        pathElement.setAttribute(\"stroke-width\", s.lineWidth.toString());\n      }\n      pathElement.setAttribute(\"opacity\", opacity.toString());\n\n      g.appendChild(pathElement);\n    } else if ((s.type as unknown) === \"text\") {\n      const [x, y] = s.coords;\n      const textElement = createSvgElement(\"text\", {\n        x,\n        y,\n        \"text-anchor\":\n          s.textAlign === \"center\" ? \"middle\"\n          : s.textAlign === \"right\" || s.textAlign === \"end\" ? \"end\"\n          : \"start\",\n      });\n\n      if (s.font) {\n        textElement.style.font = s.font;\n      }\n      if (s.fillStyle) {\n        textElement.setAttribute(\"fill\", s.fillStyle);\n      }\n      textElement.setAttribute(\"opacity\", opacity.toString());\n      textElement.textContent = s.text;\n\n      // Handle background if present\n      if (s.background) {\n        const txtSize = measureSvgText(s.text, s.font || \"\");\n        const txtPadding = 2; //s.background.padding || 0;\n\n        let bgX = x - txtSize.width / 2 - txtPadding;\n        const bgY = y - txtSize.actualHeight;\n\n        if ([\"left\", \"start\"].includes(s.textAlign || \"\")) {\n          bgX = x - txtPadding;\n        } else if ([\"right\", \"end\"].includes(s.textAlign || \"\")) {\n          bgX = x - txtSize.width - txtPadding;\n        }\n\n        const bgWidth = txtSize.width + 2 * txtPadding;\n        const bgHeight = txtSize.actualHeight + 2 * txtPadding;\n        const radius = s.background.borderRadius || 0;\n\n        // const rectBg = createSvgElement(\"path\");\n        // rectBg.setAttribute(\n        //   \"d\",\n        //   createRoundedRect(bgX, bgY, bgWidth, bgHeight, radius),\n        // );\n        const rectBg = createRoundedRect(bgX, bgY, bgWidth, bgHeight, radius);\n\n        if (s.background.fillStyle) {\n          rectBg.setAttribute(\"fill\", s.background.fillStyle);\n        }\n        if (s.background.strokeStyle) {\n          rectBg.setAttribute(\"stroke\", s.background.strokeStyle);\n        }\n        if (s.background.lineWidth) {\n          rectBg.setAttribute(\n            \"stroke-width\",\n            s.background.lineWidth.toString(),\n          );\n        }\n        rectBg.setAttribute(\"opacity\", opacity.toString());\n\n        g.appendChild(rectBg);\n      }\n\n      g.appendChild(textElement);\n    } else {\n      console.error(\"Unexpected shape type:\", (s as any).type);\n    }\n  });\n};\n\n/**\n * How much horizontal offset for control points (adjust for more/less curve)\n */\nconst controlPointFactor = 0.4;\n\nexport const drawSvgLinkLine = (\n  shapes: (ShapeV2 | LinkLine)[],\n  svg: SVGElement,\n  linkLine: LinkLine,\n) => {\n  const { sourceId, targetId, sourceYOffset, targetYOffset } = linkLine;\n  const r1 = shapes.find(\n    (r): r is Rectangle => r.type === \"rectangle\" && r.id === sourceId,\n  );\n  const r2 = shapes.find(\n    (r): r is Rectangle => r.type === \"rectangle\" && r.id === targetId,\n  );\n  if (!r1 || !r2) return;\n  const [x1, y1] = r1.coords;\n  const [x2, y2] = r2.coords;\n\n  const startP = [x1 + r1.w, y1 + sourceYOffset] as const;\n  const endP = [x2, y2 + targetYOffset] as const;\n  const startPoint = {\n    x: startP[0],\n    y: startP[1],\n  };\n  const endPoint = {\n    x: endP[0],\n    y: endP[1],\n  };\n\n  const dx = endPoint.x - startPoint.x;\n  const horizontalOffset = Math.abs(dx) * controlPointFactor;\n  const controlPoint1 = { x: startPoint.x + horizontalOffset, y: startPoint.y };\n  const controlPoint2 = { x: endPoint.x - horizontalOffset, y: endPoint.y };\n\n  const path = document.createElementNS(\"http://www.w3.org/2000/svg\", \"path\");\n  path.setAttribute(\n    \"d\",\n    [\n      `M ${startPoint.x},${startPoint.y}`,\n      `C ${controlPoint1.x},${controlPoint1.y}`,\n      `${controlPoint2.x},${controlPoint2.y}`,\n      `${endPoint.x},${endPoint.y}`,\n    ].join(\" \"),\n  );\n\n  if (linkLine.strokeStyle) {\n    path.setAttribute(\"stroke\", linkLine.strokeStyle);\n  }\n  if (linkLine.lineWidth) {\n    path.setAttribute(\"stroke-width\", linkLine.lineWidth.toString());\n  }\n  path.setAttribute(\"fill\", \"none\");\n  if (linkLine.opacity !== undefined) {\n    path.setAttribute(\"opacity\", linkLine.opacity.toString());\n  }\n\n  svg.appendChild(path);\n};\n\n// Helper function to create SVG elements\nconst createSvgElement = <T extends keyof SVGElementTagNameMap>(\n  tagName: T,\n  attrs: Record<string, string | number> = {},\n) => {\n  const element = document.createElementNS<T>(\n    \"http://www.w3.org/2000/svg\",\n    tagName,\n  );\n  Object.entries(attrs).forEach(([key, value]) => {\n    element.setAttribute(key, value);\n  });\n  return element;\n};\n\n// Helper for measuring text in SVG\nexport const measureSvgText = (text: string, font: string) => {\n  const svg = document.createElementNS(\"http://www.w3.org/2000/svg\", \"svg\");\n  svg.style.position = \"absolute\";\n  svg.style.visibility = \"hidden\";\n  document.body.appendChild(svg);\n\n  const textElement = document.createElementNS(\n    \"http://www.w3.org/2000/svg\",\n    \"text\",\n  );\n  textElement.setAttribute(\"font\", font);\n  textElement.textContent = text;\n  svg.appendChild(textElement);\n\n  const bbox = textElement.getBBox();\n  document.body.removeChild(svg);\n\n  return {\n    width: bbox.width,\n    height: bbox.height,\n    actualHeight: bbox.height,\n  };\n};\n\n// Helper for rounded rectangles in SVG\nexport const createRoundedRect = (\n  x: number,\n  y: number,\n  width: number,\n  height: number,\n  radius: number | undefined,\n) => {\n  const rect = createSvgElement(\"rect\", {\n    x: toFixed(x),\n    y: toFixed(y),\n    width: toFixed(width),\n    height: toFixed(height),\n    ...(radius && {\n      rx: toFixed(radius),\n      ry: toFixed(radius),\n    }),\n  });\n  return rect;\n};\n\nexport const drawSvgMonotoneXCurve = (coords: Point[]) => {\n  if (coords.length < 2) return \"\";\n\n  let path = `M ${coords[0]![0]},${coords[0]![1]}`;\n\n  if (coords.length === 2) {\n    path += ` L ${coords[1]![0]},${coords[1]![1]}`;\n    return path;\n  }\n\n  // Draw curves through all intermediate points\n  for (let i = 0; i < coords.length - 1; i++) {\n    const point = coords[i]!;\n    const nextPoint = coords[i + 1]!;\n\n    const xc = (point[0] + nextPoint[0]) / 2;\n    const yc = (point[1] + nextPoint[1]) / 2;\n\n    path += ` Q ${point[0]},${point[1]} ${xc},${yc}`;\n  }\n\n  // Complete the curve to the last point\n  const lastPoint = coords[coords.length - 1]!;\n  path += ` L ${lastPoint[0]},${lastPoint[1]}`;\n\n  return path;\n};\n\n/** Big lower case text appears lower than needed */\nexport function allLowerCase(str: string) {\n  return !!(str && str.toLowerCase() === str);\n}\n"
  },
  {
    "path": "client/src/dashboard/Charts/drawShapes/findShortestPathAroundRectangles.ts",
    "content": "// --- Type Definitions (Implicit via JS objects) ---\ntype Point = { x: number; y: number };\ntype Rectangle = {\n  id: string | number;\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n};\n\nconst cachedPaths = new Map<string, Point[]>();\n// --- Shortest Path Function ---\nexport function findShortestPathAroundRectangles(\n  startPoint: Point,\n  endPoint: Point,\n  rectangles: Rectangle[],\n  padding: number,\n) {\n  const cacheKey = `${startPoint.x},${startPoint.y}-${endPoint.x},${endPoint.y}`;\n  console.log(cacheKey);\n  const cachedPath = cachedPaths.get(cacheKey);\n  if (cachedPath) {\n    return cachedPath;\n  }\n\n  // 1. Define all potential nodes for the graph\n  const nodes = [startPoint, endPoint];\n  const nodeMap = new Map<string, number>(); // To easily check if a point is already added\n  nodeMap.set(`${startPoint.x},${startPoint.y}`, 0);\n  nodeMap.set(`${endPoint.x},${endPoint.y}`, 1);\n  let nodeIndex = 2;\n\n  rectangles.forEach((rect) => {\n    const corners = [\n      { x: rect.x - padding, y: rect.y - padding }, // Top-left padded\n      { x: rect.x + rect.width + padding, y: rect.y - padding }, // Top-right padded\n      { x: rect.x + rect.width + padding, y: rect.y + rect.height + padding }, // Bottom-right padded\n      { x: rect.x - padding, y: rect.y + rect.height + padding }, // Bottom-left padded\n    ];\n    corners.forEach((corner) => {\n      const key = `${corner.x},${corner.y}`;\n      if (!nodeMap.has(key)) {\n        nodes.push(corner);\n        nodeMap.set(key, nodeIndex++);\n      }\n    });\n  });\n\n  const numNodes = nodes.length;\n  const adj: { node: number; weight: number }[][] = Array(numNodes)\n    .fill(null)\n    .map(() => []);\n\n  // 2. Build the Visibility Graph (Edges)\n  for (let i = 0; i < numNodes; i++) {\n    for (let j = i + 1; j < numNodes; j++) {\n      // Check if the segment between nodes[i] and nodes[j] is clear\n      if (\n        !isSegmentIntersectingRectangles(\n          nodes[i]!,\n          nodes[j]!,\n          rectangles,\n          padding / 2,\n        )\n      ) {\n        // Use smaller padding for check? Or 0? Let's try padding/2 for robustness\n        const dist = distance(nodes[i]!, nodes[j]!);\n        adj[i]!.push({ node: j, weight: dist });\n        adj[j]!.push({ node: i, weight: dist });\n      }\n    }\n  }\n\n  // 3. Run Dijkstra's Algorithm\n  const dist = Array(numNodes).fill(Infinity);\n  const prev = Array(numNodes).fill(null);\n  const pq = new MinPriorityQueue<number>(); // Simple Priority Queue implementation\n\n  dist[0] = 0; // Distance from start (node 0) to itself is 0\n  pq.enqueue(0, 0); // Add start node with priority 0\n\n  while (!pq.isEmpty()) {\n    const { element: u, priority: d } = pq.dequeue();\n\n    if (d > dist[u]) continue; // Skip if we found a shorter path already\n    if (u === 1) break; // Reached the end node (node 1)\n\n    for (const edge of adj[u]!) {\n      const v = edge.node;\n      const weight = edge.weight;\n      if (dist[u] + weight < dist[v]) {\n        dist[v] = dist[u] + weight;\n        prev[v] = u;\n        pq.enqueue(v, dist[v]);\n      }\n    }\n  }\n\n  // 4. Reconstruct the path\n  const path: Point[] = [];\n  let current: number | null = 1; // Start from the end node index\n  if (prev[current] !== null || current === 0) {\n    // Check if end node is reachable\n    while (current !== null) {\n      path.push(nodes[current]!);\n      current = prev[current];\n    }\n    path.reverse(); // Reverse to get start -> end order\n  }\n\n  // If no path found via corners, check direct path last\n  if (path.length === 0 || path[path.length - 1] !== endPoint) {\n    if (\n      !isSegmentIntersectingRectangles(\n        startPoint,\n        endPoint,\n        rectangles,\n        padding,\n      )\n    ) {\n      return [startPoint, endPoint]; // Direct path is possible\n    } else {\n      // If Dijkstra failed and direct path blocked, return empty\n      // This might happen if start/end are inside obstacles, though padding should prevent this mostly\n      return [];\n    }\n  }\n  cachedPaths.set(cacheKey, path);\n  return path; // Return the reconstructed path\n}\n\n// --- Min Priority Queue Implementation (Simple) ---\nclass MinPriorityQueue<T> {\n  elements: { element: T; priority: number }[] = [];\n  enqueue(element: T, priority: number) {\n    this.elements.push({ element, priority });\n    this.elements.sort((a, b) => a.priority - b.priority); // Simple sort, inefficient for large graphs\n  }\n  dequeue() {\n    return this.elements.shift()!;\n  }\n  isEmpty() {\n    return this.elements.length === 0;\n  }\n}\n\n// --- Geometry Helper Functions ---\n\nfunction distance(p1: Point, p2: Point) {\n  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));\n}\n\n// Check orientation of ordered triplet (p, q, r)\n// 0 --> p, q and r are collinear\n// 1 --> Clockwise\n// 2 --> Counterclockwise\nfunction getOrientation(p: Point, q: Point, r: Point) {\n  const val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);\n  if (Math.abs(val) < 1e-9) return 0; // Collinear (within tolerance)\n  return val > 0 ? 1 : 2; // Clockwise or Counterclockwise\n}\n\n// Check if point q lies on segment pr\nfunction onSegment(p, q, r) {\n  return (\n    q.x <= Math.max(p.x, r.x) &&\n    q.x >= Math.min(p.x, r.x) &&\n    q.y <= Math.max(p.y, r.y) &&\n    q.y >= Math.min(p.y, r.y)\n  );\n}\n\n// Check if line segment 'p1q1' and 'p2q2' intersect.\nfunction doSegmentsIntersect(p1, q1, p2, q2) {\n  const o1 = getOrientation(p1, q1, p2);\n  const o2 = getOrientation(p1, q1, q2);\n  const o3 = getOrientation(p2, q2, p1);\n  const o4 = getOrientation(p2, q2, q1);\n\n  // General case\n  if (o1 !== o2 && o3 !== o4) {\n    return true;\n  }\n\n  // Special Cases for collinearity\n  // p1, q1 and p2 are collinear and p2 lies on segment p1q1\n  if (o1 === 0 && onSegment(p1, p2, q1)) return true;\n  // p1, q1 and q2 are collinear and q2 lies on segment p1q1\n  if (o2 === 0 && onSegment(p1, q2, q1)) return true;\n  // p2, q2 and p1 are collinear and p1 lies on segment p2q2\n  if (o3 === 0 && onSegment(p2, p1, q2)) return true;\n  // p2, q2 and q1 are collinear and q1 lies on segment p2q2\n  if (o4 === 0 && onSegment(p2, q1, q2)) return true;\n\n  return false; // Doesn't intersect\n}\n\n// Check if a line segment intersects a rectangle (considering padding)\nfunction isSegmentIntersectingRect(p1, p2, rect, padding) {\n  const rx = rect.x - padding;\n  const ry = rect.y - padding;\n  const rw = rect.width + 2 * padding;\n  const rh = rect.height + 2 * padding;\n\n  // Rectangle corners\n  const topLeft = { x: rx, y: ry };\n  const topRight = { x: rx + rw, y: ry };\n  const bottomLeft = { x: rx, y: ry + rh };\n  const bottomRight = { x: rx + rw, y: ry + rh };\n\n  // Check intersection with rectangle sides\n  if (doSegmentsIntersect(p1, p2, topLeft, topRight)) return true;\n  if (doSegmentsIntersect(p1, p2, topRight, bottomRight)) return true;\n  if (doSegmentsIntersect(p1, p2, bottomRight, bottomLeft)) return true;\n  if (doSegmentsIntersect(p1, p2, bottomLeft, topLeft)) return true;\n\n  // Optional: Check if segment is fully inside (though covered by intersection usually)\n  // This is more complex and often unnecessary if segment intersection is robust\n  // For simplicity, we rely on edge intersection checks.\n\n  return false;\n}\n\n// Check if a segment intersects ANY of the rectangles\nfunction isSegmentIntersectingRectangles(\n  p1: Point,\n  p2: Point,\n  rectangles: Rectangle[],\n  padding: number,\n) {\n  // Ignore check if endpoints are identical\n  if (Math.abs(p1.x - p2.x) < 1e-9 && Math.abs(p1.y - p2.y) < 1e-9) {\n    return false;\n  }\n  for (const rect of rectangles) {\n    if (isSegmentIntersectingRect(p1, p2, rect, padding)) {\n      return true;\n    }\n  }\n  return false;\n}\n"
  },
  {
    "path": "client/src/dashboard/Charts/drawShapes/getTimechartGradientPeakSections.ts",
    "content": "export const getTimechartGradientPeakSections = (\n  coords: [number, number][],\n) => {\n  // Calculate minY for gradient positioning\n  let minY = Infinity;\n  let maxY = -Infinity;\n  coords.forEach(([_, y]) => {\n    if (y < minY) minY = y;\n    if (y > maxY) maxY = y;\n  });\n  // const height = maxY;\n\n  const stops = [\n    { offset: 0.0, opacity: 0.5 },\n    { offset: 0.2, opacity: 0.4 },\n    { offset: 0.3, opacity: 0.3 },\n    { offset: 0.4, opacity: 0.2 },\n    { offset: 0.5, opacity: 0.1 },\n    { offset: 0.7, opacity: 0.02 },\n    { offset: 0.8, opacity: 0 },\n  ];\n  // const gradientLastStep = stops.at(-1)!.offset;\n  // const gradientMaxY = gradientLastStep * height;\n\n  // const peakSections: { x: number; y: number; index: number }[][] = [];\n  // coords.forEach(([x, y], index) => {\n  //   if (y > gradientMaxY) {\n  //     return;\n  //   }\n\n  //   const nextPoint = coords[index + 1];\n  //   const prevPoint = coords[index - 1];\n  //   const currentSection = peakSections.at(-1);\n  //   const currentSectionLastPoint = currentSection?.at(-1);\n  //   if (\n  //     !currentSection ||\n  //     !currentSectionLastPoint ||\n  //     index !== currentSectionLastPoint.index + 1\n  //   ) {\n  //     peakSections.push(\n  //       [\n  //         prevPoint && {\n  //           index: index - 1,\n  //           x: prevPoint[0],\n  //           y: prevPoint[1],\n  //         },\n  //         { x, y, index },\n  //         nextPoint && nextPoint[1] > gradientMaxY ?\n  //           {\n  //             index: index + 1,\n  //             x: nextPoint[0],\n  //             y: nextPoint[1],\n  //           }\n  //         : undefined,\n  //       ].filter(isDefined),\n  //     );\n  //   } else {\n  //     currentSection.push({ x, y, index });\n  //     if (nextPoint && nextPoint[1] > gradientMaxY) {\n  //       currentSection.push({\n  //         x: nextPoint[0],\n  //         y: nextPoint[1],\n  //         index: index + 1,\n  //       });\n  //     }\n  //   }\n  // });\n\n  return {\n    peakSections: [coords.map(([x, y], index) => ({ x, y, index }))],\n    minY,\n    stops,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/drawShapes/shortestLinkLineV2.ts",
    "content": "import type { Point } from \"../../Charts\";\nimport type { LinkLine, Rectangle } from \"../CanvasChart\";\nimport { drawShapes, type ShapeV2 } from \"./drawShapes\";\n\nexport const getCtx = (canvas: HTMLCanvasElement) => {\n  return canvas.getContext(\"2d\");\n};\n\nexport const drawLinkLine = <T = void>(\n  shapes: ShapeV2<T>[],\n  canvas: HTMLCanvasElement,\n  linkLine: LinkLine,\n  opts?: {\n    scale?: number;\n    translate?: { x: number; y: number };\n    isChild?: boolean;\n  },\n) => {\n  const {\n    id,\n    strokeStyle,\n    lineWidth,\n    // variant, // Not used for 90-degree lines\n    sourceId,\n    targetId,\n    sourceYOffset, // Offset from the top (y-coordinate) of the source\n    targetYOffset, // Offset from the top (y-coordinate) of the target\n  } = linkLine;\n\n  const r1 = shapes.find(\n    (r): r is Rectangle => r.type === \"rectangle\" && r.id === sourceId,\n  );\n  const r2 = shapes.find(\n    (r): r is Rectangle => r.type === \"rectangle\" && r.id === targetId,\n  );\n\n  if (!r1 || !r2) return;\n\n  const [x1, y1] = r1.coords;\n  const w1 = r1.w;\n  // const h1 = r1.h;\n  const [x2, y2] = r2.coords;\n  // const w2 = r2.w;\n  // const h2 = r2.h;\n\n  const scale = opts?.scale || 1;\n  const xTranslate = opts?.translate?.x || 0;\n  const yTranslate = opts?.translate?.y || 0;\n\n  const getTranslatedCoords = ([x, y]: Point): Point => [\n    (x - xTranslate) / scale,\n    (y - yTranslate) / scale,\n  ];\n\n  // --- 6-Point Orthogonal Routing Logic (H-V-H-V-H style) ---\n\n  const padding = 20; // Horizontal distance for the first and last segments\n\n  // Calculate the exact start and end points ON the rectangle edges\n  const p0_startOnRect: Point = [x1 + w1, y1 + sourceYOffset];\n  const p5_endOnRect: Point = [x2, y2 + targetYOffset];\n\n  // Calculate points extended horizontally from the rectangles\n  const p1_startAway: Point = [p0_startOnRect[0] + padding, p0_startOnRect[1]];\n  const p4_endNear: Point = [p5_endOnRect[0] - padding, p5_endOnRect[1]];\n\n  // Calculate the vertical midpoint Y-coordinate for the central horizontal segment\n  // This creates the \"up/down to midpoint\" behavior.\n  const midY = p1_startAway[1] + (p4_endNear[1] - p1_startAway[1]) / 2;\n\n  // Calculate the intermediate corner points\n  // P2: Moves vertically from p1_startAway to midY\n  const p2_corner1: Point = [p1_startAway[0], midY];\n\n  // P3: Moves horizontally from p2_corner1 to align vertically with p4_endNear\n  const p3_corner2: Point = [p4_endNear[0], midY];\n\n  // Assemble the 6 points for the multi-line path\n  const linePoints: Point[] = [\n    p0_startOnRect, // 0: Touch source box\n    p1_startAway, // 1: A bit out (horizontal)\n    // p2_corner1, // 2: Up/down to midpoint Y (vertical)\n    // p3_corner2, // 3: Across horizontally at midpoint Y\n    p4_endNear, // 4: Up/down to target Y level (vertical)\n    p5_endOnRect, // 5: Touch target box (horizontal)\n  ];\n\n  const coords = [\n    getTranslatedCoords(p0_startOnRect),\n    getTranslatedCoords(p1_startAway),\n    ...splitLine({\n      start: getTranslatedCoords(p1_startAway),\n      end: getTranslatedCoords(p4_endNear),\n    }),\n    getTranslatedCoords(p4_endNear),\n    getTranslatedCoords(p5_endOnRect),\n  ];\n\n  // const coordToPoint = ([x, y]: Point) => ({ x, y });\n  // const pointToCoord = ({ x, y }: { x: number; y: number }) => [x, y] as Point;\n  // const shortestPath = findShortestPathAroundRectangles(\n  //   coordToPoint(p1_startAway),\n  //   coordToPoint(p4_endNear),\n  //   shapes\n  //     .filter((s): s is Rectangle => s.type === \"rectangle\")\n  //     .map(({ id, coords, w, h }) => ({\n  //       id,\n  //       width: w,\n  //       height: h,\n  //       ...coordToPoint(coords),\n  //     })),\n  //   20,\n  // );\n  // const shortestPathAroundRectangles = [\n  //   getTranslatedCoords(p0_startOnRect),\n  //   ...shortestPath.map(pointToCoord).map(getTranslatedCoords),\n  //   getTranslatedCoords(p5_endOnRect),\n  // ];\n\n  drawShapes(\n    [\n      {\n        id,\n        type: \"multiline\",\n        coords: coords,\n        lineWidth: 4,\n        strokeStyle: strokeStyle || \"black\",\n        variant: \"smooth\",\n      },\n      // Optional markers (uncomment to add)\n      /*\n       {\n         id: `${id}-start-marker`,\n         type: \"circle\",\n         coords: getTranslatedCoords(p0_startOnRect),\n         radius: 3,\n         fillStyle: strokeStyle || \"black\",\n       },\n       {\n         id: `${id}-end-marker`,\n         type: \"circle\",\n         coords: getTranslatedCoords(p5_endOnRect),\n         radius: 3,\n         fillStyle: strokeStyle || \"black\",\n       }\n       */\n    ],\n    canvas,\n    { ...opts, isChild: true },\n  );\n};\n\nconst LINK_SEGMENT_LENGTH = 50;\nconst splitLine = ({ end, start }: { start: Point; end: Point }): Point[] => {\n  const [x1, y1] = start;\n  const [x2, y2] = end;\n  const dx = x2 - x1;\n  const dy = y2 - y1;\n  const distance = Math.sqrt(dx * dx + dy * dy);\n  const numSegments = Math.max(2, Math.ceil(distance / LINK_SEGMENT_LENGTH)); // At least 2 segments\n\n  const currentNodes: Point[] = [start]; // Start with source table reference\n\n  for (let i = 1; i < numSegments; i++) {\n    const t = i / numSegments;\n    // const node = {\n    //   x: x1 + dx * t,\n    //   y: y1 + dy * t,\n    //   vx: 0, // Initial velocity\n    //   vy: 0,\n    //   type: \"linkNode\",\n    //   linkId, // Reference to the original link\n    //   sourceId,\n    //   targetId,\n    //   isIntermediate: true, // Flag for easy identification\n    // };\n    // linkNodes.push(node);\n    currentNodes.push([x1 + dx * t, y1 + dy * t]);\n  }\n  currentNodes.push(end); // End with target table reference\n  return currentNodes;\n};\n"
  },
  {
    "path": "client/src/dashboard/Charts/roundRect.ts",
    "content": "import { getCssVariableValue } from \"./TimeChart/onRenderTimechart\";\n\nexport const DEFAULT_SHADOW = {\n  color: getCssVariableValue(\"--shadow1\"), //\"rgba(73, 56, 56, 0.5)\",\n  blur: 15,\n  offsetX: 3,\n  offsetY: 3,\n};\n\nexport function roundRect(\n  ctx: CanvasRenderingContext2D,\n  x: number,\n  y: number,\n  width: number,\n  height: number,\n  radius: number | { tl: number; tr: number; bl: number; br: number },\n  shadow: {\n    color?: string;\n    blur?: number;\n    offsetX?: number;\n    offsetY?: number;\n  } = DEFAULT_SHADOW,\n) {\n  if (typeof radius === \"number\") {\n    radius = { tl: radius, tr: radius, br: radius, bl: radius };\n  } else {\n    const defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 };\n    for (const side in defaultRadius) {\n      radius[side] = radius[side] || defaultRadius[side];\n    }\n  }\n  // Save the current context state\n  ctx.save();\n\n  // Apply shadow if provided\n  if (shadow.blur) {\n    ctx.shadowColor = shadow.color || \"rgba(0,0,0,0.5)\";\n    ctx.shadowBlur = shadow.blur;\n    ctx.shadowOffsetX = shadow.offsetX ?? 12;\n    ctx.shadowOffsetY = shadow.offsetY ?? 12;\n  }\n\n  ctx.beginPath();\n  ctx.roundRect(x, y, width, height, radius.tl);\n  ctx.closePath();\n  ctx.fill();\n  ctx.stroke();\n  // const initialLineWidth = ctx.lineWidth;\n  // const curvedLineWidth = ctx.lineWidth + 1;\n  // ctx.beginPath();\n  // ctx.moveTo(x + radius.tl, y);\n  // ctx.lineTo(x + width - radius.tr, y);\n  // ctx.lineWidth = curvedLineWidth;\n  // ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);\n  // ctx.lineWidth = initialLineWidth;\n  // ctx.lineTo(x + width, y + height - radius.br);\n  // ctx.lineWidth = curvedLineWidth;\n  // ctx.quadraticCurveTo(\n  //   x + width,\n  //   y + height,\n  //   x + width - radius.br,\n  //   y + height,\n  // );\n  // ctx.lineWidth = initialLineWidth;\n  // ctx.lineTo(x + radius.bl, y + height);\n  // ctx.lineWidth = curvedLineWidth;\n  // ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);\n  // ctx.lineWidth = initialLineWidth;\n  // ctx.lineTo(x, y + radius.tl);\n  // ctx.lineWidth = curvedLineWidth;\n  // ctx.quadraticCurveTo(x, y, x + radius.tl, y);\n  // ctx.lineWidth = initialLineWidth;\n  // ctx.closePath();\n\n  // Restore the context state\n  ctx.restore();\n}\n"
  },
  {
    "path": "client/src/dashboard/Charts.tsx",
    "content": "import React from \"react\";\nimport type { CanvasChartViewDataExtent } from \"./Charts/CanvasChart\";\nimport { CanvasChart } from \"./Charts/CanvasChart\";\nimport RTComp from \"./RTComp\";\nimport { classOverride } from \"@components/Flex\";\n\nexport const MILLISECOND = 1;\nexport const SECOND = MILLISECOND * 1000;\nexport const MINUTE = SECOND * 60;\nexport const HOUR = MINUTE * 60;\nexport const DAY = HOUR * 24;\nexport const MONTH = DAY * 30;\nexport const YEAR = DAY * 365;\n\nconst padded = (v: number) => v.toString().padStart(2, \"0\");\nexport const dateAsYMD = (date: Date) => {\n  return `${date.getFullYear()}-${padded(date.getMonth() + 1)}-${padded(date.getDate())}`;\n};\nexport const dateAsYMD_Time = (date: Date) => {\n  return (\n    dateAsYMD(date) +\n    ` ${padded(date.getHours())}:${padded(date.getMinutes())}:${padded(date.getSeconds())}`\n  );\n};\n\nexport function onPinchZoom(\n  el: HTMLElement,\n  callback: (\n    ratio: number,\n    center: { x: number; y: number; e: TouchEvent },\n  ) => any,\n) {\n  let hypo: number | undefined = undefined;\n\n  el.addEventListener(\n    \"touchmove\",\n    function (event) {\n      // Check if the two target touches are the same ones that started\n      if (event.touches.length === 2 && event.targetTouches.length === 2) {\n        //get distance between fingers\n        const hypo1 = Math.hypot(\n          event.touches[0]!.pageX - event.touches[1]!.pageX,\n          event.touches[0]!.pageY - event.touches[1]!.pageY,\n        );\n        if (hypo === undefined) {\n          hypo = hypo1;\n        } else {\n          const rect = el.getBoundingClientRect();\n          const x =\n            rect.x + (event.touches[0]!.pageX + event.touches[1]!.pageX) / 2;\n          const y =\n            rect.y + (event.touches[0]!.pageY + event.touches[1]!.pageY) / 2;\n          const ratio = hypo1 / hypo; // if > 0 then zoom in\n          callback(ratio, { x, y, e: event });\n        }\n      }\n    },\n    false,\n  );\n\n  el.addEventListener(\n    \"touchend\",\n    function (event) {\n      hypo = undefined;\n    },\n    false,\n  );\n}\n\nconst cachedVals: {\n  [key: string]: string;\n} = {};\n\nexport const toDateStr = (\n  date = new Date(),\n  opts: Intl.DateTimeFormatOptions,\n): string => {\n  const key = JSON.stringify({ d: Math.round(+date), opts });\n  if (cachedVals[key]) return cachedVals[key];\n  else cachedVals[key] = date.toLocaleString(navigator.language, opts);\n  return cachedVals[key];\n};\n\nexport type Point = [number, number];\nexport type Coords = Point | Point[];\n\ntype ChartD = {\n  xCursor: number | null;\n  yCursor: number | null;\n  extent?: CanvasChartViewDataExtent;\n};\nexport class Chart extends RTComp<\n  {\n    className?: string;\n    style?: Omit<React.CSSProperties, \"backgroundColor\" | \"background\">;\n    setRef: (chart: CanvasChart) => void;\n    onExtentChange?: (extent: CanvasChartViewDataExtent) => void;\n  },\n  {},\n  ChartD\n> {\n  d: ChartD = {\n    xCursor: null,\n    yCursor: null,\n  };\n\n  ref?: HTMLDivElement;\n  canv?: HTMLCanvasElement;\n  chart?: CanvasChart;\n  panning = false;\n\n  onMount() {\n    const { setRef, onExtentChange } = this.props;\n    if (!this.ref || !this.canv) return;\n    this.chart = new CanvasChart({\n      node: this.ref,\n      canvas: this.canv,\n      yScaleLocked: true,\n      yPanLocked: true,\n      minXScale: 1,\n      onResize: undefined,\n      events: {\n        onExtentChange: (extent) => {\n          onExtentChange?.(extent);\n          this.setData({ extent });\n        },\n        onPan: () => {\n          this.panning = true;\n        },\n        onPanEnd: () => {\n          this.panning = false;\n        },\n      },\n    });\n    setRef(this.chart);\n  }\n\n  render() {\n    const { className = \"\", style = {} } = this.props;\n    return (\n      <div\n        ref={(e) => {\n          if (e) this.ref = e;\n        }}\n        style={{\n          ...style,\n        }}\n        className={classOverride(\n          \"Chart flex-col f-1 min-h-0 min-w-0 relative \",\n          className,\n        )}\n        onMouseMove={(e) => {\n          const r = e.currentTarget.getBoundingClientRect();\n\n          this.setData({\n            xCursor: e.clientX - r.x,\n            yCursor: e.clientY - r.y,\n          });\n          // console.log(this.chart.getDataXY(screenX, screenY))\n        }}\n        onMouseOut={() => {\n          this.setData({ xCursor: null, yCursor: null });\n        }}\n      >\n        <canvas\n          ref={(e) => {\n            if (e) this.canv = e;\n          }}\n          className=\"f-1\"\n        />\n      </div>\n    );\n  }\n}\n\nexport function roundToNearest(val: number, increment: number, maxVal: number) {\n  const maxIncremented = maxVal - (maxVal % increment);\n  return Math.min(maxIncremented, Math.ceil(val / increment) * increment);\n}\n\n// export function linearScale(args: {\n//   clamped?: boolean;\n//   value: number;\n//   input: [number, number];\n//   output: [number, number];\n// }): number {\n//   const {\n//     clamped,\n//     value ,\n//     input,\n//     output\n//   } = args;\n\n//   const [val1, val2] = input;\n//   const [out1, out2] = input;\n\n//   const inputAsc = val1 < val2;\n//   const outputAsc = out1 < out2;\n\n//   let v = value;\n//   if(clamped){\n//     v = inputAsc? clamp(v, val1, val2) : clamp(v, val2, val1);\n//   }\n\n//   let result: number;\n\n//   return result;\n// }\n\nfunction clamp(num: number, min: number, max: number) {\n  return (\n    num <= min ? min\n    : num >= max ? max\n    : num\n  );\n}\n"
  },
  {
    "path": "client/src/dashboard/CodeEditor/CodeEditor.tsx",
    "content": "import React, {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type KeyboardEventHandler,\n} from \"react\";\n\nimport { useEffectDeep, usePromise } from \"prostgles-client\";\nimport { isObject } from \"@common/publishUtils\";\nimport { classOverride } from \"@components/Flex\";\nimport type { MonacoEditorProps } from \"@components/MonacoEditor/MonacoEditor\";\nimport { MonacoEditor } from \"@components/MonacoEditor/MonacoEditor\";\nimport { getMonaco } from \"../SQLEditor/W_SQLEditor\";\nimport { type editor, type Uri } from \"../W_SQL/monacoEditorTypes\";\nimport {\n  LOG_LANGUAGE_ID,\n  LOG_LANGUAGE_THEME,\n  registerLogLang,\n} from \"./registerLogLang\";\nimport { setMonacoErrorMarkers } from \"./utils/setMonacoErrorMarkers\";\nimport {\n  setMonacoEditorJsonSchemas,\n  useSetMonacoJsonSchemas,\n} from \"./utils/useSetMonacoJsonSchemas\";\nimport { useSetMonacoTsLibraries } from \"./utils/useSetMonacoTsLibraries\";\nexport type Suggestion = {\n  type: \"table\" | \"column\" | \"function\";\n  label: string;\n  detail?: string;\n  documentation?: string;\n};\n\nexport type MonacoError = {\n  message: string;\n  code?: string;\n\n  position?: number;\n  length?: number;\n\n  startLineNumber?: number;\n  startColumn?: number;\n  endLineNumber?: number;\n  endColumn?: number;\n\n  severity?: number;\n};\n\nexport type MonacoJSONSchema = {\n  id: string;\n\n  /**\n   * e.g.: \"http://myserver/foo-schema.json\", // id of the schema\n   */\n  uri: string; //Uri;\n\n  theUri: Uri;\n\n  /**\n   * [\"*\"], // associate with our model\n   */\n  fileMatch?: string[];\n\n  /**\n   * JSON Schema\n   * example >>> {\n        type: \"object\",\n        properties: {\n          p1: {\n            enum: [\"v1\", \"v2\"]\n          },\n          p2: {\n            $ref: \"http://myserver/bar-schema.json\" // reference the second schema\n          }\n        }\n      }\n   */\n  schema: Record<string, any>;\n};\n\nexport type TSLibrary = {\n  /**\n   * 'ts:filename/facts.d.ts';\n   */\n  filePath: string;\n  /**\n   * type MyType = { a: number; b: string };\n   * class MyClass { ... }\n   */\n  content: string;\n};\n\nexport type LanguageConfig =\n  | {\n      lang: \"sql\";\n      suggestions?: Suggestion[];\n    }\n  | {\n      lang: \"typescript\";\n      /**\n       * e.g.: 'myMethod2';\n       * Must be unique for each model\n       */\n      modelFileName: string;\n      tsLibraries?: TSLibrary[];\n    }\n  | {\n      lang: \"json\";\n      jsonSchemas?: CodeEditorJsonSchema[];\n    };\n\nexport type CodeEditorJsonSchema = { id: string; schema: any };\n\nexport type CodeEditorProps = Pick<\n  MonacoEditorProps,\n  \"options\" | \"value\" | \"minHeight\"\n> & {\n  value: string;\n  onChange?: (newValue: string) => void;\n  language: LanguageConfig | string;\n  /**\n   * If true then will allow saving on CTRL+S\n   */\n  onSave?: (code: string) => void;\n  error?: MonacoError;\n  style?: React.CSSProperties;\n  className?: string;\n  markers?: editor.IMarkerData[];\n  onMount?: (editor: editor.IStandaloneCodeEditor) => void;\n  onTSLibraryChange?: (tsLibraries: TSLibrary[]) => void;\n  contentTop?: React.ReactNode;\n};\n\nexport const CodeEditor = (props: CodeEditorProps) => {\n  const {\n    value = \"\",\n    onChange,\n    onMount,\n    markers,\n    onSave,\n    language: languageOrConf,\n    options = {},\n    style,\n    className = \"\",\n    error,\n    onTSLibraryChange,\n    contentTop,\n    minHeight,\n  } = props;\n\n  const language =\n    isObject(languageOrConf) ? languageOrConf.lang : languageOrConf;\n\n  const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();\n\n  const monacoResult = usePromise(async () => {\n    const monaco = await getMonaco();\n    return { monaco };\n  }, []);\n  const monaco = monacoResult?.monaco;\n\n  const languageObj = useMemo(() => {\n    return isObject(languageOrConf) ? languageOrConf : undefined;\n  }, [languageOrConf]);\n\n  useSetMonacoJsonSchemas(editor, value, languageObj);\n  useSetMonacoTsLibraries(\n    editor,\n    languageObj,\n    monaco,\n    value,\n    onTSLibraryChange,\n  );\n\n  useEffect(() => {\n    if (monaco && language === LOG_LANGUAGE_ID) {\n      registerLogLang(monaco);\n    }\n  }, [language, monaco]);\n\n  useEffectDeep(() => {\n    if (!editor || !monaco) return;\n    setMonacoErrorMarkers(editor, monaco, { error });\n  }, [editor, error, monaco]);\n\n  useEffectDeep(() => {\n    if (!editor || !monaco) return;\n    setMonacoErrorMarkers(editor, monaco, { markers });\n  }, [editor, markers, monaco]);\n\n  const onMountMonacoEditor = useCallback(\n    (newEditor: editor.IStandaloneCodeEditor) => {\n      setEditor(newEditor);\n      onMount?.(newEditor);\n    },\n    [onMount],\n  );\n\n  const onKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback(\n    (e) => {\n      const _domElement = (\n        editor as { _domElement?: HTMLDivElement } | undefined\n      )?._domElement;\n      if (\n        onSave &&\n        editor &&\n        e.ctrlKey &&\n        e.key === \"s\" &&\n        _domElement?.contains(e.target as Node)\n      ) {\n        e.preventDefault();\n        onSave(editor.getValue());\n      }\n    },\n    [onSave, editor],\n  );\n\n  const latestValueRef = useRef(value);\n  latestValueRef.current = value;\n  const onFocus = useCallback(() => {\n    const allCodeEditors = document.querySelectorAll(\".CodeEditor\");\n    if (allCodeEditors.length === 1) {\n      /** If this is the only editor the fix below will break it (viewedSqlTips) */\n      return;\n    }\n\n    /** This is needed to ensure jsonschema works. Otherwise only the first editor schema will work */\n    void setMonacoEditorJsonSchemas(\n      editor,\n      latestValueRef.current,\n      languageObj,\n    );\n  }, [editor, languageObj]);\n\n  const monacoOptions = useMemo(() => {\n    return {\n      readOnly: !onChange,\n      renderValidationDecorations: \"on\",\n      parameterHints: { enabled: true },\n      fixedOverflowWidgets: true,\n      tabSize: 2,\n      automaticLayout: true,\n      ...(language === \"json\" && {\n        formatOnType: true,\n        autoIndent: \"full\",\n      }),\n      ...options,\n      ...(language === LOG_LANGUAGE_ID && {\n        theme: LOG_LANGUAGE_THEME,\n      }),\n    } satisfies editor.IStandaloneEditorConstructionOptions;\n  }, [language, onChange, options]);\n\n  return (\n    <div\n      className={classOverride(\n        \"CodeEditor f-1 min-h-0 min-w-0 flex-col relative b b-color relative\",\n        className,\n      )}\n      style={style}\n      onFocus={onFocus}\n      onKeyDown={onKeyDown}\n    >\n      {contentTop}\n      <MonacoEditor\n        className=\"f-1 min-h-0\"\n        language={language}\n        loadedSuggestions={undefined}\n        value={value}\n        options={monacoOptions}\n        onChange={onChange}\n        onMount={onMountMonacoEditor}\n        minHeight={minHeight}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/CodeEditor/CodeEditorWithSaveButton.tsx",
    "content": "import { mdiFullscreen } from \"@mdi/js\";\nimport { useEffectDeep } from \"prostgles-client\";\nimport React, { useCallback, useRef } from \"react\";\nimport type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow, classOverride } from \"@components/Flex\";\nimport { FooterButtons } from \"@components/Popup/FooterButtons\";\nimport Popup from \"@components/Popup/Popup\";\nimport { CodeEditor, type CodeEditorProps } from \"./CodeEditor\";\nimport { isDefined } from \"../../utils/utils\";\nimport { Label } from \"@components/Label\";\n\ntype P = {\n  label: React.ReactNode;\n  onSaveButton?: Pick<BtnProps, \"children\" | \"iconPath\" | \"color\" | \"size\">;\n  onSave?: (value: string) => void;\n  autoSave?: boolean;\n  value: string | undefined | null;\n  codePlaceholder?: string;\n  codeEditorClassName?: string;\n  headerButtons?: React.ReactNode;\n} & Omit<\n  CodeEditorProps,\n  \"onSave\" | \"onChange\" | \"value\" | \"style\" | \"className\"\n>;\n\nexport const CodeEditorWithSaveButton = (props: P) => {\n  const {\n    label,\n    onSave,\n    onSaveButton,\n    value,\n    codePlaceholder,\n    autoSave,\n    codeEditorClassName = \"b\",\n    headerButtons,\n    ...codeEditorProps\n  } = props;\n  const isReadonly = !onSave && !autoSave;\n  const localValueRef = useRef<string | null | undefined>(value);\n  const propsValueRef = useRef<string | null | undefined>(value);\n  propsValueRef.current = value;\n\n  const [error, setError] = React.useState<any>();\n  const [fullScreen, setFullScreen] = React.useState(false);\n  useEffectDeep(() => {\n    if (\n      localValueRef.current === undefined &&\n      localValueRef.current !== value\n    ) {\n      localValueRef.current = value;\n    }\n  }, [value, isReadonly]);\n\n  const [didChange, setDidChange] = React.useState(false);\n\n  const onSaveMonaco = useCallback(async () => {\n    if (!didChange || !onSave) return;\n    try {\n      await onSave(localValueRef.current ?? \"\");\n      setError(undefined);\n      setDidChange(false);\n    } catch (err) {\n      setError(err);\n    }\n  }, [onSave, didChange]);\n\n  const onClickSave = !onSave || autoSave ? undefined : onSaveMonaco;\n\n  const titleNode =\n    !label && !headerButtons ?\n      null\n    : <FlexRow className={fullScreen ? \"\" : \"bg-color-1\"}>\n        {\n          <Label className=\" px-p25 f-1 \" variant=\"normal\">\n            {label}\n          </Label>\n        }\n        <FlexRow className=\"gap-0\">\n          {headerButtons}\n          <Btn\n            iconPath={mdiFullscreen}\n            onClick={() => setFullScreen(!fullScreen)}\n          />\n        </FlexRow>\n      </FlexRow>;\n\n  const footerNode = didChange && onClickSave && (\n    <FooterButtons\n      error={error}\n      className=\"bg-color-1\"\n      style={{\n        maxHeight: \"60%\",\n        alignItems: \"start\",\n        position: \"absolute\",\n        bottom: 0,\n        left: 0,\n        right: 0,\n        /** Must appear above minimap but beneath completion suggestions */\n        zIndex: 5,\n        background: \"#dfdfdf5c\",\n        backdropFilter: \"blur(1px)\",\n      }}\n      footerButtons={[\n        {\n          label: \"Cancel\",\n          className: \"mr-auto\",\n          onClick: () => {\n            localValueRef.current = value;\n            setDidChange(false);\n          },\n        },\n        error ? undefined : (\n          {\n            label: \"Save (Ctrl + S)\",\n            color: \"action\",\n            variant: \"filled\",\n            ...onSaveButton,\n            onClick: onClickSave,\n          }\n        ),\n      ]}\n    />\n  );\n\n  const onChange = useCallback(\n    (newValue: string) => {\n      if (autoSave) {\n        onSave?.(newValue);\n      }\n      localValueRef.current = newValue;\n\n      const _didChange =\n        isDefined(localValueRef.current) &&\n        localValueRef.current !== propsValueRef.current;\n      if (!autoSave && _didChange !== didChange) {\n        setDidChange(_didChange);\n      }\n    },\n    [onSave, autoSave, didChange],\n  );\n\n  const content = (\n    <FlexCol\n      className={classOverride(\n        \"SmartCodeEditor gap-0 f-1 \",\n        `${fullScreen ? \"min-h-0\" : \"\"}`,\n      )}\n    >\n      {fullScreen ? null : titleNode}\n      <FlexCol\n        className={classOverride(\n          \"relative f-1 gap-0 \",\n          `${fullScreen ? \"min-h-0\" : \"\"}`,\n        )}\n        style={\n          codePlaceholder && !value && !localValueRef.current ?\n            {\n              opacity: 0.5,\n            }\n          : {}\n        }\n      >\n        <CodeEditor\n          className={codeEditorClassName}\n          {...codeEditorProps}\n          value={\n            (isReadonly ? value : localValueRef.current) ||\n            value ||\n            (codePlaceholder ?? \"\")\n          }\n          onChange={onChange}\n          onSave={onClickSave}\n        />\n        {footerNode}\n      </FlexCol>\n    </FlexCol>\n  );\n\n  if (!fullScreen) {\n    return content;\n  }\n\n  return (\n    <Popup\n      title={titleNode}\n      positioning=\"fullscreen\"\n      contentStyle={{\n        overflow: \"hidden\",\n      }}\n      onClose={() => setFullScreen(false)}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") {\n          setFullScreen(false);\n        }\n      }}\n      onClickClose={false}\n    >\n      {content}\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/CodeEditor/monacoTsLibs.ts",
    "content": "/* eslint-disable no-useless-escape */\n\nexport const dboLib =\n  `\n\ntype AnyObject = Record<string, any>;\n\nexport type Explode<T> = keyof T extends infer K\n  ? K extends unknown\n  ? { [I in keyof T]: I extends K ? T[I] : never }\n  : never\n  : never;\nexport type AtMostOne<T> = Explode<Partial<T>>;\nexport type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]\nexport type ExactlyOne<T> = AtMostOne<T> & AtLeastOne<T>;\nexport type DBTableSchema = {\n\tis_view?: boolean;\n\tselect?: boolean;\n\tinsert?: boolean;\n\tupdate?: boolean;\n\tdelete?: boolean;\n\t/**\n\t * Used in update, insertm select and filters\n\t * fields that are nullable or with a default value are be optional \n\t */\n\tcolumns: AnyObject;\n  }\nexport type DBSchema = { \n\t[tov_name: string]: DBTableSchema\n}\n\nexport type AllowedTSType = string | number | boolean | Date | any;\nexport type AllowedTSTypes = AllowedTSType[];\n\nexport const CompareFilterKeys = [\"=\", \"$eq\",\"<>\",\">\",\"<\",\">=\",\"<=\",\"$eq\",\"$ne\",\"$gt\",\"$gte\",\"$lte\"] as const;\nexport const CompareInFilterKeys = [\"$in\", \"$nin\"] as const;\n\nexport const JsonbOperands = {\n  \"@>\": {\n    \"Operator\": \"@>\",\n    \"Right Operand Type\": \"jsonb\",\n    \"Description\": \"Does the left JSON value contain the right JSON path/value entries at the top level?\",\n    \"Example\": \\`'{\"a\":1, \"b\":2}'::jsonb @> '{\\\"b\\\":2}'::jsonb\\`\n  },\n  \"<@\": {\n    \"Operator\": \"<@\",\n    \"Right Operand Type\": \"jsonb\",\n    \"Description\": \"Are the left JSON path/value entries contained at the top level within the right JSON value?\",\n    \"Example\": \\`'{\\\"b\\\":2}'::jsonb <@ '{\\\"a\\\":1, \\\"b\\\":2}'::jsonb\\`\n  },\n  \"?\": {\n    \"Operator\": \"?\",\n    \"Right Operand Type\": \"text\",\n    \"Description\": \"Does the string exist as a top-level key within the JSON value?\",\n    \"Example\": \\`'{\\\"a\\\":1, \\\"b\\\":2}'::jsonb ? 'b'\\`\n  },\n  \"?|\": {\n    \"Operator\": \"?|\",\n    \"Right Operand Type\": \"text[]\",\n    \"Description\": \"Do any of these array strings exist as top-level keys?\",\n    \"Example\": \\`'{\\\"a\\\":1, \\\"b\\\":2, \\\"c\\\":3}'::jsonb ?| array['b', 'c']\\`\n  },\n  \"?&\": {\n    \"Operator\": \"?&\",\n    \"Right Operand Type\": \"text[]\",\n    \"Description\": \"Do all of these array strings exist as top-level keys?\",\n    \"Example\": \\`'[\\\"a\\\", \\\"b\\\"]'::jsonb ?& array['a', 'b']\\`\n  },\n  \"||\": {\n    \"Operator\": \"||\",\n    \"Right Operand Type\": \"jsonb\",\n    \"Description\": \"Concatenate two jsonb values into a new jsonb value\",\n    \"Example\": \\`'[\\\"a\\\", \\\"b\\\"]'::jsonb || '[\\\"c\\\", \\\"d\\\"]'::jsonb\\`\n  },\n  \"-\": {\n    \"Operator\": \"-\",\n    \"Right Operand Type\": \"integer\",\n    \"Description\": \"Delete the array element with specified index (Negative integers count from the end). Throws an error if top level container is not an array.\",\n    \"Example\": \\`'[\\\"a\\\", \\\"b\\\"]'::jsonb - 1\\`\n  },\n  \"#-\": {\n    \"Operator\": \"#-\",\n    \"Right Operand Type\": \"text[]\",\n    \"Description\": \"Delete the field or element with specified path (for JSON arrays, negative integers count from the end)\",\n    \"Example\": \\`'[\\\"a\\\", {\\\"b\\\":1}]'::jsonb #- '{1,b}'\\`\n  },\n  \"@?\": {\n    \"Operator\": \"@?\",\n    \"Right Operand Type\": \"jsonpath\",\n    \"Description\": \"Does JSON path return any item for the specified JSON value?\",\n    \"Example\": \\`'{\\\"a\\\":[1,2,3,4,5]}'::jsonb @? '$.a[*] ? (@ > 2)'\\`\n  },\n  \"@@\": {\n    \"Operator\": \"@@\",\n    \"Right Operand Type\": \"jsonpath\",\n    \"Description\": \"Returns the result of JSON path predicate check for the specified JSON value. Only the first item of the result is taken into account. If the result is not Boolean, then null is returned.\",\n    \"Example\": \\`'{\\\"a\\\":[1,2,3,4,5]}'::jsonb @@ '$.a[*] > 2'\\`\n  }\n} as const; \n\n/**\n * Example: col_name: { $gt: 2 }\n */\n export type CompareFilter<T extends AllowedTSType = string> =\n /**\n  * column value equals provided value\n  */\n | T \n | ExactlyOne<Record<typeof CompareFilterKeys[number], T>>\n\n | ExactlyOne<Record<typeof CompareInFilterKeys[number], T[]>>\n | { \"$between\": [T, T] }\n;\nexport const TextFilterKeys = [\"$ilike\", \"$like\", \"$nilike\", \"$nlike\"] as const;\n\nexport const TextFilterFTSKeys = [\"@@\", \"@>\", \"<@\", \"$contains\", \"$containedBy\"] as const;\n\nexport const TextFilter_FullTextSearchFilterKeys = [\"to_tsquery\",\"plainto_tsquery\",\"phraseto_tsquery\",\"websearch_to_tsquery\"] as const;\nexport type FullTextSearchFilter = \n | ExactlyOne<Record<typeof TextFilter_FullTextSearchFilterKeys[number], string[]>>\n;\n\nexport type TextFilter = \n | CompareFilter<string>\n | ExactlyOne<Record<typeof TextFilterKeys[number], string>>\n\n | ExactlyOne<Record<typeof TextFilterFTSKeys[number], FullTextSearchFilter>>\n;\n\nexport const ArrayFilterOperands = [\"@>\", \"<@\", \"=\", \"$eq\", \"$contains\", \"$containedBy\", \"&&\", \"$overlaps\"] as const;\nexport type ArrayFilter<T extends AllowedTSType[]> = \n | Record<typeof ArrayFilterOperands[number], T>\n | ExactlyOne<Record<typeof ArrayFilterOperands[number], T>>\n;\n\n/* POSTGIS */\n\n/**\n* Makes bounding box from NW and SE points\n* float xmin, float ymin, float xmax, float ymax, integer srid=unknown\n* https://postgis.net/docs/ST_MakeEnvelope.html\n*/\nexport type GeoBBox = { ST_MakeEnvelope: number[] }\n\n\n/**\n* Returns TRUE if A's 2D bounding box intersects B's 2D bounding box.\n* https://postgis.net/docs/reference.html#Operators\n*/\nexport type GeomFilter = \n\n /**\n  * A's 2D bounding box intersects B's 2D bounding box.\n  */\n | { \"&&\": GeoBBox }\n//  | { \"&&&\": GeoBBox }\n//  | { \"&<\": GeoBBox }\n//  | { \"&<|\": GeoBBox }\n//  | { \"&>\": GeoBBox }\n//  | { \"<<\": GeoBBox }\n//  | { \"<<|\": GeoBBox }\n//  | { \">>\": GeoBBox }\n\n//  | { \"=\": GeoBBox }\n\n /**\n  * A's bounding box is contained by B's\n  */\n | { \"@\": GeoBBox }\n//  | { \"|&>\": GeoBBox }\n//  | { \"|>>\": GeoBBox }\n\n /**\n  * A's bounding box contains B's.\n  */\n//  | { \"~\": GeoBBox }\n//  | { \"~=\": GeoBBox }\n;\nexport const GeomFilterKeys = [\"~\",\"~=\",\"@\",\"|&>\",\"|>>\", \">>\", \"=\", \"<<|\", \"<<\", \"&>\", \"&<|\", \"&<\", \"&&&\", \"&&\"] as const;\nexport const GeomFilter_Funcs =  [\n  \"ST_MakeEnvelope\", \n  \"st_makeenvelope\", \n  \"ST_MakePolygon\",\n  \"st_makepolygon\",\n] as const;\n \n\n// PG will try to cast strings to appropriate type\nexport type CastFromTSToPG<T extends AllowedTSType> = \n  T extends number ? (T | string) \n: T extends string ? (T | Date) \n: T extends boolean ? (T | string)\n: T extends Date ? (T | string)\n: T\n\nexport type FilterDataType<T extends AllowedTSType> = \n  T extends string ? TextFilter\n: T extends number ? CompareFilter<CastFromTSToPG<T>>\n: T extends boolean ? CompareFilter<CastFromTSToPG<T>>\n: T extends Date ? CompareFilter<CastFromTSToPG<T>>\n: T extends any[] ? ArrayFilter<T>\n: (CompareFilter<T> | TextFilter | GeomFilter)\n;\n\nexport const EXISTS_KEYS = [\"$exists\", \"$notExists\", \"$existsJoined\", \"$notExistsJoined\"] as const;\nexport type EXISTS_KEY = typeof EXISTS_KEYS[number];\n\n/**\n * { \n *    $filter: [\n *      { $funcName: [...args] },\n *      operand,\n *      value | funcFilter\n *    ] \n * }\n */\nexport const COMPLEX_FILTER_KEY = \"$filter\" as const;\nexport type ComplexFilter = Record<typeof COMPLEX_FILTER_KEY, [\n  { [funcName: string]: any[] },\n  typeof CompareFilterKeys[number]?,\n  any?\n]>; \n\n/**\n * Shortened filter operands\n */\n type BasicFilter<Field extends string, DataType extends any> = Partial<{\n  [K in Extract<typeof CompareFilterKeys[number], string> as ` +\n  \"`${Field}.${K}`\" +\n  `]: CastFromTSToPG<DataType>\n}> | Partial<{\n  [K in Extract<typeof CompareInFilterKeys[number], string> as ` +\n  \"`${Field}.${K}`\" +\n  `]: CastFromTSToPG<DataType>[]\n}>;\ntype StringFilter<Field extends string, DataType extends any> = BasicFilter<Field, DataType> & (Partial<{\n  [K in Extract<typeof TextFilterKeys[number], string> as ` +\n  \"`${Field}.${K}`\" +\n  `]: DataType\n}> | Partial<{\n  [K in Extract<typeof TextFilterFTSKeys[number], string> as ` +\n  \"`${Field}.${K}`\" +\n  `]: any\n}>);\nexport type ValueOf<T> = T[keyof T];\n\ntype ShorthandFilter<Obj extends Record<string, any>> = ValueOf<{\n  [K in keyof Obj]: Obj[K] extends string? StringFilter<K, Required<Obj>[K]> : BasicFilter<K, Required<Obj>[K]>;\n}>\n\n/* Traverses object keys to make filter */\nexport type FilterForObject<T extends AnyObject = AnyObject> = \n  /* { col: { $func: [\"value\"] } } */\n  | {\n    [K in keyof Partial<T>]: FilterDataType<T[K]>\n  } & Partial<ComplexFilter>\n  /**\n   * Filters with shorthand notation\n   * @example: { \"name.$ilike\": 'abc' }\n   */\n  | ShorthandFilter<T>\n;\n\nexport type ExistsFilter<S = void> = Partial<{ \n  [key in EXISTS_KEY]: S extends DBSchema? \n    ExactlyOne<{ \n      [tname in keyof S]: \n       | FullFilter<S[tname][\"columns\"], S> \n       | {\n          path: RawJoinPath[];\n          filter: FullFilter<S[tname][\"columns\"], S> \n        }\n    }> : any\n    /** ExactlyOne does not for any type. This produces error */\n    // ExactlyOne<{ \n    //   [key: string]: FullFilter<AnyObject,S> \n    // }>\n}>; \n\n \n/**\n * Filter that relates to a single column { col: 2 } or\n * an exists filter: { $exists: {  } }\n */\nexport type FilterItem<T extends AnyObject = AnyObject> = \n  | FilterForObject<T> \n\n\nexport type AnyObjIfVoid<T extends AnyObject | void> = T extends AnyObject? T : AnyObject;\n/**\n * Full filter\n * @example { $or: [ { id: 1 }, { status: 'live' } ] }\n */\nexport type FullFilter<T extends AnyObject | void, S extends DBSchema | void> = \n | { $and: FullFilter<T, S>[] } \n | { $or: FullFilter<T, S>[] } \n | FilterItem<AnyObjIfVoid<T>> \n | ExistsFilter<S>\n | ComplexFilter\n\n /** Not implemented yet */\n//  | { $not: FilterItem<T>  }\n;\n\n/**\n * Simpler FullFilter to reduce load on compilation\n */\nexport type FullFilterBasic<T = { [key: string]: any }> = {\n  [key in keyof Partial<T & { [key: string]: any }>]: any\n}\n \n\nexport const _PG_strings = [\n  'bpchar','char','varchar','text','citext','uuid','bytea', 'time','timetz','interval','name', \n  'cidr', 'inet', 'macaddr', 'macaddr8', \"int4range\", \"int8range\", \"numrange\",\n  'tsvector'\n] as const;\nexport const _PG_numbers = ['int2','int4','int8','float4','float8','numeric','money','oid'] as const;\nexport const _PG_json = ['json', 'jsonb'] as const;\nexport const _PG_bool = ['bool'] as const;\nexport const _PG_date = ['date', 'timestamp', 'timestamptz'] as const;\nexport const _PG_interval = ['interval'] as const;\nexport const _PG_postgis = ['geometry', 'geography'] as const;\nexport const _PG_geometric = [\n  \"point\", \n  \"line\", \n  \"lseg\", \n  \"box\", \n  \"path\",  \n  \"polygon\", \n  \"circle\",\n] as const;\n\nexport type PG_COLUMN_UDT_DATA_TYPE = \n    | typeof _PG_strings[number] \n    | typeof _PG_numbers[number] \n    | typeof _PG_geometric[number] \n    | typeof _PG_json[number] \n    | typeof _PG_bool[number] \n    | typeof _PG_date[number] \n    | typeof _PG_interval[number]\n    | typeof _PG_postgis[number];\n    \nconst TS_PG_PRIMITIVES = {\n  \"string\": [ ..._PG_strings, ..._PG_date, ..._PG_geometric, ..._PG_postgis, \"lseg\"],\n  \"number\": _PG_numbers,\n  \"boolean\": _PG_bool,\n  \"any\": [..._PG_json, ..._PG_interval], // consider as any\n\n  /** Timestamps are kept in original string format to avoid filters failing \n   * TODO: cast to dates if udt_name date/timestamp(0 - 3)\n  */\n  // \"Date\": _PG_date,\n} as const;\n\nexport const TS_PG_Types = {\n  ...TS_PG_PRIMITIVES,\n  \"number[]\": TS_PG_PRIMITIVES.number.map(s => \\`_\\${s}\\` as const),\n  \"boolean[]\": TS_PG_PRIMITIVES.boolean.map(s => \\`_\\${s}\\` as const),\n  \"string[]\": TS_PG_PRIMITIVES.string.map(s => \\`_\\${s}\\` as const),\n  \"any[]\": TS_PG_PRIMITIVES.any.map(s => \\`_\\${s}\\` as const),\n  // \"Date[]\": _PG_date.map(s => \\`_\\${s}\\` as const),\n    // \"any\": [],\n} as const;\nexport type TS_COLUMN_DATA_TYPES = keyof typeof TS_PG_Types;\n \nexport type ColumnInfo = {\n  name: string;\n\n  /**\n   * Column display name. Will be first non empty value from i18n data, comment, name \n   */\n  label: string;\n\n  /**\n   * Column description (if provided)\n   */\n  comment: string;\n\n  /**\n   * Ordinal position of the column within the table (count starts at 1)\n   */\n  ordinal_position: number;\n\n  /**\n   * True if column is nullable. A not-null constraint is one way a column can be known not nullable, but there may be others.\n   */\n  is_nullable: boolean;\n\n  is_updatable: boolean;\n\n  /**\n   * Simplified data type\n   */\n  data_type: string;\n\n  /**\n   * Postgres raw data types. values starting with underscore means it's an array of that data type\n   */\n  udt_name: PG_COLUMN_UDT_DATA_TYPE;\n\n  /**\n   * Element data type\n   */\n  element_type: string;\n\n  /**\n   * Element raw data type\n   */\n  element_udt_name: string;\n\n  /**\n   * PRIMARY KEY constraint on column. A table can have more then one PK\n   */\n  is_pkey: boolean;\n\n  /**\n   * Foreign key constraint \n   * A column can reference multiple tables\n   */\n  references?: {\n    ftable: string;\n    fcols: string[];\n    cols: string[];\n  }[];\n\n  /**\n   * true if column has a default value\n   * Used for excluding pkey from insert\n   */\n  has_default: boolean;\n\n  /**\n   * Column default value\n   */\n  column_default?: any;\n\n  /**\n   * Extracted from tableConfig\n   * Used in SmartForm\n   */\n  min?: string | number;\n  max?: string | number;\n  hint?: string;\n\n  jsonbSchema?: any;\n\n  /**\n   * If degined then this column is referencing the file table\n   * Extracted from FileTable config\n   * Used in SmartForm\n   */\n  file?: any;\n\n}\n\n\nexport type ValidatedColumnInfo = ColumnInfo & {\n\n  /**\n   * TypeScript data type\n   */\n  tsDataType: TS_COLUMN_DATA_TYPES;\n\n  /**\n   * Can be viewed/selected\n   */\n  select: boolean;\n\n  /**\n   * Can be ordered by\n   */\n  orderBy: boolean;\n\n  /**\n   * Can be filtered by\n   */\n  filter: boolean;\n\n  /**\n   * Can be inserted\n   */\n  insert: boolean;\n\n  /**\n   * Can be updated\n   */\n  update: boolean;\n\n  /**\n   * Can be used in the delete filter\n   */\n  delete: boolean;\n}\n\n\nexport type DBSchemaTable = {\n  name: string;\n  info: TableInfo;\n  columns: ValidatedColumnInfo[];\n};\n\n/**\n * List of fields to include or exclude\n */\nexport type FieldFilter<T extends AnyObject = AnyObject> = SelectTyped<T>\n\nexport type AscOrDesc = 1 | -1 | boolean;\n\n/**\n * @example\n * { product_name: -1 } -> SORT BY product_name DESC\n * [{ field_name: (1 | -1 | boolean) }]\n * true | 1 -> ascending\n * false | -1 -> descending\n * Array order is maintained\n * if nullEmpty is true then empty text will be replaced to null (so nulls sorting takes effect on it)\n */\nexport type _OrderBy<T extends AnyObject> = \n  | { [K in keyof Partial<T>]: AscOrDesc }\n  | { [K in keyof Partial<T>]: AscOrDesc }[]\n  | { key: keyof T, asc?: AscOrDesc, nulls?: \"last\" | \"first\", nullEmpty?: boolean }[] \n  | Array<keyof T>\n  | keyof T\n  ;\n  \nexport type OrderBy<T extends AnyObject | void = void> = T extends AnyObject? _OrderBy<T> :  _OrderBy<AnyObject>;\n\ntype CommonSelect =  \n| \"*\"\n| \"\"\n| { \"*\" : 1 }\n\nexport type SelectTyped<T extends AnyObject> = \n  | { [K in keyof Partial<T>]: 1 | true } \n  | { [K in keyof Partial<T>]: 0 | false } \n  | (keyof T)[]\n  | CommonSelect\n;\n\n\nexport const JOIN_KEYS = [\"$innerJoin\", \"$leftJoin\"] as const; \nexport const JOIN_PARAMS = [\"select\", \"filter\", \"$path\", \"$condition\", \"offset\", \"limit\", \"orderBy\"] as const;\n\nexport type JoinCondition = {\n  column: string;\n  rootColumn: string;\n} | ComplexFilter;\n\nexport type JoinPath = {\n  table: string;\n  /**\n   * {\n   *    leftColumn: \"rightColumn\"\n   * }\n   */\n  on?: Record<string, string>[];\n};\nexport type RawJoinPath = string | (JoinPath | string)[]\n\nexport type DetailedJoinSelect = Partial<Record<typeof JOIN_KEYS[number], RawJoinPath>> & {\n  select: Select;\n  filter?: FullFilter<void, void>;\n  offset?: number;\n  limit?: number;\n  orderBy?: OrderBy;\n} & (\n  { \n    $condition?: undefined;\n  } | {\n    /**\n     * If present then will overwrite $path and any inferred joins\n     */\n    $condition?: JoinCondition[];\n\n  }\n);\n\nexport type SimpleJoinSelect = \n| \"*\"\n/** Aliased Shorthand join: table_name: { ...select } */\n| Record<string, 1 | \"*\" | true | FunctionSelect> \n| Record<string, 0 | false> \n\nexport type JoinSelect = \n| SimpleJoinSelect\n| DetailedJoinSelect;\n\ntype FunctionShorthand = string;\ntype FunctionFull = Record<string, any[] | readonly any[] | FunctionShorthand>;\ntype FunctionSelect = FunctionShorthand | FunctionFull;\n/**\n * { computed_field: { funcName: [args] } }\n */\ntype FunctionAliasedSelect = Record<string, FunctionFull>;\n\ntype InclusiveSelect = true | 1 | FunctionSelect | JoinSelect;\n\ntype SelectFuncs<T extends AnyObject = AnyObject, IsTyped = false> = (\n  | ({ [K in keyof Partial<T>]: InclusiveSelect } & Record<string, IsTyped extends true? FunctionFull : InclusiveSelect>) \n  | FunctionAliasedSelect\n  | { [K in keyof Partial<T>]: true | 1 | string }\n  | { [K in keyof Partial<T>]: 0 | false }\n  | CommonSelect\n  | (keyof Partial<T>)[]\n);\n\n/** S param is needed to ensure the non typed select works fine */\nexport type Select<T extends AnyObject | void = void, S extends DBSchema | void = void> = { t: T, s: S } extends { t: AnyObject, s: DBSchema } ? SelectFuncs<T & { $rowhash: string }, true> : SelectFuncs<AnyObject & { $rowhash: string }, false>;\n \n\nexport type SelectBasic = \n  | { [key: string]: any } \n  | {} \n  | undefined \n  | \"\" \n  | \"*\" \n  ;\n\n/* Simpler types */\ntype CommonSelectParams = {\n\n  /**\n   * If null then maxLimit if present will be applied\n   * If undefined then 1000 will be applied as the default\n   */\n  limit?: number | null;\n  offset?: number;\n\n  /**\n   * Will group by all non aggregated fields specified in select (or all fields by default)\n   */\n  groupBy?: boolean;\n\n  returnType?: \n\n  /**\n   * Will return the first row as an object. Will throw an error if more than a row is returned. Use limit: 1 to avoid error.\n   */\n  | \"row\"\n\n  /**\n    * Will return the first value from the selected field\n    */\n  | \"value\"\n\n  /**\n    * Will return an array of values from the selected field. Similar to array_agg(field).\n    */\n  | \"values\"\n\n  /**\n    * Will return the sql statement. Requires publishRawSQL privileges if called by client\n    */\n  | \"statement\"\n\n  /**\n    * Will return the sql statement excluding the user header. Requires publishRawSQL privileges if called by client\n    */\n  | \"statement-no-rls\"\n\n  /**\n    * Will return the sql statement where condition. Requires publishRawSQL privileges if called by client\n    */\n  | \"statement-where\"\n\n} \n\nexport type SelectParams<T extends AnyObject | void = void, S extends DBSchema | void = void> = CommonSelectParams & {\n  select?: Select<T, S>;\n  orderBy?: OrderBy<S extends DBSchema? T : void>;\n}\nexport type SubscribeParams<T extends AnyObject | void = void, S extends DBSchema | void = void> = SelectParams<T, S> & {\n  throttle?: number;\n  throttleOpts?: {\n    /** \n     * False by default. \n     * If true then the first value will be emitted at the end of the interval. Instant otherwise \n     * */\n    skipFirst?: boolean;\n  };\n};\n\nexport type UpdateParams<T extends AnyObject | void = void, S extends DBSchema | void = void> = {\n  returning?: Select<T, S>;\n  onConflict?: \"DoUpdate\" | \"DoNothing\";\n  removeDisallowedFields?: boolean;\n\n  /* true by default. If false the update will fail if affecting more than one row */\n  multi?: boolean;\n} & Pick<CommonSelectParams, \"returnType\">;\n\nexport type InsertParams<T extends AnyObject | void = void, S extends DBSchema | void = void> = {\n  returning?: Select<T, S>;\n  onConflict?: \"DoUpdate\" | \"DoNothing\";\n  removeDisallowedFields?: boolean;\n} & Pick<CommonSelectParams, \"returnType\">;\n\nexport type DeleteParams<T extends AnyObject | void = void, S extends DBSchema | void = void> = {\n  returning?: Select<T, S>;\n} & Pick<CommonSelectParams, \"returnType\">;\n\nexport type PartialLax<T = AnyObject> = Partial<T>;\n\nexport type TableInfo = {\n  /**\n   * OID from the postgres database\n   */\n  oid: number;\n  /**\n   * Comment from the postgres database\n   */\n  comment?: string;\n  /**\n   * Defined if this is the fileTable\n   */\n  isFileTable?: {\n    /** \n     * Defined if direct inserts are disabled. \n     * Only nested inserts through the specified tables/columns are allowed\n     * */\n    allowedNestedInserts?: {\n      table: string;\n      column: string;\n    }[] | undefined;\n  };\n\n  /**\n   * True if fileTable is enabled and this table references the fileTable\n   */\n  hasFiles?: boolean;\n\n  isView?: boolean;\n\n  /**\n   * Name of the fileTable (if enabled)\n   */\n  fileTableName?: string;\n\n  /**\n   * Used for getColumns in cases where the columns are dynamic based on the request.\n   * See dynamicFields from Update rules\n   */\n  dynamicRules?: {\n    update?: boolean;\n  }\n\n  /**\n   * Additional table info provided through TableConfig\n   */\n  info?: {\n    label?: string;\n  }\n}\n\nexport type OnError = (err: any) => void;\n\ntype JoinedSelect = Record<string, Select>;\n\ntype ParseSelect<Select extends SelectParams<TD>[\"select\"], TD extends AnyObject> = \n(Select extends { \"*\": 1 }? Required<TD> : {})\n& {\n  [Key in keyof Omit<Select, \"*\">]: Select[Key] extends 1? Required<TD>[Key] : \n    Select[Key] extends Record<string, any[]>? any : //Function select\n    Select[Key] extends JoinedSelect? any[] : \n    any;\n}\n\ntype GetSelectDataType<S extends DBSchema | void, O extends SelectParams<TD, S>, TD extends AnyObject> = \n  O extends { returnType: \"value\" }? any : \n  O extends { returnType: \"values\"; select: Record<string, 1> }? ValueOf<Pick<Required<TD>, keyof O[\"select\"]>> : \n  O extends { returnType: \"values\" }? any : \n  O extends { select: \"*\" }? Required<TD> : \n  O extends { select: \"\" }? Record<string, never> : \n  O extends { select: Record<string, 0> }? Omit<Required<TD>, keyof O[\"select\"]> : \n  O extends { select: Record<string, any> }? ParseSelect<O[\"select\"], Required<TD>> : \n  Required<TD>;\n\nexport type GetSelectReturnType<S extends DBSchema | void, O extends SelectParams<TD, S>, TD extends AnyObject, isMulti extends boolean> = \n  O extends { returnType: \"statement\" }? string : \n  isMulti extends true? GetSelectDataType<S, O, TD>[] :\n  GetSelectDataType<S, O, TD>;\n\ntype GetReturningReturnType<O extends UpdateParams<TD, S>, TD extends AnyObject, S extends DBSchema | void = void> = \n  O extends { returning: \"*\" }? Required<TD> : \n  O extends { returning: \"\" }? Record<string, never> : \n  O extends { returning: Record<string, 1> }? Pick<Required<TD>, keyof O[\"returning\"]> : \n  O extends { returning: Record<string, 0> }? Omit<Required<TD>, keyof O[\"returning\"]> : \n  void;\n\ntype GetUpdateReturnType<O extends UpdateParams<TD, S>, TD extends AnyObject, S extends DBSchema | void = void> = \n  O extends { multi: false }? \n    GetReturningReturnType<O, TD, S> : \n    GetReturningReturnType<O, TD, S>[];\n\ntype GetInsertReturnType<Data extends AnyObject | AnyObject[], O extends UpdateParams<TD, S>, TD extends AnyObject, S extends DBSchema | void = void> = \n  Data extends any[]? \n    GetReturningReturnType<O, TD, S>[] :\n    GetReturningReturnType<O, TD, S>;\n\nexport type SubscriptionHandler = {\n  unsubscribe: () => Promise<any>;\n  filter: FullFilter<void, void> | {};\n}\n\ntype GetColumns = (lang?: string, params?: { rule: \"update\", data: AnyObject, filter: AnyObject }) => Promise<ValidatedColumnInfo[]>;\n\nexport type ViewHandler<TD extends AnyObject = AnyObject, S extends DBSchema | void = void> = {\n  getInfo?: (lang?: string) => Promise<TableInfo>;\n  getColumns?: GetColumns\n  find: <P extends SelectParams<TD, S>>(filter?: FullFilter<TD, S>, selectParams?: P) => Promise<GetSelectReturnType<S, P, TD, true>>;\n  findOne: <P extends SelectParams<TD, S>>(filter?: FullFilter<TD, S>, selectParams?: P) => Promise<undefined | GetSelectReturnType<S, P, TD, false>>;\n  subscribe: <P extends SubscribeParams<TD, S>>(\n    filter: FullFilter<TD, S>, \n    params: P, \n    onData: (items: GetSelectReturnType<S, P, TD, true>) => any,\n    onError?: OnError\n  ) => Promise<SubscriptionHandler>;\n  subscribeOne: <P extends SubscribeParams<TD, S>>(\n    filter: FullFilter<TD, S>, \n    params: P, \n    onData: (item: GetSelectReturnType<S, P, TD, false> | undefined) => any, \n    onError?: OnError\n  ) => Promise<SubscriptionHandler>;\n  count: <P extends SelectParams<TD, S>>(filter?: FullFilter<TD, S>, selectParams?: P) => Promise<number>;\n  /**\n   * Returns result size in bits\n   */\n  size: <P extends SelectParams<TD, S>>(filter?: FullFilter<TD, S>, selectParams?: P) => Promise<string>;\n}\n\nexport type UpsertDataToPGCast<TD extends AnyObject = AnyObject> = {\n  [K in keyof TD]: CastFromTSToPG<TD[K]>\n};\n\ntype UpsertDataToPGCastLax<T extends AnyObject> = PartialLax<UpsertDataToPGCast<T>>;\ntype InsertData<T extends AnyObject> = UpsertDataToPGCast<T> | UpsertDataToPGCast<T>[]\n\nexport type TableHandler<TD extends AnyObject = AnyObject, S extends DBSchema | void = void> = ViewHandler<TD, S> & {\n  update: <P extends UpdateParams<TD, S>>(filter: FullFilter<TD, S>, newData: UpsertDataToPGCastLax<TD>, params?: P) => Promise<GetUpdateReturnType<P ,TD, S> | undefined>;\n  updateBatch: <P extends UpdateParams<TD, S>>(data: [FullFilter<TD, S>, UpsertDataToPGCastLax<TD>][], params?: P) => Promise<GetUpdateReturnType<P ,TD, S> | void>;\n  upsert: <P extends UpdateParams<TD, S>>(filter: FullFilter<TD, S>, newData: UpsertDataToPGCastLax<TD>, params?: P) => Promise<GetUpdateReturnType<P ,TD, S>>; \n  insert: <P extends InsertParams<TD, S>, D extends InsertData<TD>>(data: D, params?: P ) => Promise<GetInsertReturnType<D, P ,TD, S>>;\n  delete: <P extends DeleteParams<TD, S>>(filter?: FullFilter<TD, S>, params?: P) => Promise<GetUpdateReturnType<P ,TD, S> | undefined>;\n} \n\nexport type JoinMakerOptions<TT extends AnyObject = AnyObject> = SelectParams<TT> & { path?: RawJoinPath };\nexport type JoinMaker<TT extends AnyObject = AnyObject, S extends DBSchema | void = void> = (filter?: FullFilter<TT, S>, select?: Select<TT>, options?: JoinMakerOptions<TT> ) => any;\nexport type JoinMakerBasic = (filter?: FullFilterBasic, select?: SelectBasic, options?: SelectParams & { path?: RawJoinPath }) => any;\n\nexport type TableJoin = {\n  [key: string]: JoinMaker;\n}\nexport type TableJoinBasic = {\n  [key: string]: JoinMakerBasic;\n}\n\nexport type DbJoinMaker = {\n  innerJoin: TableJoin;\n  leftJoin: TableJoin;\n  innerJoinOne: TableJoin;\n  leftJoinOne: TableJoin;\n} \n\nexport type TableHandlers = {\n\t[key: string]: Partial<TableHandler> | TableHandler;\n};\n\n\nexport type SocketSQLStreamPacket = {\n\ttype: \"data\";\n\tfields?: any[];\n\trows: any[];\n\tended?: boolean;\n\tinfo?: SQLResultInfo;\n\tprocessId: number;\n  } | {\n\ttype: \"error\";\n\terror: any;\n  };\n  export type SocketSQLStreamServer = {\n\tchannel: string;\n\tunsubChannel: string;\n  };\n  export type SocketSQLStreamHandlers = {\n\trun: (query: string, params?: any | any[]) => Promise<void>;\n\tstop: (terminate?: boolean) => Promise<void>;\n  };\n  export type SocketSQLStreamClient = SocketSQLStreamServer & {\n\tstart: (listener: (packet: SocketSQLStreamPacket) => void) => Promise<SocketSQLStreamHandlers>\n  };\n  \n\nexport type DBNoticeConfig = {\n\tsocketChannel: string;\n\tsocketUnsubChannel: string;\n  }\nexport type DBNotifConfig = DBNoticeConfig & {\n\tnotifChannel: string;\n  }\n  \n  \n  export type SQLOptions = {\n\t/**\n\t * Return type of the query\n\t */\n\treturnType?: Required<SelectParams>[\"returnType\"] | \"statement\" | \"rows\" | \"noticeSubscription\" | \"arrayMode\" | \"stream\";\n\t\n\t/**\n\t * If allowListen not specified and a LISTEN query is issued then expect error\n\t */\n\tallowListen?: boolean;\n  \n\t/**\n\t * Positive integer that works only with returnType=\"stream\". \n\t * If provided then the query will be cancelled when the specified number of rows have been streamed \n\t */\n\tstreamLimit?: number;\n\t\n\t/**\n\t * If true then the connection will be persisted and used for subsequent queries\n\t */\n\tpersistStreamConnection?: boolean;\n  \n\t/**\n\t * connectionId of the stream connection to use\n\t * Acquired from the first query with persistStreamConnection=true\n\t */\n\tstreamConnectionId?: string;\n  \n\t/**\n\t * If false then the query will not be checked for params. Used to ignore queries with param like text \n\t * Defaults to true\n\t */\n\thasParams?: boolean;\n  };\n  \n  export type SQLRequest = {\n\tquery: string;\n\tparams?: any | any[];\n\toptions?:  SQLOptions\n  }\n  \n  export type NotifSubscription = {\n\tsocketChannel: string;\n\tsocketUnsubChannel: string;\n\tnotifChannel: string;\n  }\n  \n  export type NoticeSubscription = {\n\tsocketChannel: string;\n\tsocketUnsubChannel: string;\n  }\n\nexport type DBEventHandles = {\n\tsocketChannel: string;\n\tsocketUnsubChannel: string;\n\taddListener: (listener: (event: any) => void) => { removeListener: () => void; } \n  };\n  \n  export type CheckForListen<T, O extends SQLOptions> = O[\"allowListen\"] extends true? (DBEventHandles | T) : T;\n  \nexport type SQLResultInfo = {\n\tcommand: \"SELECT\" | \"UPDATE\" | \"DELETE\" | \"CREATE\" | \"ALTER\" | \"LISTEN\" | \"UNLISTEN\" | \"INSERT\" | string;\n\trowCount: number;\n\tduration: number;\n  }\n  export type SQLResult<T extends SQLOptions[\"returnType\"]> = SQLResultInfo & {\n\trows: (T extends \"arrayMode\"? any : AnyObject)[];\n\tfields: {\n\t  name: string;\n\t  dataType: string;\n\t  udt_name: PG_COLUMN_UDT_DATA_TYPE;\n\t  tsDataType: TS_COLUMN_DATA_TYPES;\n\t  tableID?: number;\n\t  tableName?: string; \n\t  tableSchema?: string; \n\t  columnID?: number;\n\t  columnName?: string;\n\t}[];\n  }\n  export type GetSQLReturnType<O extends SQLOptions> = CheckForListen<\n\t(\n\t  O[\"returnType\"] extends \"row\"? AnyObject | null :\n\t  O[\"returnType\"] extends \"rows\"? AnyObject[] :\n\t  O[\"returnType\"] extends \"value\"? any | null :\n\t  O[\"returnType\"] extends \"values\"? any[] :\n\t  O[\"returnType\"] extends \"statement\"? string :\n\t  O[\"returnType\"] extends \"noticeSubscription\"? DBEventHandles :\n\t  O[\"returnType\"] extends \"stream\"? SocketSQLStreamClient :\n\t  SQLResult<O[\"returnType\"]>\n\t)\n  , O>;\n  \n  export type SQLHandler = \n  /**\n   * \n   * @param query <string> query. e.g.: SELECT * FROM users;\n   * @param params <any[] | object> query arguments to be escaped. e.g.: { name: 'dwadaw' }\n   * @param options <object> { returnType: \"statement\" | \"rows\" | \"noticeSubscription\" }\n   */\n  <Opts extends SQLOptions>(\n\tquery: string, \n\targs?: AnyObject | any[], \n\toptions?: Opts,\n\tserverSideOptions?: {\n\t  socket: any\n\t} | { \n\t  httpReq: any;\n\t}\n  ) => Promise<GetSQLReturnType<Opts>>\n\n  \nexport type DbTxTableHandlers = {\n\t[key: string]: Omit<Partial<TableHandler>, \"dbTx\"> | Omit<TableHandler, \"dbTx\">;\n};\nexport type TxCB<TH = DbTxTableHandlers> = {\n\t(t: TH & Pick<DBHandlerServer, \"sql\">, _t: any): (any | void);\n};\nexport type TX<TH = TableHandlers> = {\n\t(t: TxCB<TH>): Promise<(any | void)>;\n};\nexport type DBHandlerServer<TH = TableHandlers> = TH & Partial<DbJoinMaker> & {\n\tsql?: SQLHandler;\n} & {\n\ttx?: TX<TH>;\n};\nexport type DBTableHandlersFromSchema<Schema = void> = Schema extends DBSchema ? {\n\t[tov_name in keyof Schema]: Schema[tov_name][\"is_view\"] extends true ? ViewHandler<Schema[tov_name][\"columns\"]> : TableHandler<Schema[tov_name][\"columns\"]>;\n} : Record<string, TableHandler>;\nexport type DBOFullyTyped<Schema = void> = (DBTableHandlersFromSchema<Schema> & Pick<DBHandlerServer<DBTableHandlersFromSchema<Schema>>, \"tx\" | \"sql\">);\n`;\n\nexport const pgPromiseDb = `\n\ndeclare namespace pgPromise {\n\n  enum queryResult {\n    one = 1,\n    many = 2,\n    none = 4,\n    any = 6\n  }\n  interface IColumn {\n    name: string\n    oid: number\n    dataTypeID: number\n\n    // NOTE: properties below are not available within Native Bindings:\n\n    tableID: number\n    columnID: number\n    dataTypeSize: number\n    dataTypeModifier: number\n    format: string\n  }\n\n  interface IResult<T = unknown> extends Iterable<T> {\n    command: string\n    rowCount: number\n    rows: T[]\n    fields: IColumn[]\n\n    // properties below are not available within Native Bindings:\n    rowAsArray: boolean\n\n    _types: {\n      _types: any,\n      text: any,\n      binary: any\n    };\n    _parsers: Array<Function>;\n  }\n\n  interface IResult<T = unknown> extends Iterable<T> {\n    command: string\n    rowCount: number\n    rows: T[]\n    fields: IColumn[]\n\n    // properties below are not available within Native Bindings:\n    rowAsArray: boolean\n\n    _types: {\n      _types: any,\n      text: any,\n      binary: any\n    };\n    _parsers: Array<Function>;\n  }\n\n  interface IResultExt<T = unknown> extends IResult<T> {\n    // Property 'duration' exists only in the following context:\n    //  - for single-query events 'receive'\n    //  - for method Database.result\n    duration?: number\n  }\n\n  // Event context extension for tasks + transactions;\n  // See: https://vitaly-t.github.io/pg-promise/global.html#TaskContext\n  interface ITaskContext {\n\n    // these are set in the beginning of each task/transaction:\n    readonly context: any\n    readonly parent: ITaskContext | null\n    readonly connected: boolean\n    readonly inTransaction: boolean\n    readonly level: number\n    readonly useCount: number\n    readonly isTX: boolean\n    readonly start: Date\n    readonly tag: any\n    readonly dc: any\n\n    // these are set at the end of each task/transaction:\n    readonly finish?: Date\n    readonly duration?: number\n    readonly success?: boolean\n    readonly result?: any\n\n    // this exists only inside transactions (isTX = true):\n    readonly txLevel?: number\n\n    // Version of PostgreSQL Server to which we are connected;\n    // This property is not available with Native Bindings!\n    readonly serverVersion: string\n  }\n\n  // Additional methods available inside tasks + transactions;\n  // API: https://vitaly-t.github.io/pg-promise/Task.html\n  interface ITask {\n    readonly ctx: ITaskContext\n  }\n\n  interface ITaskIfOptions<Ext = {}> {\n    cnd?: boolean | ((t: ITask) => boolean)\n    tag?: any\n  }\n\n  interface ITxIfOptions<Ext = {}> extends ITaskIfOptions<Ext> {\n    mode?: any | null\n    reusable?: boolean | ((t: ITask) => boolean)\n  }\n\n  // Base database protocol\n  // API: https://vitaly-t.github.io/pg-promise/Database.html\n  interface DB {\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#query\n    query<T = any>(query: string, values?: any, qrm?: queryResult): Promise<T>\n\n    // result-specific methods;\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#none\n    none(query: string, values?: any): Promise<null>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#one\n    one<T = any>(query: string, values?: any, cb?: (value: any) => T, thisArg?: any): Promise<T>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#oneOrNone\n    oneOrNone<T = any>(query: string, values?: any, cb?: (value: any) => T, thisArg?: any): Promise<T | null>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#many\n    many<T = any>(query: string, values?: any): Promise<T[]>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#manyOrNone\n    manyOrNone<T = any>(query: string, values?: any): Promise<T[]>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#any\n    any<T = any>(query: string, values?: any): Promise<T[]>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#result\n    result<T, R = IResultExt<T>>(query: string, values?: any, cb?: (value: IResultExt<T>) => R, thisArg?: any): Promise<R>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#multiResult\n    multiResult(query: string, values?: any): Promise<IResult[]>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#multi\n    multi<T = any>(query: string, values?: any): Promise<Array<T[]>>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#stream\n    stream(qs: NodeJS.ReadableStream, init: (stream: NodeJS.ReadableStream) => void): Promise<{\n      processed: number,\n      duration: number\n    }>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#func\n    func<T = any>(funcName: string, values?: any, qrm?: queryResult): Promise<T>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#proc\n    proc<T = any>(procName: string, values?: any, cb?: (value: any) => T, thisArg?: any): Promise<T | null>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#map\n    map<T = any>(query: string, values: any, cb: (row: any, index: number, data: any[]) => T, thisArg?: any): Promise<T[]>\n\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#each\n    each<T = any>(query: string, values: any, cb: (row: any, index: number, data: any[]) => void, thisArg?: any): Promise<T[]>\n\n    // Tasks;\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#task\n    task<T>(cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    task<T>(tag: string | number, cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    task<T>(options: { tag?: any }, cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    // Conditional Tasks;\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#taskIf\n    taskIf<T>(cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    taskIf<T>(tag: string | number, cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    taskIf<T>(options: ITaskIfOptions, cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    // Transactions;\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#tx\n    tx<T>(cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    tx<T>(tag: string | number, cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    tx<T>(options: {\n      tag?: any,\n      mode?: any\n    }, cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    // Conditional Transactions;\n    // API: https://vitaly-t.github.io/pg-promise/Database.html#txIf\n    txIf<T>(cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    txIf<T>(tag: string | number, cb: (t: ITask) => T | Promise<T>): Promise<T>\n\n    txIf<T>(options: ITxIfOptions, cb: (t: ITask) => T | Promise<T>): Promise<T>\n  }\n}`;\n"
  },
  {
    "path": "client/src/dashboard/CodeEditor/registerLogLang.ts",
    "content": "/* eslint-disable no-useless-escape */\n\nimport type { Monaco } from \"../W_SQL/monacoEditorTypes\";\n\nlet logThemeLoaded = false;\nexport const LOG_LANGUAGE_ID = \"log\";\nexport const LOG_LANGUAGE_THEME = \"logview\";\nexport const registerLogLang = (monaco: Monaco) => {\n  if (logThemeLoaded) return;\n  logThemeLoaded = true;\n  monaco.languages.register({ id: LOG_LANGUAGE_ID });\n\n  const logCustomRules = [];\n  const themeRules = [];\n\n  monaco.languages.setMonarchTokensProvider(LOG_LANGUAGE_ID, {\n    keywords: [\"error\", \"warning\", \"info\", \"success\"],\n    date: /\\[[0-9]{2}:[0-9]{2}:[0-9]{2}\\]/,\n    defaultToken: \"\",\n    tokenPostfix: \".log\",\n    tokenizer: {\n      root: [\n        // Match timestamp like [HH:mm:ss]\n        [/@date/, \"date-token\"],\n        // Match log type like <info>, <error>, etc.\n        [\n          /<(\\w+)>/,\n          {\n            cases: {\n              \"$1@keywords\": { token: \"$1-token\", next: \"@log.$1\" },\n              \"@default\": \"string\",\n            },\n          },\n        ],\n        // Custom rules\n        ...logCustomRules,\n        // Trace/Verbose\n        [/\\b(Trace)\\b:/, \"verbose\"],\n        // Serilog VERBOSE\n        [/\\[(verbose|verb|vrb|vb|v)\\]/i, \"verbose\"],\n        // Android logcat Verboce\n        [/\\bV\\//, \"verbose\"],\n        // DEBUG\n        [/\\b(DEBUG|Debug)\\b|\\b([dD][eE][bB][uU][gG])\\:/, \"debug\"],\n        // Serilog DEBUG\n        [/\\[(debug|dbug|dbg|de|d)\\]/i, \"debug\"],\n        // Android logcat Debug\n        [/\\bD\\//, \"debug\"],\n        // INFO\n        [\n          /\\b(HINT|INFO|INFORMATION|Info|NOTICE|II)\\b|\\b([iI][nN][fF][oO]|[iI][nN][fF][oO][rR][mM][aA][tT][iI][oO][nN])\\:/,\n          \"info\",\n        ],\n        // serilog INFO\n        [/\\[(information|info|inf|in|i)\\]/i, \"info\"],\n        // Android logcat Info\n        [/\\bI\\//, \"info\"],\n        // WARN\n        [\n          /\\b(WARNING|WARN|Warn|WW)\\b|\\b([wW][aA][rR][nN][iI][nN][gG])\\:/,\n          \"warning\",\n        ],\n        // Serilog WARN\n        [/\\[(warning|warn|wrn|wn|w)\\]/i, \"warning\"],\n        // Android logcat Warning\n        [/\\bW\\//, \"warning\"],\n        // ERROR\n        [\n          /\\b(ALERT|CRITICAL|EMERGENCY|ERROR|FAILURE|FAIL|Fatal|FATAL|Error|EE)\\b|\\b([eE][rR][rR][oO][rR])\\:/,\n          \"error\",\n        ],\n        // Serilog ERROR\n        [/\\[(error|eror|err|er|e|fatal|fatl|ftl|fa|f)\\]/i, \"error\"],\n        // Android logcat Error\n        [/\\bE\\//, \"error\"],\n        // ISO dates (\"2020-01-01\")\n        [/\\b\\d{4}-\\d{2}-\\d{2}(T|\\b)/, \"date\"],\n        // Culture specific dates (\"01/01/2020\", \"01.01.2020\")\n        [/\\b\\d{2}[^\\w\\s]\\d{2}[^\\w\\s]\\d{4}\\b/, \"date\"],\n\n        // Git commit hashes of length 40, 10, or 7\n        [/\\b([0-9a-fA-F]{40}|[0-9a-fA-F]{10}|[0-9a-fA-F]{7})\\b/, \"constant\"],\n        // Guids\n        [\n          /[0-9a-fA-F]{8}[-]?([0-9a-fA-F]{4}[-]?){3}[0-9a-fA-F]{12}/,\n          \"constant\",\n        ],\n        // Constants\n        [/\\b([0-9]+|true|false|null)\\b/, \"constant\"],\n        // String constants\n        [/\"[^\"]*\"/, \"string\"],\n        [/(?<![\\w])'[^']*'/, \"string\"],\n        // Exception type names\n        [/\\b([a-zA-Z.]*Exception)\\b/, \"exceptiontype\"],\n        // Colorize rows of exception call stacks\n        [/^[\\t ]*at.*$/, \"exception\"],\n        // Match Urls\n        [/\\b(http|https|ftp|file):\\/\\/\\S+\\b\\/?/, \"constant\"],\n        // Match character and . sequences (such as namespaces) as well as file names and extensions (e.g. bar.txt)\n        [/(?<![\\w/\\\\])([\\w-]+\\.)+([\\w-])+(?![\\w/\\\\])/, \"constant\"],\n      ],\n      // Log content state\n      log: [\n        // Exit when next timestamp found\n        [/@date/, { token: \"@rematch\", next: \"@pop\" }],\n        // Color rest of line based on log level\n        [/.*/, { token: \"$S2-token\" }],\n      ],\n    },\n  });\n\n  monaco.editor.defineTheme(LOG_LANGUAGE_THEME, {\n    base: \"vs\",\n    inherit: true,\n    rules: [\n      { token: \"info.log\", foreground: \"#4b71ca\" },\n      { token: \"error.log\", foreground: \"#ff0000\", fontStyle: \"bold\" },\n      { token: \"warning.log\", foreground: \"#FFA500\" },\n      { token: \"date.log\", foreground: \"#008800\" },\n      { token: \"constant\", foreground: \"#00891f\" },\n      { token: \"exceptiontype.log\", foreground: \"#808080\" },\n      ...themeRules,\n    ],\n    colors: {\n      \"editor.lineHighlightBackground\": \"#ffffff\",\n      \"editorGutter.background\": \"#f7f7f7\",\n    },\n  });\n};\n"
  },
  {
    "path": "client/src/dashboard/CodeEditor/utils/getMonacoJsonSchemas.ts",
    "content": "import { isObject, omitKeys } from \"prostgles-types\";\nimport { getMonaco } from \"../../SQLEditor/W_SQLEditor\";\nimport type { CodeEditorProps, MonacoJSONSchema } from \"../CodeEditor\";\n\nexport const getMonacoJsonSchemas = async (\n  language: CodeEditorProps[\"language\"],\n): Promise<MonacoJSONSchema[] | undefined> => {\n  const monaco = await getMonaco();\n  const jsonSchemas =\n    isObject(language) && language.lang === \"json\" ?\n      language.jsonSchemas\n    : undefined;\n  if (!jsonSchemas) return;\n  const schemas = jsonSchemas.map((s) => {\n    const { id, schema } = s;\n    const fileId = id + \".json\";\n    const theUri = monaco.Uri.parse(\"internal://server/\" + fileId);\n    const uri = theUri.toString();\n    return {\n      id,\n      fileId,\n      theUri,\n      uri,\n      schema: omitKeys(schema, [\"$id\", \"$schema\"]),\n      fileMatch: [uri],\n      // fileMatch: [`*`]\n    };\n  });\n\n  return schemas;\n};\n"
  },
  {
    "path": "client/src/dashboard/CodeEditor/utils/setMonacoErrorMarkers.ts",
    "content": "import type { editor } from \"monaco-editor\";\nimport type { CodeEditorProps } from \"../CodeEditor\";\nimport type { MonacoEditorImport } from \"./useSetMonacoTsLibraries\";\n\nexport const setMonacoErrorMarkers = (\n  editor: editor.IStandaloneCodeEditor,\n  monaco: MonacoEditorImport,\n  data:\n    | { error: CodeEditorProps[\"error\"] }\n    | { markers: CodeEditorProps[\"markers\"] },\n) => {\n  if (\"error\" in data) {\n    const { error } = data;\n    const model = editor.getModel();\n    if (!error && model) monaco.editor.setModelMarkers(model, \"test\", []);\n    else if (error && model) {\n      let offset = {};\n      if (typeof error.position === \"number\") {\n        let pos = error.position - 1 || 1;\n        let len = error.length || 10;\n        const sel = editor.getSelection();\n        const selection = !sel ? undefined : model.getValueInRange(sel);\n        // let selectionOffset = 0;\n        if (selection && sel) {\n          len = Math.max(1, Math.min(len, selection.length));\n          pos += model.getOffsetAt({\n            column: sel.startColumn,\n            lineNumber: sel.startLineNumber,\n          });\n        }\n\n        const s = model.getPositionAt(pos);\n        const e = model.getPositionAt(pos + len);\n        offset = {\n          startLineNumber: s.lineNumber,\n          startColumn: s.column,\n          endLineNumber: e.lineNumber,\n          endColumn: e.column,\n        };\n        editor.setPosition(s);\n        editor.revealLine(s.lineNumber);\n        editor.revealLineInCenter(s.lineNumber);\n      }\n      monaco.editor.setModelMarkers(model, \"test\", [\n        {\n          severity: 0,\n          // @ts-ignore\n          message: \"error.message\",\n          startLineNumber: 0,\n          startColumn: 0,\n          endLineNumber: 0,\n          endColumn: 5,\n          code: \"error.code\",\n          ...error,\n          ...offset,\n        },\n      ]);\n    }\n  } else if (\"markers\" in data && Array.isArray(data.markers)) {\n    const { markers } = data;\n    const model = editor.getModel();\n    if (model) {\n      monaco.editor.setModelMarkers(model, \"test2\", []);\n      monaco.editor.setModelMarkers(model, \"test2\", markers);\n    }\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/CodeEditor/utils/useSetMonacoJsonSchemas.ts",
    "content": "import type { editor } from \"monaco-editor\";\nimport { useEffectDeep } from \"prostgles-client\";\nimport { getMonaco } from \"../../SQLEditor/W_SQLEditor\";\nimport type { LanguageConfig } from \"../CodeEditor\";\nimport { getMonacoJsonSchemas } from \"./getMonacoJsonSchemas\";\n\nexport const useSetMonacoJsonSchemas = (\n  editor: editor.IStandaloneCodeEditor | undefined,\n  value: string,\n  languageObj: LanguageConfig | undefined,\n) => {\n  useEffectDeep(() => {\n    setMonacoEditorJsonSchemas(editor, value, languageObj);\n  }, [editor, languageObj]);\n};\n\nexport const setMonacoEditorJsonSchemas = async (\n  editor: editor.IStandaloneCodeEditor | undefined,\n  value: string,\n  languageObj: LanguageConfig | undefined,\n) => {\n  if (!editor || !languageObj || languageObj.lang !== \"json\") return;\n  const monaco = await getMonaco();\n  const mySchemas = await getMonacoJsonSchemas(languageObj);\n  if (!mySchemas) return;\n  const currentSchemas =\n    monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas ?? [];\n\n  const existingUris = new Set(currentSchemas.map((s) => s.uri));\n  const newSchemas = mySchemas.filter((s) => !existingUris.has(s.uri));\n  const allSchemas = [...currentSchemas, ...newSchemas];\n  monaco.languages.json.jsonDefaults.setDiagnosticsOptions({\n    // schemaRequest: \"warning\",\n    enableSchemaRequest: true,\n    validate: true,\n    schemas: allSchemas,\n  });\n\n  // SQL Editor options not working if opened twice\n  const models = monaco.editor.getModels();\n  const matchingModel = models.find(\n    (m) => m.uri.path === mySchemas[0]?.theUri.path,\n  );\n  if (!matchingModel) {\n    try {\n      const newModel = monaco.editor.createModel(\n        /** Why might be undefined?! */\n        (value as string | undefined) ?? editor.getValue(),\n        \"json\",\n        mySchemas[0]?.theUri,\n      );\n      editor.setModel(newModel);\n    } catch (error) {\n      console.log(error);\n    }\n  } else if (editor.getModel()?.id !== matchingModel.id) {\n    editor.setModel(matchingModel);\n    editor.setValue(value);\n    return true;\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/CodeEditor/utils/useSetMonacoTsLibraries.ts",
    "content": "import type { editor } from \"monaco-editor\";\nimport type { CodeEditorProps, LanguageConfig } from \"../CodeEditor\";\nimport { useEffect } from \"react\";\nimport { useEffectDeep, useIsMounted } from \"prostgles-client\";\n\nexport type MonacoEditorImport = typeof import(\"monaco-editor\");\n\nexport const useSetMonacoTsLibraries = (\n  editor: editor.IStandaloneCodeEditor | undefined,\n  languageObj: LanguageConfig | undefined,\n  monaco: MonacoEditorImport | undefined,\n  value: string,\n  onTSLibraryChange: CodeEditorProps[\"onTSLibraryChange\"],\n) => {\n  const getIsMounted = useIsMounted();\n  useEffect(() => {\n    if (!monaco) return;\n    setTSoptions(monaco);\n  }, [monaco]);\n\n  useEffectDeep(() => {\n    if (!monaco || !editor || languageObj?.lang !== \"typescript\") return;\n    const { tsLibraries, modelFileName } = languageObj;\n    if (!tsLibraries) return;\n    monaco.languages.typescript.typescriptDefaults.setExtraLibs(tsLibraries);\n    /* \n        THIS CLOSES ALL OTHER EDITORS \n        This is/was? needed to prevent this error: Type annotations can only be used in TypeScript files. \n      */\n    // monaco.editor.getModels().forEach(model => model.dispose());\n\n    const modelUri = monaco.Uri.parse(`file:///${modelFileName}.ts`);\n    const existingModel = monaco.editor\n      .getModels()\n      .find((m) => m.uri.path === modelUri.path);\n    const model =\n      existingModel ?? monaco.editor.createModel(value, \"typescript\", modelUri);\n\n    if (!getIsMounted()) return;\n    try {\n      editor.setModel(model);\n    } catch (e) {\n      console.error(e);\n    }\n    onTSLibraryChange?.(tsLibraries);\n  }, [editor, monaco, languageObj, onTSLibraryChange]);\n};\n\nconst setTSoptions = (monaco: MonacoEditorImport) => {\n  monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);\n\n  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({\n    target: monaco.languages.typescript.ScriptTarget.ES2020,\n    allowNonTsExtensions: true,\n    moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,\n    module: monaco.languages.typescript.ModuleKind.CommonJS,\n    noEmit: true,\n    allowJs: true,\n    checkJs: true,\n    esModule: false,\n    experimentalDecorators: true,\n    keyofStringsOnly: true,\n    /** Adding this line breaks inbuild functions (setTimeout, etc) */\n    // lib: [\"ES2017\", \"es2019\", \"ES2021.String\", \"ES2020\", \"ES2022\"],\n    esModuleInterop: true,\n    allowSyntheticDefaultImports: true,\n    declaration: true,\n    declarationMap: true,\n    ignoreDeprecations: \"5.0\",\n    strict: true,\n    skipLibCheck: true,\n    typeRoots: [\"node_modules/@types\"],\n  });\n\n  // extra libraries\n  // monaco.languages.typescript.typescriptDefaults.addExtraLib(\n  //   `export declare function next() : string`,\n  //   'node_modules/@types/external/index.d.ts'\n  // );\n  // monaco.languages.typescript.javascriptDefaults.addExtraLib(getExtraLib(\"node/globals.d.ts\"),  monaco.Uri.parse(\"ts:filename/globals.d.ts\"));\n  // monaco.languages.typescript.javascriptDefaults.addExtraLib(getExtraLib(\"node/fs.d.ts\"),  monaco.Uri.parse(\"ts:filename/ts.d.ts\"));\n\n  monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({\n    noSemanticValidation: false,\n    noSyntaxValidation: false,\n  });\n};\n"
  },
  {
    "path": "client/src/dashboard/CodeExample.tsx",
    "content": "import React from \"react\";\nimport type { CodeEditorProps } from \"./CodeEditor/CodeEditor\";\nimport { CodeEditor } from \"./CodeEditor/CodeEditor\";\n\ntype P = CodeEditorProps & {\n  header?: React.ReactNode;\n};\nconst CodeExample = ({ header, ...props }: P) => {\n  return (\n    <div\n      className={\n        \"flex-col gap-p1 f-1 b b-color rounded o-hidden \" +\n        (props.className || \"\")\n      }\n    >\n      {header}\n      <CodeEditor\n        style={{ minWidth: \"200px\", minHeight: \"100px\" }}\n        {...props}\n        className={\"f-1 b-none\"}\n        options={{\n          tabSize: 2,\n          minimap: {\n            enabled: false,\n          },\n          lineNumbers: \"off\",\n          ...props.options,\n        }}\n      />\n    </div>\n  );\n};\n\nexport default CodeExample;\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/APIDetails/APICodeExamples.tsx",
    "content": "import { mdiDownload } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport Tabs from \"@components/Tabs\";\nimport { Zip } from \"../../API/zip\";\nimport CodeExample from \"../../CodeExample\";\nimport { download } from \"../../W_SQL/W_SQL\";\n\nexport const APICodeExamples = ({\n  token,\n  projectPath,\n  dbSchemaTypes,\n}: {\n  token?: string;\n  projectPath: string;\n  dbSchemaTypes: string | undefined;\n}) => {\n  const { htmlExample, indexTs, tsExample } = getCodeSamples({\n    token,\n    projectPath,\n  });\n\n  const DownloadCodeSample = (isTS = false) => (\n    <Btn\n      variant=\"filled\"\n      className=\"mb-p5 ml-auto\"\n      iconPath={mdiDownload}\n      color=\"action\"\n      onClick={() => {\n        if (isTS) {\n          const zip = new Zip(\"prostgles-api-example\");\n          const folder = \"\";\n          zip.str2zip(\"index.ts\", tsExample, folder);\n          zip.str2zip(\n            \"package.json\",\n            JSON.stringify(packageJson, null, 2),\n            folder,\n          );\n          zip.str2zip(\n            \"tsconfig.json\",\n            JSON.stringify(tsconfigJson, null, 2),\n            folder,\n          );\n          zip.str2zip(\"README.md\", readme, folder);\n          zip.str2zip(\n            \"DBoGenerated.d.ts\",\n            dbSchemaTypes ?? \"export type DBGeneratedSchema = any;\",\n            folder,\n          );\n          zip.makeZip();\n        } else {\n          download(htmlExample, \"index.html\", \"text/html\");\n        }\n      }}\n    >\n      Download code sample\n    </Btn>\n  );\n\n  return (\n    <FlexCol>\n      <Tabs\n        variant={\"horizontal\"}\n        defaultActiveKey=\"Typescript\"\n        contentClass=\"pt-2 f-0\"\n        className=\"f-0\"\n        items={{\n          \"React (Typescript)\": {\n            content: (\n              <FlexCol>\n                <CodeExample\n                  key=\"t\"\n                  value={indexTs}\n                  language=\"typescript\"\n                  markers={[]}\n                  style={{ minHeight: \"400px\" }}\n                />\n                {DownloadCodeSample(true)}\n              </FlexCol>\n            ),\n          },\n          Typescript: {\n            content: (\n              <FlexCol>\n                <CodeExample\n                  key=\"t\"\n                  value={tsExample}\n                  language=\"typescript\"\n                  markers={[]}\n                  style={{ minHeight: \"400px\" }}\n                />\n                {DownloadCodeSample(true)}\n              </FlexCol>\n            ),\n          },\n          \"Vanilla JS\": {\n            content: (\n              <FlexCol>\n                <CodeExample\n                  key=\"h\"\n                  value={htmlExample}\n                  language=\"html\"\n                  style={{ minHeight: \"400px\" }}\n                />\n                {DownloadCodeSample()}\n              </FlexCol>\n            ),\n          },\n        }}\n      />\n    </FlexCol>\n  );\n};\n\nfunction getCodeSamples({\n  token: _token,\n  projectPath,\n}: {\n  token?: string;\n  projectPath?: string;\n}) {\n  const token = _token || \"YOUR_TOKEN\";\n  const authStr = `auth: { sid_token: ${JSON.stringify(token)} },`;\n  const uri = JSON.stringify(window.location.origin);\n  const path = JSON.stringify(projectPath);\n  const indexTs = `import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { useProstglesClient } from \"prostgles-client/dist/prostgles\";\n\nconst App = () => {\n  const { dbo, isLoading } = useProstglesClient({\n    socketOptions: { \n      uri: ${uri},\n      path: ${path}, \n      query: { sid_token: ${JSON.stringify(token)} }, \n    }, \n  });\n\n  return <div>\n    {isLoading? \"Loading...\" : Object.keys(dbo).join(\", \")}\n  </div>\n}\n\nconst root = createRoot(document.getElementById(\"root\")); \nroot.render(\n  <App />\n);\n\n`;\n\n  const initLogic = `const socket = io(${JSON.stringify(window.location.origin)}, { \n  ${authStr}\n  path: ${path} \n});`;\n  const getCommonLogic = (forServer: boolean) => `${initLogic}\n\nprostgles({\n  socket,\n  onReload: () => {},\n  onReady: async (db, methods, tableSchema, auth) => {\n    console.log(db);\n    ${forServer ? \"\" : \"window.db = db;\"}\n    ${forServer ? \"\" : \"document.body.append(JSON.stringify({ db, methods, auth }, null, 2))\"}\n  },\n});`;\n\n  const getClientLogic = (forServer: boolean) =>\n    forServer ? \"\" : (\n      `\nif(typeof document !== 'undefined'){\n  console.log(typeof document)\n  document.body.append(info)\n} else {\n  console.log(info)\n}`\n    );\n\n  const tsExample = `\nimport prostgles from \"prostgles-client\";\nimport io from \"socket.io-client\";\nimport type { DBGeneratedSchema } from \"./DBoGenerated.d.ts\";\n\n${initLogic}\n\nprostgles<DBGeneratedSchema>({\n  socket,\n  onReload: () => {},\n  onReady: async (db, methods, tableSchema, auth) => {\n    const info = JSON.stringify({ \n      tableHandlers: Object.keys(db), \n      methods, \n      auth \n    }, null, 2);\n    console.log(info);\n    ${getClientLogic(false)}\n  },\n});\n\n`;\n\n  const htmlExample = `\n<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<title> Prostgles </title>\n\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t\t<script src=\"https://unpkg.com/socket.io-client@latest/dist/socket.io.min.js\" type=\"text/javascript\"></script>\n\t\t<script src=\"https://unpkg.com/prostgles-client@latest/dist/index.js\" type=\"text/javascript\"></script>\t\n\t</head>\n\t<body style=\"white-space: pre\">\n\n\t\t<script>\n\n    ${getCommonLogic(false)\n      .split(\"\\n\")\n      .map((v, i) => (!i ? `  ` + v : `      ` + v))\n      .join(\"\\n\")}\n\n\t\t</script>\n\t\t\n\t</body>\n</html>`;\n\n  return { htmlExample, tsExample, indexTs };\n}\n\nconst packageJson = {\n  name: \"prostgles-api-example\",\n  version: \"1.0.0\",\n  description: \"Example server-side usage of the Prostgles API\",\n  main: \"index.ts\",\n  scripts: {\n    start: 'npm i && tsc-watch --onSuccess \"node --inspect index.js\"',\n  },\n  author: \"\",\n  license: \"MIT\",\n  devDependencies: {\n    \"@types/node\": \"^20.6.5\",\n    \"tsc-watch\": \"^6.0.4\",\n    typescript: \"^5.2.2\",\n  },\n  dependencies: {\n    \"prostgles-client\": \"^4.0.21\",\n    \"socket.io-client\": \"^4.7.2\",\n  },\n};\n\nconst tsconfigJson = {\n  files: [\"./index.ts\"],\n  compilerOptions: {\n    outDir: \".\",\n    target: \"ES2022\",\n    lib: [\"ES2017\", \"es2019\", \"ES2021.String\", \"ES2022\"],\n    esModuleInterop: true,\n    allowSyntheticDefaultImports: true,\n    allowJs: true,\n    module: \"commonjs\",\n    sourceMap: true,\n    moduleResolution: \"node\",\n    declaration: true,\n    declarationMap: true,\n    keyofStringsOnly: true,\n    ignoreDeprecations: \"5.0\",\n    strict: true,\n    skipLibCheck: true,\n  },\n  exclude: [\"dist\", \"DBoGenerated.ts\", \"*.conf\"],\n};\n\nconst readme = `\nAfter ensuring nodejs is installed run this in your terminal: \n    \\`npm start\\`\n`;\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/APIDetails/APIDetails.tsx",
    "content": "import React, { useMemo, useState } from \"react\";\nimport type { Prgl, PrglState } from \"../../../App\";\nimport { FlexCol } from \"@components/Flex\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { getActiveTokensFilter } from \"../../../pages/Account/Sessions\";\nimport { APIDetailsHttp } from \"./APIDetailsHttp\";\nimport { APIDetailsTokens } from \"./APIDetailsTokens\";\nimport { APIDetailsWs } from \"./APIDetailsWs\";\nimport { AllowedOriginCheck } from \"./AllowedOriginCheck\";\nimport { ELECTRON_USER_AGENT } from \"@common/OAuthUtils\";\nimport { useOnErrorAlert } from \"@components/AlertProvider\";\n\nexport type APIDetailsProps = PrglState & {\n  connection: Prgl[\"connection\"];\n  projectPath: string;\n};\nexport const APIDetails = (props: APIDetailsProps) => {\n  const [newToken, setToken] = useState(\"\");\n\n  const tokens = useAPITokens(props);\n\n  const electronSession = tokens?.find(\n    (t) => t.user_agent === ELECTRON_USER_AGENT,\n  );\n  const token = electronSession?.id ?? newToken;\n  const { dbsTables, dbs } = props;\n  const { table, urlPathCol } = useMemo(() => {\n    const table = dbsTables.find((t) => t.name === \"connections\");\n    const urlPathCol = table?.columns.find((c) => c.name === \"url_path\");\n    return { table, urlPathCol };\n  }, [dbsTables]);\n  const { onErrorAlert } = useOnErrorAlert();\n  return (\n    <FlexCol className=\"APIDetails f-1 min-s-0 o-auto gap-2\">\n      {table && urlPathCol && (\n        <FormFieldDebounced\n          id=\"url_path\"\n          label={urlPathCol.label}\n          hint={urlPathCol.hint}\n          value={props.connection.url_path}\n          style={{\n            padding: \"2px\",\n            maxWidth: \"300px\",\n          }}\n          onChange={(v) => {\n            void onErrorAlert(async () => {\n              if (typeof v !== \"string\") return;\n              await dbs.connections.update(\n                { id: props.connection.id },\n                { url_path: v },\n              );\n            });\n          }}\n        />\n      )}\n\n      {!!(dbs as any).global_settings && <AllowedOriginCheck dbs={dbs} />}\n      <APIDetailsWs {...props} token={token} />\n      <APIDetailsHttp {...props} token={token} />\n      <APIDetailsTokens\n        {...props}\n        token={token}\n        setToken={setToken}\n        tokenCount={tokens?.length ?? 0}\n      />\n    </FlexCol>\n  );\n};\n\nexport const useAPITokens = ({\n  dbs,\n  user,\n}: Pick<PrglState, \"dbs\" | \"user\">) => {\n  const { data: tokens } = dbs.sessions.useSubscribe(\n    getActiveTokensFilter(\"api_token\", user?.id),\n  );\n  return tokens;\n};\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/APIDetails/APIDetailsHttp.tsx",
    "content": "import { mdiCodeBraces } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport CodeExample from \"../../CodeExample\";\nimport type { APIDetailsProps } from \"./APIDetails\";\nimport { getConnectionPaths } from \"@common/utils\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport { download } from \"../../W_SQL/W_SQL\";\n\nexport const APIDetailsHttp = ({\n  dbs,\n  connection,\n  token,\n}: APIDetailsProps & { token?: string }) => {\n  const { data: dbConfig } = dbs.database_configs.useSubscribeOne({\n    $existsJoined: { connections: { id: connection.id } },\n  });\n  const restPath = `${window.location.origin}${getConnectionPaths(connection).rest}`;\n  const restExample = getRestExample(restPath, token);\n  return (\n    <FlexCol>\n      <h4 className=\"m-0 p-0\">HTTP API</h4>\n      {dbConfig && (\n        <FlexCol className=\" \">\n          <SwitchToggle\n            label={t.common.Enabled}\n            variant=\"row\"\n            checked={!!dbConfig.rest_api_enabled}\n            onChange={(rest_api_enabled) => {\n              dbs.database_configs.update(\n                { id: dbConfig.id },\n                { rest_api_enabled },\n              );\n            }}\n          />\n          <div>\n            {\n              t.APIDetailsHttp[\n                \"Provides similar level of access to the Websocket API with the following limitations: no subscriptions, no sync, no file upload\"\n              ]\n            }\n          </div>\n          {dbConfig.rest_api_enabled && (\n            <PopupMenu\n              title={t.APIDetailsWs.Examples}\n              data-command=\"APIDetailsHttp.Examples\"\n              button={\n                <Btn\n                  variant=\"filled\"\n                  iconPath={mdiCodeBraces}\n                  color=\"action\"\n                  disabledInfo={\n                    token ? undefined : \"Must generate an access token first\"\n                  }\n                >\n                  {t.APIDetailsWs.Examples}\n                </Btn>\n              }\n              onClickClose={false}\n              positioning=\"center\"\n              contentStyle={{\n                width: \"700px\",\n                height: \"500px\",\n                maxWidth: \"100%\",\n              }}\n              clickCatchStyle={{ opacity: 0.6 }}\n              content={\n                <CodeExample\n                  language=\"javascript\"\n                  style={{ minHeight: \"400px\" }}\n                  value={restExample}\n                />\n              }\n              footerButtons={[\n                {\n                  label: t.common.Close,\n                  onClickClose: true,\n                },\n                {\n                  label: \"Download\",\n                  variant: \"filled\",\n                  className: \"ml-auto\",\n                  color: \"action\",\n                  onClick: () => {\n                    download(restExample, \"index.js\", \"text/javascript\");\n                  },\n                },\n              ]}\n            />\n          )}\n        </FlexCol>\n      )}\n    </FlexCol>\n  );\n};\n\nconst getRestExample = (path: string, token?: string) => `\nconst headers = new Headers({\n  'Authorization': \\`Bearer ${!token ? \"YOUR_TOKEN_IN_BASE64\" : btoa(token)}\\`, \n  'Accept': 'application/json',\n  'Content-Type': 'application/json'\n})\nconst api = (route, ...params) => fetch(\n  \\`${path}/\\${route.join(\"/\")}\\`, \n  { \n    method: \"POST\", \n    headers,\n    body: JSON.stringify(params ?? [])\n  })\n  .then(res => res.json())\n  .catch(res => res.text())\n  .catch(res => res.statusText) \n  \nconst schema = await api([\"schema\"]);\nconsole.log(schema);\nconst data = await api([\"db\", schema.tableSchema[0]?.name ?? \"someTable\", \"find\"], {}, { select: \"*\", limit: 2 })\n// const methodResult = await api([\"methods\", \"someMethod\"], {})\n`;\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/APIDetails/APIDetailsTokens.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { CopyToClipboardBtn } from \"@components/CopyToClipboardBtn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport { Sessions } from \"../../../pages/Account/Sessions\";\nimport type { APIDetailsProps } from \"./APIDetails\";\n\nexport const APIDetailsTokens = ({\n  dbs,\n  dbsMethods,\n  dbsTables,\n  user,\n  token,\n  setToken,\n  tokenCount,\n}: APIDetailsProps & {\n  token: string;\n  tokenCount: number;\n  setToken: (value: string) => void;\n}) => {\n  return (\n    <FlexCol data-command=\"APIDetailsTokens\">\n      <h4 className=\"m-0 p-0\">\n        {t.APIDetailsTokens[`Access tokens (`]({ tokenCount })}\n      </h4>\n      <div>\n        {\n          t.APIDetailsTokens[\n            \"Provide the same level of access as the current account\"\n          ]\n        }\n      </div>\n      <FlexCol className=\"w-fit  \">\n        <Sessions\n          dbs={dbs}\n          dbsTables={dbsTables}\n          dbsMethods={dbsMethods}\n          user={user}\n          displayType=\"api_token\"\n        />\n        <PopupMenu\n          title={t.APIDetailsTokens[\"Create access token\"]}\n          data-command=\"APIDetailsTokens.CreateToken\"\n          button={\n            <FlexRow>\n              <Btn color=\"action\" variant=\"filled\" iconPath={mdiPlus}>\n                {t.APIDetailsTokens[\"Create token\"]}\n              </Btn>\n            </FlexRow>\n          }\n          positioning=\"center\"\n          initialState={{ days: 100 }}\n          clickCatchStyle={{ opacity: 0.5 }}\n          render={(pClose, state, setState) => {\n            return (\n              <FlexCol>\n                {!token ?\n                  <FormField\n                    label={t.APIDetailsTokens[\"Expires in\"]}\n                    value={state.days}\n                    data-command=\"APIDetailsTokens.CreateToken.daysUntilExpiration\"\n                    type=\"number\"\n                    inputProps={{\n                      step: 1,\n                      min: 1,\n                      style: {\n                        maxWidth: \"6ch\",\n                      },\n                    }}\n                    onChange={(days) => {\n                      setState({ days });\n                    }}\n                    rightIcons={\n                      <FlexRow className=\"h-full px-1 py-p75\">\n                        {t.APIDetailsTokens.Days}\n                      </FlexRow>\n                    }\n                    rightContentAlwaysShow={true}\n                  />\n                : <FlexCol>\n                    <div className=\" ta-start mt-1\">\n                      {\n                        t.APIDetailsTokens[\n                          \"These token values will not be shown again\"\n                        ]\n                      }\n                    </div>\n                    <TokenCopy\n                      label={t.APIDetailsTokens[\"Websocket API\"]}\n                      token={token}\n                    />\n                    <TokenCopy\n                      label={t.APIDetailsTokens[\"HTTP API (base64 encoded)\"]}\n                      token={btoa(token)}\n                    />\n                  </FlexCol>\n                }\n                <Btn\n                  variant=\"filled\"\n                  color=\"action\"\n                  className=\"ml-auto\"\n                  data-command=\"APIDetailsTokens.CreateToken.generate\"\n                  disabledInfo={\n                    token ?\n                      t.APIDetailsTokens[\n                        \"Already generated. Close and re-open the popup\"\n                      ]\n                    : undefined\n                  }\n                  onClickPromise={async () => {\n                    const token = await dbsMethods.generateToken!(+state.days);\n                    setToken(token);\n                  }}\n                >\n                  {t.common.Generate}\n                </Btn>\n              </FlexCol>\n            );\n          }}\n        />\n      </FlexCol>\n    </FlexCol>\n  );\n};\n\nconst TokenCopy = ({ token, label }: { token: string; label: string }) => {\n  return (\n    <div className=\"flex-col gap-p5 my-1 ta-start\">\n      <div className=\"text-1\">{label}</div>\n      <div\n        className=\"b b-color flex-row ai-center rounded w-fit\"\n        style={{ maxWidth: \"400px\" }}\n      >\n        <div className=\"p-p5 w-fit w-min-0 text-ellipsis\">{token}</div>\n        <CopyToClipboardBtn variant=\"faded\" color=\"action\" content={token} />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/APIDetails/APIDetailsWs.tsx",
    "content": "import { mdiCodeBraces, mdiLanguageTypescript } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useMemo } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport { download } from \"../../W_SQL/W_SQL\";\nimport { APICodeExamples } from \"./APICodeExamples\";\nimport type { APIDetailsProps } from \"./APIDetails\";\n\nexport const APIDetailsWs = ({\n  dbsMethods,\n  connection,\n  token,\n  projectPath,\n}: APIDetailsProps & { token?: string }) => {\n  const dbSchemaTypes = usePromise(async () => {\n    if (connection.id) {\n      const dbSchemaTypes = await dbsMethods.getConnectionDBTypes?.(\n        connection.is_state_db ? undefined : connection.id,\n      );\n      // ?.catch((e) => {\n      //   console.error(\"Failed to get connection DB types\", e);\n      // });\n      return dbSchemaTypes;\n    }\n  }, [connection.id, connection.is_state_db, dbsMethods]);\n\n  return (\n    <FlexCol>\n      <h4 className=\"m-0 p-0\">\n        {t.APIDetailsWs[\"Websocket API (recommended)\"]}\n      </h4>\n      <div className=\" \">\n        {t.APIDetailsWs[\"Realtime Isomorphic API using\"]}{\" \"}\n        <a\n          target=\"_blank\"\n          href=\"https://github.com/prostgles/prostgles-client-js\"\n          rel=\"noreferrer\"\n        >\n          prostgles-client\n        </a>{\" \"}\n        {\n          t.APIDetailsWs[\n            \"library. End-to-end type-safety between client & server using the database Typescript schema provided below:\"\n          ]\n        }\n      </div>\n\n      <FlexRow className=\"ai-end mb-1 \">\n        <PopupMenu\n          title={t.APIDetailsWs.Examples}\n          data-command=\"APIDetailsWs.Examples\"\n          button={\n            <Btn\n              variant=\"filled\"\n              iconPath={mdiCodeBraces}\n              color=\"action\"\n              disabledInfo={\n                token ? undefined : \"Must generate an access token first\"\n              }\n            >\n              {t.APIDetailsWs.Examples}\n            </Btn>\n          }\n          onClickClose={false}\n          positioning=\"center\"\n          contentStyle={{ width: \"700px\", maxWidth: \"100%\" }}\n          clickCatchStyle={{ opacity: 0.6 }}\n          content={\n            <APICodeExamples\n              token={token}\n              projectPath={projectPath}\n              dbSchemaTypes={dbSchemaTypes}\n            />\n          }\n        />\n        <Btn\n          title={t.APIDetailsWs[\"Download typescript schema\"]}\n          disabledInfo={\n            dbsMethods.getConnectionDBTypes ? undefined : (\n              t.common[\"Not permitted\"]\n            )\n          }\n          onClick={() => {\n            download(dbSchemaTypes, \"DBoGenerated.d.ts\", \"text/plain\");\n          }}\n          iconPath={mdiLanguageTypescript}\n          variant=\"faded\"\n          color=\"action\"\n        >\n          {t.APIDetailsWs[\"Database schema types\"]}\n        </Btn>\n      </FlexRow>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/APIDetails/AllowedOriginCheck.tsx",
    "content": "import React from \"react\";\nimport type { APIDetailsProps } from \"./APIDetails\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { mdiAlert } from \"@mdi/js\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport FormField from \"@components/FormField/FormField\";\n\nexport const AllowedOriginCheck = ({ dbs }: Pick<APIDetailsProps, \"dbs\">) => {\n  const { data: serverSettings } = dbs.global_settings.useSubscribeOne({});\n  const [allowed_origin, setAllowedOrigin] = React.useState(\n    serverSettings?.allowed_origin,\n  );\n\n  if (serverSettings?.allowed_origin) {\n    return null;\n  }\n\n  return (\n    <PopupMenu\n      data-command=\"AllowedOriginCheck\"\n      button={\n        <Btn iconPath={mdiAlert} color=\"warn\" variant=\"faded\">\n          {t.APIDetailsWs[\"Allowed origin not set\"]}\n        </Btn>\n      }\n      clickCatchStyle={{ opacity: 1 }}\n      contentStyle={{ maxWidth: \"500px\" }}\n      footerButtons={(pClose) => [\n        { label: t.common.Close, onClick: pClose },\n        {\n          label: t.common.Confirm,\n          color: \"action\",\n          variant: \"filled\",\n          className: \"ml-auto\",\n          disabledInfo:\n            !allowed_origin ?\n              t.APIDetailsWs[\"Allowed origin is required\"]\n            : undefined,\n          onClickPromise: async () => {\n            await dbs.global_settings.update({}, { allowed_origin });\n          },\n        },\n      ]}\n      render={() => (\n        <FlexCol>\n          <InfoRow className=\"ws-pre\">\n            Allowed origin controls which domains can make cross-origin requests\n            to this app by setting the Access-Control-Allow-Origin header.\n            <ul>\n              <li>\n                Use 'null' to allow requests from local HTML files (file://\n                protocol)\n              </li>\n              <li>\n                Use '*' to allow all domains (recommended for testing only)\n              </li>\n              <li>\n                Use specific URLs (e.g., 'https://your-website.com') for\n                production environments\n              </li>\n            </ul>\n            <p>\n              ⚠️ Security Note: Using '*' in production can expose your API to\n              unauthorized access from any\n            </p>\n          </InfoRow>\n\n          <FormField\n            label={t.APIDetailsWs[\"Allowed origin\"]}\n            data-command=\"AllowedOriginCheck.FormField\"\n            value={allowed_origin}\n            onChange={setAllowedOrigin}\n          />\n        </FlexCol>\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/ConnectionConfig.tsx",
    "content": "import {\n  mdiAccountMultiple,\n  mdiApplicationBracesOutline,\n  mdiChartLine,\n  mdiDatabaseSync,\n  mdiImage,\n  mdiLanguageTypescript,\n  mdiPencil,\n  mdiTableEdit,\n} from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport type { CONNECTION_CONFIG_SECTIONS } from \"@common/utils\";\nimport { dataCommand } from \"../../Testing\";\nimport { FlexRow } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport type { TabItem } from \"@components/Tabs\";\nimport Tabs from \"@components/Tabs\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport NewConnection from \"../../pages/NewConnection/NewConnnectionForm\";\nimport { usePrgl } from \"../../pages/ProjectConnection/PrglContextProvider\";\nimport type { Connections } from \"../../pages/ProjectConnection/ProjectConnection\";\nimport { TopControls } from \"../../pages/TopControls\";\nimport { getKeys } from \"../../utils/utils\";\nimport { AccessControl } from \"../AccessControl/AccessControl\";\nimport { useAccessControlSearchParams } from \"../AccessControl/useAccessControlSearchParams\";\nimport { BackupsControls } from \"../BackupAndRestore/BackupsControls\";\nimport { APIDetails } from \"../ConnectionConfig/APIDetails/APIDetails\";\nimport { FileTableConfigControls } from \"../FileTableControls/FileTableConfigControls\";\nimport { StatusMonitor } from \"../StatusMonitor/StatusMonitor\";\nimport { TableConfig } from \"../TableConfig/TableConfig\";\nimport { ServerSideFunctions } from \"./ServerSideFunctions\";\nimport { useConnectionConfigSearchParams } from \"./useConnectionConfigSearchParams\";\n\ntype ConnectionConfigProps = Pick<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"style\" | \"className\" | \"children\"\n> & {\n  connection: Connections;\n};\n\nexport const ConnectionConfig = (props: ConnectionConfigProps) => {\n  const { className = \"\", style = {}, connection } = props;\n  const prgl = usePrgl();\n  const { serverState, dbs, connectionId, db, dbsMethods } = prgl;\n  const propsWithPrgl = useMemo(() => ({ ...props, prgl }), [props, prgl]);\n  const disabledText =\n    (dbs.access_control as any)?.update ?\n      undefined\n    : t.ConnectionConfig[\"Must be admin to access this\"];\n  const stateDisabledInfo =\n    connection.is_state_db ?\n      t.TopControls[\"Not allowed for state database\"]\n    : undefined;\n  const { isElectron } = serverState;\n\n  const acParams = useAccessControlSearchParams();\n  const sectionItems = useMemo(\n    () =>\n      ({\n        details: {\n          label: t.ConnectionConfig[\"Connection details\"],\n          leftIconPath: mdiPencil,\n          disabledText: disabledText || stateDisabledInfo,\n          listProps: dataCommand(\"config.details\"),\n          content: (\n            <NewConnection\n              showTitle={false}\n              prglState={prgl}\n              contentOnly={true}\n              db={db}\n              connectionId={connectionId}\n            />\n          ),\n        },\n        status: {\n          label: t.ConnectionConfig[\"Status monitor\"],\n          listProps: dataCommand(\"config.status\"),\n          leftIconPath: mdiChartLine,\n          disabledText:\n            !dbsMethods.getStatus ? \"Must be admin to access this\" : undefined,\n          content:\n            !dbsMethods.getStatus || !dbsMethods.runConnectionQuery ?\n              null\n            : <StatusMonitor\n                {...prgl}\n                getStatus={dbsMethods.getStatus}\n                runConnectionQuery={dbsMethods.runConnectionQuery}\n              />,\n        },\n        access_control: {\n          label: t.ConnectionConfig[\"Access control\"],\n          listProps: dataCommand(\"config.ac\"),\n          leftIconPath: mdiAccountMultiple,\n          disabledText:\n            disabledText ||\n            stateDisabledInfo ||\n            (isElectron ? \"Not available for desktop\" : undefined),\n          content: (\n            <AccessControl className=\"min-h-0\" prgl={prgl} {...acParams} />\n          ),\n        },\n        file_storage: {\n          label: t.ConnectionConfig[\"File storage\"],\n          listProps: dataCommand(\"config.files\"),\n          leftIconPath: mdiImage,\n          disabledText: disabledText || stateDisabledInfo,\n          content: <FileTableConfigControls {...propsWithPrgl} />,\n        },\n        backups: {\n          label: t.ConnectionConfig[\"Backup/Restore\"],\n          listProps: dataCommand(\"config.bkp\"),\n          leftIconPath: mdiDatabaseSync,\n          disabledText,\n          content: <BackupsControls {...propsWithPrgl} />,\n        },\n        API: {\n          label: t.ConnectionConfig[\"API\"],\n          listProps: dataCommand(\"config.api\"),\n          leftIconPath: mdiApplicationBracesOutline,\n          disabledText:\n            disabledText ||\n            (isElectron ? \"Not available for desktop\" : undefined),\n          content: <APIDetails {...prgl} />,\n        },\n        table_config: {\n          label: (\n            <>\n              {t.ConnectionConfig[\"Table config\"]}{\" \"}\n              <span className=\"text-2 font-14\">\n                ({t.ConnectionConfig.experimental})\n              </span>\n            </>\n          ),\n          disabledText: disabledText || stateDisabledInfo,\n          listProps: dataCommand(\"config.tableConfig\"),\n          leftIconPath: mdiTableEdit,\n          content: <TableConfig {...propsWithPrgl} />,\n        },\n        methods: {\n          label: (\n            <>\n              {t.ConnectionConfig[\"Server-side functions\"]}{\" \"}\n              <span className=\"text-2 font-14\">\n                ({t.ConnectionConfig.experimental})\n              </span>\n            </>\n          ),\n          disabledText: disabledText || stateDisabledInfo,\n          listProps: dataCommand(\"config.methods\"),\n          leftIconPath: mdiLanguageTypescript,\n          content: <ServerSideFunctions {...prgl} />,\n        },\n      }) as const satisfies Record<\n        (typeof CONNECTION_CONFIG_SECTIONS)[number],\n        TabItem\n      >,\n    [\n      acParams,\n      connectionId,\n      db,\n      dbsMethods.getStatus,\n      dbsMethods.runConnectionQuery,\n      disabledText,\n      isElectron,\n      prgl,\n      propsWithPrgl,\n      stateDisabledInfo,\n    ],\n  );\n  const { activeSection, setSection } = useConnectionConfigSearchParams(\n    getKeys(sectionItems),\n  );\n\n  return (\n    <div className={`flex-col f-1 min-s-0 ${className}`} style={style}>\n      <TopControls\n        location=\"config\"\n        prgl={prgl}\n        loadedSuggestions={undefined}\n      />\n\n      <div className=\"flex-col f-1 min-h-0 as-center  bg-color-2 w-full pt-1\">\n        <Tabs\n          variant={{\n            controlsBreakpoint: 200,\n            contentBreakpoint: 500,\n            controlsCollapseWidth: 350,\n          }}\n          className=\"f-1 as-center w-full shadow\"\n          style={{ maxWidth: \"1200px\" }}\n          activeKey={activeSection}\n          compactMode={window.isMediumWidthScreen ? \"hide-label\" : undefined}\n          onChange={(section) => {\n            setSection({ section });\n          }}\n          items={sectionItems}\n          contentClass=\"o-auto flex-row jc-center bg-color-2 f-1\"\n          onRender={(item) => (\n            <div className=\"flex-col f-1  min-w-0 bg-color-0 shadow\">\n              <FlexRow\n                className=\"w-full text-0\"\n                style={{\n                  paddingLeft: window.isLowWidthScreen ? \"16px\" : \"32px\",\n                }}\n              >\n                {item.leftIconPath && (\n                  <Icon size={1.5} path={item.leftIconPath} />\n                )}\n                <h2>{item.label}</h2>\n              </FlexRow>\n              <div\n                className={\n                  \" f-1 o-auto flex-row w-full \" +\n                  (window.isLowWidthScreen ? \"p-1\" : \" p-2  \")\n                }\n                style={{ alignSelf: \"stretch\" }}\n              >\n                {item.content}\n              </div>\n            </div>\n          )}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/ServerSideFunctions.tsx",
    "content": "import React, { useCallback, useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { useCodeEditorTsTypes } from \"../AccessControl/Methods/useCodeEditorTsTypes\";\nimport { CodeEditorWithSaveButton } from \"../CodeEditor/CodeEditorWithSaveButton\";\nimport { ProcessLogs } from \"../TableConfig/ProcessLogs\";\nimport Loading from \"@components/Loader/Loading\";\nimport { PublishedMethods } from \"../W_Method/PublishedMethods\";\n\nexport const ServerSideFunctions = (props: Prgl) => {\n  const { dbsMethods, dbs, connectionId, dbKey, tables } = props;\n  const { data: connection } = dbs.connections.useSubscribeOne({\n    id: connectionId,\n  });\n  const languageObj = useCodeEditorTsTypes({\n    connectionId,\n    dbsMethods,\n    dbKey,\n    tables,\n    dbs,\n    method: undefined,\n  });\n  const { setOnMount } = dbsMethods;\n\n  const onSave = useCallback(\n    async (value: string) => {\n      await setOnMount?.(connectionId, { on_mount_ts: value });\n    },\n    [setOnMount, connectionId],\n  );\n  /**\n   * Hiding PublishedMethods until OnMountFunction is loaded\n   * is done to prevent flaky tests when creating function\n   */\n  const [libsLoaded, setLibsLoaded] = useState(false);\n\n  const onLoaded = useCallback(() => {\n    setLibsLoaded(true);\n  }, []);\n  if (!connection) return <Loading />;\n\n  return (\n    <FlexCol className=\"w-full\" style={{ gap: \"2em\" }}>\n      <FlexRow>\n        <h3>On mount</h3>\n        <SwitchToggle\n          label={\"Enabled\"}\n          disabledInfo={\n            !connection.on_mount_ts ?\n              \"No on mount function. Provide a function or edit and save the example\"\n            : undefined\n          }\n          data-command=\"ServerSideFunctions.onMountEnabled\"\n          checked={!!connection.on_mount_ts && !connection.on_mount_ts_disabled}\n          onChange={async (checked) => {\n            await dbsMethods.setOnMount?.(connectionId, {\n              on_mount_ts_disabled: !checked,\n            });\n          }}\n        />\n      </FlexRow>\n      <FlexCol>\n        {languageObj && (\n          <>\n            <CodeEditorWithSaveButton\n              key={dbKey}\n              label=\"Server-side function executed after the table is created and server started or schema changed\"\n              language={languageObj}\n              codePlaceholder={example}\n              value={connection.on_mount_ts}\n              onSave={onSave}\n              onTSLibraryChange={onLoaded}\n            />\n            <ProcessLogs\n              key={dbKey + \"logs\"}\n              connectionId={connectionId}\n              dbsMethods={dbsMethods}\n              type=\"onMount\"\n              dbs={dbs}\n            />\n          </>\n        )}\n      </FlexCol>\n      {!libsLoaded ?\n        <Loading />\n      : <PublishedMethods\n          prgl={props}\n          editedRule={undefined}\n          accessRuleId={undefined}\n        />\n      }\n    </FlexCol>\n  );\n};\n\nconst example = `/* Example */\nimport { WebSocket } from \"ws\";\nexport const onMount: ProstglesOnMount = async ({ dbo }) => {\n\n  await dbo.sql('CREATE TABLE IF NOT EXISTS symbols(pair text primary key);');\n  await dbo.sql('CREATE TABLE IF NOT EXISTS futures (price float, symbol text, \"timestamp\" timestamptz);');\n  const socket = new WebSocket(\"wss://fstream.binance.com/ws/!markPrice@arr@1s\");\n  \n  socket.onmessage = async (rawData) => {\n    const dataItems = JSON.parse(rawData.data as string);\n    const data = dataItems.map(data => ({ symbol: data.s, price: data.p, timestamp: new Date(data.E) }))\n    await dbo.symbols.insert(data.map(({ symbol }) => ({ pair: symbol })), { onConflict: \"DoNothing\" });\n    await dbo.futures.insert(data);\n  }\n}\n`;\n"
  },
  {
    "path": "client/src/dashboard/ConnectionConfig/useConnectionConfigSearchParams.ts",
    "content": "import { useSearchParams } from \"react-router-dom\";\n\nexport const useConnectionConfigSearchParams = <ItemKey extends string>(\n  connectionConfigItems: ItemKey[],\n) => {\n  const [searchParams, setSearchParams] = useSearchParams();\n\n  const activeSection =\n    connectionConfigItems.find((s) => s === searchParams.get(\"section\")) ??\n    connectionConfigItems[0];\n  const setSection = <S extends (typeof connectionConfigItems)[number]>({\n    section,\n    opts,\n  }: {\n    section: S | undefined;\n    opts?: S extends \"access_control\" ? { ruleId?: string; tableName?: string }\n    : never;\n  }) => {\n    if (!section) {\n      searchParams.delete(\"section\");\n      setSearchParams(searchParams);\n    } else {\n      setSearchParams({ section, ...opts });\n    }\n  };\n  return { activeSection, setSection };\n};\n"
  },
  {
    "path": "client/src/dashboard/ConnectionSelector.tsx",
    "content": "import { mdiDatabase } from \"@mdi/js\";\nimport React from \"react\";\nimport { Select } from \"@components/Select/Select\";\nimport { t } from \"../i18n/i18nUtils\";\nimport type { Connection } from \"../pages/NewConnection/NewConnnectionForm\";\nimport type { DBS } from \"./Dashboard/DBS\";\nimport type { DBSSchema } from \"@common/publishUtils\";\n\ntype P = {\n  dbs: DBS;\n  connection: Connection;\n  location: \"workspace\" | \"config\";\n};\n\nexport const ConnectionSelector = ({ connection, dbs, location }: P) => {\n  const { data: connections } = dbs.connections.useFind();\n  return (\n    <Select\n      title={t.ConnectionSelector[\"Switch database\"]}\n      data-command=\"ConnectionSelector\"\n      fullOptions={(connections ?? []).map((c) => ({\n        key: c.id,\n        label: c.name || c.db_name || c.id,\n        subLabel: !c.db_name ? undefined : getServerInfoStr(c, true),\n      }))}\n      onChange={(cId) => {\n        const subpath =\n          location === \"workspace\" ? \"connections\" : \"connection-config\";\n        const newLocation = `/${subpath}/${cId}${window.location.search}`;\n        window.location.href = newLocation;\n      }}\n      value={connection.id}\n      btnProps={{\n        iconPath: mdiDatabase,\n        iconPosition: \"left\",\n        variant: \"faded\",\n        style: {\n          flex: 1,\n          minWidth: 0,\n          maxWidth: \"fit-content\",\n        },\n      }}\n    />\n  );\n};\n\nconst getServerInfoStr = (c: DBSSchema[\"connections\"], showuser = false) => {\n  const userInfo = showuser && c.db_user ? `${c.db_user}@` : \"\";\n  return `${userInfo}${c.db_host || \"localhost\"}:${c.db_port || \"5432\"}/${c.db_name}`;\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/CloseSaveSQLPopup.tsx",
    "content": "import React from \"react\";\nimport type { WindowSyncItem } from \"./dashboardUtils\";\nimport Popup from \"@components/Popup/Popup\";\nimport FormField from \"@components/FormField/FormField\";\nimport Btn from \"@components/Btn\";\nimport { mdiContentSave, mdiDelete } from \"@mdi/js\";\n\ntype P = {\n  namePopupWindow?: { w: WindowSyncItem; node: HTMLButtonElement };\n  onClose: VoidFunction;\n  windows: WindowSyncItem[];\n};\nexport const CloseSaveSQLPopup = ({\n  namePopupWindow: nw,\n  windows,\n  onClose,\n}: P) => {\n  const namePopupWindow = nw ? windows.find((w) => w.id === nw.w.id) : null;\n  if (namePopupWindow && nw) {\n    return (\n      <Popup\n        title={\"Save query?\"}\n        onClose={onClose}\n        anchorEl={nw.node}\n        positioning={\"beneath-left\"}\n        clickCatchStyle={{ opacity: 0.2 }}\n        footerButtons={[\n          {\n            label: \"Delete\",\n            color: \"danger\",\n            \"data-command\": \"CloseSaveSQLPopup.delete\",\n            variant: \"outline\",\n            iconPath: mdiDelete,\n            onClickPromise: async () => {\n              await namePopupWindow.$update({ closed: true, deleted: true });\n              onClose();\n            },\n          },\n          {\n            label: \"Save\",\n            color: \"action\",\n            variant: \"filled\",\n            iconPath: mdiContentSave,\n            disabledInfo:\n              !namePopupWindow.name ? \"Cannot have an empty name\" : undefined,\n            onClickPromise: async () => {\n              await namePopupWindow.$update(\n                {\n                  closed: true,\n                  deleted: false,\n                  options: { sqlWasSaved: true },\n                },\n                { deepMerge: true },\n              );\n              setTimeout(() => {\n                onClose();\n              }, 500);\n            },\n          },\n        ]}\n        content={\n          <div className=\"flex-col\">\n            <FormField\n              type=\"text\"\n              label=\"Name\"\n              defaultValue={namePopupWindow.name}\n              required={true}\n              onChange={(v) => {\n                namePopupWindow.$update(\n                  { name: v, options: { sqlWasSaved: true } },\n                  { deepMerge: true },\n                );\n              }}\n            />\n          </div>\n        }\n      />\n    );\n  }\n\n  return <></>;\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/DBS.ts",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types/lib\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { InstalledPrograms } from \"@common/electronInitTypes\";\nimport type { LLMMessage } from \"@common/llmUtils\";\nimport type { McpToolCallResponse } from \"@common/mcp\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type {\n  ConnectionStatus,\n  PGDumpParams,\n  ProcStats,\n  SampleSchema,\n} from \"@common/utils\";\nimport type { Connection } from \"../../pages/NewConnection/NewConnnectionForm\";\nimport type { FileTableConfigReferences } from \"../FileTableControls/FileColumnConfigControls\";\nimport type { ConnectionTableConfig } from \"../FileTableControls/FileTableConfigControls\";\nimport type { Backups } from \"./dashboardUtils\";\nimport type { AllowedChatTool } from \"@common/prostglesMcp\";\n\nexport type DBSMethods = Partial<{\n  mkdir: (path: string, folderName: string) => Promise<string>;\n  glob: (\n    pattern?: string,\n    timeout?: number,\n  ) => Promise<{\n    result: {\n      path: string;\n      name: string;\n      type: string;\n      size: number | undefined;\n      lastModified: number | undefined;\n      created: number | undefined;\n    }[];\n    pattern: string;\n    path: string;\n  }>;\n  sendFeedback: (feedback: {\n    details: string;\n    email?: string;\n  }) => Promise<void>;\n  prostglesSignup: (\n    email: string,\n    otpCode: string,\n  ) => Promise<{ token: string; host: string; hasError?: boolean; error: any }>;\n  askLLM: (\n    connectionId: string,\n    userMessage: LLMMessage[\"message\"],\n    schema: string,\n    chatId: number,\n    type: \"new-message\" | \"approve-tool-use\",\n  ) => Promise<AnyObject>;\n  getFullPrompt: ({\n    prompt,\n    schema,\n    dashboardTypesContent,\n  }: {\n    prompt: string;\n    schema: string;\n    dashboardTypesContent: string;\n  }) => Promise<string>;\n  stopAskLLM: (chatId: number) => Promise<void>;\n  pgDump: (\n    conId: string,\n    credId: number | null | undefined,\n    dumpParams: PGDumpParams,\n  ) => Promise<void>;\n  pgRestore: (\n    arg1: { bkpId: string; connId?: string },\n    opts: Backups[\"restore_options\"],\n  ) => Promise<void>;\n  streamBackupFile: (\n    c: \"start\" | \"chunk\" | \"end\",\n    id: null | string,\n    conId: string | null,\n    chunk: Buffer | undefined,\n    sizeBytes: number | undefined,\n    opts?: Backups[\"restore_options\"],\n  ) => Promise<string>;\n  bkpDelete: (bkpId: string, force?: boolean) => Promise<string>;\n  getFileFolderSizeInBytes: (conId?: string) => Promise<string>;\n  reloadSchema: (conId: string) => Promise<void>;\n  startConnection: (conId: string) => Promise<string>;\n  testDBConnection: (con: Connection) => Promise<true>;\n  deleteConnection: (\n    conId: string,\n    opts: { dropDatabase: boolean },\n  ) => Promise<Connection>;\n  createConnection: (\n    con: Connection,\n    sampleSchemaName?: string,\n  ) => Promise<{\n    connection: Required<Connection>;\n    database_config: DBSSchema[\"database_configs\"];\n  }>;\n  getDBSize: (conId: string) => Promise<string>;\n  getIsSuperUser: (conId: string) => Promise<boolean>;\n  validateConnection: (\n    con: Connection,\n  ) => Promise<{ connection: Required<Connection>; warning?: string }>;\n  disconnect: (conId: string) => Promise<boolean>;\n  create2FA: () => Promise<{\n    url: string;\n    secret: string;\n    recoveryCode: string;\n  }>;\n  enable2FA: (confirmationCode: string) => Promise<string>;\n  disable2FA: () => Promise<string>;\n  toggleService: (serviceName: string, enable: boolean) => Promise<void>;\n  setFileStorage: (\n    connId: string,\n    tableConfig?:\n      | ConnectionTableConfig\n      | { referencedTables?: FileTableConfigReferences },\n    opts?: { keepS3Data?: boolean; keepFileTable?: boolean },\n  ) => Promise<any>;\n  getConnectedIds: () => Promise<string[]>;\n  generateToken: (daysValidity: number) => Promise<string>;\n  getMyIP: () => Promise<{ ip: string; isAllowed: boolean }>;\n  disablePasswordless: (newAdmin: {\n    username: string;\n    password: string;\n  }) => Promise<void>;\n  getNodeTypes: () => Promise<\n    {\n      filePath: string;\n      content: string;\n    }[]\n  >;\n  getConnectionDBTypes: (\n    conId: string | undefined,\n  ) => Promise<string | undefined>;\n  getStatus: (conId: string) => Promise<ConnectionStatus>;\n  killPID: (\n    connId: string,\n    id_query_hash: string,\n    type: \"cancel\" | \"terminate\",\n  ) => Promise<any>;\n  runConnectionQuery: (\n    connId: string,\n    query: string,\n    args?: AnyObject | any[],\n  ) => Promise<AnyObject[]>;\n  getSampleSchemas: () => Promise<SampleSchema[]>;\n  setOnMount: (\n    connId: string,\n    changes: Partial<\n      Pick<DBSSchema[\"connections\"], \"on_mount_ts\" | \"on_mount_ts_disabled\">\n    >,\n  ) => Promise<void>;\n  setTableConfig: (\n    connId: string,\n    changes: Partial<\n      Pick<\n        DBSSchema[\"database_configs\"],\n        \"table_config_ts\" | \"table_config_ts_disabled\"\n      >\n    >,\n  ) => Promise<void>;\n  getForkedProcStats: (connId: string) => Promise<{\n    server: {\n      mem: number;\n      freemem: number;\n    };\n    methodRunner: ProcStats | undefined;\n    onMountRunner: ProcStats | undefined;\n    tableConfigRunner: ProcStats | undefined;\n  }>;\n  getInstalledPsqlVersions: () => Promise<InstalledPrograms>;\n  changePassword: (oldPassword: string, newPassword: string) => Promise<void>;\n  installMCPServer: (serverName: string) => Promise<void>;\n  getMCPServersStatus: (serverName: string) => Promise<{\n    ok: boolean;\n    error?: string;\n    message: string;\n  }>;\n  callMCPServerTool: (\n    chat_id: number,\n    serverName: string,\n    toolName: string,\n    args: any,\n  ) => Promise<McpToolCallResponse>;\n  reloadMcpServerTools: (serverName: string) => Promise<number>;\n  getMcpHostInfo: () => Promise<{\n    os: string;\n    npmVersion: string;\n    uvxVersion: string;\n  }>;\n  refreshModels: () => Promise<void>;\n  getLLMAllowedChatTools: (\n    chatId: number,\n  ) => Promise<AllowedChatTool[] | undefined>;\n  transcribeAudio: (\n    audio: Blob,\n    language?: string,\n  ) => Promise<\n    | { error: string }\n    | {\n        success: true;\n        transcription: string;\n        language: string;\n        language_probability: number;\n        segments: { start: number; end: number; text: string }[];\n      }\n  >;\n}>;\n\nconst AdminTableNames = [\"connections\", \"global_settings\"] as const;\n\nexport type DBS = DBHandlerClient<DBGeneratedSchema> & {\n  sql: DBHandlerClient[\"sql\"];\n};\n\ntype AsOptional<T, Keys extends keyof Partial<T> & string> = Omit<T, Keys> & {\n  [K in Keys]?: Partial<T[K]>;\n};\n\nexport type DbsByUserType = AsOptional<DBS, (typeof AdminTableNames)[number]>;\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/Dashboard.tsx",
    "content": "import Loading from \"@components/Loader/Loading\";\nimport type {\n  MultiSyncHandles,\n  SingleSyncHandles,\n} from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport React from \"react\";\nimport RTComp, { type DeltaOfData } from \"../RTComp\";\nimport { getSqlSuggestions } from \"../SQLEditor/SQLEditorSuggestions\";\nimport type { DBObject } from \"../SearchAll/SearchAll\";\n\nimport { ROUTES } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { mdiArrowLeft } from \"@mdi/js\";\nimport { isEmpty } from \"prostgles-types\";\nimport type { NavigateFunction } from \"react-router-dom\";\nimport { useNavigate } from \"react-router-dom\";\nimport type { Prgl } from \"../../App\";\nimport { createReactiveState } from \"../../appUtils\";\nimport { usePrgl } from \"../../pages/ProjectConnection/PrglContextProvider\";\nimport { TopControls } from \"../../pages/TopControls\";\nimport { DashboardMenu } from \"../DashboardMenu/DashboardMenu\";\nimport type { ActiveRow } from \"../W_Table/W_Table\";\nimport { getWorkspacePath } from \"../WorkspaceMenu/useWorkspaces\";\nimport type { LocalSettings } from \"../localSettings\";\nimport { useLocalSettings } from \"../localSettings\";\nimport { CloseSaveSQLPopup } from \"./CloseSaveSQLPopup\";\nimport { DashboardCenteredLayoutResizer } from \"./DashboardCenteredLayoutResizer\";\nimport type { ViewRendererProps } from \"./ViewRenderer\";\nimport { ViewRendererWrapped } from \"./ViewRenderer\";\nimport { cloneWorkspace } from \"./cloneWorkspace\";\nimport type {\n  ChartType,\n  DBSchemaTablesWJoins,\n  LinkSyncItem,\n  LoadedSuggestions,\n  OnAddChart,\n  WindowData,\n  WindowSyncItem,\n  Workspace,\n  WorkspaceSchema,\n  WorkspaceSyncItem,\n} from \"./dashboardUtils\";\nimport { TopHeaderClassName } from \"./dashboardUtils\";\nimport { getTables } from \"./getTables\";\nimport { loadTable, type LoadTableArgs } from \"./loadTable\";\n\nconst FORCED_REFRESH_PREFIX = \"force-\" as const;\nexport const CENTERED_WIDTH_CSS_VAR = \"--centered-width\";\nexport type DashboardProps = {\n  prgl: Prgl;\n  workspaceId?: string;\n  localSettings: LocalSettings;\n  onLoaded: VoidFunction;\n  navigate: NavigateFunction;\n};\nexport type DashboardState = {\n  tables?: DBSchemaTablesWJoins;\n  loading: boolean;\n  minimised: boolean;\n  namePopupWindow?: { w: WindowSyncItem; node: HTMLButtonElement };\n\n  /* Updated after FileImport closes */\n  imported?: number;\n\n  suggestions?: LoadedSuggestions;\n\n  db_objects?: DBObject[];\n\n  wspError?: any;\n  error?: any;\n  reRender?: number;\n\n  /**\n   * If true then hide some dashboard controls (close window, minimize, window menus)\n   */\n  isReadonly?: boolean;\n};\nexport type DashboardData = {\n  links: LinkSyncItem[];\n  linksSync?: MultiSyncHandles<LinkSyncItem>;\n  closedWindows: WindowSyncItem[];\n  allWindows: WindowSyncItem[];\n  windows: WindowSyncItem[];\n  windowsSync?: MultiSyncHandles<WindowData<ChartType>>;\n  workspace?: WorkspaceSyncItem;\n  workspaceSync?: SingleSyncHandles<Workspace>;\n};\n\nexport class _Dashboard extends RTComp<\n  DashboardProps,\n  DashboardState,\n  DashboardData\n> {\n  state: DashboardState = {\n    loading: true,\n    minimised: false,\n    imported: 0,\n  };\n  d: DashboardData = {\n    closedWindows: [],\n    allWindows: [],\n    windows: [],\n    links: [],\n    linksSync: undefined,\n  };\n\n  onUnmount() {\n    const { workspaceSync, windowsSync, linksSync } = this.d;\n\n    workspaceSync?.$unsync();\n    windowsSync?.$unsync();\n    linksSync?.$unsync();\n  }\n\n  loadingSchema: DashboardState[\"suggestions\"];\n  loadSchema = async (force = false): Promise<void> => {\n    const {\n      db,\n      connectionId,\n      tables: dbSchemaTables,\n      connection,\n    } = this.props.prgl;\n    const workspace = this.d.workspace;\n    const dbKey =\n      force ? `${FORCED_REFRESH_PREFIX}${Date.now()}` : this.props.prgl.dbKey;\n    if (\n      workspace &&\n      (this.loadingSchema?.dbKey !== dbKey || force) &&\n      connectionId\n    ) {\n      this.loadingSchema = {\n        dbKey,\n        settingSuggestions: [],\n        connectionId,\n        suggestions: [],\n        onRenew: () => this.loadSchema(true),\n      };\n\n      const { tables = [] } = getTables(\n        dbSchemaTables,\n        connection.table_options,\n        db,\n      );\n\n      const ns: Pick<\n        DashboardState,\n        \"tables\" | \"suggestions\" | \"loading\" | \"error\"\n      > = {\n        tables,\n        loading: false,\n        error: undefined,\n        suggestions: undefined,\n      };\n\n      try {\n        if (db.sql) {\n          const { sql } = db;\n\n          const suggestions = await getSqlSuggestions({ sql });\n          const schema = {\n            ...suggestions,\n            connectionId,\n            dbKey,\n          };\n          this.loadingSchema = { ...this.loadingSchema, ...schema };\n          ns.suggestions = { ...this.loadingSchema };\n        }\n      } catch (e) {\n        this.loadingSchema = undefined;\n        console.error(\"Issue with getSuggestions:\", e);\n      }\n      this.props.onLoaded();\n      this.setState(ns);\n    }\n  };\n\n  loadingTables = false;\n  syncsSet = false;\n  onDelta = async (\n    dp: DashboardProps,\n    ds: DashboardState,\n    dd: DeltaOfData<DashboardData>,\n  ) => {\n    const delta = { ...dp, ...ds, ...dd };\n    const {\n      prgl: { connectionId, dbs },\n      workspaceId,\n    } = this.props;\n    const { workspace } = this.d;\n    const user_id = this.props.prgl.user?.id;\n    let ns: Partial<DashboardState> = {};\n\n    const workspaces = dbs.workspaces;\n    if (workspaces.syncOne && !this.syncsSet && connectionId) {\n      this.syncsSet = true;\n\n      const wspFilter = { connection_id: connectionId, deleted: false };\n      const wspCount = +(await workspaces.count(wspFilter));\n      if (!wspCount) {\n        const defaultIsDeleted = +(await workspaces.count({\n          connection_id: connectionId,\n          name: \"default\",\n          deleted: true,\n        }));\n\n        await workspaces.insert({\n          connection_id: connectionId,\n          name: !defaultIsDeleted ? \"default\" : `default ${Date.now()}`,\n          ...({} as Pick<WorkspaceSchema, \"user_id\" | \"last_updated\">),\n        });\n      }\n      let wsp: Workspace | undefined;\n      try {\n        wsp = (await workspaces.findOne(\n          workspaceId ? { id: workspaceId, ...wspFilter } : wspFilter,\n          { orderBy: { last_used: -1 } },\n        )) as Workspace;\n\n        await cloneEditableWorkpsaces({ dbs, user_id });\n\n        /** If this is an editable workspace then ensure we're working on a clone */\n        if (\n          wsp.published &&\n          wsp.user_id !== this.props.prgl.user?.id &&\n          wsp.layout_mode !== \"fixed\"\n        ) {\n          let myClonedWsp = await workspaces.findOne({\n            parent_workspace_id: wsp.id,\n          });\n          if (!myClonedWsp) {\n            myClonedWsp = (await cloneWorkspace(dbs, wsp.id, true)).clonedWsp;\n          }\n          if (wsp.id !== myClonedWsp.id) {\n            window.location.href = getWorkspacePath(myClonedWsp);\n          }\n          wsp = myClonedWsp;\n        }\n      } catch (e) {\n        this.setState({ wspError: e });\n        return;\n      }\n\n      if (!wsp as any) {\n        this.setState({ wspError: true });\n        return;\n      }\n\n      const validatedCols = await this.props.prgl.dbs.workspaces\n        .getColumns(\"en\", {\n          rule: \"update\",\n          filter: { id: wsp.id },\n        })\n        .catch((error) => {\n          console.error(error);\n          return [];\n        });\n\n      const isReadonly = validatedCols.every((c) => !c.update);\n      this.setState({\n        isReadonly,\n      });\n      const { connection } = this.props.prgl;\n      window.document.title = `Prostgles UI - ${connection.name || connection.db_host}`;\n\n      let updatedLastUsed = false;\n      const workspaceSync = await workspaces.syncOne(\n        { id: wsp.id, deleted: false },\n        { handlesOnData: true },\n        (workspace, delta) => {\n          if (!this.mounted) return;\n          if (!updatedLastUsed) {\n            updatedLastUsed = true;\n            workspace.$update?.({ last_used: new Date() as any });\n          }\n          this.setData({ workspace }, { workspace: delta });\n        },\n      );\n\n      const linksSync = await dbs.links.sync?.(\n        { workspace_id: wsp.id },\n        { handlesOnData: true },\n        (links, delta) => {\n          if (!this.mounted) return;\n\n          this.setData({ links: links as any }, { links: delta as any });\n        },\n      );\n\n      const windowsSync = await dbs.windows.sync?.(\n        { workspace_id: wsp.id },\n        { handlesOnData: true, select: \"*\", patchText: false },\n        (_wnds, deltas) => {\n          const wnds: WindowSyncItem[] = _wnds as any;\n          if (!this.mounted) return;\n\n          const windows = wnds.sort(\n            (a, b) => +a.last_updated - +b.last_updated,\n          );\n          const closedWindows = windows.filter(\n            (w) => w.closed && !w.table_name && w.name,\n          );\n          const openWindows = windows.filter((w) => !w.closed);\n\n          /** Dashboard only re-renders on window ids or names change OR linked windows filters change\n           * Maybe ....\n           */\n          const stringOpts = (w: WindowSyncItem) =>\n            `${w.id} ${w.type} ${w.fullscreen} ${JSON.stringify(w.filter)} ${JSON.stringify(w.having)} ${w.parent_window_id} ${w.minimised} ${w.created}`; // ${JSON.stringify((w as any).options?.extent ?? {})}\n          if (\n            this.d.windows.map(stringOpts).sort().join() ===\n            openWindows.map(stringOpts).sort().join()\n          ) {\n            return;\n          }\n          this.setData(\n            { allWindows: windows, windows: openWindows, closedWindows },\n            { windows: deltas as any },\n          );\n        },\n      );\n      this.setData(\n        { windowsSync, workspaceSync, linksSync },\n        { windowsSync, workspaceSync, linksSync },\n      );\n    }\n\n    this.checkIfNoOpenWindows();\n\n    const needToRecalculateCounts =\n      \"workspace\" in delta &&\n      ((delta.workspace && \"hideCounts\" in delta.workspace) ||\n        delta.workspace?.options?.tableListEndInfo ||\n        delta.workspace?.options?.tableListSortBy);\n    const schemaChanged = this.props.prgl.dbKey !== this.loadingSchema?.dbKey; //  !this.loadingSchema?.dbKey.startsWith(FORCED_REFRESH_PREFIX) &&\n    const dataWasImported = !!delta.imported;\n    if (\n      workspace &&\n      (schemaChanged || needToRecalculateCounts || dataWasImported)\n    ) {\n      void this.loadSchema();\n    }\n\n    if (dd) {\n      [\"windows\", \"workspace\", \"links\"].map((key) => {\n        if (dd[key]) ns[key] = { ...dd[key] };\n      });\n      if (isEmpty(ns) && dd.windowsSync) {\n        ns = { reRender: Date.now() };\n      }\n    }\n    if (!isEmpty(ns)) {\n      this.setState(ns as any);\n    }\n  };\n\n  loadTable = async (\n    args: Omit<LoadTableArgs, \"db\" | \"dbs\" | \"workspace_id\">,\n  ): Promise<string> => {\n    const { db, dbs } = this.props.prgl;\n    const { workspace } = this.d;\n    if (!workspace) throw new Error(\"Workspace not found\");\n    return loadTable({ ...args, db, dbs, workspace_id: workspace.id });\n  };\n\n  checkedIfNoOpenWindows = false;\n  checkIfNoOpenWindows = async () => {\n    const {\n      workspaceId,\n      prgl: { dbs },\n    } = this.props;\n    if (\n      workspaceId &&\n      !this.d.windows.some(\n        (w) => w.workspace_id === workspaceId && !w.closed,\n      ) &&\n      !this.checkedIfNoOpenWindows\n    ) {\n      this.checkedIfNoOpenWindows = true;\n      const hasOpenWindows = await dbs.windows.findOne(\n        {\n          workspace_id: workspaceId,\n          closed: false,\n        },\n        { select: \"\" },\n      );\n      if (!hasOpenWindows) {\n        const menuBtn = document.querySelector<HTMLButtonElement>(\n          `[data-command=\"menu\"]`,\n        );\n        menuBtn?.click();\n      }\n    }\n  };\n\n  /**\n   * Used to reduce useless re-renders\n   */\n  menuAnchorState = createReactiveState<HTMLElement | undefined>(undefined);\n\n  isOk = false;\n  render() {\n    const { localSettings, prgl } = this.props;\n    const { connectionId } = prgl;\n    const {\n      tables,\n      loading,\n      isReadonly,\n      suggestions,\n      wspError,\n      error,\n      namePopupWindow,\n    } = this.state;\n\n    const { centeredLayout } = localSettings;\n\n    if (wspError || error) {\n      const errorNode =\n        wspError ?\n          <FlexCol className=\"w-full h-full ai-center text-0 pt-2\">\n            Workspace not found\n            <Btn\n              color=\"action\"\n              variant=\"filled\"\n              asNavLink={true}\n              href={`${ROUTES.CONNECTIONS}/${connectionId}`}\n              iconPath={mdiArrowLeft}\n            >\n              Go back\n            </Btn>\n          </FlexCol>\n        : <ErrorComponent error={error} />;\n      return <div className=\"flex-col p-1 text-white\">{errorNode}</div>;\n    }\n\n    const { windowsSync, workspace } = this.d;\n\n    let mainContent: React.ReactNode;\n\n    if (!windowsSync || !workspace || !tables) {\n      let loadingMessage = \"\";\n      if (!windowsSync || !workspace) {\n        loadingMessage = \"Loading dashboard...\";\n      } else if (!tables) {\n        loadingMessage = \"Loading schema...\";\n      }\n\n      return (\n        <div className=\"absolute flex-row bg-color-0 \" style={{ inset: 0 }}>\n          <Loading\n            id=\"main\"\n            message={loadingMessage}\n            className=\"m-auto\"\n            refreshPageTimeout={5000}\n          />\n        </div>\n      );\n    }\n\n    if (connectionId) {\n      mainContent = (\n        <ViewRendererWrapped\n          /** Do not re-render on dbKey change because it breaks sql editor */\n          // key={prgl.dbKey}\n          isReadonly={isReadonly}\n          prgl={prgl}\n          workspace={workspace}\n          loadTable={this.loadTable}\n          links={this.d.links}\n          windows={this.d.windows}\n          tables={tables}\n          onCloseUnsavedSQL={(q, e) => {\n            this.setState({ namePopupWindow: { w: q, node: e.currentTarget } });\n          }}\n          suggestions={suggestions}\n        />\n      );\n    }\n\n    this.isOk = true;\n\n    const pinnedMenu = getIsPinnedMenu(workspace);\n    const isReadonlyWorkspace =\n      workspace.published && workspace.user_id !== prgl.user?.id;\n    const isFixed = isReadonlyWorkspace && workspace.layout_mode === \"fixed\";\n    const dashboardMenu = (\n      <DashboardMenu\n        menuAnchorState={this.menuAnchorState}\n        prgl={prgl}\n        suggestions={suggestions}\n        loadTable={this.loadTable}\n        tables={tables}\n        workspace={workspace}\n      />\n    );\n    const pinnedDashboardMenu = pinnedMenu ? dashboardMenu : null;\n    const popupDashboardMenu = pinnedDashboardMenu ? null : dashboardMenu;\n\n    const getCenteredEdgeStyle = (): React.CSSProperties | undefined =>\n      !centeredLayout?.enabled ?\n        undefined\n      : {\n          flex: pinnedDashboardMenu ? 0.5 : 0,\n          minWidth: pinnedDashboardMenu ? \"200px\" : 0,\n          width:\n            workspace.options.pinnedMenuWidth ?\n              `${workspace.options.pinnedMenuWidth}px`\n            : \"auto\",\n          maxWidth: `calc((100% - var(${CENTERED_WIDTH_CSS_VAR}))/2)`,\n          overflow: \"hidden\",\n          display: \"flex\",\n        };\n\n    const centeredWidth =\n      !centeredLayout?.enabled ? undefined : `var(${CENTERED_WIDTH_CSS_VAR})`;\n    const centerStyle: React.CSSProperties | undefined =\n      !centeredWidth ? undefined : (\n        {\n          flex: 1000,\n          minWidth: 0,\n          width: centeredWidth,\n          maxWidth: centeredWidth,\n          margin: \"0 auto\",\n        }\n      );\n\n    return (\n      <FlexCol\n        className={\"Dashboard gap-0 f-1 min-w-0 min-h-0 w-full \"}\n        style={{\n          maxWidth: \"100vw\",\n          opacity: connectionId ? 1 : 0,\n          transition: \"opacity .5s\",\n          ...(centeredLayout?.enabled && {\n            [CENTERED_WIDTH_CSS_VAR]: `${centeredLayout.maxWidth}px`,\n          }),\n        }}\n      >\n        <div\n          className={`${TopHeaderClassName} f-0 flex-row  min-h-0 min-w-0 o-hidden`}\n        >\n          <>{popupDashboardMenu}</>\n          <TopControls\n            onClick={(e) => {\n              this.menuAnchorState.set(e.currentTarget);\n            }}\n            location=\"workspace\"\n            pinned={pinnedMenu}\n            prgl={this.props.prgl}\n            workspace={workspace}\n            loadedSuggestions={suggestions}\n          />\n        </div>\n\n        <CloseSaveSQLPopup\n          namePopupWindow={namePopupWindow}\n          onClose={() => this.setState({ namePopupWindow: undefined })}\n          windows={this.d.windows}\n        />\n\n        <div\n          className=\"Dashboard_Wrapper min-h-0 f-1 flex-row relative\"\n          style={{\n            gap: \"1px\",\n            marginTop: \"2px\",\n          }}\n        >\n          <div style={getCenteredEdgeStyle()}>{pinnedDashboardMenu}</div>\n\n          <FlexRow\n            style={centerStyle}\n            className=\"Dashboard_MainContentWrapper f-1 gap-0 relative ai-none jc-none\"\n          >\n            <DashboardCenteredLayoutResizer />\n            {mainContent}\n          </FlexRow>\n\n          <div\n            style={{\n              ...getCenteredEdgeStyle(),\n              /** Ensure right section shrinks to 0 if the left dashboard menu is pinned and we have a centered layout */\n              ...(!!pinnedDashboardMenu && { minWidth: 0 }),\n            }}\n          ></div>\n        </div>\n\n        {loading ?\n          <Loading className=\"p-2\" />\n        : null}\n      </FlexCol>\n    );\n  }\n}\n\nexport const Dashboard = (\n  p: Omit<DashboardProps, \"localSettings\" | \"navigate\" | \"prgl\">,\n) => {\n  const localSettings = useLocalSettings();\n  const navigate = useNavigate();\n  const prgl = usePrgl();\n  return (\n    <_Dashboard\n      {...p}\n      prgl={prgl}\n      localSettings={localSettings}\n      navigate={navigate}\n    />\n  );\n};\n\nexport type CommonWindowProps<T extends ChartType = ChartType> = Pick<\n  DashboardProps,\n  \"prgl\"\n> & {\n  key: string;\n  \"data-key\": string;\n  \"data-table-name\": string | null;\n  \"data-view-type\":\n    | \"table\"\n    | \"map\"\n    | \"timechart\"\n    | \"sql\"\n    | \"card\"\n    | \"method\"\n    | \"barchart\";\n  \"data-title\": string;\n  w: WindowSyncItem<T>;\n  childWindows: WindowSyncItem[];\n  getLinksAndWindows: () => {\n    links: LinkSyncItem[];\n    windows: WindowSyncItem<ChartType>[];\n  };\n  /**\n   * e is undefined when the table window was closed due to dropped table\n   */\n  onClose: (\n    e: React.MouseEvent<HTMLButtonElement, MouseEvent> | undefined,\n  ) => any;\n  /**\n   * used to force re-render after links changed\n   */\n  onForceUpdate: () => void;\n  titleIcon?: React.ReactNode;\n  tables: Required<DashboardState>[\"tables\"];\n  isReadonly: boolean;\n  suggestions: LoadedSuggestions | undefined;\n  myLinks: LinkSyncItem[];\n  onAddChart: OnAddChart | undefined;\n  active_row: ActiveRow | undefined;\n  workspace: WorkspaceSyncItem;\n} & Pick<ViewRendererProps, \"searchParams\" | \"setSearchParams\">;\n\nexport const getIsPinnedMenu = (workspace: WorkspaceSyncItem) => {\n  return workspace.options.pinnedMenu && !window.isLowWidthScreen;\n};\n\nconst cloneEditableWorkpsaces = async ({\n  dbs,\n  user_id,\n}: {\n  dbs: Prgl[\"dbs\"];\n  user_id: string | undefined;\n}) => {\n  /** Clone published editable workspaces */\n  const editablePublished =\n    !user_id ?\n      []\n    : await dbs.workspaces.find({\n        published: true,\n        user_id: { $ne: user_id },\n        layout_mode: { $isDistinctFrom: \"fixed\" },\n        $notExistsJoined: {\n          workspaces: {\n            user_id,\n          },\n        },\n      });\n  await Promise.all(\n    editablePublished.map(async (wsp) => {\n      return cloneWorkspace(dbs, wsp.id, true);\n    }),\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/DashboardCenteredLayoutResizer.tsx",
    "content": "import React, { useCallback } from \"react\";\nimport { useLocalSettings } from \"../localSettings\";\nimport { debounce } from \"../Map/DeckGLWrapped\";\nimport { CENTERED_WIDTH_CSS_VAR } from \"./Dashboard\";\nimport { Pan } from \"@components/Pan\";\n\nexport const DashboardCenteredLayoutResizer = () => {\n  const localSettings = useLocalSettings();\n\n  const updateCenteredLayoutWidth = useCallback(\n    debounce((newWidth: number) => {\n      localSettings.$set({\n        centeredLayout: {\n          enabled: localSettings.centeredLayout?.enabled ?? false,\n          maxWidth: newWidth,\n        },\n      });\n    }, 200),\n    [localSettings],\n  );\n\n  if (!localSettings.centeredLayout?.enabled) {\n    return null;\n  }\n  return (\n    <Pan\n      key={\"wsp-centered-resize\"}\n      data-command=\"dashboard.centered-layout.resize\"\n      style={{\n        height: \"100%\",\n        width: \"25px\",\n        cursor: \"ew-resize\",\n      }}\n      onPress={(e, node) => {\n        node.classList.toggle(\"resizing-ew\", true);\n      }}\n      onRelease={(e, node) => {\n        node.classList.toggle(\"resizing-ew\", false);\n      }}\n      onPan={(e) => {\n        const dashboardNode = e.node.closest<HTMLDivElement>(\".Dashboard\");\n        const dashboardContent =\n          dashboardNode?.querySelector<HTMLDivElement>(\".ViewRenderer\");\n        if (dashboardNode && dashboardContent) {\n          const newCenteredWidth = Math.max(200, window.innerWidth - 2 * e.x);\n          dashboardNode.style.setProperty(\n            CENTERED_WIDTH_CSS_VAR,\n            `${newCenteredWidth}px`,\n          );\n          updateCenteredLayoutWidth(newCenteredWidth);\n        }\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/PALETTE.ts",
    "content": "import { fromEntries, getEntries } from \"@common/utils\";\n\nconst getRandomElement = <Arr>(\n  items: Arr[],\n): { elem: Arr | undefined; index: number } => {\n  const randomIndex = Math.floor(Math.random() * items.length);\n  return { elem: items[randomIndex], index: randomIndex };\n};\n\nexport const PALETTE = fromEntries(\n  getEntries({\n    c1: [0, 129, 167],\n    c2: [240, 113, 103],\n    c3: [58, 134, 255],\n    c4: [131, 56, 236],\n    c5: [203, 149, 0],\n  } satisfies Record<string, [number, number, number]>).map(([key, rgb]) => [\n    key,\n    {\n      getStr: (opacity = 1) => `rgba(${[...rgb, opacity].join(\", \")})`,\n      getDeckRGBA: (opacity = 1) =>\n        [...rgb, Math.round(opacity * 255)] as [number, number, number, number],\n    },\n  ]),\n);\n\nexport const getRandomColor = (\n  opacity = 1,\n  usedColors?: number[][],\n): number[] => {\n  const results = Object.values(PALETTE).map((p) => p.getDeckRGBA(opacity));\n  const nonUsedColors = results.filter(\n    (c) => !usedColors?.some((uc) => uc.join() === c.join()),\n  );\n  return getRandomElement(nonUsedColors).elem ?? results[0]!;\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/ViewRenderer.tsx",
    "content": "import ErrorComponent, { ErrorTrap } from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport { isDefined, isEqual } from \"prostgles-types\";\nimport React from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { DashboardHotkeys } from \"../DashboardMenu/DashboardHotkeys\";\nimport { LinkMenu } from \"../LinkMenu\";\nimport RTComp from \"../RTComp\";\nimport type { SilverGridProps } from \"../SilverGrid/SilverGrid\";\nimport { SilverGridReact } from \"../SilverGrid/SilverGrid\";\nimport W_Map from \"../W_Map/W_Map\";\nimport {\n  getLinkColorV2,\n  getMapLayerQueries,\n} from \"../W_Map/fetchData/getMapLayerQueries\";\nimport { W_Method } from \"../W_Method/W_Method\";\nimport { W_SQL } from \"../W_SQL/W_SQL\";\nimport type { ActiveRow } from \"../W_Table/W_Table\";\nimport W_Table from \"../W_Table/W_Table\";\nimport { W_TimeChart } from \"../W_TimeChart/W_TimeChart\";\nimport { default as WNDOW } from \"../Window\";\nimport { getCrossFilters } from \"../joinUtils\";\nimport type { LocalSettings } from \"../localSettings\";\nimport { useLocalSettings } from \"../localSettings\";\nimport { findShortestPath, makeReversibleGraph } from \"../shortestPath\";\nimport type {\n  CommonWindowProps,\n  DashboardData,\n  DashboardProps,\n  DashboardState,\n  _Dashboard,\n} from \"./Dashboard\";\nimport type {\n  ChartType,\n  Link,\n  WindowData,\n  WindowSyncItem,\n} from \"./dashboardUtils\";\nimport { getViewRendererUtils } from \"./getViewRendererUtils\";\nimport { W_Barchart } from \"../W_Barchart/W_Barchart\";\n\nexport type ViewRendererProps = Pick<DashboardProps, \"prgl\"> &\n  Pick<DashboardData, \"workspace\" | \"links\" | \"windows\"> &\n  Pick<DashboardState, \"tables\" | \"suggestions\" | \"isReadonly\"> & {\n    loadTable: _Dashboard[\"loadTable\"];\n    onCloseUnsavedSQL: (\n      q: WindowSyncItem<ChartType>,\n      e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n    ) => any;\n    localSettings: LocalSettings;\n    searchParams: URLSearchParams;\n    setSearchParams: ReturnType<typeof useSearchParams>[1];\n  };\ntype ViewRendererState = {\n  active_row?: ActiveRow;\n  linkMenuWindow?: {\n    w: WindowSyncItem;\n    anchorEl: HTMLElement | Element;\n  };\n};\n\ntype D = {\n  links: Link[];\n  windows: WindowData[];\n};\nexport class ViewRenderer extends RTComp<\n  ViewRendererProps,\n  ViewRendererState,\n  D\n> {\n  state: ViewRendererState = {};\n  gridRef?: HTMLDivElement;\n  gridWrapperRef?: HTMLDivElement;\n\n  getLinkChain(w1_id: string, w2_id: string): string[] | undefined {\n    const { links = [] } = this.d;\n    const graph = makeReversibleGraph(links.map((l) => [l.w1_id, l.w2_id]));\n    const path = findShortestPath(graph, w1_id, w2_id);\n    return path.distance < Infinity ? path.path : undefined;\n  }\n\n  getTableChain = (w1_id: string, w2_id: string): string[] | undefined => {\n    const path = this.getLinkChain(w1_id, w2_id);\n    if (path) {\n      const { windows } = this.props;\n      const tableChain = path.map((wid) => {\n        return windows.find((w) => w.id === wid)?.table_name;\n      });\n      if (tableChain.every(isDefined)) {\n        return tableChain;\n      }\n    }\n    return undefined;\n  };\n\n  getOpenedLinksAndWindows() {\n    const windows = this.props.windows.filter((w) => !w.deleted && !w.closed);\n    const links = this.props.links.filter(\n      (l) =>\n        !l.closed &&\n        !l.deleted &&\n        [l.w1_id, l.w2_id].every((wid) => windows.some((w) => w.id === wid)),\n    );\n\n    return { links, windows };\n  }\n\n  render() {\n    const {\n      workspace,\n      tables,\n      suggestions,\n      isReadonly,\n      searchParams,\n      setSearchParams,\n      prgl,\n    } = this.props;\n    const { links, windows } = this.getOpenedLinksAndWindows();\n    const { linkMenuWindow } = this.state;\n\n    if (!workspace || !tables) return;\n\n    const { onClickRow, onAddChart, onLinkTable } = getViewRendererUtils.bind(\n      this,\n    )({ ...this.props, windows, links, workspace, tables });\n\n    const getRenderedWindow = (\n      w: WindowSyncItem,\n      childWindow: React.ReactNode | null,\n      childWindows: WindowSyncItem[],\n    ) => {\n      const onClose: CommonWindowProps[\"onClose\"] = async (e) => {\n        if (!e) return;\n        w = w.$get() as WindowSyncItem;\n        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n        if (!w.$get) {\n          console.error(this.d.windows);\n        }\n        this.setState({ active_row: undefined });\n\n        if (w.type === \"sql\" && (w as WindowData<\"sql\">).options?.sqlWasSaved) {\n          await w.$update({ closed: true });\n        } else if (\n          w.sql.trim().length &&\n          w.type === \"sql\" &&\n          !(w as WindowData<\"sql\">).options?.sqlWasSaved\n        ) {\n          this.props.onCloseUnsavedSQL(w, e);\n\n          /** Is table or chart. Delete permanently */\n        } else {\n          await w.$update({ closed: true, deleted: true });\n\n          /* This does not work because the server query adds the filter which now \n          // await dbs.windows.update({ id: q.id }, { closed: true });\n          */\n\n          await this.props.prgl.dbs.links.update(\n            { $or: [{ w1_id: w.id }, { w2_id: w.id }] },\n            { closed: true, deleted: true },\n          );\n        }\n      };\n\n      const onForceUpdate = () => {\n        const { links, windows } = this.getOpenedLinksAndWindows();\n        const haveActiveLinks = links.some((l) =>\n          windows.find(\n            (otherWindow) =>\n              !otherWindow.closed &&\n              otherWindow.id !== w.id &&\n              [l.w1_id, l.w2_id].sort().join() ===\n                [w.id, otherWindow.id].sort().join(),\n          ),\n        );\n        if (haveActiveLinks) {\n          setTimeout(() => this.forceUpdate(), 1);\n        }\n      };\n\n      const { links, windows } = this.getOpenedLinksAndWindows();\n      const myLinks = links.filter((l) => [l.w1_id, l.w2_id].includes(w.id));\n\n      const key = w.id + w.fullscreen;\n\n      const commonProps: CommonWindowProps = {\n        key,\n        \"data-key\": w.id,\n        \"data-table-name\": w.table_name,\n        \"data-view-type\": w.type,\n        \"data-title\": WNDOW.getTitle(w),\n        w,\n        workspace,\n        childWindows,\n        prgl: this.props.prgl,\n        tables,\n        onClose,\n        onForceUpdate,\n        searchParams,\n        setSearchParams,\n        suggestions,\n        isReadonly: !!isReadonly,\n        myLinks,\n        active_row: this.state.active_row,\n        getLinksAndWindows: () => this.getOpenedLinksAndWindows(),\n        onAddChart: !onAddChart ? undefined : (args) => onAddChart(args, w),\n      };\n      const setLinkMenu =\n        isReadonly ? undefined : (\n          (linkMenuWindow: ViewRendererState[\"linkMenuWindow\"]) =>\n            this.setState({ linkMenuWindow })\n        );\n\n      let result: Required<SilverGridProps>[\"children\"][number] | null = null;\n      const linkIcon = null;\n\n      if (w.type === \"method\") {\n        result = <W_Method {...commonProps} w={w} />;\n      } else if (w.type === \"sql\") {\n        result = (\n          <W_SQL\n            titleIcon={linkIcon}\n            setLinkMenu={setLinkMenu}\n            {...commonProps}\n            childWindow={childWindow}\n            w={w}\n          />\n        );\n      } else {\n        const { colorStr } = getLinkColorV2(\n          myLinks.find((l) => l.options.type !== \"table\"),\n          0.1,\n        );\n        const active_row = this.state.active_row;\n\n        if (w.type === \"map\") {\n          const { active_row } = this.state;\n\n          const layerQueries = getMapLayerQueries({\n            active_row,\n            links,\n            myLinks,\n            windows,\n            w,\n          });\n\n          result = (\n            <W_Map\n              myActiveRow={\n                active_row?.window_id === w.id ? active_row : undefined\n              }\n              onClickRow={(row, table_name) => {\n                onClickRow(row, table_name, w.id, { type: \"table-row\" });\n              }}\n              titleIcon={linkIcon}\n              layerQueries={layerQueries}\n              {...commonProps}\n              w={w}\n            />\n          );\n        } else if (w.type === \"barchart\") {\n          result = (\n            <W_Barchart\n              {...commonProps}\n              activeRowColor={colorStr}\n              myActiveRow={\n                active_row?.window_id === w.id ? active_row : undefined\n              }\n              onClickRow={(row, tableName, value) => {\n                onClickRow(row, tableName, w.id, { type: \"barchart\", value });\n              }}\n              w={w}\n            />\n          );\n        } else if (w.type === \"timechart\") {\n          result = (\n            <W_TimeChart\n              {...commonProps}\n              activeRowColor={colorStr}\n              myActiveRow={\n                active_row?.window_id === w.id ? active_row : undefined\n              }\n              onClickRow={(row, tableName, value) => {\n                onClickRow(row, tableName, w.id, { type: \"timechart\", value });\n              }}\n              w={w}\n            />\n          );\n        } else if (w.type === \"table\") {\n          const crossF = getCrossFilters(w, active_row, links, windows);\n          result = (\n            <W_Table\n              setLinkMenu={setLinkMenu}\n              activeRowColor={colorStr}\n              activeRow={\n                active_row?.window_id === w.id ? active_row : undefined\n              }\n              onLinkTable={\n                this.props.isReadonly ?\n                  undefined\n                : (tblName, path) => {\n                    void onLinkTable(w, tblName, path);\n                  }\n              }\n              joinFilter={crossF.activeRowFilter}\n              externalFilters={crossF.all}\n              onClickRow={(row) =>\n                onClickRow(row, w.table_name!, w.id, { type: \"table-row\" })\n              }\n              childWindow={childWindow}\n              {...commonProps}\n              key={commonProps.key + this.props.prgl.dbKey}\n              w={w}\n            />\n          );\n        } else {\n          result = (\n            <ErrorComponent error={`Unsupported window type: ${w.type}`} />\n          );\n        }\n      }\n\n      const res = {\n        id: w.id,\n        title: w.name || w.table_name || w.id,\n        linkIcon,\n        onClose: isReadonly ? undefined : onClose,\n        q: w,\n        elem: result,\n      };\n\n      return res;\n    };\n\n    const parentWindows = windows.filter((w) => !w.parent_window_id);\n    const renderedWindows = parentWindows\n      .map((w) => {\n        const latestChildWindows = windows\n          .filter((cw) => cw.parent_window_id === w.id)\n          .sort(\n            (b, a) =>\n              new Date(a.created).getTime() - new Date(b.created).getTime(),\n          );\n        // .sort((b,a) => Number(a.last_updated) - Number(b.last_updated));\n        const latestChildWindow = latestChildWindows.find(\n          (cw) => !cw.minimised,\n        );\n\n        const renderedChildNode =\n          latestChildWindow &&\n          getRenderedWindow(latestChildWindow, undefined, []).elem;\n        return getRenderedWindow(w, renderedChildNode, latestChildWindows);\n      })\n      .filter(isDefined);\n\n    return (\n      <div\n        className=\"ViewRenderer min-h-0 f-1 flex-row relative\"\n        ref={(r) => {\n          if (r) this.gridWrapperRef = r;\n        }}\n      >\n        {!renderedWindows.length && (\n          <FlexCol\n            className=\"absolute inset-0 jc-center ai-center\"\n            style={{\n              opacity: prgl.theme === \"light\" ? 1 : 0.5,\n              background:\n                prgl.theme === \"light\" ? \"var(--gray-100)\" : \"var(--gray-800)\",\n            }}\n          >\n            <DashboardHotkeys\n              style={{\n                color: prgl.theme === \"light\" ? \"var(--gray-400)\" : \"white\",\n              }}\n              keyStyle={{\n                background:\n                  prgl.theme === \"light\" ? \"var(--gray-200)\" : \"black\",\n                textTransform: \"uppercase\",\n              }}\n            />\n          </FlexCol>\n        )}\n        {linkMenuWindow && this.gridWrapperRef && (\n          <LinkMenu\n            w={linkMenuWindow.w}\n            tables={tables}\n            links={links}\n            windows={windows}\n            anchorEl={linkMenuWindow.anchorEl}\n            db={prgl.db}\n            dbs={prgl.dbs}\n            onClose={() => this.setState({ linkMenuWindow: undefined })}\n            gridRef={this.gridWrapperRef}\n            onLinkTable={(tableName, path) =>\n              onLinkTable(linkMenuWindow.w, tableName, path)\n            }\n          />\n        )}\n        <SilverGridReact\n          _ref={(r) => {\n            this.gridRef = r;\n          }}\n          layoutMode={workspace.layout_mode === \"fixed\" ? \"fixed\" : \"editable\"}\n          defaultLayoutType={workspace.options.defaultLayoutType}\n          className=\"min-h-0 relative\"\n          layout={workspace.layout}\n          onChange={(newLayout) => {\n            if (!isEqual(newLayout, workspace.layout)) {\n              workspace.$update({ layout: newLayout });\n            }\n          }}\n          hideButtons={\n            !isReadonly ? undefined : (\n              {\n                minimize: true,\n                close: true,\n                pan: true,\n              }\n            )\n          }\n          onClose={async (key, e) => {\n            const w = renderedWindows.find((d) => d.id === key);\n            if (w) await w.onClose?.(e);\n            return 1;\n          }}\n        >\n          {renderedWindows.map((d) => d.elem)}\n        </SilverGridReact>\n      </div>\n    );\n  }\n}\n\nexport const ViewRendererWrapped = (\n  props: Omit<\n    ViewRendererProps,\n    \"localSettings\" | \"searchParams\" | \"setSearchParams\"\n  >,\n) => {\n  const localSettings = useLocalSettings();\n  const [searchParams, setSearchParams] = useSearchParams();\n\n  return (\n    <ErrorTrap>\n      <ViewRenderer\n        {...props}\n        localSettings={localSettings}\n        searchParams={searchParams}\n        setSearchParams={setSearchParams}\n      />\n    </ErrorTrap>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/cloneWorkspace.ts",
    "content": "import { omitKeys } from \"prostgles-types\";\nimport type { LayoutConfig } from \"../SilverGrid/SilverGrid\";\nimport type { DBS } from \"./DBS\";\n\nexport const cloneWorkspace = async (\n  dbs: DBS,\n  workspaceId: string,\n  keepName = false,\n) => {\n  const id = workspaceId;\n  const wsp = await dbs.workspaces.findOne({ id });\n  if (!wsp) throw new Error(\"Workspace not found\");\n  const name = `${wsp.name} (Copy)`;\n  const existing = await dbs.workspaces.findOne(\n    { name: { $like: `${name.slice(0, -1)}%` } },\n    { orderBy: [{ name: -1 }] },\n  );\n  const tryParseNumber = (str: string) => {\n    const num = parseInt(str.trim());\n    return isNaN(num) ? 0 : num;\n  };\n  const existingDigit =\n    tryParseNumber(existing?.name.slice(name.length, -1) || \"\") + 1;\n  const newName = existing ? `${wsp.name} (Copy ${existingDigit})` : name;\n  const clonedWsp = await dbs.workspaces.insert(\n    {\n      ...omitKeys(wsp, [\"id\", \"user_id\", \"layout_mode\", \"published\"]),\n      user_id: undefined as any,\n      name: keepName ? wsp.name : newName,\n      published: false,\n      parent_workspace_id: wsp.id,\n    },\n    { returning: \"*\" },\n  );\n\n  const windows = await dbs.windows.find({ workspace_id: id });\n  const links = await dbs.links.find({ workspace_id: id });\n  const clonedWindows = await Promise.all(\n    windows.map(async (w) => {\n      const win = {\n        ...omitKeys(w, [\"id\", \"parent_window_id\", \"user_id\"]),\n        workspace_id: clonedWsp.id,\n        user_id: undefined as any,\n      };\n      const clonedWindow = await dbs.windows.insert(win, { returning: \"*\" });\n      return clonedWindow;\n    }),\n  );\n\n  windows.forEach((w, i) => {\n    const clonedWindow = clonedWindows[i];\n    if (!clonedWindow) throw new Error(\"clonedWindow not found\");\n    if (w.parent_window_id) {\n      const parentIndex = windows.findIndex(\n        (pw) => pw.id === w.parent_window_id,\n      );\n      const parent = clonedWindows[parentIndex];\n      if (!parent) throw new Error(\"parent not found\");\n      dbs.windows.update(\n        { id: clonedWindow.id },\n        { parent_window_id: parent.id },\n      );\n    }\n  });\n\n  const clonedLinks = await Promise.all(\n    links.map(async (l) => {\n      const lin: typeof l = {\n        ...omitKeys(l, [\"id\", \"user_id\"]),\n        workspace_id: clonedWsp.id,\n        user_id: undefined as any,\n        id: undefined as any,\n        w1_id: clonedWindows[windows.findIndex((w) => w.id === l.w1_id)]!.id,\n        w2_id: clonedWindows[windows.findIndex((w) => w.id === l.w2_id)]!.id,\n      };\n      const clonedLinks = await dbs.links.insert(lin, { returning: \"*\" });\n      return clonedLinks;\n    }),\n  );\n\n  const replaceIds = (oldId: string) => {\n    return clonedWindows[windows.findIndex((w) => w.id === oldId)]?.id ?? oldId;\n  };\n  const fixIds = (layout: LayoutConfig) => {\n    if (\"items\" in layout) {\n      layout.items.forEach((item) => {\n        fixIds(item);\n      });\n    } else {\n      layout.id = replaceIds(layout.id);\n    }\n  };\n  const newLayout = { ...(wsp.layout || {}) };\n  fixIds(newLayout);\n  await dbs.workspaces.update({ id: clonedWsp.id }, { layout: newLayout });\n\n  return {\n    clonedWsp,\n    clonedLinks,\n    clonedWindows,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/dashboardUtils.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { DBSchemaTable, ValidatedColumnInfo } from \"prostgles-types\";\n\nimport type {\n  MissingBinsOption,\n  ShowBinLabelsMode,\n  StatType,\n  TimeChartBinSize,\n  TimechartRenderStyle,\n  TooltipPosition,\n} from \"../W_TimeChart/W_TimeChartMenu\";\n\nimport type { SQLSuggestion } from \"../SQLEditor/W_SQLEditor\";\nimport type { RefreshOptions } from \"../W_Table/TableMenu/W_TableMenu\";\n\nimport type { CardLayout } from \"@common/DashboardTypes\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DetailedFilter } from \"@common/filterUtils\";\nimport { type OmitDistributive } from \"@common/utils\";\nimport type { Extent, MapExtentBehavior } from \"../Map/DeckGLMap\";\nimport type {\n  ColumnConfig,\n  ColumnSort,\n  ColumnSortSQL,\n} from \"../W_Table/ColumnMenu/ColumnMenu\";\n\nexport type ChartType =\n  | \"table\"\n  | \"map\"\n  | \"timechart\"\n  | \"barchart\"\n  | \"sql\"\n  | \"card\"\n  | \"method\";\n\nexport type DBSSchemaForHandlers = {\n  [K in keyof DBGeneratedSchema]: DBGeneratedSchema[K][\"columns\"];\n};\n\nexport const vibrateFeedback = (duration = 15) => {\n  try {\n    navigator.vibrate(duration);\n  } catch (e) {\n    console.error(e);\n  }\n};\n\nexport type ChartLink = DBSSchema[\"links\"][\"options\"];\nexport type LinkableCharts = \"table\" | \"map\" | \"timechart\";\n\nexport type Link = DBSSchema[\"links\"];\n\nexport type NewChartOpts = {\n  name: string;\n  linkOpts: OmitDistributive<ChartLink, \"color\" | \"colorKey\">;\n};\n\nexport type OnAddChart = (args: NewChartOpts) => void;\n\nexport type Query = {\n  id: string;\n  tableName: string;\n  filter?: any;\n  sort?: ColumnSortSQL;\n  geo?: {\n    field: string;\n    filterField: string;\n    getData: (ext4326: number[]) => Promise<any[]>;\n  };\n  layout: { x: number; y: number; w: number; h: number };\n};\n\nexport type JoinFilter = {\n  tablePath: string[];\n  filter: any;\n};\n\nexport type WQuery = Query & {\n  joins?: string[];\n  joinFilter?: JoinFilter;\n};\n\nexport type MapExtent = [[number, number], [number, number]];\n\nexport type ChartOptions<CType extends ChartType = \"table\"> =\n  CType extends \"table\" ?\n    {\n      hideCount?: boolean;\n      maxRowHeight?: number;\n      maxCellChars?: number;\n      quickFilterGroups?: QuickFilterGroups;\n      viewAs?:\n        | { type: \"table\" }\n        | { type: \"json\" }\n        | {\n            type: \"card\";\n            hideCardFieldNames?: boolean;\n            cardRows?: number;\n            hideEmptyCardCells?: boolean;\n            cardCellMinWidth?: string;\n            cardGroupBy?: string;\n            cardOrderBy?: string;\n            maxCardRowHeight?: number;\n            maxCardWidth?: string;\n          };\n      cardLayout?: CardLayout;\n      hideEditRow?: boolean;\n      hideInsertButton?: boolean;\n      showFilters?: boolean;\n      showSubLabel?: boolean;\n      filterOperand?: \"AND\" | \"OR\";\n      havingOperand?: \"AND\" | \"OR\";\n    }\n  : CType extends \"method\" ?\n    {\n      args?: Record<string, any>;\n      disabledArgs?: string[];\n      hiddenArgs?: string[];\n      showCode?: boolean;\n      showLogs?: boolean;\n    }\n  : CType extends \"card\" ?\n\n    {\n      // sortableFields: string[];\n      // filterFields: string[];\n      // fieldConfigs?: FieldConfigNested[]\n    }\n  : CType extends \"map\" ?\n    Partial<{\n      extent: [number, number, number, number];\n      latitude: number;\n      longitude: number;\n      zoom: number;\n      pitch: number;\n      bearing: number;\n      colorField?: string;\n      tileURLs?: string[];\n      tileSize?: number;\n      showAddShapeBtn?: boolean;\n      hideLayersBtn?: boolean;\n      showCardOnClick?: boolean;\n      tileAttribution?: {\n        title: string;\n        url: string;\n      };\n      extentBehavior?: MapExtentBehavior;\n      projection?: \"mercator\" | \"orthographic\";\n      target?: [number, number, number];\n      aggregationMode?: {\n        type: \"limit\" | \"wait\";\n        limit: number;\n        wait: number;\n      };\n      dataOpacity: number;\n      basemapDesaturate: number;\n      basemapOpacity: number;\n      basemapImage?: {\n        url: string;\n        bounds: Extent;\n      };\n    }>\n  : CType extends \"timechart\" ?\n    {\n      yScaleMode?: \"single\" | \"multiple\";\n      binSize?: TimeChartBinSize;\n      tooltipPosition?: TooltipPosition;\n      missingBins?: MissingBinsOption;\n      renderStyle?: TimechartRenderStyle;\n      showBinLabels?: ShowBinLabelsMode;\n      showGradient?: boolean;\n      binValueLabelMaxDecimals?: number | null;\n      filter?: { min: number; max: number } | null;\n    }\n  : CType extends \"sql\" ?\n    {\n      /**\n       * Used to show/hide results table in\n       */\n      hideTable?: boolean;\n\n      /**\n       * If false then ask user about saving the query\n       */\n      sqlWasSaved?: boolean;\n\n      /**\n       * Used for sql queries table inserted from search\n       * If sql wasn't changed by user then allow closing window without asking for saving\n       */\n      sqlChanged?: boolean;\n\n      /**\n       * Used to restore cursor position within sql query\n       */\n      cursorPosition?: {\n        column: number;\n        lineNumber: number;\n      };\n\n      sqlResultCols?: (Pick<\n        ValidatedColumnInfo,\n        \"tsDataType\" | \"udt_name\" | \"name\"\n      > & {\n        idx: number;\n        key: string;\n        // label: string;\n        subLabel: string;\n        width?: number;\n        sortable: boolean;\n      })[];\n\n      lastSQL?: string;\n    }\n  : {\n      notSEtYet: \"a\";\n    };\n\nexport const IsMap = (w?: any): w is SyncDataItem<WindowData<\"map\">> => {\n  return w?.type === \"map\";\n};\nexport const IsTable = (w?: any): w is SyncDataItem<WindowData<\"table\">> => {\n  return w?.type === \"table\";\n};\nexport const isMethod = (\n  w?: SyncDataItem<WindowData> | WindowData,\n): w is WindowSyncItem<\"method\"> => {\n  return w?.type === \"method\";\n};\nexport const IsSQL = (w: any): w is SyncDataItem<WindowData<\"sql\">> => {\n  return w.type === \"sql\";\n};\nexport const IsTimeChart = (\n  w?: any,\n): w is SyncDataItem<WindowData<\"timechart\">> => {\n  return w?.type === \"timechart\";\n};\n\ntype Windows = Required<DBSSchema>[\"windows\"];\n\nexport const TopHeaderClassName = \"TopHeader\";\n\nexport type WindowData<CType extends ChartType = ChartType> = Omit<\n  Windows,\n  \"columns\" | \"options\" | \"sort\" | \"filter\" | \"type\" | \"having\"\n> & {\n  type: CType;\n  id: string;\n  table_oid: number;\n  sql?: string;\n  table_name: CType extends \"table\" ? Exclude<Windows[\"table_name\"], null>\n  : null | string;\n  method_name: CType extends \"method\" ? Exclude<Windows[\"method_name\"], null>\n  : null | string;\n  name: string;\n  last_updated: string;\n  fullscreen?: boolean;\n  show_menu?: boolean;\n  closed?: boolean;\n  deleted: boolean;\n  workspace_id?: string;\n  options?: RefreshOptions & ChartOptions<CType>;\n  filter?: DetailedFilter[];\n  having?: DetailedFilter[];\n  columns?: ColumnConfig[] | null;\n  /**\n   * This is either the sql user has selected OR the current code block\n   */\n  selected_sql?: string;\n\n  nested_tables?: Record<\n    string,\n    {\n      label?: string;\n      cols: ColumnConfig[];\n      path: string[];\n    }\n  >;\n  user_id: string;\n  limit: number | null;\n  sort: null | ColumnSort[];\n};\n\nexport const windowIs = <T extends ChartType>(\n  w: WindowData,\n  type: T,\n): w is WindowData<T> => {\n  return w.type === type;\n};\n\ntype ChartsObj = {\n  [type in ChartType]: SyncDataItem<Required<WindowData<type>>, true>;\n};\ntype ChartsObjOfUnion<U extends ChartType> = { [K in U]: ChartsObj[K] }[U];\n\nexport type WindowSyncItem<T extends ChartType = ChartType> =\n  ChartsObjOfUnion<T>;\nexport type LinkSyncItem = SyncDataItem<Link, true>;\n\nexport type WorkspaceSchema = DBSSchema[\"workspaces\"];\n\nexport type Workspace = Required<WorkspaceSchema>;\n\nexport type WorkspaceSyncItem = SyncDataItem<Workspace, true>;\n\nexport type UserData = Omit<DBSSchema[\"users\"], \"password\">;\n\n/**\n * A user group is defined by a filter\n */\nexport type UserFilter = Record<keyof UserData, any>;\n\nexport type UserGroupData = {\n  id: string;\n  filter: UserFilter;\n};\nexport type UserTypeData = {\n  id: string;\n};\n\nexport type AcessControlUserTypes = DBSSchema[\"access_control_user_types\"];\n\nexport type Backups = DBSSchema[\"backups\"];\n\nexport type LoadedSuggestions = {\n  dbKey: string;\n  connectionId: string;\n  suggestions: SQLSuggestion[];\n  settingSuggestions: SQLSuggestion[];\n  /**\n   * Must refresh suggestions for CREATE/DROP/ALTER USER because\n   * it is not being picked up by the event trigger\n   */\n  onRenew: VoidFunction;\n};\n\nexport type Join = {\n  tableName: string;\n  hasFkeys?: boolean;\n  on: [string, string][];\n};\nexport type JoinV2 = Omit<Join, \"on\"> & { on: [string, string][][] };\n\nexport type DBSchemaTableColumn = ValidatedColumnInfo & {\n  icon: string | undefined;\n  label: string;\n};\n\ntype TableOptions = NonNullable<\n  NonNullable<DBSSchema[\"connections\"][\"table_options\"]>[string]\n>;\nexport type DBSchemaTableWJoins = Omit<DBSchemaTable, \"columns\"> & {\n  label: string;\n  joins: Join[];\n  joinsV2: JoinV2[];\n  columns: DBSchemaTableColumn[];\n} & Omit<TableOptions, \"label\" | \"columns\">;\nexport type DBSchemaTablesWJoins = DBSchemaTableWJoins[];\n\n/**\n * Predefined quick filters that the user can toggle on/off\n * These are shown in the filter bar under \"Quick Filters\"\n */\nexport type QuickFilterGroups = {\n  [groupName: string]: {\n    toggledFilterName?: string;\n    filters: {\n      [filterName: string]: {};\n    };\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/debuggingUtils.ts",
    "content": "export const debugObjectChanges = <T extends object>(\n  obj: T,\n  deep = false,\n): T => {\n  return observeObjectChanges(\n    obj,\n    () => {\n      // eslint-disable-next-line no-debugger\n      debugger;\n    },\n    deep ? new WeakSet<WeakKey>() : undefined,\n  );\n};\n\nfunction observeObjectChanges<T extends object>(\n  obj: T,\n  onChange: () => void,\n  seen: WeakSet<WeakKey> | undefined,\n): T {\n  console.error(\"watchDeep started\");\n  if (seen) {\n    if (obj !== Object(obj) || seen.has(obj)) {\n      return obj;\n    }\n    seen.add(obj);\n  }\n\n  if (Array.isArray(obj)) {\n    const trackedArray = observeArray(obj, onChange);\n    trackedArray.forEach((item) => {\n      return observeObjectChanges(item, onChange, seen);\n    });\n    return trackedArray;\n  }\n  return new Proxy(obj, {\n    get(target, prop, receiver) {\n      const value = Reflect.get(target, prop, receiver);\n\n      if (seen && typeof value === \"object\" && value !== null) {\n        return observeObjectChanges(value, onChange, seen);\n      }\n      return value;\n    },\n\n    set(target, prop, value, receiver) {\n      const oldValue = target[prop];\n      const result = Reflect.set(target, prop, value, receiver);\n\n      if (oldValue !== value) {\n        onChange();\n      }\n      return result;\n    },\n\n    deleteProperty(target, prop) {\n      const result = Reflect.deleteProperty(target, prop);\n\n      onChange();\n\n      return result;\n    },\n  });\n}\n\nconst observeArray = <T extends any[]>(arr: T, onChange: VoidFunction): T => {\n  const arrayMethods = [\n    \"push\",\n    \"pop\",\n    \"shift\",\n    \"unshift\",\n    \"splice\",\n    \"sort\",\n    \"reverse\",\n  ];\n  arrayMethods.forEach((method) => {\n    arr[method] = function (...args) {\n      onChange();\n      const result = Array.prototype[method].apply(this, args);\n      return result;\n    };\n  });\n  return arr;\n};\n\nconst internalFunctions = new Set([\n  \"Generator.next\",\n  \"asyncGeneratorStep\",\n  \"_next\",\n  \"Promise.then\",\n  \"new Promise\",\n  \"Object.<anonymous>\",\n  \"Module._compile\",\n  \"Module.load\",\n]);\n\nexport function stackTrace(arg, check?: (chain: string) => void): void {\n  const stack = new Error().stack?.split(\"\\n\").slice(2) ?? [];\n\n  const chain = stack\n    .map((line) => {\n      const [content, filePath] = line.slice(\"    at \".length).split(\" (\");\n      if (!content) return;\n      if (filePath?.includes(\"vendors-node_modules\")) return;\n      if (content.startsWith(\"http\")) return;\n      if (internalFunctions.has(content)) return;\n      return content;\n    })\n    .filter(Boolean)\n    .join(\" -> \");\n  check?.(chain);\n  console.error(chain, arg);\n}\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/getTables.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { DBSchemaTable } from \"prostgles-types\";\nimport { getJoinedTables } from \"../W_Table/tableUtils/tableUtils\";\nimport type { DBSchemaTablesWJoins } from \"./dashboardUtils\";\n\nexport const getTables = (\n  schemaTables: DBSchemaTable[],\n  connectionTableOptions: DBSSchema[\"connections\"][\"table_options\"],\n  db: DBHandlerClient,\n  capitaliseNames = false,\n): { tables: DBSchemaTablesWJoins } => {\n  const tables = schemaTables.map((t) => {\n    const { columns, label, ...tableOpts } =\n      connectionTableOptions?.[t.name] ?? {};\n    const result = {\n      ...tableOpts,\n      label:\n        label || (capitaliseNames ? convertSnakeToReadable(t.name) : t.name),\n      ...t,\n      ...getJoinedTables(schemaTables, t.name, db),\n      columns: t.columns\n        .map((c) => ({\n          ...c,\n          label: capitaliseNames ? convertSnakeToReadable(c.name) : c.name,\n          icon: columns?.[c.name]?.icon,\n        }))\n        .sort((a, b) => {\n          return a.ordinal_position - b.ordinal_position;\n        }),\n    };\n    return result;\n  });\n  return { tables };\n};\n\nconst convertSnakeToReadable = (str: string) => {\n  // ^[a-z0-9]+    : Starts with one or more lowercase letters or digits\n  // (?:_[a-z0-9]+)* : Followed by zero or more groups of an underscore and one or more lowercase letters/digits\n  // $             : Ends the string\n  const snakeCaseRegex = /^[a-z0-9]+(?:_[a-z0-9]+)*$/;\n\n  if (str && snakeCaseRegex.test(str)) {\n    const words = str.split(\"_\");\n    const readableWords = words.map((word) => {\n      return word.charAt(0).toUpperCase() + word.slice(1);\n    });\n    return readableWords.join(\" \");\n  }\n  return str;\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/getViewRendererUtils.ts",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { OmitDistributive } from \"@common/utils\";\nimport { matchObj } from \"@common/utils\";\nimport { pageReload } from \"@components/Loader/Loading\";\nimport { type AnyObject, type ParsedJoinPath, isEmpty } from \"prostgles-types\";\nimport type { ActiveRow } from \"../W_Table/W_Table\";\nimport type {\n  ChartType,\n  DBSchemaTablesWJoins,\n  Link,\n  LinkSyncItem,\n  NewChartOpts,\n  WindowData,\n  WindowSyncItem,\n  WorkspaceSyncItem,\n} from \"./dashboardUtils\";\nimport { PALETTE } from \"./PALETTE\";\nimport type { ViewRenderer, ViewRendererProps } from \"./ViewRenderer\";\n\ntype Args = ViewRendererProps & {\n  links: LinkSyncItem[];\n  windows: WindowSyncItem[];\n  workspace: WorkspaceSyncItem;\n  tables: DBSchemaTablesWJoins;\n};\nexport const getViewRendererUtils = function (\n  this: ViewRenderer,\n  { prgl, workspace, windows, links, tables }: Args,\n) {\n  const addWindow = async <CT extends ChartType>(\n    w: { type: CT } & Partial<\n      Pick<WindowData, \"name\" | \"table_name\" | \"options\" | \"parent_window_id\">\n    >,\n    filter: DetailedFilter[] = [],\n  ) => {\n    const {\n      options = {\n        showFilters: false,\n        refresh: { type: \"Realtime\", throttleSeconds: 1 },\n      },\n      type,\n      table_name,\n      name,\n      ...otherWindowOpts\n    } = w;\n    const res = await prgl.dbs.windows.insert(\n      {\n        ...otherWindowOpts,\n        name,\n        type,\n        table_name,\n        options,\n        filter,\n        fullscreen: false,\n        workspace_id: workspace.id,\n        limit: 500,\n      } as DBSSchema[\"windows\"],\n      { returning: \"*\" },\n    );\n\n    setTimeout(() => {\n      if (\n        !document.querySelector(`[data-box-id=\"${res.id}\"]`) &&\n        !otherWindowOpts.parent_window_id\n      ) {\n        console.error(\"SYNC FAIL BUG, REFRESHING\");\n        pageReload(\"SYNC FAIL BUG\");\n      }\n    }, 1000);\n\n    return res;\n  };\n\n  const addLink = (l: {\n    w1_id: string;\n    w2_id: string;\n    linkOpts: OmitDistributive<Link[\"options\"], \"color\" | \"colorKey\">;\n  }) => {\n    const { links } = this.getOpenedLinksAndWindows();\n    const { w1_id, w2_id } = l;\n\n    const myLinks = links.filter((l) =>\n      [l.w1_id, l.w2_id].find((wid) => [w1_id, w2_id].includes(wid)),\n    );\n    const cLinkColor = myLinks\n      .map((l) => (l.options.type === \"table\" ? l.options.colorArr : undefined))\n      .find((c) => c);\n    const colorOpts =\n      l.linkOpts.type === \"table\" ?\n        {\n          colorArr: cLinkColor ?? PALETTE.c4.getDeckRGBA(),\n        }\n      : ({} as const);\n\n    const options: Link[\"options\"] = {\n      ...colorOpts,\n      ...l.linkOpts,\n    };\n\n    return prgl.dbs.links.insert(\n      {\n        w1_id,\n        w2_id,\n        workspace_id: workspace.id,\n        options,\n        last_updated: undefined as any,\n        user_id: undefined as any,\n      },\n      { returning: \"*\" },\n    );\n  };\n\n  const onLinkTable = async (\n    q: WindowSyncItem,\n    tblName: string,\n    tablePath: ParsedJoinPath[],\n  ) => {\n    const w2_id = await this.props.loadTable({\n      type: \"table\",\n      table: tblName,\n      fullscreen: false,\n    });\n    await addLink({\n      w1_id: q.id,\n      w2_id,\n      linkOpts: { type: \"table\", tablePath },\n    });\n    if (q.fullscreen) q.$update({ fullscreen: false });\n  };\n\n  const onAddChart =\n    (!prgl.dbs.windows.insert as boolean) ?\n      undefined\n    : async (args: NewChartOpts, parentW: WindowData) => {\n        const { name, linkOpts } = args;\n        const type = args.linkOpts.type;\n        let extra:\n          | Pick<WindowData<\"map\">, \"parent_window_id\" | \"options\">\n          | Pick<WindowData<\"timechart\">, \"parent_window_id\" | \"options\"> = {\n          parent_window_id: null,\n        };\n\n        if (type === \"map\") {\n          extra = {\n            parent_window_id: parentW.id,\n            options: {\n              dataOpacity: 0.5,\n              basemapOpacity: 0.25,\n              basemapDesaturate: 0,\n              tileAttribution: {\n                title: \"© OpenStreetMap\",\n                url: \"https://www.openstreetmap.org/\",\n              },\n              aggregationMode: {\n                type: \"limit\",\n                limit: 2000,\n                wait: 2,\n              },\n              refresh: {\n                type: \"Realtime\",\n                throttleSeconds: 1,\n                intervalSeconds: 1,\n              },\n              showCardOnClick: true,\n              showAddShapeBtn: true,\n            },\n          };\n        } else if (type === \"timechart\") {\n          extra = {\n            parent_window_id: parentW.id,\n            options: {\n              showBinLabels: \"off\",\n              binValueLabelMaxDecimals: 3,\n              missingBins: \"ignore\",\n              refresh: {\n                type: \"Realtime\",\n                throttleSeconds: 1,\n                intervalSeconds: 1,\n              },\n            },\n          };\n        } else if (type === \"barchart\") {\n          extra = {\n            parent_window_id: parentW.id,\n          };\n        }\n        // const existingCharts = await windows.filter(cw => cw.parent_window_id === parentW.id);\n        // if(existingCharts.length){\n        //   // alert(\"Close existing chart before adding new one\");\n        // } else {\n\n        //   const w = await addWindow({ name, type, ...extra }) as WindowData;\n        // }\n        const w =\n          windows.find(\n            (cw) => cw.type === type && cw.parent_window_id === parentW.id,\n          ) ?? ((await addWindow({ name, type, ...extra })) as WindowData);\n        await addLink({ w1_id: parentW.id, w2_id: w.id, linkOpts });\n      };\n\n  type ClickRowOpts =\n    | { type: \"table-row\" }\n    | { type: \"timechart\"; value: ActiveRow[\"timeChart\"] }\n    | { type: \"barchart\"; value: ActiveRow[\"barChart\"] };\n  const onClickRow = (\n    rowOrFilter: AnyObject | undefined,\n    table_name: string,\n    wid: string,\n    opts: ClickRowOpts,\n  ) => {\n    if (\n      !rowOrFilter ||\n      !table_name ||\n      (this.state.active_row && this.state.active_row.window_id !== wid)\n    ) {\n      if (this.state.active_row) {\n        this.setState({ active_row: undefined });\n      }\n      return;\n    }\n\n    let row_filter: AnyObject = {};\n    const cols = tables.find((t) => t.name === table_name)?.columns ?? [];\n    const pKeys = cols.filter((c) => c.is_pkey);\n\n    if (opts.type === \"timechart\") {\n      row_filter = { ...rowOrFilter };\n\n      /**\n       * Prefer pkey but if missing then use other non formated columns\n       */\n    } else if (pKeys.length && pKeys.every((pk) => pk.name in rowOrFilter)) {\n      pKeys.map((pk) => {\n        row_filter[pk.name] = rowOrFilter[pk.name];\n      });\n    } else if (\"$rowhash\" in rowOrFilter) {\n      row_filter.$rowhash = rowOrFilter.$rowhash;\n    } else {\n      cols.map((c) => {\n        if (\n          c.tsDataType === \"number\" ||\n          c.tsDataType === \"string\" ||\n          c.is_pkey\n        ) {\n          row_filter[c.name] = rowOrFilter[c.name];\n        }\n      });\n    }\n\n    /* Must link to at least one other table */\n    const rl_ids = links.filter(\n      (l) =>\n        [l.w1_id, l.w2_id].includes(wid) &&\n        windows\n          .filter((_w) => _w.id !== wid)\n          .find((_w) => [l.w1_id, l.w2_id].includes(_w.id)),\n    );\n\n    let active_row: ActiveRow | undefined =\n      !rl_ids.length ? undefined : (\n        {\n          window_id: wid,\n          table_name: table_name,\n          row_filter,\n          timeChart: opts.type === \"timechart\" ? opts.value : undefined,\n        }\n      );\n\n    /* If clicking on the same row then disable active_row */\n    if (\n      active_row &&\n      !isEmpty(this.state.active_row) &&\n      this.state.active_row?.window_id === active_row.window_id &&\n      matchObj(this.state.active_row.row_filter, active_row.row_filter)\n    ) {\n      active_row = undefined;\n    }\n\n    if (this.state.active_row !== active_row) {\n      this.setState({ active_row });\n    }\n  };\n\n  return {\n    onClickRow,\n    onAddChart,\n    onLinkTable,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/Dashboard/loadTable.ts",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport type { Prgl } from \"../../App\";\nimport { type WindowData } from \"./dashboardUtils\";\n\nexport type LoadTableArgs = Pick<Prgl, \"db\" | \"dbs\"> & {\n  type: \"sql\" | \"table\" | \"method\";\n  workspace_id: string;\n  table?: string;\n  fullscreen?: boolean;\n  filter?: DetailedFilter[];\n  sql?: string;\n  name?: string;\n  method_name?: string;\n};\n\nexport const loadTable = async (args: LoadTableArgs): Promise<string> => {\n  const {\n    db,\n    dbs,\n    type,\n    table = null,\n    filter = [],\n    sql = \"\",\n    name = table,\n    method_name,\n    workspace_id,\n  } = args;\n  let options: WindowData[\"options\"] = { hideTable: true };\n  const defaultTableOptions: WindowData[\"options\"] = {\n    maxCellChars: 500,\n    showFilters: false,\n    refresh: { type: \"Realtime\", throttleSeconds: 1, intervalSeconds: 1 },\n  };\n  let table_oid: number | undefined;\n  if (table) {\n    const tableHandler = db[table];\n    if (tableHandler?.getInfo) {\n      if (\"getInfo\" in tableHandler) {\n        const info = await tableHandler.getInfo();\n        table_oid = info.oid;\n      }\n      options = defaultTableOptions;\n    } else {\n      const err =\n        db[table] ?\n          \"Not allowed to view data from this table\"\n        : \"Table not found\";\n      alert(err);\n      throw err;\n    }\n  }\n\n  const r = await dbs.windows.insert(\n    {\n      sql,\n      filter,\n      options,\n      type,\n      table_name: table,\n      table_oid,\n      name,\n      method_name,\n      fullscreen: false,\n      workspace_id,\n      last_updated: undefined as any,\n      user_id: undefined as any,\n    },\n    { returning: \"*\" },\n  );\n  return r.id;\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/CreateTable.tsx",
    "content": "import { SuccessMessage } from \"@components/Animations\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Popup from \"@components/Popup/Popup\";\nimport { mdiPlus } from \"@mdi/js\";\nimport React, { useMemo, useState } from \"react\";\nimport { isDefined } from \"../../utils/utils\";\nimport { SQLSmartEditor } from \"../SQLEditor/SQLSmartEditor\";\nimport type { ColumnOptions } from \"../W_Table/ColumnMenu/AlterColumn/ColumnEditor\";\nimport { ColumnEditor } from \"../W_Table/ColumnMenu/AlterColumn/ColumnEditor\";\nimport { getColumnDefinitionQuery } from \"../W_Table/ColumnMenu/AlterColumn/CreateColumn\";\nimport { getReferencesQuery } from \"../W_Table/ColumnMenu/AlterColumn/ReferenceEditor\";\nimport { getColumnIconPath } from \"../W_Table/ColumnMenu/ColumnSelect/getColumnListItem\";\nimport type { DashboardMenuProps } from \"./DashboardMenu\";\n\nexport const CreateTable = ({\n  prgl: { db, tables },\n  onClose,\n  suggestions,\n}: DashboardMenuProps & { onClose: VoidFunction }) => {\n  const [tableOpts, setTableOpts] = useState<\n    Partial<{\n      name: string;\n      cols: ColumnOptions[];\n      finished: boolean;\n    }>\n  >({});\n\n  const [success, setSuccess] = useState(false);\n\n  const duplicateColumn = tableOpts.cols?.find((c, i) =>\n    tableOpts.cols?.some(\n      (_c, _i) => isDefined(c.name) && c.name === _c.name && i !== _i,\n    ),\n  );\n  const duplicateTable = tables.find((t) => t.name === tableOpts.name);\n  const error =\n    success ? undefined\n    : duplicateTable ? \"A table with this name already exists\"\n    : duplicateColumn ?\n      `Cannot have two columns with the same name ${JSON.stringify(duplicateColumn.name)}`\n    : \"\";\n\n  const getColSQL = (c: ColumnOptions) => {\n    const [firstRef] = c.references ?? [];\n    return (\n      \"  \" +\n      [\n        getColumnDefinitionQuery(c),\n        firstRef ?\n          getReferencesQuery(firstRef)\n            .filter((v) => v)\n            .join(\" \")\n        : \"\",\n        // c.check?.trim() ? `CHECK(${c.check})` : \"\"\n      ]\n        .filter((v) => v)\n        .join(\" \")\n    );\n  };\n  const query = `CREATE TABLE ${JSON.stringify(tableOpts.name)} ( \\n${tableOpts.cols?.map((c) => `  ${getColSQL(c)}`).join(\", \\n\") ?? \"\"} \\n);`;\n\n  const updateColumn = (\n    columnIndex: number,\n    columnChanges: Partial<ColumnOptions> | undefined,\n  ) => {\n    const existingColumn = tableOpts.cols?.[columnIndex];\n    if (!existingColumn) return;\n    setTableOpts((topts) => ({\n      ...topts,\n      cols: (topts.cols ?? [])\n        .map((c, i) =>\n          i === columnIndex ?\n            columnChanges && { ...existingColumn, ...columnChanges }\n          : c,\n        )\n        .filter(isDefined),\n    }));\n  };\n\n  const dataTypeSuggestions = useMemo(\n    () => suggestions?.suggestions.map((s) => s.dataTypeInfo).filter(isDefined),\n    [suggestions],\n  );\n\n  const [activeColumnInfo, setActiveColumnInfo] = useState<{\n    index: number;\n    anchor: HTMLElement;\n    isAddingNew: boolean;\n  }>();\n  const activeColumn =\n    activeColumnInfo && tableOpts.cols?.[activeColumnInfo.index];\n  const activeColumnIndex = activeColumnInfo?.index;\n  const displayedColumns = tableOpts.cols?.filter(\n    (c, i) =>\n      c.name &&\n      !(activeColumnInfo?.isAddingNew && activeColumnInfo.index === i),\n  );\n  return (\n    <Popup\n      title=\"Create new table\"\n      positioning=\"top-center\"\n      clickCatchStyle={{ opacity: 0.3 }}\n      contentClassName=\"flex-col gap-1 p-0\"\n      onClose={onClose}\n    >\n      {activeColumn && isDefined(activeColumnIndex) && tableOpts.name && (\n        <Popup\n          title={\n            !activeColumnInfo.isAddingNew ?\n              `Edit ${activeColumn.name}`\n            : \"Add new column\"\n          }\n          anchorEl={activeColumnInfo.anchor}\n          positioning=\"beneath-left\"\n          onClose={() => {\n            if (activeColumnInfo.isAddingNew) {\n              updateColumn(activeColumnIndex, undefined);\n            }\n            setActiveColumnInfo(undefined);\n          }}\n          footerButtons={[\n            {\n              label: \"Remove column\",\n              variant: \"faded\",\n              color: \"danger\",\n              onClick: () => {\n                updateColumn(activeColumnIndex, undefined);\n              },\n            },\n            {\n              label: !activeColumnInfo.isAddingNew ? \"Done\" : \"Add column\",\n              variant: \"filled\",\n              color: \"action\",\n              \"data-command\": \"dashboard.menu.createTable.addColumn.confirm\",\n              disabledInfo:\n                !activeColumn.name ? \"Name required\"\n                : !activeColumn.dataType ? \"Data type required\"\n                : undefined,\n              onClick: () => {\n                setActiveColumnInfo(undefined);\n              },\n            },\n          ]}\n        >\n          <ColumnEditor\n            key={activeColumnIndex}\n            {...activeColumn}\n            suggestions={suggestions}\n            isAlter={false}\n            tables={tables}\n            tableName={tableOpts.name}\n            onChange={(k, v) => {\n              updateColumn(activeColumnIndex, v);\n            }}\n            onAddReference={(newCol, reference) => {\n              updateColumn(activeColumnIndex, {\n                ...newCol,\n                references: [...(activeColumn.references ?? []), reference],\n              });\n            }}\n            onEditReference={(r, i) => {\n              updateColumn(activeColumnIndex, {\n                references: activeColumn\n                  .references!.map((_r, _i) => {\n                    if (i === _i) {\n                      return r;\n                    }\n\n                    return _r;\n                  })\n                  .filter(isDefined),\n              });\n            }}\n          />\n        </Popup>\n      )}\n      {success ?\n        <SuccessMessage message={`Created ${tableOpts.name}`} />\n      : <>\n          <FlexCol className=\"flex-col gap-1 p-1 pr-2 pb-2\">\n            <FormField\n              className=\"ml-p5\"\n              label=\"Table name\"\n              data-command=\"dashboard.menu.createTable.tableName\"\n              value={tableOpts.name}\n              error={duplicateTable ? error : undefined}\n              onChange={(name) => setTableOpts((t) => ({ ...t, name }))}\n            />\n\n            {tableOpts.name && !!displayedColumns?.length && (\n              <div className=\"flex-col pl-p5 gap-1\">\n                <h3 className=\"ta-left p-0 m-0 mt-1\">Columns</h3>\n                {displayedColumns.map((colOpt, colI) => {\n                  const colDataType = dataTypeSuggestions?.find(\n                    (dt) =>\n                      dt.udt_name.toLowerCase() ===\n                      colOpt.dataType?.toLowerCase(),\n                  );\n                  return (\n                    <Btn\n                      key={colI}\n                      title=\"Press to edit\"\n                      variant=\"faded\"\n                      iconPath={getColumnIconPath({\n                        is_pkey: colOpt.isPkey,\n                        references: colOpt.references as any,\n                        udt_name: colOpt.dataType as any,\n                      })}\n                      onClick={({ currentTarget }) =>\n                        setActiveColumnInfo({\n                          index: colI,\n                          anchor: currentTarget,\n                          isAddingNew: false,\n                        })\n                      }\n                    >\n                      {getColumnDefinitionQuery({\n                        ...colOpt,\n                        dataType:\n                          colDataType?.udt_name ?? colOpt.dataType ?? \"\",\n                      })}\n                    </Btn>\n                  );\n                })}\n              </div>\n            )}\n\n            {tableOpts.name && (\n              <Btn\n                data-command=\"dashboard.menu.createTable.addColumn\"\n                variant=\"faded\"\n                color=\"action\"\n                iconPath={mdiPlus}\n                className=\"ml-p5 mt-1\"\n                onClick={({ currentTarget }) => {\n                  setTableOpts((topts) => ({\n                    ...topts,\n                    cols: (topts.cols ?? []).concat([{}]),\n                  }));\n                  setActiveColumnInfo({\n                    anchor: currentTarget,\n                    index: tableOpts.cols?.length ?? 0,\n                    isAddingNew: true,\n                  });\n                }}\n              >\n                Add column\n              </Btn>\n            )}\n          </FlexCol>\n\n          <FlexRow className=\"p-1\">\n            {/* <Btn\n        disabledInfo={!tableOpts.name?.trim().length? \"Table name cannot be empty\" : undefined}\n        onClick={onClose}\n      >\n        Cancel\n      </Btn> */}\n            {error ?\n              <InfoRow color=\"danger\" className=\"mx-2 mb-1\">\n                {error}\n              </InfoRow>\n            : tableOpts.finished ?\n              <SQLSmartEditor\n                asPopup={false}\n                title=\"\"\n                onCancel={() =>\n                  setTableOpts((topts) => ({ ...topts, finished: false }))\n                }\n                key={query}\n                sql={db.sql!}\n                query={query}\n                suggestions={suggestions}\n                onSuccess={() => {\n                  setSuccess(true);\n                  setTimeout(() => {\n                    onClose();\n                  }, 1000);\n                }}\n              />\n            : <Btn\n                variant=\"filled\"\n                color=\"action\"\n                className=\"ml-auto\"\n                data-command=\"dashboard.menu.createTable.confirm\"\n                disabledInfo={\n                  !tableOpts.name?.trim().length ?\n                    \"Table name cannot be empty\"\n                  : undefined\n                }\n                onClick={() =>\n                  setTableOpts((opts) => ({ ...opts, finished: true }))\n                }\n              >\n                Create table...\n              </Btn>\n            }\n          </FlexRow>\n        </>\n      }\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/DashboardHotkeys.tsx",
    "content": "import { mdiKeyboard } from \"@mdi/js\";\nimport React from \"react\";\nimport { Hotkey } from \"@components/Hotkey\";\nimport { SectionHeader } from \"../AccessControl/AccessControlRuleEditor\";\n\ntype P = {\n  keyStyle?: React.CSSProperties;\n  style?: React.CSSProperties;\n};\nexport const DashboardHotkeys = ({ keyStyle, style }: P) => {\n  return (\n    <div className=\"flex-col ai-start gap-p5  mt-1\" color=\"info\" style={style}>\n      <SectionHeader icon={mdiKeyboard} size=\"small\">\n        Hotkeys\n      </SectionHeader>\n      <Hotkey\n        keyStyle={keyStyle}\n        label=\"Open Command Palette\"\n        keys={[\"ctrl\", \"k\"]}\n      />\n      <Hotkey\n        keyStyle={keyStyle}\n        label=\"Open a table or script\"\n        keys={[\"ctrl\", \"p\"]}\n      />\n      <Hotkey\n        keyStyle={keyStyle}\n        label=\"Search all data\"\n        keys={[\"ctrl\", \"shift\", \"f\"]}\n      />\n      <Hotkey\n        keyStyle={keyStyle}\n        label=\"Open an SQL File\"\n        keys={[\"ctrl\", \"o\"]}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/DashboardMenu.tsx",
    "content": "import type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport React, { useCallback, useMemo } from \"react\";\nimport type { ReactiveState } from \"../../appUtils\";\nimport { useReactiveState } from \"../../appUtils\";\nimport Popup from \"@components/Popup/Popup\";\nimport type {\n  CommonWindowProps,\n  DashboardProps,\n  DashboardState,\n  _Dashboard,\n} from \"../Dashboard/Dashboard\";\nimport type { WindowData, Workspace } from \"../Dashboard/dashboardUtils\";\nimport type { SEARCH_TYPES } from \"../SearchAll/SearchAll\";\nimport { SearchAll } from \"../SearchAll/SearchAll\";\nimport { DashboardMenuContent } from \"./DashboardMenuContent\";\nimport { DashboardMenuHeader } from \"./DashboardMenuHeader\";\nimport { DashboardMenuHotkeys } from \"./DashboardMenuHotkeys\";\nimport { useTableSizeInfo } from \"./useTableSizeInfo\";\n\nexport type DashboardMenuProps = Pick<DashboardProps, \"prgl\"> & {\n  suggestions: DashboardState[\"suggestions\"];\n  tables: CommonWindowProps[\"tables\"];\n  loadTable: _Dashboard[\"loadTable\"];\n  workspace: SyncDataItem<Workspace, true>;\n};\n\nexport type DashboardMenuState = {\n  showSearchAll?: {\n    mode: (typeof SEARCH_TYPES)[number][\"key\"];\n    term?: string;\n  };\n  queries: SyncDataItem<WindowData<\"sql\">>[];\n};\n\nexport const DashboardMenu = ({\n  menuAnchorState,\n  ...props\n}: Omit<DashboardMenuProps, \"localSettings\" | \"anchor\"> & {\n  menuAnchorState: ReactiveState<HTMLElement | undefined>;\n}) => {\n  const { state: menuAnchor, setState } = useReactiveState(menuAnchorState);\n\n  const [showSearchAll, setShowSearchAll] =\n    React.useState<DashboardMenuState[\"showSearchAll\"]>();\n  const { suggestions, tables, loadTable, workspace, prgl } = props;\n  const { db, dbs } = prgl;\n\n  const filter =\n    workspace.options.showAllMyQueries ? {} : { workspace_id: workspace.id };\n  const { data: windows } = dbs.windows.useSync!(filter, {\n    handlesOnData: true,\n    select: \"*\",\n    patchText: false,\n  });\n  const queries = useMemo(() => {\n    return (windows?.filter((w) => w.type === \"sql\" && !w.deleted) ??\n      []) as SyncDataItem<WindowData<\"sql\">>[];\n  }, [windows]);\n  const anchor = { node: menuAnchor, onClose: () => setState(undefined) };\n  const { tablesWithInfo } = useTableSizeInfo({ tables, db, workspace });\n\n  const onClickSearchAll = useCallback(() => {\n    setShowSearchAll({\n      mode: \"views and queries\",\n      term: undefined,\n    });\n  }, []);\n  const hotKeys = (\n    <>\n      <DashboardMenuHotkeys {...props} setShowSearchAll={setShowSearchAll} />\n    </>\n  );\n  if (!(dbs as any).workspaces.insert) return hotKeys;\n\n  const pinnedMenu = workspace.options.pinnedMenu && !window.isLowWidthScreen;\n  if (!pinnedMenu && !anchor.node && !showSearchAll) return hotKeys;\n  const isReadonlyWorkspace =\n    workspace.published && workspace.user_id !== prgl.user?.id;\n  const isFixed = isReadonlyWorkspace && workspace.layout_mode === \"fixed\";\n  const mainContent =\n    isFixed ? null\n    : pinnedMenu ?\n      <DashboardMenuContent\n        {...props}\n        queries={queries}\n        onClickSearchAll={onClickSearchAll}\n        tablesWithInfo={tablesWithInfo}\n        onClose={undefined}\n      />\n    : anchor.node ?\n      <Popup\n        key=\"main menu\"\n        data-command=\"DashboardMenu\"\n        showFullscreenToggle={{}}\n        title={\n          <DashboardMenuHeader\n            {...props}\n            onClickSearchAll={onClickSearchAll}\n            onClose={anchor.onClose}\n          />\n        }\n        onClickClose={false}\n        onClose={anchor.onClose}\n        positioning=\"beneath-left\"\n        anchorEl={anchor.node}\n        clickCatchStyle={{\n          backdropFilter: \"blur(1px)\",\n          background: \"rgba(var(--text-color-0), 0.11)\",\n          opacity: 1,\n        }}\n        contentStyle={{\n          overflow: \"hidden\",\n          padding: 0,\n        }}\n        autoFocusFirst={\n          isReadonlyWorkspace ? undefined : (\n            {\n              selector: `.search-list-tables input`,\n            }\n          )\n        }\n      >\n        <DashboardMenuContent\n          {...props}\n          queries={queries}\n          tablesWithInfo={tablesWithInfo}\n          onClickSearchAll={onClickSearchAll}\n          onClose={anchor.onClose}\n        />\n      </Popup>\n    : null;\n\n  return (\n    <>\n      {showSearchAll && (\n        <SearchAll\n          db={db}\n          methods={props.prgl.methods}\n          tables={tables}\n          searchType={showSearchAll.mode}\n          defaultTerm={showSearchAll.term}\n          suggestions={suggestions?.suggestions}\n          queries={queries}\n          loadTable={loadTable}\n          onOpenDBObject={(s, method_name) => {\n            if (method_name) {\n              void loadTable({ type: \"method\", method_name });\n            } else if (!s) {\n            } else if (s.type === \"function\") {\n              void loadTable({ type: \"sql\", sql: s.definition, name: s.name });\n            } else if ((s as any).type === \"table\") {\n              if (db[s.name]) {\n                void loadTable({ type: \"table\", table: s.name, name: s.name });\n              } else {\n                void loadTable({\n                  type: \"sql\",\n                  sql: `SELECT *\\nFROM ${s.escapedIdentifier}\\nLIMIT 25`,\n                  name: s.name,\n                });\n              }\n            } else {\n              throw s;\n            }\n          }}\n          onOpen={({ filter, table }) => {\n            void loadTable({ type: \"table\", table, filter });\n          }}\n          onClose={() => {\n            setShowSearchAll(undefined);\n          }}\n        />\n      )}\n      {hotKeys}\n      {mainContent}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/DashboardMenuContent.tsx",
    "content": "import {\n  mdiFile,\n  mdiFilter,\n  mdiFunction,\n  mdiRefresh,\n  mdiScriptTextPlay,\n  mdiTable,\n  mdiTableEdit,\n  mdiTableEye,\n} from \"@mdi/js\";\nimport type { MethodFullDef } from \"prostgles-types\";\nimport { isObject } from \"prostgles-types\";\nimport React, { useRef } from \"react\";\nimport { dataCommand } from \"../../Testing\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { SchemaFilter } from \"../../pages/NewConnection/SchemaFilter\";\nimport { getIsPinnedMenu } from \"../Dashboard/Dashboard\";\nimport { SchemaGraph } from \"../SchemaGraph/SchemaGraph\";\nimport { WorkspaceAddBtn } from \"../WorkspaceMenu/WorkspaceAddBtn\";\nimport { useSetActiveWorkspace } from \"../WorkspaceMenu/useWorkspaces\";\nimport { useLocalSettings } from \"../localSettings\";\nimport type { DashboardMenuProps, DashboardMenuState } from \"./DashboardMenu\";\nimport { DashboardMenuHeader } from \"./DashboardMenuHeader\";\nimport { DashboardMenuResizer } from \"./DashboardMenuResizer\";\nimport { NewTableMenu } from \"./NewTableMenu\";\nimport type { TablesWithInfo } from \"./useTableSizeInfo\";\n\ntype P = DashboardMenuProps & {\n  onClose: undefined | VoidFunction;\n  onClickSearchAll: VoidFunction;\n  tablesWithInfo: TablesWithInfo;\n} & Pick<DashboardMenuState, \"queries\">;\n\nexport const DashboardMenuContent = (props: P) => {\n  const {\n    tables,\n    loadTable,\n    workspace,\n    prgl,\n    queries,\n    onClose,\n    onClickSearchAll,\n    tablesWithInfo,\n  } = props;\n  const {\n    db,\n    methods,\n    theme,\n    user,\n    dbsMethods: { reloadSchema },\n    dbs,\n  } = prgl;\n\n  const pinnedMenu = getIsPinnedMenu(workspace);\n  const isPublishedReadonlyWorkspace =\n    workspace.published && workspace.user_id !== user?.id;\n\n  const { centeredLayout } = useLocalSettings();\n  const maxWidth =\n    centeredLayout?.enabled ?\n      (window.innerWidth - centeredLayout.maxWidth) / 2 + \"px\"\n    : \"50vw\";\n\n  const detailedMethods: (MethodFullDef & { name: string })[] = Object.keys(\n    methods,\n  )\n    .filter((n) => {\n      const m = methods[n];\n      return m && typeof m !== \"function\" && isObject(m) && m.run;\n    })\n    .map((methodName) => ({\n      name: methodName,\n      ...(methods[methodName] as MethodFullDef),\n    }));\n\n  const { setWorkspace } = useSetActiveWorkspace(workspace.id);\n\n  const ref = useRef<HTMLDivElement>(null);\n  const ensureFadeDoesNotShowForOneItem = { minHeight: \"120px\" } as const;\n  const bgColorClass =\n    theme === \"light\" || !pinnedMenu ? \"bg-color-0\" : \"bg-color-1\";\n\n  return (\n    <FlexCol\n      data-command=\"DashboardMenuContent\"\n      className={\n        \"relative f-1 min-h-0 \" +\n        bgColorClass +\n        (window.isMobileDevice ? \" p-p25 \" : \" p-1  \")\n      }\n      ref={ref}\n      style={{\n        ...(pinnedMenu && {\n          minWidth: \"200px\",\n          maxWidth,\n          width:\n            workspace.options.pinnedMenuWidth ?\n              `${workspace.options.pinnedMenuWidth}px`\n            : \"fit-content\",\n          height: \"100%\",\n        }),\n      }}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") {\n          onClose?.();\n        }\n      }}\n    >\n      {isPublishedReadonlyWorkspace && (\n        <FlexCol\n          className=\"jc-center ai-center bg-color-1 p-1\"\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            zIndex: 1,\n            opacity: 0.95,\n            backdropFilter: \"blur(2px)\",\n          }}\n        >\n          <div>\n            This is a read-only published workspace.\n            <br></br>\n            Create your own workspace to open table/views.\n          </div>\n          <WorkspaceAddBtn\n            connection_id={workspace.connection_id}\n            dbs={prgl.dbs}\n            setWorkspace={setWorkspace}\n            btnProps={{\n              children: \"Create workspace\",\n              size: undefined,\n            }}\n          />\n        </FlexCol>\n      )}\n\n      <DashboardMenuResizer\n        dashboardMenuRef={ref.current}\n        workspace={workspace}\n      />\n\n      {pinnedMenu && (\n        <DashboardMenuHeader\n          {...props}\n          onClickSearchAll={onClickSearchAll}\n          onClose={onClose}\n        />\n      )}\n\n      {Boolean(queries.length) && (\n        <SearchList\n          id=\"search-list-queries\"\n          data-command=\"dashboard.menu.savedQueriesList\"\n          className={\" b-t f-1 min-h-0 \"}\n          style={{\n            ...ensureFadeDoesNotShowForOneItem,\n            maxHeight: \"fit-content\",\n          }}\n          placeholder={`${queries.length} saved queries`}\n          noSearchLimit={0}\n          items={queries\n            .sort(\n              (a, b) =>\n                +b.closed - +a.closed || +b.last_updated - +a.last_updated,\n            )\n            .map((t, i) => ({\n              key: i,\n              contentLeft: (\n                <div className=\"flex-col ai-start f-0 text-1\">\n                  <Icon path={mdiScriptTextPlay} size={1} />\n                </div>\n              ),\n              label: t.name,\n              disabledInfo: !t.closed ? \"Already opened\" : undefined,\n              contentRight: (\n                <span className=\"text-2 ml-auto italic\">\n                  {t.sql.trim().slice(0, 10)}...\n                </span>\n              ),\n              onPress: () => {\n                t.$update?.({ closed: false, workspace_id: workspace.id });\n                onClose?.();\n              },\n            }))}\n        />\n      )}\n\n      {!tables.length ?\n        <div className=\"text-1p5 p-1\">0 tables/views</div>\n      : <SearchList\n          className={\"search-list-tables min-h-0  f-1\"}\n          data-command=\"dashboard.menu.tablesSearchList\"\n          limit={100}\n          style={ensureFadeDoesNotShowForOneItem}\n          noSearchLimit={0}\n          leftContent={\n            <SchemaFilter\n              asSelect={{\n                btnProps: {\n                  children: \"\",\n                  title: t.NewConnectionForm[\"Schemas\"],\n                  iconPath: mdiFilter,\n                  variant: \"icon\",\n                },\n                label: \"\",\n              }}\n              db={db}\n              db_schema_filter={props.prgl.connection.db_schema_filter}\n              onChange={(newDbSchemaFilter) => {\n                void dbs.connections.update(\n                  {\n                    id: prgl.connectionId,\n                  },\n                  {\n                    db_schema_filter: newDbSchemaFilter,\n                  },\n                );\n              }}\n            />\n          }\n          inputProps={{\n            \"data-command\": \"dashboard.menu.tablesSearchListInput\",\n          }}\n          placeholder={`${tables.length} tables/views`}\n          noResultsContent={\n            <FlexCol>\n              <InfoRow color=\"info\" variant=\"filled\">\n                Table/view not found.\n              </InfoRow>\n              <Btn\n                variant=\"faded\"\n                color=\"action\"\n                disabledInfo={!reloadSchema ? \"Must be admin\" : \"\"}\n                onClickPromise={async () => {\n                  await reloadSchema!(props.prgl.connectionId);\n                }}\n                iconPath={mdiRefresh}\n              >\n                Refresh schema\n              </Btn>\n            </FlexCol>\n          }\n          items={tablesWithInfo.map((t, i) => {\n            return {\n              contentLeft: (\n                <div\n                  className=\"flex-col ai-start f-0 text-1\"\n                  {...(t.info.isFileTable ?\n                    dataCommand(\"dashboard.menu.fileTable\")\n                  : {})}\n                >\n                  {t.icon ?\n                    <SvgIcon icon={t.icon} />\n                  : <Icon\n                      title={\n                        t.info.isFileTable ? \"File table\"\n                        : t.info.isView ?\n                          \"View\"\n                        : \"Table\"\n                      }\n                      path={\n                        t.info.isFileTable ? mdiFile\n                        : db[t.name]?.insert ?\n                          mdiTableEdit\n                        : mdiTableEye\n                      }\n                      size={1}\n                    />\n                  }\n                </div>\n              ),\n              key: t.name,\n              label: t.label,\n              title: t.info.comment,\n              contentRight: t.endText.length > 0 && (\n                <span title={t.endTitle} className=\"text-2 ml-auto\">\n                  {t.endText}\n                </span>\n              ),\n              onPress: () => {\n                loadTable({ type: \"table\", table: t.name, name: t.label });\n                onClose?.();\n              },\n            };\n          })}\n        />\n      }\n      {detailedMethods.length > 0 && (\n        <SearchList\n          limit={100}\n          noSearchLimit={0}\n          data-command=\"dashboard.menu.serverSideFunctionsList\"\n          className={\"search-list-functions b-t f-1 min-h-0 max-h-fit \"}\n          style={ensureFadeDoesNotShowForOneItem}\n          placeholder={\"Search \" + detailedMethods.length + \" functions\"}\n          items={detailedMethods.map((t, i) => ({\n            contentLeft: (\n              <div className=\"flex-col ai-start f-0 text-1\">\n                <Icon path={mdiFunction} />\n              </div>\n            ),\n            key: t.name,\n            label: t.name,\n            onPress: () => {\n              void loadTable({ type: \"method\", method_name: t.name });\n              onClose?.();\n            },\n          }))}\n        />\n      )}\n      <FlexRowWrap className=\"f-0 mt-1 mx-p5 jc-between\">\n        {!tables.length && !db.sql && (\n          <InfoRow>\n            You have not been granted any permissions. <br></br> Check with\n            system administrator\n          </InfoRow>\n        )}\n\n        <NewTableMenu\n          {...props}\n          loadTable={(args) => {\n            onClose?.();\n            return loadTable(args);\n          }}\n        />\n\n        <SchemaGraph\n          tables={tables}\n          connectionId={props.prgl.connectionId}\n          db_schema_filter={props.prgl.connection.db_schema_filter}\n          dbs={dbs}\n          db={db}\n          theme={theme}\n        />\n      </FlexRowWrap>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/DashboardMenuHeader.tsx",
    "content": "import {\n  mdiPinOffOutline,\n  mdiPinOutline,\n  mdiScriptTextPlay,\n  mdiSearchWeb,\n} from \"@mdi/js\";\nimport React from \"react\";\nimport { dataCommand } from \"../../Testing\";\nimport Btn from \"@components/Btn\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport type { DashboardMenuProps } from \"./DashboardMenu\";\nimport { DashboardMenuSettings } from \"./DashboardMenuSettings\";\nimport { getIsPinnedMenu } from \"../Dashboard/Dashboard\";\nimport { t } from \"../../i18n/i18nUtils\";\n\ntype P = Pick<DashboardMenuProps, \"prgl\" | \"loadTable\" | \"workspace\"> & {\n  onClose: VoidFunction | undefined;\n  onClickSearchAll: VoidFunction;\n};\n\nexport const DashboardMenuHeader = ({\n  prgl,\n  loadTable,\n  onClose,\n  workspace,\n  onClickSearchAll,\n}: P) => {\n  const db = prgl.db;\n  const pinnedMenu = getIsPinnedMenu(workspace);\n  return (\n    <FlexRowWrap className=\"DashboardMenuHeader gap-p5 f-0\">\n      <Btn\n        key=\"sql\"\n        {...dataCommand(\"dashboard.menu.sqlEditor\")}\n        className=\"f-1 jc-start max-w-fit\"\n        title={t.DashboardMenuHeader[\"Opens SQL Query editor\"]}\n        onClickPromise={async () => {\n          await loadTable({ type: \"sql\", name: \"SQL Query\" });\n          onClose?.();\n        }}\n        color=\"action\"\n        variant=\"filled\"\n        iconPath={mdiScriptTextPlay}\n        disabledInfo={db.sql ? undefined : t.common[\"Not permitted\"]}\n      >\n        {window.isLowWidthScreen ? null : t.DashboardMenuHeader[\"SQL Editor\"]}\n      </Btn>\n      <Btn\n        iconPath={mdiSearchWeb}\n        data-command=\"dashboard.menu.quickSearch\"\n        title={t.DashboardMenuHeader[\"Show quick search menu (CTRL + P)\"]}\n        className=\"ml-auto\"\n        onClick={() => {\n          onClickSearchAll();\n          onClose?.();\n        }}\n      />\n      <DashboardMenuSettings prgl={prgl} workspace={workspace} />\n      <Btn\n        iconPath={!pinnedMenu ? mdiPinOutline : mdiPinOffOutline}\n        disabledInfo={\n          window.isLowWidthScreen ?\n            t.DashboardMenuHeader[\"Cannot be used in a low width device\"]\n          : undefined\n        }\n        title={t.DashboardMenuHeader[\"Pin/Unpin\"]}\n        data-command=\"DashboardMenuHeader.togglePinned\"\n        data-key={pinnedMenu ? \"pinned\" : \"unpinned\"}\n        className=\"ml-p25\"\n        onClick={() => {\n          workspace.$update(\n            { options: { pinnedMenu: !workspace.options.pinnedMenu } },\n            { deepMerge: true },\n          );\n          onClose?.();\n        }}\n      />\n    </FlexRowWrap>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/DashboardMenuHotkeys.tsx",
    "content": "import React, { useEffect, useRef } from \"react\";\nimport { getFileText } from \"../W_SQL/W_SQLMenu\";\nimport type { DashboardMenuProps, DashboardMenuState } from \"./DashboardMenu\";\nimport { getKeys } from \"../../utils/utils\";\nimport { includes } from \"../W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\n\ntype P = Pick<DashboardMenuProps, \"loadTable\"> & {\n  setShowSearchAll: React.Dispatch<\n    React.SetStateAction<DashboardMenuState[\"showSearchAll\"]>\n  >;\n};\nexport const DashboardMenuHotkeys = ({ loadTable, setShowSearchAll }: P) => {\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    const onKeyDown = (e) => {\n      const term = window.getSelection()?.toString().trim();\n      const mode = getHotkey(e);\n      if (mode && !includes(mode, [\"rows\", \"open file\", \"commands\"])) {\n        e.preventDefault();\n        setShowSearchAll({ mode, term });\n      }\n    };\n\n    const onKeyUp = (e) => {\n      const term = window.getSelection()?.toString().trim();\n      const mode = getHotkey(e);\n      if (mode === \"open file\") {\n        e.preventDefault();\n        if (inputRef.current) {\n          inputRef.current.click();\n        }\n      } else if (mode === \"views and queries\" || mode === \"rows\") {\n        e.preventDefault();\n        setShowSearchAll({ mode, term });\n      } else if (e.key === \"Escape\") {\n        setShowSearchAll(undefined);\n      }\n    };\n    window.addEventListener(\"keyup\", onKeyUp, true);\n    window.addEventListener(\"keydown\", onKeyDown, true);\n\n    return () => {\n      window.removeEventListener(\"keyup\", onKeyUp, true);\n      window.removeEventListener(\"keydown\", onKeyDown, true);\n    };\n  }, [inputRef, setShowSearchAll]);\n\n  const fileSelected: React.ChangeEventHandler<HTMLInputElement> = async (\n    e,\n  ) => {\n    const file = e.target.files?.[0];\n    if (file) {\n      // if(file.name.toLowerCase().endsWith(\".sql\")){\n      loadTable({ sql: await getFileText(file), type: \"sql\", name: file.name });\n      /* CSV?? */\n      // } else {\n\n      // }\n    }\n  };\n\n  {\n    /* Used to CTRL+O open an sql file */\n  }\n  return (\n    <input\n      ref={inputRef}\n      type=\"file\"\n      accept=\"text/*, .sql, .txt\"\n      className=\"hidden\"\n      onChange={(e) => {\n        return fileSelected(e);\n      }}\n    />\n  );\n};\n\nconst hotkeyCommands = {\n  \"Ctrl+k\": \"commands\",\n  \"Ctrl+p\": \"views and queries\",\n  \"Ctrl+Shift+F\": \"rows\",\n  \"Ctrl+o\": \"open file\",\n} as const;\nconst getHotkey = (e: KeyboardEvent) => {\n  const hotKey = getKeys(hotkeyCommands).find((k) => {\n    const keys = k.split(\"+\");\n    return keys.every((k) => {\n      if (k === \"Ctrl\") return e.ctrlKey;\n      if (k === \"Shift\") return e.shiftKey;\n      return e.key === k;\n    });\n  });\n\n  return hotKey ? hotkeyCommands[hotKey] : undefined;\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/DashboardMenuResizer.tsx",
    "content": "import React, { useRef } from \"react\";\nimport type { DashboardMenuProps } from \"./DashboardMenu\";\nimport { useLocalSettings } from \"../localSettings\";\nimport { Pan } from \"@components/Pan\";\n\ntype P = {\n  dashboardMenuRef: HTMLDivElement | null;\n} & Pick<DashboardMenuProps, \"workspace\">;\n\nexport const DashboardMenuResizer = ({ dashboardMenuRef, workspace }: P) => {\n  const localSettings = useLocalSettings();\n\n  const dragging = useRef<{\n    startClientX: number;\n    clientX: number;\n    startWidth: number;\n  }>();\n\n  if (!dashboardMenuRef) return null;\n\n  return (\n    <Pan\n      key={\"wsp\"}\n      data-command=\"dashboard.menu.resize\"\n      style={{\n        position: \"absolute\",\n        right: 0,\n        top: 0,\n        bottom: 0,\n        width: \"25px\",\n        cursor: \"ew-resize\",\n      }}\n      onPress={(e, node) => {\n        node.classList.toggle(\"resizing-ew\", true);\n      }}\n      onRelease={(e, node) => {\n        node.classList.toggle(\"resizing-ew\", false);\n      }}\n      onPanStart={(e) => {\n        if (!dashboardMenuRef.isConnected) return;\n        dragging.current = {\n          startClientX: e.x,\n          clientX: e.x,\n          startWidth: dashboardMenuRef.clientWidth,\n        };\n      }}\n      onPan={(e) => {\n        if (!dashboardMenuRef.isConnected || !dragging.current) {\n          return false;\n        }\n        const deltaX = e.x - dragging.current.startClientX;\n        const newWidth = dragging.current.startWidth + deltaX;\n        const newWidthStr = `${newWidth}px`;\n        if (localSettings.centeredLayout?.enabled) {\n          dashboardMenuRef.style.maxWidth = newWidthStr;\n        }\n        dashboardMenuRef.style.width = newWidthStr;\n      }}\n      onPanEnd={(e) => {\n        dragging.current = undefined;\n        workspace.$update(\n          { options: { pinnedMenuWidth: dashboardMenuRef.clientWidth } },\n          { deepMerge: true },\n        );\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/DashboardMenuSettings.tsx",
    "content": "import { mdiCog, mdiTable, mdiViewGridPlus } from \"@mdi/js\";\nimport type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport { useEffectAsync, usePromise } from \"prostgles-client\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport { pageReload } from \"@components/Loader/Loading\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { DashboardProps } from \"../Dashboard/Dashboard\";\nimport type { Workspace } from \"../Dashboard/dashboardUtils\";\nimport { useLocalSettings } from \"../localSettings\";\nimport { DashboardHotkeys } from \"./DashboardHotkeys\";\nimport { SettingsSection } from \"./SettingsSection\";\nimport { SmartForm } from \"../SmartForm/SmartForm\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nexport { useEffectAsync };\n\nconst layoutType = [\n  { key: \"tab\", label: \"Tabs\", subLabel: \"Windows placed in the same tab\" },\n  { key: \"col\", label: \"Columns\", subLabel: \"Windows placed top to bottom\" },\n  { key: \"row\", label: \"Rows\", subLabel: \"Windows placed left to right\" },\n];\n\ntype P = Pick<DashboardProps, \"prgl\"> & {\n  workspace: SyncDataItem<Workspace, true>;\n};\n\nexport const DashboardMenuSettings = ({\n  workspace,\n  prgl: { dbsMethods, dbs, dbsTables },\n}: P) => {\n  const dbSize = usePromise(\n    async () => dbsMethods.getDBSize?.(workspace.connection_id),\n    [dbsMethods, workspace],\n  );\n\n  const localSettings = useLocalSettings();\n\n  return (\n    <PopupMenu\n      button={\n        <Btn\n          iconPath={mdiCog}\n          title=\"Show settings\"\n          className=\"\"\n          data-command=\"dashboard.menu.settingsToggle\"\n        />\n      }\n      data-command=\"dashboard.menu.settings\"\n      positioning=\"center\"\n      clickCatchStyle={{ opacity: 0.2 }}\n      title=\"Dashboard settings\"\n      contentClassName=\"p-0\"\n      render={() => {\n        return (\n          <div className=\"flex-col gap-2 p-1\">\n            <SettingsSection title=\"Display options\" iconPath={mdiTable}>\n              <SmartForm\n                label=\"\"\n                db={dbs as DBHandlerClient}\n                tableName=\"connections\"\n                rowFilter={[\n                  {\n                    fieldName: \"id\",\n                    value: workspace.connection_id,\n                  },\n                ]}\n                contentClassname=\"p-0\"\n                jsonbSchemaWithControls={{ noLabels: false }}\n                methods={dbsMethods}\n                tables={dbsTables}\n                columns={{\n                  display_options: {\n                    hideLabel: true,\n                  },\n                }}\n                confirmUpdates={false}\n                showJoinedTables={false}\n              />\n            </SettingsSection>\n            <SettingsSection title=\"Dashboard menu\" iconPath={mdiTable}>\n              <FormField\n                label={{\n                  label: \"Sort table list\",\n                }}\n                fullOptions={[\n                  {\n                    key: \"name\",\n                    label: \"Name\",\n                    subLabel: \"Sort by name\",\n                  },\n                  {\n                    key: \"extraInfo\",\n                    label: \"Extra info\",\n                    subLabel: \"Sort by table list extra info (size/count)\",\n                  },\n                ]}\n                value={workspace.options.tableListSortBy}\n                onChange={(tableListSortBy) => {\n                  workspace.$update(\n                    { options: { tableListSortBy } },\n                    { deepMerge: true },\n                  );\n                }}\n              />\n              <FormField\n                label={{\n                  label: \"Table list extra info\",\n                  info: \"Table/view information shown after the table name in the table list\",\n                }}\n                fullOptions={[\n                  {\n                    key: \"none\",\n                    label: \"None\",\n                    subLabel: \"No extra info\",\n                  },\n                  {\n                    key: \"count\",\n                    label: \"Count\",\n                    subLabel: \"Total number of rows current user has access to\",\n                  },\n                  {\n                    key: \"size\",\n                    label: \"Size\",\n                    subLabel: \"Total size of the table (must be superuser)\",\n                  },\n                ]}\n                value={workspace.options.tableListEndInfo}\n                onChange={(tableListEndInfo) => {\n                  workspace.$update(\n                    { options: { tableListEndInfo } },\n                    { deepMerge: true },\n                  );\n                }}\n              />\n              <SwitchToggle\n                label={{\n                  label: \"Show all my queries\",\n                  info: \"Will allow using queries from all your connections and not just the current one\",\n                }}\n                style={{ marginLeft: \"-.5em\" }}\n                checked={!!workspace.options.showAllMyQueries}\n                onChange={(showAllMyQueries) => {\n                  workspace.$update(\n                    { options: { showAllMyQueries } },\n                    { deepMerge: true },\n                  );\n                }}\n              />\n            </SettingsSection>\n\n            <SettingsSection\n              title=\"Dashboard layout\"\n              iconPath={mdiViewGridPlus}\n            >\n              <SwitchToggle\n                label={{\n                  variant: \"normal\",\n                  label: \"Hide all counts\",\n                  info: \"This will disable counts for the table/view headers. Usefull when there is a performance downgrade\",\n                }}\n                style={{\n                  marginLeft: \"-.5em\",\n                }}\n                checked={!!workspace.options.hideCounts}\n                onChange={(hideCounts) => {\n                  workspace.$update(\n                    { options: { hideCounts } },\n                    { deepMerge: true },\n                  );\n                }}\n              />\n              <SwitchToggle\n                label={{\n                  label: \"Centered layout\",\n                  info: \"Centered layout allows you to center align the dashboard area. This is particularly useful when working on a wide monitor\",\n                }}\n                style={{ marginLeft: \"-.5em\" }}\n                checked={!!localSettings.centeredLayout?.enabled}\n                onChange={(enabled) => {\n                  localSettings.$set({\n                    centeredLayout: {\n                      enabled,\n                      maxWidth: localSettings.centeredLayout?.maxWidth ?? 1200,\n                    },\n                  });\n                  pageReload(\"centeredLayout toggled\");\n                }}\n              />\n\n              <FormField\n                label={{\n                  label: \"Default layout type\",\n                  info: \"Controls new window placement\",\n                }}\n                data-command=\"dashboard.menu.settings.defaultLayoutType\"\n                fullOptions={layoutType}\n                value={workspace.options.defaultLayoutType}\n                onChange={(defaultLayoutType) => {\n                  workspace.$update(\n                    { options: { defaultLayoutType } },\n                    { deepMerge: true },\n                  );\n                }}\n              />\n            </SettingsSection>\n            <div className=\"flex-col gap-1\">\n              <DashboardHotkeys />\n            </div>\n            {dbSize && (\n              <div className=\"mt-2 text-1 font-18 ta-left\">\n                Database total size: {dbSize}\n              </div>\n            )}\n          </div>\n        );\n      }}\n    ></PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/NewTableMenu.tsx",
    "content": "import { mdiFileUploadOutline, mdiFunction, mdiPlus, mdiTable } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport type { FullOption } from \"@components/Select/Select\";\nimport { Select } from \"@components/Select/Select\";\nimport { FileImporter } from \"../FileImporter/FileImporter\";\nimport { NewMethod } from \"../W_Method/NewMethod\";\nimport { CreateTable } from \"./CreateTable\";\nimport type { DashboardMenuProps } from \"./DashboardMenu\";\n\nconst items = [\n  { key: \"new table\", label: \"Create table\", iconPath: mdiTable },\n  {\n    key: \"import file\",\n    label: \"Import file\",\n    subLabel: \"Supported types: csv/geojson/json\",\n    iconPath: mdiFileUploadOutline,\n  },\n  {\n    key: \"new function\",\n    label: \"Create TS Function\",\n    subLabel: \"(Experimental)\",\n    iconPath: mdiFunction,\n  },\n] as const satisfies FullOption[];\n\nexport const NewTableMenu = (p: DashboardMenuProps) => {\n  const { prgl, tables, loadTable } = p;\n  const sql = prgl.db.sql;\n  const [show, setShow] = useState<(typeof items)[number][\"key\"]>();\n\n  if (!sql) return null;\n\n  return (\n    <>\n      <Select\n        title=\"Create/Import\"\n        data-command=\"dashboard.menu.create\"\n        iconPath=\"\"\n        btnProps={{\n          iconPath: mdiPlus,\n          iconPosition: \"left\",\n          iconClassname: \"\",\n          color: \"action\",\n          variant: \"filled\",\n          className: \"\",\n          children: null,\n        }}\n        fullOptions={items}\n        onChange={(o) => {\n          setShow(o);\n        }}\n      />\n      {show === \"new table\" && (\n        <CreateTable\n          {...p}\n          onClose={() => {\n            setShow(undefined);\n          }}\n        />\n      )}\n      {show === \"import file\" && (\n        <FileImporter\n          tables={tables}\n          db={prgl.db}\n          onClose={() => {\n            setShow(undefined);\n          }}\n          openTable={(table) => {\n            loadTable({ type: \"table\", table });\n          }}\n        />\n      )}\n      {show === \"new function\" && (\n        <NewMethod\n          {...prgl}\n          access_rule_id={undefined}\n          onClose={() => setShow(undefined)}\n          methodId={undefined}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/SettingsSection.tsx",
    "content": "import React from \"react\";\nimport { FlexCol } from \"@components/Flex\";\nimport { SectionHeader } from \"../AccessControl/AccessControlRuleEditor\";\n\nexport const SettingsSection = ({\n  title,\n  iconPath,\n  children,\n}: {\n  title: string;\n  iconPath: string;\n  children: React.ReactNode;\n}) => {\n  return (\n    <FlexCol>\n      <SectionHeader size=\"small\" icon={iconPath}>\n        {title}\n      </SectionHeader>\n      <FlexCol className=\"pl-2\">{children}</FlexCol>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DashboardMenu/useTableSizeInfo.ts",
    "content": "import { usePromise } from \"prostgles-client\";\nimport type { DashboardMenuProps } from \"./DashboardMenu\";\nimport { kFormatter } from \"../W_Table/W_Table\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { useMemo } from \"react\";\nimport { bytesToSize } from \"../BackupAndRestore/BackupsControls\";\nimport type { DBSchemaTablesWJoins } from \"../Dashboard/dashboardUtils\";\nimport type { DBS } from \"../Dashboard/DBS\";\n\ntype Args = Pick<DashboardMenuProps, \"workspace\" | \"tables\"> & {\n  db: DBHandlerClient;\n};\nexport type TablesWithInfo = (DBSchemaTablesWJoins[number] & {\n  endText: string;\n  endTitle: string;\n  count: number;\n  sizeNum: number;\n})[];\nexport const useTableSizeInfo = ({\n  workspace,\n  tables,\n  db,\n}: Args): { tablesWithInfo: TablesWithInfo } => {\n  const { tableListEndInfo, tableListSortBy } = workspace.options;\n  const tablesWithInfoNonSorted = usePromise(async () => {\n    return await Promise.all(\n      tables.map(async (t) => {\n        let count = 0;\n        let sizeNum = 0;\n        const countRequestedAndAllowed =\n          tableListEndInfo === \"count\" && db[t.name]?.count;\n        const tableHasColumnsAndWillNotError = !!t.columns.length;\n        const shouldGetCount =\n          countRequestedAndAllowed && tableHasColumnsAndWillNotError;\n\n        try {\n          count = Number(\n            (shouldGetCount ? await db[t.name]?.count?.() : 0) ?? 0,\n          );\n          sizeNum = Number(\n            tableListEndInfo === \"size\" ? await db[t.name]?.size?.() : 0,\n          );\n        } catch (e) {}\n        if (!Number.isFinite(sizeNum)) sizeNum = 0;\n        if (!Number.isFinite(count)) count = 0;\n\n        const endTitle =\n          tableListEndInfo === \"none\" ? \"\"\n          : tableListEndInfo === \"size\" ? \"Table size\"\n          : \"Row count\";\n        const endText =\n          tableListEndInfo === \"none\" ? \"\" : (\n            (tableListEndInfo === \"size\" ?\n              sizeNum === 0 ?\n                \"0\"\n              : bytesToSize(sizeNum, 0)\n            : +count > 0 ? kFormatter(+count)\n            : \"\") || \"\"\n          );\n\n        return {\n          ...t,\n          endText,\n          endTitle,\n          count,\n          sizeNum,\n        };\n      }),\n    );\n  }, [tables, tableListEndInfo, db]);\n\n  const tablesWithInfo = useMemo(() => {\n    if (!tablesWithInfoNonSorted) {\n      const tablesWithEmptyInfo = tables.map((t) => ({\n        ...t,\n        endText: \"\",\n        endTitle: \"\",\n        count: 0,\n        sizeNum: 0,\n      }));\n      if (tableListSortBy === \"name\")\n        return tablesWithEmptyInfo.sort((a, b) => a.name.localeCompare(b.name));\n      return tablesWithEmptyInfo;\n    }\n    return tablesWithInfoNonSorted.sort((a, b) => {\n      if (tableListSortBy === \"name\") return a.name.localeCompare(b.name);\n      if (tableListSortBy === \"extraInfo\" && tableListEndInfo === \"count\")\n        return +b.count - +a.count;\n      if (tableListSortBy === \"extraInfo\" && tableListEndInfo === \"size\")\n        return b.sizeNum - a.sizeNum;\n      return 0;\n    });\n  }, [tableListSortBy, tableListEndInfo, tablesWithInfoNonSorted, tables]);\n\n  return {\n    tablesWithInfo,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/DetailedFilterBaseControl.tsx",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport { FlexRow } from \"@components/Flex\";\nimport { includes, isEqual, omitKeys } from \"prostgles-types\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { ContextDataSelector } from \"../AccessControl/ContextDataSelector\";\nimport { FilterWrapper, type FilterWrapperProps } from \"./FilterWrapper\";\nimport type { BaseFilterProps } from \"../SmartFilter/SmartFilter\";\nimport { DetailedFilterBaseControlRouter } from \"./DetailedFilterBaseControlRouter\";\nimport { validateFilter } from \"./validateFilter\";\n\nexport type DetailedFilterBaseControlProps = BaseFilterProps &\n  Pick<FilterWrapperProps, \"rootFilter\" | \"selectedColumns\" | \"label\"> & {\n    hideToggle?: boolean;\n    minimised?: boolean;\n  };\n\nexport const DetailedFilterBaseControl = (\n  props: DetailedFilterBaseControlProps,\n) => {\n  const {\n    column,\n    tables,\n    onChange: propsOnChange,\n    db,\n    tableName,\n    contextData,\n    filter: propsFilter,\n  } = props;\n\n  const [error, setError] = useState<unknown>(undefined);\n  /**\n   * Disable invalid filters\n   */\n  const onChangeWithValidation = useCallback(\n    async (_newFilter: DetailedFilter | undefined) => {\n      let newFilter = _newFilter;\n      let newError;\n      if (newFilter) {\n        const { hasError, error } = await validateFilter(newFilter, {\n          db,\n          tableName,\n          column,\n          tables,\n        });\n        newError = error;\n        if (hasError) {\n          newFilter = {\n            ...newFilter,\n            disabled: true,\n          };\n        }\n      }\n      if (!isEqual(error, newError)) {\n        setError(newError);\n      }\n      propsOnChange(newFilter);\n    },\n    [propsOnChange, column, db, error, tableName, tables],\n  );\n\n  const onChange = onChangeWithValidation;\n\n  const filter = useMemo(\n    () => ({\n      ...propsFilter,\n      fieldName: propsFilter?.fieldName ?? column.name,\n      type: propsFilter?.type ?? \"=\",\n    }),\n    [propsFilter, column.name],\n  );\n\n  const withContextSelector = useMemo(() => {\n    if (!contextData) return;\n    const ctxCols = contextData.flatMap((t) =>\n      t.columns\n        .filter((c) => c.tsDataType === column.tsDataType)\n        .map((c) => ({\n          id: t.name + \".\" + c.name,\n          tableName: t.name,\n          ...c,\n        })),\n    );\n    if (\n      ctxCols.length &&\n      includes(\n        [\"$eq\", \"=\", \"$ne\", \"<>\"] satisfies (typeof filter.type)[],\n        filter.type,\n      )\n    ) {\n      return { ctxCols, contextData };\n    }\n  }, [column.tsDataType, contextData, filter]);\n\n  const filterProps = useMemo(\n    () => ({\n      ...props,\n      onChange,\n    }),\n    [props, onChange],\n  );\n\n  return (\n    <FilterWrapper error={error} {...filterProps} filter={filter}>\n      {!withContextSelector ?\n        <DetailedFilterBaseControlRouter {...filterProps} error={error} />\n      : <FlexRow className={\"gap-p5\"}>\n          {!propsFilter?.contextValue ?\n            <DetailedFilterBaseControlRouter {...filterProps} error={error} />\n          : null}\n          <ContextDataSelector\n            className=\"\"\n            value={propsFilter?.contextValue}\n            column={column}\n            contextData={withContextSelector.contextData}\n            onChange={(contextValue) => {\n              if (!contextValue) {\n                void onChange(\n                  omitKeys(\n                    {\n                      ...filter,\n                      disabled: true,\n                    },\n                    [\"contextValue\"],\n                  ),\n                );\n              } else {\n                void onChange({\n                  ...filter,\n                  disabled: false,\n                  contextValue,\n                });\n              }\n            }}\n          />\n        </FlexRow>\n      }\n    </FilterWrapper>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/DetailedFilterBaseControlRouter.tsx",
    "content": "import { FTS_FILTER_TYPES, TEXT_FILTER_TYPES } from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiCog, mdiFormatLetterMatches } from \"@mdi/js\";\nimport { includes, isDefined } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { AgeFilter, AgeFilterTypes } from \"./DetailedFilterBaseTypes/AgeFilter\";\nimport { type FilterWrapperProps } from \"./FilterWrapper\";\nimport { GeoFilter, GeoFilterTypes } from \"./DetailedFilterBaseTypes/GeoFilter\";\nimport { ListFilter } from \"./DetailedFilterBaseTypes/ListFilter/ListFilter\";\nimport { NumberOrDateFilter } from \"./DetailedFilterBaseTypes/NumberOrDateFilter\";\nimport type { BaseFilterProps } from \"../SmartFilter/SmartFilter\";\nimport { SmartSearch } from \"../SmartFilter/SmartSearch/SmartSearch\";\nimport { colIs, getInputType } from \"../SmartForm/SmartFormField/fieldUtils\";\nimport { FTS_LANGUAGES } from \"./FTS_LANGUAGES\";\n\nexport type FilterProps = BaseFilterProps &\n  Pick<FilterWrapperProps, \"rootFilter\" | \"selectedColumns\" | \"label\"> & {\n    hideToggle?: boolean;\n    minimised?: boolean;\n    error: unknown;\n  };\n\nexport const DetailedFilterBaseControlRouter = (props: FilterProps) => {\n  const {\n    column,\n    tables,\n    onChange,\n    db,\n    tableName,\n    extraFilters,\n    filter: propsFilter,\n    selectedColumns,\n    error,\n  } = props;\n\n  const filter = useMemo(\n    () => ({\n      ...propsFilter,\n      fieldName: propsFilter?.fieldName ?? column.name,\n      type: propsFilter?.type ?? \"=\",\n    }),\n    [propsFilter, column.name],\n  );\n\n  if (\n    propsFilter &&\n    includes(GeoFilterTypes, propsFilter.type) &&\n    colIs(column, \"_PG_postgis\")\n  ) {\n    return <GeoFilter filter={propsFilter} {...props} error={error} />;\n  } else if (includes(AgeFilterTypes, propsFilter?.type)) {\n    return <AgeFilter {...props} error={error} />;\n  } else if (propsFilter?.type === \"not null\" || propsFilter?.type === \"null\") {\n    return null;\n  } else if (includes(ListFilter.TYPES, propsFilter?.type)) {\n    return <ListFilter {...props} error={error} />;\n  } else {\n    const textFilterType =\n      [\"$ilike\", \"$like\", \"$nilike\", \"$nlike\"].includes(filter.type) ? \"text\"\n      : FTS_FILTER_TYPES.some((f) => f.key === filter.type) ? \"fts\"\n      : undefined;\n    /** Disable suggestions, allow text only */\n    if (textFilterType) {\n      return (\n        <FormFieldDebounced\n          className=\"m-p5\"\n          type=\"text\"\n          autoComplete=\"off\"\n          value={filter.value}\n          onChange={(value) => {\n            void onChange({\n              ...filter,\n              disabled: false,\n              value: `${value}`,\n            });\n          }}\n          rightIcons={\n            textFilterType === \"text\" ? undefined : (\n              <PopupMenu\n                button={\n                  <Btn\n                    iconPath={\n                      filter.ftsFilterOptions?.lang === \"simple\" ?\n                        mdiFormatLetterMatches\n                      : mdiCog\n                    }\n                    color={filter.ftsFilterOptions?.lang ? \"action\" : undefined}\n                  />\n                }\n                title={\"FTS options\"}\n                positioning=\"beneath-left\"\n                clickCatchStyle={{ opacity: 0 }}\n                render={(pClose) => (\n                  <FlexCol>\n                    <Select\n                      label={{\n                        label: \"Dictionary\",\n                        info: \"Choose 'simple' for exact word matching. Dictionaries are used to eliminate words that should not be considered in a search (stop words), and to normalize words so that different derived forms of the same word will match. A successfully normalized word is called a lexeme.\",\n                      }}\n                      options={FTS_LANGUAGES}\n                      value={filter.ftsFilterOptions?.lang ?? \"english\"}\n                      onChange={(lang) => {\n                        void onChange({\n                          ...filter,\n                          ftsFilterOptions: {\n                            ...filter.ftsFilterOptions,\n                            lang,\n                          },\n                        });\n                        pClose();\n                      }}\n                    />\n                  </FlexCol>\n                )}\n              />\n            )\n          }\n        />\n      );\n    } else if (filter.type === \"$between\") {\n      return (\n        <NumberOrDateFilter\n          {...props}\n          type={\n            column.tsDataType.toLowerCase() === \"number\" ? \"number\" : \"date\"\n          }\n          inputType={getInputType(column)}\n        />\n      );\n\n      /** Show suggestions */\n    } else {\n      const key = JSON.stringify(filter.value ?? \"\");\n\n      const filterColumn = selectedColumns?.find(\n        (c) => c.name === filter.fieldName,\n      );\n      return (\n        <SmartSearch\n          className=\" \"\n          key={key}\n          db={db}\n          extraFilters={extraFilters}\n          selectedColumns={selectedColumns}\n          tableName={tableName}\n          variant=\"search-no-shadow\"\n          tables={tables}\n          defaultValue={filter.value}\n          column={filterColumn ?? filter.fieldName}\n          searchEmpty={true}\n          noBorder={true}\n          noResultsComponent={\n            <FlexRow>\n              <div className=\"text-0p75\">No results</div>\n              <div className=\"text-2\">Press enter to confirm</div>\n            </FlexRow>\n          }\n          onPressEnter={(term) => {\n            const f = { ...filter };\n            f.value = term;\n            f.disabled = false;\n\n            void onChange(f);\n          }}\n          onChange={(val) => {\n            if (!isDefined(val)) {\n              void onChange({\n                ...filter,\n                value: undefined,\n                disabled: true,\n              });\n              return;\n            }\n            const { columnValue, term } = val;\n            const f = { ...filter };\n            f.value =\n              includes([...TEXT_FILTER_TYPES, \"$term_highlight\"], f.type) ?\n                val.columnTermValue\n              : (columnValue ?? term);\n            f.disabled = false;\n\n            void onChange(f);\n          }}\n        />\n      );\n    }\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/DetailedFilterBaseTypes/AgeFilter.tsx",
    "content": "import type {\n  DetailedFilterBase,\n  FilterType,\n  DetailedFilter,\n} from \"@common/filterUtils\";\nimport { CORE_FILTER_TYPES, NUMERIC_FILTER_TYPES } from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { Select } from \"@components/Select/Select\";\nimport React from \"react\";\nimport type { BaseFilterProps } from \"../../SmartFilter/SmartFilter\";\n\ntype FilterProps = BaseFilterProps;\n\nexport const getDefaultAgeFilter = (\n  fieldName: string,\n  type: (typeof AgeFilterTypes)[number],\n) =>\n  ({\n    fieldName,\n    complexFilter: {\n      type: \"controlled\",\n      funcName: type,\n      argsLeftToRight: false,\n      comparator: \">\",\n    },\n    type,\n    value: \"1day\",\n  }) satisfies DetailedFilter;\n\nexport const AgeFilterTypes = [\n  \"$age\",\n  \"$duration\",\n  \"$ageNow\",\n] satisfies FilterType[];\n\nexport const AgeFilter = (props: FilterProps) => {\n  const { filter, onChange, tables, column, tableName } = props;\n\n  const otherDateCols = tables\n    .find((t) => t.name === tableName)\n    ?.columns.filter(\n      (c) =>\n        c.name !== column.name &&\n        [\"date\", \"timestamp\", \"timestamptz\"].includes(c.udt_name),\n    );\n  if (!filter || !otherDateCols)\n    return <>Something went wrong. Could not find column {column.name}</>;\n  if (filter.complexFilter?.type !== \"controlled\")\n    return <>Something went wrong. Could not find complex filter</>;\n\n  const complex: DetailedFilterBase[\"complexFilter\"] = {\n    ...filter.complexFilter,\n  };\n\n  const updateComplex = (\n    newOpt: Partial<\n      Extract<DetailedFilterBase[\"complexFilter\"], { type: \"controlled\" }>\n    >,\n  ) => {\n    onChange({\n      ...filter,\n      complexFilter: {\n        ...complex,\n        ...newOpt,\n      },\n    });\n  };\n\n  const colOptsions = [\n    { key: null, label: \"NOW\" },\n    ...otherDateCols.map((c) => ({ key: c.name, label: c.label })),\n  ];\n\n  return (\n    <div className=\"AgeFilter flex-row gap-p25\">\n      {filter.type !== \"$age\" && (\n        <div className=\"flex-row p-p25 gap-p25 ai-center\">\n          <Btn\n            size=\"small\"\n            color=\"action\"\n            data-command=\"AgeFilter.argsLeftToRight\"\n            onClick={() => {\n              updateComplex({ argsLeftToRight: !complex.argsLeftToRight });\n            }}\n          >\n            {complex.argsLeftToRight === false ? \"Up to\" : \"Since\"}\n          </Btn>\n          <Select\n            value={complex.otherField ?? null}\n            fullOptions={colOptsions}\n            btnProps={{\n              size: \"small\",\n            }}\n            onChange={(otherField) => {\n              updateComplex({ otherField });\n            }}\n          />\n        </div>\n      )}\n      <div className=\"flex-row p-p2d5 gap-p5\">\n        <Select\n          className=\"text-action\"\n          iconPath=\"\"\n          data-command=\"AgeFilter.comparator\"\n          btnProps={{\n            style: { borderRadius: 0 },\n            color: \"default\",\n            variant: \"default\",\n          }}\n          value={complex.comparator}\n          fullOptions={[\n            ...NUMERIC_FILTER_TYPES,\n            ...CORE_FILTER_TYPES.filter(\n              ({ key }) => key === \"<>\" || key === \"=\",\n            ),\n          ]}\n          onChange={(comparator) => {\n            updateComplex({ comparator });\n          }}\n        />\n        <FormFieldDebounced\n          type=\"text\"\n          autoComplete=\"off\"\n          value={filter.value}\n          maxWidth=\"10em\"\n          inputStyle={{\n            minHeight: \"44px\",\n            padding: \"8px 0 8px 1em\",\n          }}\n          wrapperStyle={{\n            borderRadius: \"0\",\n            borderTop: \"none\",\n            borderBottom: \"none\",\n          }}\n          placeholder=\"1 month 2 days\"\n          onChange={(value) => {\n            onChange({\n              ...filter,\n              disabled: false,\n              value: `${value}`,\n            });\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/DetailedFilterBaseTypes/GeoFilter.tsx",
    "content": "import { mdiMagnify } from \"@mdi/js\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { isDefined, isEmpty } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { DetailedFilterBase } from \"@common/filterUtils\";\nimport { GEO_FILTER_TYPES, getFinalFilter } from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport { ExpandSection } from \"@components/ExpandSection\";\nimport { FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport Popup from \"@components/Popup/Popup\";\nimport { Select } from \"@components/Select/Select\";\nimport type { DBSchemaTablesWJoins } from \"../../Dashboard/dashboardUtils\";\nimport type { BaseFilterProps } from \"../../SmartFilter/SmartFilter\";\nimport type { SmartSearchOnChangeArgs } from \"../../SmartFilter/SmartSearch/SmartSearch\";\nimport { SmartSearch } from \"../../SmartFilter/SmartSearch/SmartSearch\";\n\nexport const GeoFilterTypes = GEO_FILTER_TYPES.map((f) => f.key);\nexport const getDefaultGeoFilter = (fieldName: string): DetailedFilterBase => ({\n  fieldName,\n  type: \"$ST_DWithin\",\n  value: {\n    distance: 1,\n    lat: 51,\n    lng: 0,\n    name: \"\",\n    unit: \"km\",\n  } satisfies ST_DWithinFilterValue,\n});\n\ntype ST_DWithinFilterValue = {\n  lat: number;\n  lng: number;\n  unit: \"km\" | \"m\";\n  name: string;\n\n  /** Metres */\n  distance: number;\n};\nexport const GeoFilter = ({\n  filter,\n  db,\n  tables,\n  onChange,\n}: BaseFilterProps & { filter: DetailedFilterBase }) => {\n  const args: Partial<ST_DWithinFilterValue> = filter.value;\n\n  const updateValue = (newArgs: typeof args) => {\n    const newValue = {\n      ...args,\n      ...newArgs,\n      unit: \"km\",\n    };\n    const disabled =\n      !newValue.distance ||\n      ![newValue.lat, newValue.lng, newValue.distance].every(\n        (v, i, arr) => arr.length && Number.isFinite(v),\n      );\n    onChange({\n      disabled,\n      ...filter,\n      value: newValue,\n    });\n  };\n\n  const geoTables = tables\n    .map((t) => {\n      const geoCols = t.columns.filter((c) => c.udt_name.startsWith(\"geo\"));\n      if (geoCols.length) {\n        return {\n          ...t,\n          columns: geoCols,\n        };\n      }\n\n      return undefined;\n    })\n    .filter(isDefined);\n  const firstGeoTable = geoTables[0];\n\n  const [open, setOpen] = useState<HTMLButtonElement>();\n  const [refPoint, setRefPoint] = useState<{\n    table?: DBSchemaTablesWJoins[number];\n    row?: AnyObject;\n  }>({ table: firstGeoTable });\n\n  const setRow = async (\n    args: (SmartSearchOnChangeArgs & { row: AnyObject }) | undefined,\n  ) => {\n    if (!args || !refPoint.table) return;\n    const { row, colName, columnValue } = args;\n    const firstGeoCol = refPoint.table.columns[0]; //.find(c => c.udt_name === \"geography\") ?? refPoint.table?.columns.find(c => c.udt_name === \"geometry\");\n    if (!firstGeoCol) return;\n\n    const select = {\n      c: { $ST_Centroid: [firstGeoCol.name] },\n    };\n    const filter = getRowFilterFromPkey(\n      row,\n      tables.find((t) => t.name === refPoint.table!.name)!,\n    );\n    const rowRes = await db[refPoint.table.name]?.findOne!(filter, { select });\n    if (\n      rowRes?.c &&\n      Array.isArray(rowRes.c.coordinates) &&\n      rowRes.c.coordinates.length === 2 &&\n      rowRes.c.coordinates.every((v) => Number.isFinite(v))\n    ) {\n      const [lng, lat] = rowRes.c.coordinates;\n      const name = (columnValue || `${lat} ${lng}`).toString();\n      updateValue({\n        lat,\n        lng,\n        name,\n      });\n      setOpen(undefined);\n    }\n  };\n\n  return (\n    <div className=\"flex-row p-p5 gap-p5 ai-center\">\n      <FormFieldDebounced\n        placeholder=\"Km\"\n        style={{ padding: 0 }}\n        inputProps={{\n          // step: 1,\n          style: { maxWidth: \"7ch\", minWidth: \"7ch\" },\n        }}\n        type=\"number\"\n        maxWidth={\"16ch\"}\n        rightContentAlwaysShow={true}\n        rightIcons={\n          <div className=\"ai-center jc-center flex-col bg-color-2 f-1 h-full px-p5 noselect text-2\">\n            Km\n          </div>\n        }\n        inputStyle={{ minHeight: \"38px\" }}\n        value={args.distance}\n        onChange={(distance) => updateValue({ distance: Number(distance) })}\n      />\n      {!!args.distance && (\n        <>\n          <div>of</div>\n          <Btn\n            variant=\"faded\"\n            onClick={(e) => {\n              setOpen(open ? undefined : (e.target as any));\n            }}\n          >\n            {args.name || `Select point`}\n          </Btn>\n          {open && (\n            <Popup\n              title={`${filter.fieldName} is within ${args.distance}${args.unit} of ${args.name || \"...\"}`}\n              // rootStyle={{ position: \"absolute\" }}\n              anchorEl={open}\n              positioning=\"center\"\n              onClose={() => {\n                setOpen(undefined);\n              }}\n              contentClassName=\"flex-col relative p-1 gap-1\"\n              // contentStyle={{\n              //   minWidth: \"800px\",\n              //   minHeight: \"500px\"\n              // }}\n            >\n              <FlexRow>\n                <FormField\n                  type=\"number\"\n                  value={args.lat}\n                  label={\"Latitude\"}\n                  onChange={(lat) => updateValue({ lat, name: \"\" })}\n                />\n                <FormField\n                  type=\"number\"\n                  value={args.lng}\n                  label={\"Longitude\"}\n                  onChange={(lng) => updateValue({ lng, name: \"\" })}\n                />\n              </FlexRow>\n\n              <ExpandSection label=\"Find from data...\" iconPath={mdiMagnify}>\n                <Select\n                  label=\"Table\"\n                  fullOptions={geoTables.map((t) => ({ key: t.name }))}\n                  value={refPoint.table?.name}\n                  labelAsValue={true}\n                  onChange={(tableName) => {\n                    setRefPoint({\n                      table: geoTables.find((t) => t.name === tableName),\n                    });\n                  }}\n                />\n                {\n                  refPoint.table && (\n                    <SmartSearch\n                      db={db}\n                      tableName={refPoint.table.name}\n                      tables={tables}\n                      onChange={async (val) => {\n                        if (!val) {\n                          setRow(undefined);\n                          return;\n                        }\n                        const { filter } = val;\n                        const table = refPoint.table!;\n                        const rowFilter = {\n                          $and: filter.map((f) => getFinalFilter(f as any)),\n                        };\n                        const row = await db[table.name]?.findOne!(rowFilter);\n                        setRow(!row ? undefined : { ...val, row });\n                      }}\n                    />\n                  )\n                  // <SmartTable\n                  //   title=\"\"\n                  //   db={db}\n                  //   tableName={refPoint?.table.name}\n                  //   tables={tables}\n                  //   onClickRow={async row => {\n                  //     }\n                  //     setRefPoint({ ...refPoint, row });\n\n                  //   }}\n                  // />\n                }\n                {/* <DeckGLMap \n            onGetFullExtent={async () => [[-180, -90], [180, 90]]} \n            options={{}} \n            initialState={{ latitude: 51, longitude: 0, basemap: { opacity: 1, tileURLs: [] } }} \n            onOptionsChange={console.log} \n          /> */}\n              </ExpandSection>\n            </Popup>\n          )}\n        </>\n      )}\n    </div>\n  );\n};\n\nexport const getRowFilterFromPkey = (\n  row: AnyObject,\n  table: DBSchemaTablesWJoins[number],\n) => {\n  const res = {};\n  table.columns.map((c) => {\n    if (c.is_pkey) {\n      res[c.name] = row[c.name];\n    }\n  });\n\n  return isEmpty(res) ? row : res;\n};\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/DetailedFilterBaseTypes/ListFilter/ListFilter.tsx",
    "content": "import type { Primitive } from \"d3\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { isObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type { FilterType } from \"@common/filterUtils\";\nimport { FlexRow } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport type { SearchListItem } from \"@components/SearchList/SearchList\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport RTComp from \"../../../RTComp\";\nimport type { BaseFilterProps } from \"../../../SmartFilter/SmartFilter\";\nimport { fetchListFilterOptions } from \"./fetchListFilterOptions\";\n\ntype ListFilterProps = BaseFilterProps;\n\ntype ListFilterState = {\n  searchTerm?: string;\n\n  popupAnchor?: HTMLElement;\n  defaultSearch?: string;\n  options?: any[];\n  allOptionsCount?: number;\n  searchingItems?: boolean;\n  error?: any;\n};\n\nexport class ListFilter extends RTComp<ListFilterProps, ListFilterState> {\n  static TYPES: FilterType[] = [\"$in\", \"$nin\"];\n  state: ListFilterState = {\n    searchTerm: \"\",\n  };\n\n  searching?: {\n    term: string;\n    timeout: NodeJS.Timeout;\n  };\n\n  setTerm = (term: string) => {\n    if (this.searching) {\n      if (this.searching.term === term) {\n        return;\n      } else {\n        clearTimeout(this.searching.timeout);\n      }\n    }\n\n    if (typeof term !== \"string\") {\n      this.setState({ searchingItems: false });\n    } else {\n      if (!this.state.searchingItems) {\n        this.setState({ searchingItems: true });\n      }\n\n      this.searching = {\n        term,\n        timeout: setTimeout(async () => {\n          this.setState({ searchingItems: true });\n\n          try {\n            const { db, column, tableName, tables, otherFilters } = this.props;\n\n            const { options = [] } = await fetchListFilterOptions({\n              db,\n              column,\n              tableName,\n              tables,\n              otherFilters,\n              term,\n            });\n\n            let newState: Partial<ListFilterState> = {\n              options,\n              searchingItems: false,\n              error: undefined,\n            };\n            if (!this.state.options) {\n              newState = {\n                ...newState,\n                allOptionsCount: options.length,\n              };\n            }\n\n            if (term === this.searching?.term) {\n              this.setState(newState);\n              this.searching = undefined;\n            } else {\n              this.setState({ searchingItems: false, error: undefined });\n            }\n          } catch (error) {\n            this.setState({ error });\n          }\n        }, 400),\n      };\n    }\n  };\n\n  onDelta = (dP, dS = {}) => {\n    const { searchTerm = \"\" } = this.state;\n\n    if (!this.state.options || \"searchTerm\" in dS) {\n      this.setTerm(searchTerm);\n    }\n  };\n\n  getParsedOption = (\n    v: AnyObject | Primitive | null | undefined,\n  ): Pick<SearchListItem, \"key\" | \"label\" | \"title\" | \"data\" | \"checked\"> => {\n    const { filter, column } = this.props;\n    const parseKey = (key: typeof v) => {\n      if (key && isObject(key) && !(key instanceof Date)) {\n        return JSON.stringify(key);\n      }\n      return key;\n    };\n    if (column.udt_name === \"interval\") {\n    }\n\n    const checkedValues = (\n      filter?.value && Array.isArray(filter.value) ?\n        filter.value\n      : []).map(parseKey);\n    const key = parseKey(v);\n\n    return {\n      key,\n      title: key === null ? \"NULL\" : key?.toString(),\n      data: v,\n      checked: checkedValues.includes(key),\n    };\n  };\n\n  render(): React.ReactNode {\n    const { filter, onChange, column } = this.props;\n    const { options, searchTerm, searchingItems } = this.state;\n\n    if (!filter)\n      return <>Something went wrong. Could not find column {column.name}</>;\n\n    if (!options) {\n      return (\n        <div className=\"p-1 relative f-1\">\n          <Loading delay={200} />\n        </div>\n      );\n    }\n\n    const searchedItems = options.map(this.getParsedOption);\n    let filterExtraItems: SearchListItem[] = [];\n    let filterItems: SearchListItem[] = [];\n    if (Array.isArray(filter.value) && filter.value.length) {\n      filterItems = filter.value.map(this.getParsedOption);\n      filterExtraItems = filterItems.filter(\n        (v) => !searchedItems.map((o) => o.key).includes(v.key),\n      );\n    }\n\n    const renderedItems = [\n      /** Need to ensure that the selected values are displayed (even if missing in the suggestions) */\n      ...filterExtraItems,\n      ...searchedItems,\n    ];\n    const items = renderedItems.map((item) => {\n      return {\n        ...item,\n        style:\n          item.data === null ?\n            {\n              fontStyle: \"italic\",\n              opacity: 0.7,\n            }\n          : undefined,\n        onPress: () => {\n          let newSelectedKeys = filterItems.map((d) => d.key);\n\n          if (!newSelectedKeys.includes(item.key)) {\n            newSelectedKeys.push(item.key);\n          } else {\n            newSelectedKeys = newSelectedKeys.filter((v) => v !== item.key);\n          }\n          const newValue = renderedItems\n            .filter((d) => newSelectedKeys.includes(d.key))\n            .map((d) => d.data);\n\n          onChange({\n            ...filter,\n            disabled: !newValue.length,\n            value: newValue,\n          });\n        },\n      };\n    });\n\n    return (\n      <>\n        {searchingItems && <Loading variant=\"cover\" />}\n        <SearchList<true>\n          onSearch={(searchTerm) => {\n            this.setState({ searchTerm });\n          }}\n          searchStyle={{\n            margin: \"0 .5em\",\n          }}\n          noSearchLimit={0}\n          items={items}\n          onMultiToggle={(items) => {\n            const vals = items.filter((d) => d.checked);\n            onChange({\n              ...filter,\n              value: !vals.length ? undefined : vals.map((v) => v.key),\n            });\n          }}\n          noResultsContent={\n            <FlexRow>\n              <div className=\"text-0p75\">No matches</div>\n              <div className=\"text-2\">Press enter to add</div>\n            </FlexRow>\n          }\n          onPressEnter={() => {\n            const currentValues =\n              Array.isArray(filter.value) ? filter.value : [];\n            onChange({\n              ...filter,\n              value: currentValues.concat([searchTerm]),\n            });\n            this.setState({ searchTerm: \"\" });\n          }}\n          onChange={(newOptions) => {\n            onChange({\n              ...filter,\n              value: !newOptions.length ? undefined : newOptions,\n            });\n          }}\n        />\n      </>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/DetailedFilterBaseTypes/ListFilter/fetchListFilterOptions.ts",
    "content": "import { getSmartGroupFilter } from \"@common/filterUtils\";\nimport { fetchColumnValueSuggestions } from \"../../../SmartForm/SmartFormField/fetchColumnValueSuggestions\";\nimport { type BaseFilterProps } from \"../../../SmartFilter/smartFilterUtils\";\n\ntype Args = Pick<\n  BaseFilterProps,\n  \"db\" | \"column\" | \"tableName\" | \"tables\" | \"otherFilters\"\n> & {\n  term: string;\n};\nexport const fetchListFilterOptions = async (args: Args) => {\n  const { db, column, tableName, tables, otherFilters, term } = args;\n\n  /** Will render $in $nin filter */\n  let finalTableName = tableName;\n  const isTableColumn = column.type === \"column\";\n  const firstReference = isTableColumn ? column.references?.[0] : undefined;\n  let finalColumnName = column.name;\n  let isReference = false;\n  if (firstReference) {\n    finalTableName = firstReference.ftable;\n    finalColumnName = firstReference.fcols[0] || finalColumnName;\n    isReference = true;\n  }\n  let groupBy = true; // !isReference;\n\n  if (finalTableName && db[finalTableName]?.find) {\n    let finalColumn = column;\n    if (isTableColumn) {\n      const tableColumn = tables\n        .find((t) => t.name === finalTableName)\n        ?.columns.find((c) => c.name === finalColumnName);\n      if (!tableColumn) throw new Error(\"Column not found: \" + finalColumnName);\n      if (tableColumn.udt_name === \"jsonb\") groupBy = false;\n      finalColumn = {\n        type: \"column\",\n        ...tableColumn,\n      };\n    }\n\n    const filter = getSmartGroupFilter(otherFilters);\n    /**\n     * If this is a foreign key column then we search the foreign table\n     */\n    const finalFilter =\n      isReference ?\n        {\n          $existsJoined: {\n            path: [tableName],\n            filter,\n          },\n        }\n      : filter;\n    const rawOptions = await fetchColumnValueSuggestions({\n      db,\n      column: finalColumn,\n      table: finalTableName,\n      term,\n      groupBy,\n      filter: finalFilter,\n    });\n\n    if (\n      isTableColumn &&\n      isReference &&\n      column.is_nullable &&\n      !rawOptions.includes(null)\n    ) {\n      rawOptions.unshift(null);\n    }\n\n    return { options: rawOptions };\n  }\n\n  return { options: [] };\n};\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/DetailedFilterBaseTypes/NumberOrDateFilter.tsx",
    "content": "import React from \"react\";\nimport type { FilterType, DetailedFilter } from \"@common/filterUtils\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport RTComp from \"../../RTComp\";\nimport { colIs, parseValue } from \"../../SmartForm/SmartFormField/fieldUtils\";\nimport { getTableSelect } from \"../../W_Table/tableUtils/getTableSelect\";\nimport type { BaseFilterProps } from \"../../SmartFilter/SmartFilter\";\nimport { _PG_numbers } from \"prostgles-types\";\nimport { FlexRowWrap } from \"@components/Flex\";\n\ntype NumberOrDateFilterProps = BaseFilterProps & {\n  type: \"number\" | \"date\";\n  inputType: string;\n};\n\ntype Limits = {\n  min: number;\n  max: number;\n};\ntype NumberOrDateFilterState = {\n  limits?: Limits;\n};\n\nexport class NumberOrDateFilter extends RTComp<\n  NumberOrDateFilterProps,\n  NumberOrDateFilterState\n> {\n  static TYPES: FilterType[] = [\"$between\"];\n\n  state: NumberOrDateFilterState = {};\n\n  onDelta = async (dP, dS, dD) => {\n    const { db, column, tableName, filter, tables } = this.props;\n\n    const tableHandler = db[tableName];\n    if (\n      !this.state.limits &&\n      tableName &&\n      tableHandler?.findOne &&\n      !filter?.value?.length\n    ) {\n      let limits: Limits | undefined;\n\n      if (column.type === \"column\") {\n        limits = (await tableHandler.findOne(\n          {},\n          {\n            select: {\n              min: { $min: [column.name] },\n              max: { $max: [column.name] },\n            },\n          },\n        )) as Limits | undefined;\n      } else {\n        const { select } = await getTableSelect(\n          { table_name: tableName, columns: column.columns },\n          tables,\n          db,\n          {},\n        );\n        const _minVal = await tableHandler.findOne(\n          {},\n          { select, orderBy: { [column.name]: 1 } },\n        );\n        const _maxVal = await tableHandler.findOne(\n          {},\n          { select, orderBy: { [column.name]: -1 } },\n        );\n\n        if (_minVal && _maxVal) {\n          const isNumeric = _PG_numbers.includes(column.udt_name as any);\n          const minVal = _minVal[column.name];\n          const maxVal = _maxVal[column.name];\n          limits = {\n            min: isNumeric ? Number(minVal) : minVal,\n            max: isNumeric ? Number(maxVal) : maxVal,\n          };\n        }\n      }\n      if (!limits) return;\n      this.setState({ limits });\n      this.setValue(limits);\n    }\n  };\n\n  setValue = (val: { min?: number; max?: number; _val?: number }) => {\n    const { column, filter } = this.props;\n    let {\n      min = filter?.value?.[0],\n      max = filter?.value?.[1],\n      _val = filter?.value,\n    } = val;\n\n    const isDate = colIs(column, \"_PG_date\");\n    if (isDate) {\n      min = new Date(min);\n      max = new Date(max);\n      _val = new Date(_val);\n    }\n\n    /** Change the other value if needed */\n    if (\n      min?.toString().length &&\n      max?.toString().length &&\n      typeof min === typeof max &&\n      +min > +max\n    ) {\n      if (\"min\" in val) {\n        max = +min;\n      } else {\n        min = +max;\n      }\n    }\n\n    const type: DetailedFilter[\"type\"] = this.props.filter?.type ?? \"$between\";\n    let value: any = [min, max];\n\n    if (Object.values(val)[0] === (\"\" as any)) {\n    } else if (column.udt_name === \"time\") {\n      if (type === \"$between\") {\n        if (min && max) {\n          if (min > max) min = max;\n        }\n        value = [min, max];\n      } else {\n        value = _val;\n      }\n    } else if (_val === null) {\n      value = _val;\n    } else if (_val && Number.isFinite(+_val)) {\n      value = +_val;\n    } else if (min && Number.isFinite(+min) && max && Number.isFinite(+max)) {\n      if (+min > +max) min = +max;\n      value = [+min, +max];\n    } else if (min && Number.isFinite(+min) && type === \"$between\") {\n      value = [+min, +min + 1];\n    } else if (max && Number.isFinite(+max) && type === \"$between\") {\n      value = [+max - 1, +max];\n    } else {\n      console.warn(\"Invalid filter value\");\n\n      return;\n    }\n\n    if (isDate && value !== null) {\n      if (Array.isArray(value)) value = value.map((v) => new Date(v));\n      else value = new Date(value);\n    }\n\n    const filterIsValid =\n      Array.isArray(value) &&\n      value.length === 2 &&\n      value.every((v) => `${v}`.length && Number.isFinite(+v));\n\n    this.props.onChange({\n      fieldName: column.name,\n      type,\n      value,\n      disabled: !filterIsValid,\n    });\n  };\n\n  render() {\n    const { filter, column, type: dataType, inputType } = this.props;\n\n    const type = filter?.type ?? \"=\";\n    const min =\n        type === \"$between\" ? filter?.value?.[0]\n        : type === \">=\" ? filter?.value\n        : null,\n      max =\n        type === \"$between\" ? filter?.value?.[1]\n        : type === \"<=\" ? filter?.value\n        : null;\n\n    const commonProps = {\n      className: \"p-p25 mr-p5\",\n      style: colIs(column, \"_PG_date\") ? {} : { maxWidth: \"125px\" },\n      inputStyle: { padding: \"4px 8px\" },\n      type: inputType as \"number\",\n      asColumn: true,\n    };\n\n    return (\n      <FlexRowWrap className=\"gap-0 f-1 \" style={{ minWidth: \"150px\" }}>\n        <FormFieldDebounced\n          {...commonProps}\n          value={parseValue(column, min)}\n          placeholder=\"Min\"\n          onChange={(min) => {\n            this.setValue({ min });\n          }}\n        />\n        <FormFieldDebounced\n          {...commonProps}\n          value={parseValue(column, max)}\n          placeholder=\"Max\"\n          onChange={(max) => {\n            this.setValue({ max });\n          }}\n        />\n      </FlexRowWrap>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/DetailedFilterControl.tsx",
    "content": "import type { DetailedFilter, DetailedJoinedFilter } from \"@common/filterUtils\";\nimport { isDetailedFilter, isJoinedFilter } from \"@common/filterUtils\";\nimport { isObject } from \"@common/publishUtils\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport React from \"react\";\nimport type { DBSchemaTableWJoins } from \"../Dashboard/dashboardUtils\";\nimport {\n  DetailedFilterBaseControl,\n  type DetailedFilterBaseControlProps,\n} from \"./DetailedFilterBaseControl\";\nimport {\n  DEFAULT_VALIDATED_COLUMN_INFO,\n  type FilterColumn,\n} from \"../SmartFilter/SmartFilter\";\n\ntype P = {\n  filterItem: DetailedFilter;\n  table: DBSchemaTableWJoins;\n  tables: DBSchemaTableWJoins[];\n  minimisedOverride: boolean | undefined;\n  className?: string;\n} & Pick<\n  DetailedFilterBaseControlProps,\n  | \"db\"\n  | \"variant\"\n  | \"contextData\"\n  | \"selectedColumns\"\n  | \"hideToggle\"\n  | \"extraFilters\"\n  | \"onChange\"\n  | \"otherFilters\"\n>;\nexport const DetailedFilterControl = ({\n  filterItem,\n  table,\n  tables,\n  className,\n  minimisedOverride,\n  variant,\n  db,\n  contextData,\n  extraFilters,\n  selectedColumns,\n  hideToggle,\n  onChange,\n  otherFilters,\n}: P) => {\n  const tableColumns = table.columns;\n  const tableName = table.name;\n  let filterColumn: FilterColumn | undefined;\n  let fieldName: string | undefined;\n  let label: string | undefined;\n  let filterTableName = tableName;\n  let tableColumn: ValidatedColumnInfo | undefined;\n  if (isJoinedFilter(filterItem)) {\n    ({ fieldName } = filterItem.filter);\n    const lastPathItem = filterItem.path.at(-1);\n    if (!lastPathItem) return <>Filter path lastPathItem missing</>;\n    const lastTableName =\n      isObject(lastPathItem) ? lastPathItem.table : lastPathItem;\n    const nestedTableCol = tables\n      .find((t) => t.name === lastTableName)\n      ?.columns.find((c) => c.name === fieldName);\n    tableColumn = nestedTableCol;\n    filterTableName = lastTableName;\n    label =\n      filterItem.path.map((p) => (isObject(p) ? p.table : p)).join(\" > \") +\n      \".\" +\n      fieldName;\n  } else {\n    ({ fieldName } = filterItem);\n    tableColumn = tableColumns.find((c) => c.name === fieldName);\n    const selectedColumn = selectedColumns?.find((c) => c.name === fieldName);\n    const computedConfig = selectedColumn?.computedConfig;\n    if (computedConfig) {\n      filterColumn = {\n        type: \"computed\",\n        columns: selectedColumns ?? [],\n        label: selectedColumn.name,\n        name: selectedColumn.name,\n        computedConfig,\n        ...computedConfig,\n      };\n      tableColumn = undefined;\n    }\n    label = filterColumn?.name ?? fieldName;\n  }\n\n  if (tableColumn) {\n    filterColumn = {\n      type: \"column\",\n      ...tableColumn,\n    };\n  }\n\n  /**\n   * Maybe add computed columns to dbo schema?!!\n   */\n  if (!filterColumn) {\n    filterColumn = {\n      type: \"column\",\n      ...DEFAULT_VALIDATED_COLUMN_INFO,\n      name: fieldName,\n      label: fieldName,\n    };\n  }\n  const filter = isJoinedFilter(filterItem) ? filterItem.filter : filterItem;\n  return (\n    <DetailedFilterBaseControl\n      className={`${className} min-w-0 min-h-0`}\n      label={label}\n      db={db}\n      tableName={filterTableName}\n      column={filterColumn}\n      variant={variant}\n      tables={tables}\n      contextData={contextData}\n      hideToggle={hideToggle}\n      selectedColumns={selectedColumns}\n      filter={{\n        ...filter,\n        minimised: minimisedOverride ?? filter.minimised,\n      }}\n      extraFilters={extraFilters}\n      otherFilters={\n        isJoinedFilter(filterItem) ?\n          otherFilters.filter(isDetailedFilter).map((filter) => {\n            const res: DetailedJoinedFilter = {\n              type: filterItem.type,\n              path: [...filterItem.path.slice(0).reverse().slice(1), tableName],\n              filter,\n            };\n            return res;\n          })\n        : otherFilters\n      }\n      onChange={(newFilterItem) => {\n        if (newFilterItem && isJoinedFilter(filterItem)) {\n          // Won't this case be handler by rootFilter.onChange??\n          if (isJoinedFilter(newFilterItem)) {\n            throw \"Nested join filters not allowed\";\n          }\n          newFilterItem = {\n            ...filterItem,\n            disabled: newFilterItem.disabled,\n            minimised: newFilterItem.minimised,\n            filter: { ...newFilterItem },\n          };\n        }\n        onChange(newFilterItem);\n      }}\n      rootFilter={\n        isJoinedFilter(filterItem) ? { value: filterItem, onChange } : undefined\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/FTS_LANGUAGES.ts",
    "content": "/**\n  SELECT cfgname\n  FROM pg_catalog.pg_ts_config\n */\nexport const FTS_LANGUAGES = [\n  \"simple\",\n  \"arabic\",\n  \"armenian\",\n  \"basque\",\n  \"catalan\",\n  \"danish\",\n  \"dutch\",\n  \"english\",\n  \"finnish\",\n  \"french\",\n  \"german\",\n  \"greek\",\n  \"hindi\",\n  \"hungarian\",\n  \"indonesian\",\n  \"irish\",\n  \"italian\",\n  \"lithuanian\",\n  \"nepali\",\n  \"norwegian\",\n  \"portuguese\",\n  \"romanian\",\n  \"russian\",\n  \"serbian\",\n  \"spanish\",\n  \"swedish\",\n  \"tamil\",\n  \"turkish\",\n  \"yiddish\",\n] as const;\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/FilterWrapper.css",
    "content": ".filter-bg {\n  background: #f1faff;\n}\n\n.dark-theme .filter-bg {\n  background: #002445;\n}\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/FilterWrapper.tsx",
    "content": "import type {\n  DetailedFilterBase,\n  DetailedJoinedFilter,\n  FilterType,\n} from \"@common/filterUtils\";\nimport \"./FilterWrapper.css\";\nimport {\n  CORE_FILTER_TYPES,\n  DATE_FILTER_TYPES,\n  FTS_FILTER_TYPES,\n  GEO_FILTER_TYPES,\n  NUMERIC_FILTER_TYPES,\n  TEXT_FILTER_TYPES,\n} from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiCheckBold, mdiDelete } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { includes } from \"prostgles-types\";\nimport React from \"react\";\nimport { CONTEXT_FILTER_OPERANDS } from \"../AccessControl/ContextFilter\";\nimport RTComp from \"../RTComp\";\nimport { colIs } from \"../SmartForm/SmartFormField/fieldUtils\";\nimport type { ColumnConfig } from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport { JOIN_FILTER_TYPES } from \"../SmartFilter/AddJoinFilter\";\nimport {\n  AgeFilterTypes,\n  getDefaultAgeFilter,\n} from \"./DetailedFilterBaseTypes/AgeFilter\";\nimport {\n  GeoFilterTypes,\n  getDefaultGeoFilter,\n} from \"./DetailedFilterBaseTypes/GeoFilter\";\nimport { MinimisedFilter } from \"../SmartFilter/MinimisedFilter\";\nimport {\n  DEFAULT_VALIDATED_COLUMN_INFO,\n  type FilterColumn,\n} from \"../SmartFilter/smartFilterUtils\";\n\nexport type FilterWrapperProps = {\n  db: DBHandlerClient;\n  tableName: string;\n  onChange: (filter?: DetailedFilterBase) => void;\n  filter?: DetailedFilterBase;\n  column: FilterColumn;\n  selectedColumns: ColumnConfig[] | undefined;\n  label?: string;\n  className?: string;\n  style?: React.CSSProperties;\n  hideToggle?: boolean;\n  variant?: \"row\";\n  error?: unknown;\n  children?: React.ReactNode;\n  rootFilter:\n    | {\n        value: DetailedJoinedFilter;\n        onChange: (value: DetailedJoinedFilter | undefined) => void;\n      }\n    | undefined;\n};\n\ntype FilterWrapperState = {\n  popupAnchor?: HTMLElement;\n  defaultSearch?: string;\n  options?: string[];\n  searchTerm?: string;\n  allOptionsCount?: number;\n  error?: unknown;\n};\nexport class FilterWrapper extends RTComp<\n  FilterWrapperProps,\n  FilterWrapperState\n> {\n  state: FilterWrapperState = {\n    searchTerm: \"\",\n  };\n\n  validatedFilterStr?: string;\n\n  render() {\n    const {\n      onChange,\n      filter,\n      column,\n      children,\n      className = \"\",\n      style = {},\n      label = column.name,\n      hideToggle = false,\n      rootFilter,\n    } = this.props;\n    const error = this.state.error ?? this.props.error;\n    let variant: typeof this.props.variant = this.props.variant ?? \"row\";\n    if (\n      filter?.type === \"$in\" ||\n      filter?.type === \"$nin\" ||\n      window.isLowWidthScreen\n    ) {\n      variant = undefined;\n    }\n\n    const minimised = !!filter?.minimised;\n\n    const fieldName = filter?.fieldName ?? column.name;\n\n    const toggle = () => {\n      onChange({\n        ...filter,\n        fieldName,\n        minimised: !minimised,\n      });\n    };\n\n    const rowVariant = variant === \"row\";\n\n    const isSearchAll =\n      column.type === \"column\" &&\n      fieldName === \"*\" &&\n      column.ordinal_position ===\n        DEFAULT_VALIDATED_COLUMN_INFO.ordinal_position;\n    const allowedTypes: {\n      key: FilterType;\n      label: string;\n      subLabel?: string;\n    }[] =\n      isSearchAll ?\n        CORE_FILTER_TYPES.filter((t) => t.key === \"$term_highlight\")\n      : [\n          ...CORE_FILTER_TYPES,\n          ...(colIs(column, \"_PG_date\") ?\n            [...DATE_FILTER_TYPES, ...NUMERIC_FILTER_TYPES]\n          : []),\n          ...(colIs(column, \"_PG_postgis\") ? GEO_FILTER_TYPES : []),\n          ...(colIs(column, \"_PG_numbers\") ? NUMERIC_FILTER_TYPES : []),\n          ...(colIs(column, \"_PG_strings\") ?\n            [...TEXT_FILTER_TYPES, ...FTS_FILTER_TYPES]\n          : []),\n        ].filter(\n          (v, i, arr) => !arr.some((_v, _i) => v.key === _v.key && i !== _i),\n        );\n\n    const btnColor = filter?.disabled ? undefined : \"action\";\n    const disabledToggle = !hideToggle && filter && (\n      <Btn\n        title={(filter.disabled ? \"Enable\" : \"Disable\") + \" filter\"}\n        iconPath={mdiCheckBold}\n        className={`DisableEnableToggle ${minimised ? \"round\" : \"rounded-l\"}`}\n        style={{\n          ...(minimised && {\n            background: \"transparent\",\n            padding: 0,\n          }),\n        }}\n        onClick={() => onChange({ ...filter, disabled: !filter.disabled })}\n        color={btnColor}\n      />\n    );\n\n    const toggleTitle = \"Click to expand/collapse\";\n    if (minimised) {\n      const filterTypeLabel =\n        allowedTypes.find((t) => t.key === filter.type)?.label ?? filter.type;\n      return (\n        <MinimisedFilter\n          {...this.props}\n          label={label}\n          toggle={toggle}\n          toggleTitle={toggleTitle}\n          filterTypeLabel={filterTypeLabel}\n          disabledToggle={disabledToggle}\n        />\n      );\n    }\n\n    const FilterTypeSelector = (\n      <Select\n        className=\"FilterWrapper_Type\"\n        data-command=\"FilterWrapper.typeSelect\"\n        title=\"Choose filter type\"\n        iconPath=\"\"\n        fullOptions={allowedTypes.map((ft) => ({ ...ft }))}\n        value={filter?.type || \"\"}\n        btnProps={{\n          style: { borderRadius: 0 },\n          color: \"default\",\n          variant: \"default\",\n        }}\n        onChange={(type) => {\n          let newF: DetailedFilterBase = {\n            ...filter,\n            type: type,\n            fieldName,\n          };\n\n          if (includes(AgeFilterTypes, type)) {\n            newF = getDefaultAgeFilter(fieldName, type);\n          }\n\n          if (includes(GeoFilterTypes, type)) {\n            newF = getDefaultGeoFilter(fieldName);\n          } else if (\n            colIs(column, \"_PG_date\") ||\n            (Array.isArray(filter?.value) &&\n              type !== \"$between\" &&\n              type !== \"$in\" &&\n              type !== \"$nin\") ||\n            ((type === \"$between\" || type === \"$in\" || type === \"$nin\") &&\n              !Array.isArray(filter?.value))\n          ) {\n            delete newF.value;\n            newF.disabled = true;\n          } else if (\n            typeof newF.value !== \"string\" &&\n            TEXT_FILTER_TYPES.some((t) => t.key === type)\n          ) {\n            newF.value = (newF.value ?? \"\") + \"\";\n            newF.disabled = true;\n          }\n\n          if (\n            newF.contextValue &&\n            !includes(CONTEXT_FILTER_OPERANDS, newF.type)\n          ) {\n            delete newF.contextValue;\n            newF.value ??= \"\";\n          }\n\n          if (type === \"not null\" || type === \"null\") {\n            newF.disabled = false;\n          }\n\n          onChange(newF);\n        }}\n      />\n    );\n\n    const filterNeedsValue = !filter?.type?.endsWith(\"null\");\n    const filterValueContent =\n      filterNeedsValue ?\n        <div\n          className=\"FilterWrapper_Children flex-col min-h-0 gap-p5\"\n          style={{\n            minWidth: \"200px\",\n          }}\n        >\n          {children}\n        </div>\n      : null;\n\n    const filterContentNode =\n      rowVariant && children && filterNeedsValue ? filterValueContent : null;\n    const isWithoutControls = !filterContentNode;\n\n    return (\n      <FlexCol\n        className={`FilterWrapper variant-${variant ?? \"\"} filter-bg relative gap-0 ${error ? \"b-danger\" : \"\"} ${className}`}\n        data-command=\"FilterWrapper\"\n        data-key={filter?.fieldName}\n        style={{\n          maxWidth: rowVariant ? undefined : \"400px\",\n          maxHeight: \"50vh\",\n          borderColor: error ? \"var(--danger)\" : undefined,\n          ...style,\n        }}\n      >\n        <FlexRow\n          className={`gap-0 mx-p5 ${\n            isWithoutControls ? \" \"\n            : rowVariant ? \" ai-center\"\n            : \" ai-start mt-p5 \"\n          }`}\n        >\n          <FlexRowWrap\n            style={rowVariant ? { flex: \"none\" } : {}}\n            className=\"FilterWrapper__TypeLabelContainer gap-0 f-1 font-medium noselect pointer noselect  ai-center\"\n          >\n            <FlexRow className=\"FilterWrapper__LabelContainer gap-0\">\n              {disabledToggle}\n              {rootFilter && (\n                <Select\n                  fullOptions={JOIN_FILTER_TYPES}\n                  value={rootFilter.value.type}\n                  btnProps={{\n                    color: btnColor,\n                    variant: \"default\",\n                  }}\n                  showIconOnly={true}\n                  onChange={(type) => {\n                    rootFilter.onChange({\n                      ...rootFilter.value,\n                      type,\n                    });\n                  }}\n                />\n              )}\n              <Btn\n                className={`FilterWrapper_Field flex-row `}\n                data-command=\"FilterWrapper_Field\"\n                onClick={toggle}\n                variant=\"text\"\n                title={toggleTitle}\n                color={btnColor}\n              >\n                {label}\n              </Btn>\n            </FlexRow>\n            {FilterTypeSelector}\n          </FlexRowWrap>\n\n          {filterContentNode}\n\n          <Btn\n            iconPath={mdiDelete}\n            title=\"Delete filter\"\n            onClick={() => {\n              onChange();\n            }}\n          />\n        </FlexRow>\n\n        {rowVariant ? null : filterValueContent}\n        <ErrorComponent\n          className=\"FilterWrapper_Error bt b-danger p-p5\"\n          error={error}\n          findMsg={true}\n        />\n      </FlexCol>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/DetailedFilterControl/validateFilter.ts",
    "content": "import { getFinalFilter, type DetailedFilter } from \"@common/filterUtils\";\nimport type { BaseFilterProps } from \"../SmartFilter/SmartFilter\";\nimport { getTableSelect } from \"../W_Table/tableUtils/getTableSelect\";\n\nexport const validateFilter = async (\n  filter: DetailedFilter,\n  {\n    db,\n    tableName,\n    column,\n    tables,\n  }: Pick<BaseFilterProps, \"db\" | \"tableName\" | \"column\" | \"tables\">,\n) => {\n  try {\n    const tableHandler = db[tableName];\n    const finalFilter = getFinalFilter(filter);\n    const isHaving =\n      column.type === \"computed\" && column.computedConfig.funcDef.isAggregate;\n    const select =\n      column.type === \"column\" ?\n        \"\"\n      : (\n          await getTableSelect(\n            { table_name: tableName, columns: column.columns },\n            tables,\n            db,\n            finalFilter ?? {},\n          )\n        ).select;\n    await tableHandler?.find?.(isHaving ? {} : finalFilter, {\n      select,\n      having: isHaving ? finalFilter : undefined,\n      limit: 0,\n    });\n    return {\n      hasError: false,\n      error: undefined,\n    };\n  } catch (error: unknown) {\n    return {\n      hasError: true,\n      error,\n    };\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/Feedback.tsx",
    "content": "import { mdiMessageBookmarkOutline } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../App\";\nimport { Success } from \"@components/Animations\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { t } from \"../i18n/i18nUtils\";\nimport { tout } from \"../utils/utils\";\nimport { useIsMounted } from \"prostgles-client\";\n\nexport const Feedback = (props: Pick<Prgl, \"dbsMethods\" | \"dbs\">) => {\n  const { dbsMethods, dbs } = props;\n  const getIsMounted = useIsMounted();\n  const { sendFeedback } = dbsMethods;\n  const [feedback, setFeedBack] = useState<{\n    email?: string;\n    details: string;\n    success?: boolean;\n    error?: any;\n    sending?: boolean;\n  }>({\n    email: \"\",\n    details: \"\",\n  });\n\n  if (!sendFeedback) return null;\n\n  return (\n    <PopupMenu\n      title={t.Feedback[\"Send feedback\"]}\n      positioning=\"beneath-left\"\n      clickCatchStyle={{ opacity: 0.5 }}\n      onClickClose={false}\n      onClose={() => {\n        setFeedBack({\n          email: feedback.email,\n          details: \"\",\n        });\n      }}\n      button={\n        <Btn\n          title={t.Feedback[\"Leave feedback\"]}\n          variant=\"faded\"\n          data-command=\"Feedback\"\n          iconPath={mdiMessageBookmarkOutline}\n        >\n          {/* {window.isMediumWidthScreen ? null : t.Feedback.Feedback} */}\n        </Btn>\n      }\n      footerButtons={\n        feedback.sending || feedback.success ?\n          undefined\n        : (pClose) => [\n            {\n              label: t.common.Cancel,\n              variant: \"outline\",\n              onClick: (e) => {\n                setFeedBack({ email: \"\", details: \"\" });\n                pClose?.(e);\n              },\n            },\n            {\n              label: t.common.Send,\n              variant: \"filled\",\n              color: \"action\",\n              disabledInfo:\n                feedback.sending && !feedback.error ? t.Feedback[\"Already sent\"]\n                : !feedback.details ? t.Feedback[\"Must provide some details\"]\n                : undefined,\n              onClickPromise: async (e) => {\n                try {\n                  setFeedBack({ ...feedback, sending: true });\n                  await sendFeedback(feedback);\n                  setFeedBack({ ...feedback, success: true });\n                  await tout(3000);\n                  if (getIsMounted()) {\n                    setFeedBack({ email: \"\", details: \"\" });\n                    pClose?.(e);\n                  }\n                } catch (error) {\n                  setFeedBack({ ...feedback, error });\n                }\n              },\n            },\n          ]\n      }\n    >\n      <FlexCol>\n        {feedback.success ?\n          <>\n            <Success />\n            <p>{t.Feedback[\"Feedback was sent! Thanks a lot!\"]}</p>\n          </>\n        : feedback.error ?\n          <ErrorComponent error={feedback.error} />\n        : <>\n            <FormField\n              id=\"email\"\n              type=\"email\"\n              label={t.Feedback[\"Email (optional)\"]}\n              value={feedback.email}\n              onChange={(email) => {\n                setFeedBack({ ...feedback, email });\n              }}\n            />\n            <FormField\n              id=\"text\"\n              type=\"text\"\n              label={t.Feedback[\"Details\"]}\n              value={feedback.details}\n              asTextArea={true}\n              onChange={(details) => {\n                if (details.length < 500) {\n                  setFeedBack({ ...feedback, details });\n                }\n              }}\n              hint={`${500 - `${feedback.details}`.length}/500`}\n            />\n            <p>\n              {t.Feedback[\"Other options\"]}:{\" \"}\n              <a\n                target=\"_blank\"\n                href=\"https://github.com/prostgles/ui/issues/new/choose\"\n                rel=\"noreferrer\"\n              >\n                {t.Feedback[\"open an issue\"]}\n              </a>{\" \"}\n              {t.Feedback.or}{\" \"}\n              <a href=\"mailto:prostgles@protonmail.com\">\n                {t.Feedback[\"email us\"]}\n              </a>\n            </p>\n          </>\n        }\n      </FlexCol>\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/FileImporter/FileImporter.tsx",
    "content": "import { mdiAlertCircleOutline, mdiFormatText } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport FormField from \"@components/FormField/FormField\";\nimport Loading from \"@components/Loader/Loading\";\nimport Popup from \"@components/Popup/Popup\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Table } from \"@components/Table/Table\";\nimport { bytesToSize } from \"../BackupAndRestore/BackupsControls\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport RTComp from \"../RTComp\";\nimport type { ProstglesColumn } from \"../W_SQL/W_SQL\";\nimport { getFileText } from \"../W_SQL/W_SQLMenu\";\nimport { ApplySuggestedDataTypes } from \"./checkCSVColumnDataTypes\";\nimport { FileImporterFooter } from \"./FileImporterFooter\";\nimport { importFile, type ImportProgress } from \"./importFile\";\nimport { setFile } from \"./setFile\";\nconst streamColumnDataTypes = [\"TEXT\", \"JSON\", \"JSONB\"] as const;\n\ntype Papa = typeof import(\"papaparse\");\nexport const getPapa = () =>\n  import(/* webpackChunkName: \"papaparse\" */ \"papaparse\");\n\nexport type FileImporterProps = {\n  db: DBHandlerClient;\n  onClose: VoidFunction;\n  openTable: (tableName: string) => void;\n  style?: object;\n  className?: string;\n  id?: string;\n  button?: Element;\n  parentDiv?: Element;\n  tables: CommonWindowProps[\"tables\"];\n};\n\ntype HeaderType = \"First row\" | \"Custom\";\n\nconst INSERT_AS = [\n  \"Single text value\",\n  \"JSONB Rows\",\n  \"Properties with Geometry\",\n] as const;\n\nexport type FileImporterState = {\n  bytesPerRow?: number;\n  json?: AnyObject;\n\n  loadingFileName?: string;\n\n  // file?: File;\n  selectedFile?: {\n    fileTooBig?: boolean;\n    file: File;\n    type: \"csv\" | \"json\" | \"geojson\";\n    header: boolean;\n\n    /** If geojson */\n    srid?: string;\n\n    preview?: {\n      rows: Record<string, any>[];\n      cols: ({ dataType: string; escapedName: string } & Pick<\n        ProstglesColumn,\n        \"key\" | \"label\" | \"sortable\" | \"width\" | \"subLabel\"\n      >)[];\n\n      /** If json/geojson */\n      allRows?: Record<string, any>[];\n\n      /**\n       * Only used for non-csv (csvs specify the chunk size to the stream reader)\n       * Must send less data than maxHttpBufferSize: (1e8 which is 100Mb)\n       * 1 char = 4 bytes\n       */\n      rowsPerBatch?: number;\n    };\n  };\n\n  destination: {\n    newTable?: boolean;\n    newTableName?: string;\n    existingTable?: string;\n  };\n  config?: {\n    header?: boolean;\n    quoteChar?: string;\n    newline?: string;\n\n    /**\n     * Must send less data than maxHttpBufferSize: (1e8 which is 100Mb)\n     * 1 char = 4 bytes\n     */\n    // rowsPerBatch: number;\n  };\n  importing?: ImportProgress;\n  // {\n  //   tableName: string;\n  //   importedRows?: number;\n  //   progress?: number;\n  //   timeElapsed?: string;\n  //   finished?: boolean;\n  //   errors: any[];\n  // }\n\n  open: boolean;\n  error?: any;\n  reCreateTable?: boolean;\n  inferAndApplyDataTypes: boolean;\n  headerType: HeaderType;\n  customHeaders?: string;\n  customHeadersError?: string;\n  results?: any;\n  colNo?: string;\n\n  /**\n   * If true then all data will be inserted as a single text cell\n   */\n  insertAs?: (typeof INSERT_AS)[number];\n\n  streamColumnDataType: (typeof streamColumnDataTypes)[number];\n\n  streamBatchMb: number;\n\n  multiImport?: {\n    fileName: string;\n    content: string;\n  }[];\n  files?: FileList | File[];\n\n  streamColDelimiter: string;\n};\n\nexport class FileImporter extends RTComp<FileImporterProps, FileImporterState> {\n  state: FileImporterState = {\n    streamColumnDataType: \"TEXT\",\n    streamBatchMb: 10,\n    insertAs: \"JSONB Rows\",\n    bytesPerRow: 0,\n    config: {\n      header: true,\n    },\n    destination: {\n      newTable: true,\n      newTableName: undefined,\n      existingTable: undefined,\n    },\n    open: true,\n    reCreateTable: false,\n    headerType: \"First row\",\n    results: undefined,\n    colNo: \"\",\n    streamColDelimiter: \"_$_\",\n    inferAndApplyDataTypes: true,\n  };\n\n  getExitMessage = () => {\n    return this.isImporting ?\n        `Currently importing into table ${this.isImporting.tableName}. Data might be lost`\n      : undefined;\n  };\n\n  onBeforeUnload = (e) => {\n    const confirmationMessage = this.getExitMessage();\n    if (confirmationMessage) {\n      (e || window.event).returnValue = confirmationMessage; //Gecko + IE\n      return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.\n    }\n  };\n\n  isImporting: FileImporterState[\"importing\"];\n  onMount() {\n    window.addEventListener(\"beforeunload\", this.onBeforeUnload);\n  }\n\n  onUnmount() {\n    const confirmationMessage = this.getExitMessage();\n    if (!confirmationMessage) {\n      window.removeEventListener(\"beforeunload\", this.onBeforeUnload);\n    } else {\n      window.confirm(confirmationMessage);\n    }\n    window.__prglIsImporting = false;\n  }\n\n  onDelta = (dP, dS, dD) => {\n    const { selectedFile } = this.state;\n    const delta = { ...dP, ...dS, ...dD };\n\n    window.__prglIsImporting = Boolean(this.state.importing);\n\n    if (selectedFile && delta && (delta.customHeaders || delta.headerType)) {\n      // this.setFile(selectedFile.file);\n    }\n  };\n\n  parseFiles = async (files: File[]) => {\n    const multiImport: FileImporterState[\"multiImport\"] = [];\n    for (const file of files) {\n      this.setState({ loadingFileName: file.name, importing: undefined });\n      const content = await getFileText(file);\n      multiImport.push({ fileName: file.name, content });\n    }\n    this.setState({ loadingFileName: undefined });\n\n    return multiImport;\n  };\n\n  setFiles = async (files: FileList) => {\n    if (files.length > 1) {\n      const allowedTypesForMultiImport = [\"txt\", \"svg\", \"html\", \"json\"];\n      const arr = Array.from(files);\n      const [firstFile] = arr;\n      if (\n        firstFile &&\n        arr.every(\n          (f) =>\n            allowedTypesForMultiImport.find((ext) =>\n              f.name.toLowerCase().endsWith(`.${ext}`),\n            ) && f.size < 1e6,\n        )\n      ) {\n        this.setState({ files });\n\n        const multiImport = await this.parseFiles(arr.slice(0, 40));\n\n        this.setState({\n          files: arr,\n          multiImport,\n          selectedFile: {\n            file: firstFile,\n            type: \"csv\",\n            header: false,\n            preview: {\n              cols: [\n                {\n                  key: \"fileName\",\n                  dataType: \"TEXT\",\n                  escapedName: \"fileName\",\n                  label: \"fileName\",\n                  sortable: false,\n                },\n                {\n                  key: \"content\",\n                  dataType: \"TEXT\",\n                  escapedName: \"content\",\n                  label: \"content\",\n                  sortable: false,\n                },\n              ],\n              rows: multiImport.slice(0, 40),\n              // allRows: multiImport,\n            },\n          },\n\n          loadingFileName: undefined,\n        });\n      } else {\n        alert(\n          `Multi import is only allowed for text files (${allowedTypesForMultiImport}) less than 1Mb each`,\n        );\n      }\n    } else if (files.length) {\n      this.setFile(files[0]!);\n    }\n  };\n\n  setFile = setFile.bind(this);\n\n  canceled = false;\n  importFile = async () => {\n    const onError = (error: any) => {\n      if (!this.mounted) return;\n\n      this.setState({ error });\n    };\n    try {\n      await importFile({\n        ...this.state,\n        db: this.props.db,\n        onError,\n        onProgress: (importing) => {\n          if (!this.mounted) {\n            return { canContinue: false };\n          }\n          this.setState({ importing });\n          return { canContinue: true };\n        },\n      });\n    } catch (error) {\n      onError(error);\n    }\n  };\n\n  cancel = async () => {\n    this.canceled = true;\n    const { onClose, db } = this.props;\n    const { importing } = this.state;\n\n    /**\n     * Drop table if import is canceled\n     */\n    if (importing && !importing.finished)\n      await db.sql!(\"DROP TABLE IF EXISTS \" + importing.tableName);\n\n    this.setState({\n      importing: undefined,\n      open: false,\n      selectedFile: undefined,\n    });\n    onClose();\n  };\n\n  render() {\n    const {\n      selectedFile,\n      colNo,\n      destination,\n      importing,\n      headerType,\n      error,\n      open,\n      reCreateTable,\n      inferAndApplyDataTypes,\n      insertAs,\n      loadingFileName,\n      files,\n    } = this.state;\n\n    const { openTable, parentDiv, db } = this.props;\n    const { newTableName } = destination;\n    let tblName = newTableName;\n    if (!newTableName && selectedFile) tblName = selectedFile.file.name; //.slice(0, -4);\n\n    const readonlyName = Boolean(importing);\n    return (\n      <Popup\n        title={\n          !selectedFile?.file ? \"Import data from file\"\n          : files ?\n            `Import ${files.length} files`\n          : `Import ${selectedFile.file.name}`\n        }\n        onClose={this.cancel}\n        clickCatchStyle={{ opacity: 1 }}\n        positioning=\"center\"\n        contentClassName=\"p-1\"\n        footer={\n          <FileImporterFooter\n            {...this.state}\n            openTable={openTable}\n            db={db}\n            onImport={this.importFile}\n            onCancel={this.cancel}\n          />\n        }\n      >\n        <div className=\"flex-col o-auto p-p5\">\n          {loadingFileName && !error && (\n            <div className=\"flex-row mt-2\">\n              <Loading className=\"mr-1\" />\n              <div className=\"text-1p5 mt-p5\" style={{ fontSize: \"1em\" }}>\n                Loading {loadingFileName}\n              </div>\n            </div>\n          )}\n\n          {(\n            (importing && !importing.finished) ||\n            loadingFileName ||\n            this.state.selectedFile\n          ) ?\n            null\n          : <FormField\n              className=\"mb-1 \"\n              label=\"File (csv/txt/json/geojson)\"\n              type=\"file\"\n              inputProps={{\n                multiple: true,\n                \"data-command\": \"FileBtn\",\n              }}\n              accept=\"text/*, .csv, .txt, .json, .geojson, .tsv\"\n              onChange={(files) => {\n                this.setFiles(files);\n              }}\n            />}\n\n          {selectedFile && (\n            <>\n              <FormField\n                key={\"new-table-name\"}\n                readOnly={readonlyName}\n                className=\"mb-1 \"\n                label=\"Table name\"\n                value={tblName}\n                onChange={\n                  readonlyName ? undefined : (\n                    (newTableName) => {\n                      this.setState({\n                        destination: { ...destination, newTableName },\n                      });\n                    }\n                  )\n                }\n                rightIcons={\n                  readonlyName ? undefined : (\n                    <Btn\n                      iconPath={mdiFormatText}\n                      onClick={() =>\n                        this.setState({\n                          destination: {\n                            ...destination,\n                            newTableName: toSnakeCase(tblName ?? \"\"),\n                          },\n                        })\n                      }\n                      title=\"Transform name to snake case\"\n                    />\n                  )\n                }\n              />\n\n              {!importing?.finished && (\n                <>\n                  <FormField\n                    readOnly={readonlyName}\n                    className=\"mb-1 \"\n                    label=\"Drop table if exists\"\n                    type=\"checkbox\"\n                    value={reCreateTable}\n                    onChange={(reCreateTable) => {\n                      this.setState({ reCreateTable });\n                    }}\n                  />\n                  <FormField\n                    readOnly={readonlyName}\n                    className=\"mb-1 \"\n                    label=\"Try to infer and apply column data types\"\n                    type=\"checkbox\"\n                    value={inferAndApplyDataTypes}\n                    onChange={(inferAndApplyDataTypes) => {\n                      this.setState({ inferAndApplyDataTypes });\n                    }}\n                  />\n\n                  {selectedFile.type !== \"csv\" && (\n                    <FormField\n                      className=\"mb-1 \"\n                      label=\"Insert as\"\n                      value={insertAs}\n                      options={INSERT_AS}\n                      onChange={(insertAs) => {\n                        this.setState({ insertAs });\n                      }}\n                    />\n                  )}\n\n                  {selectedFile.type === \"geojson\" && (\n                    <FormField\n                      type=\"text\"\n                      className=\"mb-1 \"\n                      label=\"SRID\"\n                      value={selectedFile.srid}\n                      onChange={(srid) => {\n                        this.setState({\n                          selectedFile: {\n                            ...this.state.selectedFile!,\n                            srid,\n                          },\n                        });\n                      }}\n                    />\n                  )}\n                </>\n              )}\n            </>\n          )}\n\n          {selectedFile && !importing && (\n            <div\n              className=\"f-1 flex-col\"\n              style={{ maxHeight: \"400px\", maxWidth: \"900px\" }}\n            >\n              {loadingFileName ?\n                <Loading key=\"preview-loader\" />\n              : <div className=\"flex-col f-1 min-w-0 min-h-0\">\n                  <div className=\"divider-h\"></div>\n\n                  <div className=\"flex-row gap-1 jc-center\">\n                    <div className=\"f-0 mb-1 noselect bold\">\n                      {selectedFile.file.name}:{\" \"}\n                      {bytesToSize(selectedFile.file.size)}\n                    </div>\n                    <div className=\"f-0 mb-1 noselect\">\n                      Preview ({selectedFile.preview?.allRows?.length} rows)\n                    </div>\n                  </div>\n\n                  {selectedFile.preview?.rows && (\n                    <Table\n                      maxCharsPerCell={500}\n                      className=\"f-1 o-auto b-color \"\n                      {...selectedFile.preview}\n                      cols={selectedFile.preview.cols.map(\n                        (c) =>\n                          ({\n                            ...c,\n                            filter: false,\n                            name: c.key.toString(),\n                            tsDataType: \"string\",\n                            udt_name: \"text\",\n                            computed: false,\n                          }) satisfies ProstglesColumn,\n                      )}\n                    />\n                  )}\n                </div>\n              }\n            </div>\n          )}\n\n          {importing?.finished && (\n            <div className=\"flex-row-wrap gap-1 ai-center\">\n              {importing.errors.length > 0 && (\n                <PopupMenu\n                  button={\n                    <Btn\n                      color=\"warn\"\n                      variant=\"outline\"\n                      iconPath={mdiAlertCircleOutline}\n                    >\n                      {importing.errors.length} Error/s\n                    </Btn>\n                  }\n                  title=\"Import errors (ignored)\"\n                  onClickClose={true}\n                  positioning=\"fullscreen\"\n                  render={(pClose) => (\n                    <CodeEditor\n                      language=\"json\"\n                      value={JSON.stringify(importing.errors, null, 2)}\n                      className=\"w-full h-full\"\n                    />\n                  )}\n                />\n              )}\n\n              <div className=\"f-1 pre\">\n                <span className=\"bold\">{importing.importedRows}</span> rows\n                imported into\n                <span className=\"text-warning ml-p5\">\n                  {importing.tableName}\n                </span>\n              </div>\n              {db.sql && (\n                <ApplySuggestedDataTypes\n                  types={importing.types}\n                  onDone={this.cancel}\n                  sql={db.sql}\n                  tableName={importing.tableName}\n                />\n              )}\n            </div>\n          )}\n\n          {!!error && (\n            <ErrorComponent\n              className=\"p-1\"\n              withIcon={true}\n              error={error.err_msg || error}\n              pre={true}\n            />\n          )}\n        </div>\n      </Popup>\n    );\n  }\n}\n\nexport function getRowsPerBatch(maxCharsPerRow, bytesPerChar = 4) {\n  let rowsPerBatch = 1;\n  const bitsPerRow = (maxCharsPerRow + 100) * bytesPerChar;\n  if (bitsPerRow > batchMaxBitSize) {\n    throw `${bytesToSize(bitsPerRow)} row size limit exceeded (> ${bytesToSize(batchMaxBitSize)} batchMaxBitSize). Cannot send upload data`;\n  } else {\n    rowsPerBatch = Math.floor(\n      Math.min(\n        1500, // no more than 1.5k rows\n\n        Math.max(\n          preferredBatchBitSize / bitsPerRow,\n          batchMaxBitSize / bitsPerRow,\n        ),\n      ),\n    );\n  }\n\n  return rowsPerBatch;\n}\n\nconst batchMaxBitSize = 1e8,\n  preferredBatchBitSize = 2e7;\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\nif (preferredBatchBitSize > batchMaxBitSize) {\n  throw \"preferredBatchBitSize must be less than batchMaxBitSize\";\n}\n\ntype StreamFileArgs = Omit<\n  Papa.ParseLocalConfig<string[], Papa.LocalFile>,\n  \"chunk\" | \"chunkSize\" | \"complete\"\n> & {\n  file: File;\n\n  /**\n   * Must send less data than maxHttpBufferSize: (1e8 which is 100Mb)\n   * 1 char = 4 bytes\n   */\n  streamBatchMb?: number;\n  onChunk: (result: Papa.ParseResult<string[]>, parser: Papa.Parser) => void;\n  onDone?: VoidFunction;\n  onError?: (error: any) => void;\n  papa: Papa;\n};\n\nexport const streamBIGFile = ({\n  file,\n  onChunk,\n  streamBatchMb = 1,\n  onDone = () => {},\n  onError,\n  papa,\n  ...opts\n}: StreamFileArgs) => {\n  return papa.parse<string[]>(file, {\n    ...opts,\n    chunkSize: 1024 * 1024 * streamBatchMb,\n    chunk: function (results, parser) {\n      onChunk(results, parser);\n    },\n    error: onError,\n    complete: onDone,\n  });\n};\n\nexport const getCSVFirstChunk = (\n  args: Omit<StreamFileArgs, \"onChunk\" | \"onError\">,\n): Promise<Papa.ParseResult<string[]>> => {\n  return new Promise(async (resolve, reject) => {\n    const papa = await getPapa();\n    streamBIGFile({\n      ...args,\n      papa,\n      error: reject,\n      onChunk: (results, parser) => {\n        const { data } = results;\n        parser.abort();\n        resolve(results);\n      },\n    });\n  });\n};\n\nconst toSnakeCase = (str: string) => {\n  let res = \"\";\n  const _s = str.trim();\n  const arr = _s.split(\"\");\n  arr.map((k, i) => {\n    if (\n      i &&\n      res.slice(-1) !== \"_\" &&\n      (!arr[i - 1]!.match(/[A-Z]/) || k.match(/[\\W_]+/))\n    ) {\n      res += \"_\";\n    }\n\n    if (!k.match(/[\\W_]+/)) {\n      res += k.toLowerCase();\n    }\n  });\n\n  return res;\n};\n// rowsToJson = (_results: any) => {\n//   const { config, headerType, customHeaders = \"\" } = this.state;\n\n//   const results = { ..._results };\n//   const rowArr = results.data;\n//   let cols: Required<Required<FileImporterState>[\"selectedFile\"]>[\"preview\"][\"cols\"] = [], rows = [];\n//   if(rowArr && rowArr.length){\n\n//     if(headerType === \"Custom\"){\n//       const firstRowKeys = Object.keys(results.data[0]);\n//       const defaultColNames = new Array(firstRowKeys.length).fill(null).map((d, i) => `c${i+1}`).join(\", \");\n//       if(!customHeaders) this.setState({ customHeaders: defaultColNames })\n//       const headers = customHeaders.split(\", \").map(d => d.trim());\n\n//       let ns = {\n//         colNo: ` (${firstRowKeys.length})`,\n//         customHeadersError: \"\"\n//       }\n\n//       if(firstRowKeys.length !== headers.length){\n//         ns.customHeadersError = \"Extra/missing column names\";\n\n//       } else if(Array.from(new Set(headers)).length !== headers.length){\n//         ns.customHeadersError = \"Duplicate column names\";\n\n//       } else {\n\n//         cols = headers.map(key => ({\n//           key,\n//           label: key,\n//           dataType: \"text\",\n//           escapedName: asName(key),\n//           sortable: false,\n//         }));\n//         rows = rowArr.map(row => {\n//           let res = {};\n//           let vals = Array.isArray(row)? row : Object.values(row);\n//           vals.map((r, i) => {\n//             res[headers[i]!] = r;\n//           })\n//           return res;\n//         });\n//       }\n//       this.setState(ns)\n\n//     } else if(headerType === \"First row\"){\n//       cols = results.meta.fields.map(key => ({\n//         key,\n//         label: key,\n//         dataType: \"text\",\n//         escapedName: asName(key),\n//         sortable: false,\n//       }));\n//       rows = results.data;\n//     } else {\n//       cols = new Array(rowArr[0].length).fill(null).map((d, i) => ({\n//         key: `c${i}`,\n//         label: `Column ${i}`,\n//         dataType: \"text\",\n//         escapedName: asName(`c${i}`),\n//         sortable: false,\n//       }));\n//       rows = rowArr.map(row => {\n//         let res = {};\n//         row.map((r, i) => {\n//           res[`c${i}`] = r;\n//         })\n//         return res;\n//       });\n//     }\n//   }\n\n//   return { cols, rows };\n// }\n"
  },
  {
    "path": "client/src/dashboard/FileImporter/FileImporterFooter.tsx",
    "content": "import React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { ProgressBar } from \"@components/ProgressBar\";\nimport type { FileImporterProps, FileImporterState } from \"./FileImporter\";\n\ntype P = FileImporterState &\n  Pick<FileImporterProps, \"db\" | \"openTable\"> & {\n    onCancel: VoidFunction;\n    onImport: VoidFunction;\n  };\nexport const FileImporterFooter = (props: P) => {\n  const {\n    db,\n    selectedFile,\n    onImport,\n    onCancel,\n    importing,\n    openTable,\n    error,\n    customHeadersError,\n  } = props;\n\n  const hideOpenTable =\n    !importing?.finished || !importing.tableName || !db[importing.tableName];\n  return (\n    <>\n      <div className={\"ai-center flex-row f-1 gap-1\"}>\n        <Btn onClick={onCancel} className=\"mr-auto\">\n          Cancel\n        </Btn>\n\n        {selectedFile && !importing && (\n          <Btn\n            className=\"ml-auto\"\n            onClick={onImport}\n            disabledInfo={customHeadersError}\n            variant=\"filled\"\n            color=\"action\"\n            data-command=\"FileImporterFooter.import\"\n          >\n            Import\n          </Btn>\n        )}\n\n        {hideOpenTable ? null : (\n          <Btn\n            className=\"ml-1\"\n            onClick={() => {\n              openTable(importing.tableName);\n              onCancel();\n            }}\n            disabledInfo={customHeadersError}\n            style={{\n              background: \"var(--blue-600)\",\n              color: \"white\",\n              padding: \"8px 16px\",\n            }}\n          >\n            Open table\n          </Btn>\n        )}\n\n        {!error && importing && !importing.progress ?\n          <div className=\"flex-row ai-center ml-auto ta-right\">\n            Loading file ...\n          </div>\n        : null}\n\n        {!importing || importing.finished || error ? null : (\n          <div className=\"ImportingProgress flex-row ai-center ml-auto ta-right\">\n            <div\n              className={\n                \"mr-1 flex-col \" +\n                (!importing.progress && !importing.importedRows ?\n                  \" hidden \"\n                : \"\")\n              }\n            >\n              <div style={{ fontSize: \"1.5em\" }}>\n                {importing.progress.toFixed(0)}%\n              </div>\n            </div>\n            <div className=\"flex-col ai-center\">\n              <ProgressBar\n                totalValue={100}\n                value={importing.progress || 0}\n                message={`${(importing.importedRows || 0).toLocaleString()} rows`}\n              />\n              <div className=\"text-1p5 mt-p5\" style={{ fontSize: \"1em\" }}>\n                {importing.timeElapsed}\n              </div>\n            </div>\n          </div>\n        )}\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/FileImporter/checkCSVColumnDataTypes.tsx",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { PG_COLUMN_UDT_DATA_TYPE } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { FlexCol } from \"@components/Flex\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport Btn from \"@components/Btn\";\n\nexport type SuggestedColumnDataType = {\n  table_schema: string;\n  table_name: string;\n  column_name: string;\n  suggested_type: PG_COLUMN_UDT_DATA_TYPE;\n  alter_query: string;\n};\nexport const getTextColumnPotentialDataTypes = async (\n  sql: Required<DBHandlerClient>[\"sql\"],\n  { schema, tableName }: { schema?: string; tableName: string },\n): Promise<SuggestedColumnDataType[]> => {\n  await sql(`ANALYZE \\${tableName:name}`, { tableName });\n  const q = `\n    WITH text_column_values AS (\n      SELECT \n        c.table_schema,\n        c.table_name,\n        c.column_name,\n        nullif(common_value, '') as common_value,\n        c.udt_name as current_data_type,\n        CASE \n          WHEN common_value ~ '^(true|false)$' THEN 'BOOLEAN'\n          WHEN common_value ~ '^(0|[1-9][0-9]*)$' THEN 'INTEGER'\n          WHEN common_value ~ '^[+-]?[0-9]+([.][0-9]+)?$' THEN 'NUMERIC'\n          WHEN common_value ~ '^\\\\d{4}\\\\-(0?[1-9]|1[012])\\\\-(0?[1-9]|[12][0-9]|3[01])$' THEN 'DATE'\n          WHEN common_value ~ '[0-9]{1,4}/[0-9]{1,2}/[0-9]{1,2} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}' THEN 'TIMESTAMP'\n        END AS suggested_type\n      FROM pg_stats s\n      INNER JOIN information_schema.columns c\n        ON s.schemaname = c.table_schema\n        AND s.tablename = c.table_name\n        AND s.attname = c.column_name\n        , unnest(most_common_vals::text::_text) as common_value\n      WHERE table_schema = current_schema()\n      and table_name = \\${tableName}\n      and nullif(common_value, '') is not null\n    )\n    SELECT table_schema, table_name, column_name, suggested_type, format('ALTER COLUMN %1$I SET DATA TYPE %2$s USING NULLIF(%1$I, '''')::%2$s', column_name, suggested_type) as alter_query\n    FROM text_column_values\n    GROUP BY table_schema, table_name, column_name, suggested_type\n    HAVING COUNT(DISTINCT suggested_type) = 1\n  `;\n\n  const result = (await sql(\n    q,\n    { tableName },\n    { returnType: \"rows\" },\n  )) as SuggestedColumnDataType[];\n  return result;\n};\n\nexport const applySuggestedDataTypes = async ({\n  types,\n  sql,\n  tableName,\n}: {\n  types: SuggestedColumnDataType[];\n  sql: Required<DBHandlerClient>[\"sql\"];\n  tableName: string;\n}) => {\n  const query =\n    `ALTER TABLE ${JSON.stringify(tableName)}\\n ` +\n    types.map((d) => d.alter_query).join(\",\\n\");\n  await sql(query);\n};\n\ntype P = {\n  types: SuggestedColumnDataType[] | undefined;\n  onDone: VoidFunction;\n  sql: Required<DBHandlerClient>[\"sql\"];\n  tableName: string;\n};\n\nexport const ApplySuggestedDataTypes = ({\n  types,\n  onDone,\n  sql,\n  tableName,\n}: P) => {\n  const [selectedColumns, setSelectedColumns] = useState<string[]>(\n    types?.map((t) => t.column_name) ?? [],\n  );\n  if (!types?.length) return null;\n\n  return (\n    <FlexCol>\n      <SearchList\n        label=\"Suggested column data types\"\n        items={types.map((d) => ({\n          key: d.column_name,\n          label: d.column_name,\n          subLabel: d.suggested_type,\n          checked: selectedColumns.includes(d.column_name),\n          onPress: () => {\n            const newSelected =\n              selectedColumns.includes(d.column_name) ?\n                selectedColumns.filter((colKey) => colKey !== d.column_name)\n              : selectedColumns.concat(d.column_name);\n            setSelectedColumns(newSelected);\n          },\n        }))}\n        checkboxed={true}\n        onMultiToggle={(selected) => {\n          setSelectedColumns(\n            selected.filter((d) => d.checked).map((d) => d.key) as string[],\n          );\n        }}\n      />\n      <Btn\n        color=\"action\"\n        variant=\"filled\"\n        onClickPromise={async () => {\n          const selectedTypes = types.filter((d) =>\n            selectedColumns.includes(d.column_name),\n          );\n          await applySuggestedDataTypes({\n            types: selectedTypes,\n            sql,\n            tableName,\n          });\n          onDone();\n        }}\n      >\n        Apply suggested data types types\n      </Btn>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/FileImporter/importFile.ts",
    "content": "import type { AnyObject, DBHandler } from \"prostgles-types\";\nimport { asName } from \"prostgles-types\";\nimport type { FileImporterState } from \"./FileImporter\";\nimport { getPapa, streamBIGFile } from \"./FileImporter\";\nimport {\n  applySuggestedDataTypes,\n  getTextColumnPotentialDataTypes,\n  type SuggestedColumnDataType,\n} from \"./checkCSVColumnDataTypes\";\n\nexport type ImportProgress = {\n  importedRows: number;\n  totalRows: number;\n  tableName: string;\n  timeElapsed: string;\n  progress: number;\n  finished?: boolean;\n  types?: SuggestedColumnDataType[];\n  errors: any[];\n};\ntype Args = Pick<\n  FileImporterState,\n  | \"destination\"\n  | \"selectedFile\"\n  | \"reCreateTable\"\n  | \"insertAs\"\n  | \"streamBatchMb\"\n  | \"streamColumnDataType\"\n  | \"streamColDelimiter\"\n  | \"inferAndApplyDataTypes\"\n> & {\n  db: DBHandler;\n  onError: (err: any) => void;\n  onProgress: (stats: ImportProgress) => { canContinue: boolean };\n};\nexport const importFile = async (args: Args) => {\n  const { selectedFile, db, destination, streamBatchMb, onError } = args;\n\n  let canceled = false;\n  const onProgress: typeof args.onProgress = (stats) => {\n    if (stats.finished) {\n      canceled = true;\n    }\n    const res = args.onProgress(stats);\n    if (!res.canContinue) {\n      canceled = true;\n    }\n\n    return res;\n  };\n\n  onProgress({\n    tableName: destination.newTableName ?? destination.existingTable ?? \"\",\n    timeElapsed: \"00:00\",\n    totalRows: 0,\n    progress: 0,\n    importedRows: 0,\n    errors: [],\n    finished: false,\n  });\n\n  try {\n    if (!selectedFile?.file) {\n      throw \"No file to import\";\n    }\n    if (!selectedFile.preview) {\n      throw \"Preview missing\";\n    }\n\n    const { tableName, insertQueryPrefix, columns } = await createTable(args);\n    const importing = {\n      tableName,\n      importedRows: 0,\n      totalRows: 0,\n      progress: 0,\n      errors: [],\n      timeElapsed: \"\",\n    };\n\n    /* Elapsed time counter */\n    const elapsedCounter = {\n      timeStarted: Date.now(),\n      interval: setInterval(() => {\n        if (canceled || !elapsedCounter.timeStarted) {\n          clearInterval(elapsedCounter.interval);\n          return;\n        }\n        const s = elapsedCounter.timeStarted;\n        const secs = Math.round((Date.now() - s) / 1000);\n        const mins = Math.floor(secs / 60);\n\n        onProgress({\n          ...importing,\n          timeElapsed: [mins, secs - mins * 60]\n            .map((v) => v.toString().padStart(2, \"0\"))\n            .join(\":\"),\n        });\n      }, 1000),\n    };\n\n    const stepSize = selectedFile.preview.rowsPerBatch ?? 1000;\n    let currIdx = 0;\n    let batch: any[] = [];\n    const errors: any[] = [];\n    try {\n      const insertRowsObj = (rowsObj: Record<string, any[]>) => {\n        return db.sql!(\n          `${insertQueryPrefix} ${Object.keys(rowsObj)\n            .map((key) => `(\\${${key}:csv})`)\n            .join(\", \")}`,\n          rowsObj,\n        );\n      };\n\n      if (selectedFile.type === \"csv\") {\n        let rowsImported = 0,\n          importedSize = 0;\n\n        const papa = await getPapa();\n        streamBIGFile({\n          papa,\n          file: selectedFile.file,\n          streamBatchMb,\n          header: selectedFile.header,\n          onChunk: async (d, p) => {\n            if (canceled) {\n              p.abort();\n            } else {\n              p.pause();\n              try {\n                const rowsObj: AnyObject = {};\n                d.data.forEach((row, rowIdx) => {\n                  if (Object.keys(row).length !== columns.length) {\n                    console.error(\"Field mismatch: \", row);\n                  } else {\n                    const rowKey = `r${rowIdx}`;\n                    rowsObj[rowKey] = row;\n                  }\n                });\n                if (d.errors.length) {\n                  errors.push(...d.errors);\n                  d.errors.forEach((e) => console.error(e));\n                }\n                await insertRowsObj(rowsObj);\n              } catch (error) {\n                onError(error);\n                console.error(error);\n              }\n\n              importedSize = d.meta.cursor;\n              rowsImported += d.data.length;\n\n              importing.progress =\n                (100 * importedSize) / selectedFile.file.size;\n              importing.importedRows = rowsImported;\n\n              p.resume();\n            }\n          },\n          onError: (error) => {\n            console.error(error);\n          },\n          onDone: async () => {\n            let types = await getTextColumnPotentialDataTypes(db.sql!, {\n              tableName,\n            });\n            if (args.inferAndApplyDataTypes) {\n              await applySuggestedDataTypes({ types, sql: db.sql!, tableName });\n              types = [];\n            }\n            onProgress({\n              ...importing,\n              progress: 100,\n              importedRows: rowsImported,\n              errors,\n              types,\n              finished: true,\n            });\n          },\n        });\n      } else {\n        const allRows = selectedFile.preview.allRows ?? [];\n        importing.totalRows = allRows.length;\n\n        do {\n          batch = allRows.slice(currIdx, currIdx + stepSize);\n\n          const rowsObj = Object.fromEntries(\n            batch.map((r, ri) => {\n              return [`r${ri}`, columns.map((c) => r[c])];\n            }),\n          );\n          await insertRowsObj(rowsObj);\n\n          importing.importedRows = currIdx + stepSize;\n          importing.progress = Math.round(\n            (100 * importing.importedRows) / allRows.length,\n          );\n\n          currIdx += stepSize;\n        } while (\n          (!currIdx || currIdx < allRows.length) &&\n          !(canceled as boolean)\n        );\n\n        onProgress({\n          ...importing,\n          progress: 100,\n          importedRows: allRows.length,\n          errors,\n          finished: true,\n        });\n      }\n    } catch (e) {\n      console.error(e, batch);\n      throw {\n        startRow: currIdx,\n        // batchSize: stepSize,\n        err: e,\n      };\n    }\n    // this.isImporting = undefined;\n  } catch (e) {\n    console.error(e);\n    canceled = true;\n    onError(e);\n  }\n};\n\nconst createTable = async (\n  args: Args,\n): Promise<{\n  tableName: string;\n  escapedTableName: string;\n  insertQueryPrefix: string;\n  columns: string[];\n}> => {\n  const { db, selectedFile, destination, reCreateTable, insertAs } = args;\n\n  if (!selectedFile?.preview) {\n    throw \"selectedFile missing\";\n  }\n\n  if (!db.sql) {\n    throw \"Cannot create new table\";\n  }\n\n  /** There is a maximum length on table name in postgresql which is 63 characters */\n  const tableName = (destination.newTableName || selectedFile.file.name).slice(\n    0,\n    63,\n  );\n  const escapedTableName = await db.sql(`SELECT quote_ident($1)`, [tableName], {\n    returnType: \"value\",\n  });\n  if (reCreateTable && db[tableName]) {\n    await db.sql(\"DROP TABLE IF EXISTS \" + escapedTableName);\n  }\n\n  let create = \"CREATE TABLE \" + escapedTableName + \" ( \\n\";\n\n  const cols = selectedFile.preview.cols;\n\n  if (\n    cols.some((c) => c.dataType === \"geometry\" || c.dataType === \"geography\")\n  ) {\n    create = `CREATE EXTENSION IF NOT EXISTS postgis;\\n${create}`;\n  }\n\n  const columns: typeof cols =\n    insertAs === \"Single text value\" ?\n      [\n        {\n          dataType: \"TEXT\",\n          key: \"all_data\",\n          escapedName: asName(\"all_data\"),\n          label: \"All data\",\n          sortable: false,\n        },\n      ]\n    : cols;\n  const colTypes = columns\n    .map((col, i) => {\n      return col.escapedName + \" \" + col.dataType;\n    })\n    .join(\",\\n\");\n\n  const _q = create + colTypes + \" \\n);\",\n    /** No insert into BIGSERIAL */\n    escapedColnames = cols\n      .filter((c) => c.dataType !== \"BIGSERIAL\")\n      .map((col, i) => col.escapedName),\n    insertQueryPrefix = await db.sql(\n      \"INSERT INTO \" + escapedTableName + \" (${colTypes:raw}) VALUES \",\n      { colTypes: escapedColnames.join(\", \") },\n      { returnType: \"statement\" },\n    );\n\n  await db.sql(_q);\n\n  return {\n    tableName,\n    escapedTableName,\n    insertQueryPrefix,\n    columns: columns.map((c) => c.key.toString()),\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/FileImporter/parseCSVFile.ts",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport { asName } from \"prostgles-types\";\nimport { getStringFormat } from \"../../utils/utils\";\nimport type { FileImporterState } from \"./FileImporter\";\nimport { getCSVFirstChunk, getPapa } from \"./FileImporter\";\n\nexport type Col = { key: string; dataType: string; escapedName: string };\n\nexport async function parseCSVFile(\n  file: File,\n  config: FileImporterState[\"config\"],\n): Promise<{\n  rows: AnyObject[];\n  cols: Col[];\n  header: boolean;\n}> {\n  const hasHeaders = (dataPreview: string[][]) => {\n    /** check if headers are in first row */\n    const [r0, r1] = dataPreview;\n\n    if (!dataPreview.length || !r0) {\n      throw \"File is empty or could not be parsed\";\n    }\n    const header = r0.some((firstRowValue, i) => {\n      if (!r1) return false;\n      const secondRowValue = r1[i];\n      const f0 = getStringFormat(firstRowValue)\n        .map((f) => f.type)\n        .join();\n      const f1 = getStringFormat(secondRowValue)\n        .map((f) => f.type)\n        .join();\n      return firstRowValue && f0 !== f1;\n    });\n\n    return header;\n  };\n  const papa = await getPapa();\n  const { data } = await getCSVFirstChunk({\n    file,\n    preview: 4,\n    skipEmptyLines: true,\n    papa,\n  });\n\n  const header = await hasHeaders(data);\n\n  const results = await getCSVFirstChunk({\n    file,\n    preview: 50,\n    header,\n    papa,\n  });\n\n  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n  let rows = results.data || [];\n  const [firstRow] = rows;\n\n  /** Filter out rows with a column number less than the first row */\n  if (firstRow) {\n    const badRows: { row_number: number; row_value: any }[] = [];\n    const colLen = firstRow.length;\n    rows = rows.filter((r, i) => {\n      if (r.length !== colLen) {\n        badRows.push({ row_number: i, row_value: r });\n        return false;\n      }\n      return true;\n    });\n\n    if (badRows.length) {\n      console.error(\n        \"Some rows have been ommited due to the number of columns not coinciding with first row:\",\n        badRows,\n      );\n    }\n  }\n\n  let cols: Col[] = [];\n  let rowArr: AnyObject[] = [];\n\n  if (!header) {\n    const [firstRow] = rows;\n    if (!firstRow) {\n      throw \"Could not import: No data to import\";\n    } else {\n      cols = firstRow.map((_d, i) => ({\n        key: `c${i + 1}`,\n        dataType: \"text\",\n        escapedName: asName(`c${i + 1}`),\n      }));\n      rowArr = rows.map((r) =>\n        cols.reduce(\n          (a, c, i) => ({\n            ...a,\n            [c.key]: r[i],\n          }),\n          {},\n        ),\n      );\n    }\n  } else {\n    cols = (results.meta.fields ?? [])\n      .map((v) => v.trim())\n      .map((key) => ({\n        key,\n        dataType: \"text\",\n        escapedName: asName(key),\n      }));\n    rowArr = rows;\n  }\n\n  let maxCharsPerRow = 0;\n  rows.map((r) => {\n    maxCharsPerRow = Math.max(maxCharsPerRow, JSON.stringify(r).length);\n  });\n\n  return {\n    rows: rowArr,\n    cols,\n    header,\n  };\n}\n"
  },
  {
    "path": "client/src/dashboard/FileImporter/setFile.ts",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport { asName, getKeys, isDefined, isObject } from \"prostgles-types\";\nimport { getFileText } from \"../W_SQL/W_SQLMenu\";\nimport type { FileImporter } from \"./FileImporter\";\nimport { getRowsPerBatch } from \"./FileImporter\";\nimport type { Col } from \"./parseCSVFile\";\nimport { parseCSVFile } from \"./parseCSVFile\";\nimport type { GeoJSONFeature } from \"../Map/DeckGLMap\";\n\nexport const setFile = function (this: FileImporter, file: File) {\n  const lowerName = file.name.toLowerCase();\n  const type =\n    lowerName.endsWith(\".geojson\") ? \"geojson\"\n    : lowerName.endsWith(\".json\") ? \"json\"\n    : \"csv\";\n  const { headerType, customHeaders, streamColumnDataType, streamBatchMb } =\n    this.state;\n  this.setState({ loadingFileName: file.name, importing: undefined });\n  const maxSize = 1e8 / 2; // 50Mb\n\n  if (type.endsWith(\"json\")) {\n    parseJSONFile(file)\n      .then(({ rows, cols, srid, type, rowsPerBatch }) => {\n        this.setState({\n          loadingFileName: undefined,\n          selectedFile: {\n            file,\n            type,\n            srid,\n            header: false,\n            preview: {\n              allRows: rows,\n              rowsPerBatch,\n              cols: cols.map((col) => ({\n                ...col,\n                label: col.key,\n                subLabel: col.dataType,\n                sortable: false,\n                //dataType: \"text\",// key === \"geometry\"? \"geometry\" : key === \"feature\"? \"jsonb\" : \"text\"\n              })),\n              rows: rows.slice(0, 50).map((_r) => {\n                const r = { ..._r };\n                // r.geometry = JSON.stringify(r.geometry).slice(0, 250) + \"...\";\n                return r;\n              }),\n            },\n          },\n        });\n      })\n      .catch((err) => {\n        this.setState({ error: err });\n      });\n\n    /** csv */\n  } else {\n    const { config } = this.state;\n    try {\n      const hasCustHeaders = !(headerType === \"First row\") && customHeaders;\n      parseCSVFile(file, { ...config, header: !hasCustHeaders })\n        .then(({ rows, cols: _cols, header }) => {\n          const cols =\n            !hasCustHeaders ? _cols : (\n              customHeaders.split(\",\").map((k) => {\n                const key = k.trim();\n                return { key, dataType: \"text\", escapedName: asName(key) };\n              })\n            );\n          this.setState({\n            loadingFileName: undefined,\n            selectedFile: {\n              file,\n              type: \"csv\",\n              header,\n              preview: {\n                allRows: rows,\n                cols: cols.map((col) => ({\n                  ...col,\n                  label: col.key,\n                  subLabel: col.dataType,\n                  sortable: false,\n                  //dataType: \"text\",// key === \"geometry\"? \"geometry\" : key === \"feature\"? \"jsonb\" : \"text\"\n                })),\n                rows: rows.slice(0, 50).map((_r) => {\n                  const r = { ..._r };\n                  // r.geometry = JSON.stringify(r.geometry).slice(0, 250) + \"...\";\n                  return r;\n                }),\n              },\n            },\n          });\n        })\n        .catch((err) => {\n          this.setState({ error: err });\n        });\n    } catch (e) {\n      console.error(e);\n    }\n  }\n};\n\nasync function parseJSONFile(file: File): Promise<{\n  type: \"json\" | \"geojson\";\n  rows: AnyObject[];\n  cols: Col[];\n  srid?: string;\n\n  /**\n   * Must send less data than maxHttpBufferSize: (1e8 which is 100Mb)\n   * 1 char = 4 bytes\n   */\n  rowsPerBatch: number;\n}> {\n  let type: \"json\" | \"geojson\" = \"json\";\n  const txt = await getFileText(file);\n  let data = JSON.parse(txt);\n  if (isObject(data) && Object.values(data).length === 1) {\n    const firstValue = Object.values(data)[0];\n    if (Array.isArray(firstValue)) {\n      data = firstValue;\n    }\n  }\n\n  const pg_types = [\"text\", \"geometry\", \"jsonb\", \"numeric\"] as const;\n\n  const actualBytesPerChar = txt.length / file.size;\n  let rows: AnyObject[] = [];\n  let srid;\n  let _cols: Record<string, (typeof pg_types)[number]> = {};\n  let maxCharsPerRow = 0;\n  const setCol = (row) => {\n    const getType = (v) =>\n      typeof v === \"number\" ? \"numeric\"\n      : isObject(v) ? \"jsonb\"\n      : \"text\";\n    Object.keys(row).map((k) => {\n      const currentValueType = getType(row[k]);\n      const columnType = _cols[k];\n      if (\n        !columnType ||\n        pg_types.indexOf(currentValueType) < pg_types.indexOf(columnType)\n      ) {\n        _cols[k] = currentValueType;\n      }\n    });\n  };\n  const getGeoJSONFeatures = (): undefined | GeoJSONFeature[] => {\n    if (\n      data.type === \"FeatureCollection\" &&\n      data.features &&\n      Array.isArray(data.features)\n    ) {\n      return data.features;\n    } else if (\n      Array.isArray(data) &&\n      data.length &&\n      data[0].type === \"Feature\" &&\n      typeof data[0].geometry?.type === \"string\" &&\n      Array.isArray(data[0].geometry.coordinates)\n    ) {\n      return data;\n    }\n  };\n\n  const geoJSONFeatures = getGeoJSONFeatures();\n  if (geoJSONFeatures) {\n    type = \"geojson\";\n    _cols = { geometry: \"geometry\" };\n    let hasID = false;\n    geoJSONFeatures.map((f: GeoJSON.Feature, i) => {\n      if ((f.type as any) !== \"Feature\") {\n        console.warn(\n          `Could not import feature number ${i}. Feature.type is not \"Feature\"`,\n          f,\n        );\n      } else {\n        let row = f.properties;\n        hasID = hasID || f.id !== undefined;\n        if (hasID) {\n          row = { ...row, dI: f.id };\n        }\n        setCol(row);\n      }\n    });\n    const colKeys = getKeys(_cols);\n    rows = geoJSONFeatures\n      .map((f: GeoJSON.Feature, i) => {\n        if ((f.type as any) !== \"Feature\") {\n          console.warn(\n            `Could not import feature number ${i}. Feature.type is not \"Feature\"`,\n            f,\n          );\n        } else {\n          srid =\n            srid ||\n            (f as any).geometry?.crs?.type?.name?.properties?.name ||\n            \"EPSG:4326\";\n\n          const row: Record<string, any> = {};\n          colKeys.map((k) => {\n            if (k === \"id\" && hasID) {\n              row[k] = f.id ?? null;\n            } else if (k === \"geometry\") {\n              // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n              row[k] = f.geometry ?? null;\n            } else if (f.properties) {\n              row[k] = f.properties[k] ?? null;\n            }\n          });\n\n          maxCharsPerRow = Math.max(maxCharsPerRow, JSON.stringify(row).length);\n          return row;\n        }\n      })\n      .filter(isDefined);\n  } else if (Array.isArray(data)) {\n    data.forEach((row) => {\n      setCol(row);\n    });\n    const colKeys = Object.keys(_cols);\n    rows = data.map((row) => {\n      colKeys.map((k) => {\n        row[k] = row[k] ?? null;\n      });\n\n      maxCharsPerRow = Math.max(maxCharsPerRow, JSON.stringify(row).length);\n      return row;\n    });\n  }\n\n  return {\n    type,\n    rows,\n    srid,\n    cols: Object.entries(_cols).map(([key, dataType]) => ({\n      key,\n      escapedName: asName(key),\n      dataType,\n    })),\n    rowsPerBatch: getRowsPerBatch(maxCharsPerRow, actualBytesPerChar),\n  };\n}\n"
  },
  {
    "path": "client/src/dashboard/FileTableControls/CreateFileColumn.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport { asName } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport type { Prgl, PrglCore } from \"../../App\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Popup from \"@components/Popup/Popup\";\nimport { Select } from \"@components/Select/Select\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { FileColumnConfigEditor } from \"./FileColumnConfigEditor\";\nimport { useFileTableConfigControls } from \"./useFileTableConfigControls\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\n\ntype CreateReferencedColumnProps = Omit<PrglCore, \"methods\"> & {\n  fileTable: string | undefined;\n  tableName?: string;\n  onClose?: VoidFunction;\n};\n\nexport const CreateFileColumn = ({\n  db,\n  tables,\n  fileTable,\n  tableName: _tableName,\n  onClose,\n}: CreateReferencedColumnProps) => {\n  const prgl = usePrgl();\n  const [tableName, setTableName] = useState(_tableName);\n  if (!fileTable) {\n    return (\n      <Popup\n        title=\"Create file column\"\n        clickCatchStyle={{ opacity: 0.5 }}\n        onClickClose={true}\n        positioning=\"top-center\"\n        data-command=\"CreateFileColumn\"\n        onClose={onClose}\n      >\n        <InfoRow variant=\"naked\">\n          Must enable{\" \"}\n          <Link\n            to={`/connection-config/${prgl.connectionId}?section=file_storage`}\n          >\n            file storage\n          </Link>{\" \"}\n          first\n        </InfoRow>\n      </Popup>\n    );\n  }\n  if (tableName) {\n    return (\n      <CreateFileColumnOptions\n        db={db}\n        tables={tables}\n        fileTable={fileTable}\n        tableName={tableName}\n        prgl={prgl}\n        onDone={() => {\n          onClose?.();\n          if (!_tableName) {\n            setTableName(undefined);\n          }\n        }}\n      />\n    );\n  }\n  return (\n    <Select\n      value={tableName}\n      className=\"mt-1\"\n      btnProps={{\n        children: \"Add new link\",\n        color: \"action\",\n        iconPath: mdiPlus,\n      }}\n      fullOptions={tables\n        .filter((t) => !t.info.isFileTable)\n        .map((t) => ({\n          key: t.name,\n          disabledInfo:\n            t.columns.some((c) => c.is_pkey) ?\n              undefined\n            : \"Needs a primary key\",\n        }))}\n      onChange={(table) => setTableName(table)}\n    />\n  );\n};\n\nconst CreateFileColumnOptions = ({\n  db,\n  fileTable,\n  tableName,\n  onDone,\n  prgl,\n}: Omit<CreateReferencedColumnProps, \"fileTable\"> & {\n  fileTable: string;\n  tableName: string;\n  onDone: VoidFunction;\n  prgl: Prgl;\n}) => {\n  const [colName, setColName] = useState<string>();\n  const [optional, setOptional] = useState(true);\n  const {\n    refsConfig,\n    setRefsConfig,\n    updateRefsConfig,\n    canUpdateRefColumns: canUpdate,\n  } = useFileTableConfigControls(prgl);\n  const query =\n    !tableName ? \"\" : (\n      [\n        `ALTER TABLE ${asName(tableName || \"empty\")} `,\n        `ADD COLUMN ${asName(colName || \"empty\")} UUID ${!optional ? \" NOT NULL \" : \"\"} `,\n        `REFERENCES ${asName(fileTable)} (id)`,\n      ].join(\"\\n\")\n    );\n  const [error, setError] = useState<any>();\n\n  return (\n    <Popup\n      title=\"Add new file link\"\n      positioning=\"top-center\"\n      clickCatchStyle={{ opacity: 0.5 }}\n      data-command=\"CreateFileColumn\"\n      onClose={onDone}\n      contentClassName=\"gap-1 p-1\"\n      autoFocusFirst={{ selector: \"input\" }}\n      content={\n        <>\n          <FormFieldDebounced\n            label=\"New column name\"\n            value={colName}\n            type=\"text\"\n            onChange={(col) => setColName(col)}\n          />\n          <SwitchToggle\n            label=\"Optional\"\n            onChange={(e) => setOptional(e)}\n            checked={!!optional}\n          />\n          <InfoRow iconPath=\"\">\n            This column will reference the <strong>{fileTable}</strong> table\n          </InfoRow>\n          {colName && (\n            <>\n              <FileColumnConfigEditor\n                columnName={colName}\n                tableName={tableName}\n                refsConfig={refsConfig}\n                onChange={setRefsConfig}\n                onSetError={setError}\n              />\n            </>\n          )}\n        </>\n      }\n      footerButtons={[\n        { label: \"Cancel\", onClickClose: true },\n        {\n          label: \"Create\",\n          variant: \"filled\",\n          color: \"action\",\n          \"data-command\": \"CreateFileColumn.confirm\",\n          className: \"ml-auto\",\n          disabledInfo:\n            error ? \"Must fix error\"\n            : !colName ? \"New column name missing\"\n            : undefined,\n          onClickMessage: async (_, setM) => {\n            try {\n              if (!colName) return;\n              setM({ loading: 1 });\n              if (!db.sql)\n                throw \"Not enough privileges. Must be allowed to run SQL queries\";\n              await db.sql(query);\n              if (canUpdate) {\n                updateRefsConfig();\n              }\n              // const newColConfig = getMergedRefFileColConfig({\n              //   colConfig: { acceptedContent: \"*\" },\n              //   columnName: colName,\n              //   refsConfig,\n              //   tableName\n              // })\n              // await updateRefsConfig(newColConfig);\n              setM({ ok: \"Created!\" }, () => onDone());\n            } catch (err) {\n              setM({ err });\n            }\n          },\n        },\n      ]}\n    ></Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/FileTableControls/FileColumnConfigControls.tsx",
    "content": "import { mdiFileCogOutline, mdiLink } from \"@mdi/js\";\nimport { type FileColumnConfig, isEmpty } from \"prostgles-types\";\nimport { isDefined } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { type CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport { FileColumnConfigEditor } from \"./FileColumnConfigEditor\";\nimport { quickClone } from \"../../utils/utils\";\n\nexport type FileTableConfigReferences = Record<\n  string,\n  { referenceColumns: Record<string, FileColumnConfig> }\n>;\n\ntype FileColumnConfigControlsProps = {\n  tables: CommonWindowProps[\"tables\"];\n  refsConfig?: FileTableConfigReferences | undefined;\n  onChange: (newConfig: FileTableConfigReferences) => void;\n};\nexport const FileColumnConfigControls = (\n  props: FileColumnConfigControlsProps,\n) => {\n  const { refsConfig = {}, tables, onChange } = props;\n  const linkedTables = tables\n    .flatMap((t) => {\n      // Exclude mapping/lookup tables\n      const isNotLookupTable =\n        t.columns.length > 2 || t.columns.some((c) => !c.references?.length);\n\n      const fileColumns = t.columns.filter((c) => c.file);\n      const droppedFileColumns = Object.keys(\n        refsConfig[t.name]?.referenceColumns ?? {},\n      ).filter(\n        (colName) =>\n          !t.columns.find((c) => c.name === colName) &&\n          !fileColumns.find((c) => c.name === colName),\n      );\n      if (\n        (fileColumns.length || droppedFileColumns.length) &&\n        isNotLookupTable\n      ) {\n        return [\n          ...fileColumns.map((fileColumn) => {\n            return {\n              ...t,\n              key: `${t.name}.${fileColumn.name}`,\n              columnName: fileColumn.name,\n              fileColumn,\n            };\n          }),\n          ...droppedFileColumns.map((columnName) => {\n            return {\n              ...t,\n              key: `${t.name}.${columnName}`,\n              columnName,\n              fileColumn: undefined,\n              wasDropped: true,\n            };\n          }),\n        ];\n      }\n    })\n    .filter(isDefined);\n  const [editColumn, setEditColumn] = useState<(typeof linkedTables)[number]>();\n  const [error, setError] = useState<any>();\n\n  if (!linkedTables.length) return null;\n\n  return (\n    <FlexCol className=\"FileColumnConfigControls w-fit min-h-0 mt-1\">\n      {editColumn && (\n        <Popup\n          title={`Configure ${editColumn.name}.${editColumn.columnName}`}\n          onClose={() => setEditColumn(undefined)}\n          positioning=\"center\"\n          persistInitialSize={true}\n          clickCatchStyle={{ opacity: 1 }}\n          contentClassName=\"p-1 \"\n          footerButtons={[\n            {\n              label: \"Done\",\n              color: \"action\",\n              variant: \"filled\",\n              onClickClose: true,\n            },\n          ]}\n        >\n          <FileColumnConfigEditor\n            refsConfig={refsConfig}\n            tableName={editColumn.name}\n            columnName={editColumn.columnName}\n            onChange={onChange}\n            onSetError={setError}\n          />\n        </Popup>\n      )}\n      <SearchList\n        className=\"b b-color\"\n        items={linkedTables.map((linkedTable, i) => {\n          return {\n            key: linkedTable.key,\n            rowStyle:\n              !linkedTable.fileColumn ? { border: \"1px solid var(--danger)\" }\n              : !i ? undefined\n              : { borderTop: `1px solid var(--b-color)` },\n            subLabel:\n              linkedTable.fileColumn ?\n                getFileColumnConfigDescription(linkedTable.fileColumn.file!)\n                  .full\n              : \"Missing from table\",\n            contentLeft: <Icon path={mdiLink} className=\"mr-p5\" />,\n            contentRight:\n              linkedTable.fileColumn ?\n                <Btn\n                  title=\"Configure allowed file types and limits\"\n                  color=\"action\"\n                  iconPath={mdiFileCogOutline}\n                  onClick={() => {\n                    setEditColumn(linkedTable);\n                  }}\n                >\n                  Configure\n                </Btn>\n              : <Btn\n                  color=\"danger\"\n                  onClickPromise={() => {\n                    const newConfig = quickClone({ ...refsConfig });\n                    delete newConfig[linkedTable.name]!.referenceColumns[\n                      linkedTable.columnName\n                    ];\n                    if (\n                      isEmpty(newConfig[linkedTable.name]?.referenceColumns)\n                    ) {\n                      delete newConfig[linkedTable.name];\n                    }\n                    onChange(newConfig);\n                  }}\n                >\n                  Remove config\n                </Btn>,\n          };\n        })}\n      />\n    </FlexCol>\n  );\n};\n\nconst getFileColumnConfigDescription = (fc: FileColumnConfig) => {\n  const getStrValue = (spec: string[] | Record<string, any>) =>\n    Array.isArray(spec) ?\n      spec.join(\", \")\n    : Object.entries(spec)\n        .filter(([t, v]) => v)\n        .map((e) => e[0])\n        .join(\", \");\n\n  let contentType = \"any file type\";\n  if (\n    \"acceptedContent\" in fc &&\n    fc.acceptedContent &&\n    fc.acceptedContent !== \"*\"\n  ) {\n    contentType = getStrValue(fc.acceptedContent);\n  } else if (\n    \"acceptedFileTypes\" in fc &&\n    fc.acceptedFileTypes &&\n    fc.acceptedFileTypes !== \"*\"\n  ) {\n    contentType = getStrValue(fc.acceptedFileTypes);\n  } else if (\n    \"acceptedContentType\" in fc &&\n    fc.acceptedContentType &&\n    fc.acceptedContentType !== \"*\"\n  ) {\n    contentType = getStrValue(fc.acceptedContentType);\n  }\n\n  const sizeLimit = `up to ${fc.maxFileSizeMB ?? 100}MB`;\n  return {\n    sizeLimit,\n    contentType,\n    full: `${contentType}, ${sizeLimit}`,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/FileTableControls/FileColumnConfigEditor.tsx",
    "content": "import type { FieldFilter, FileColumnConfig } from \"prostgles-types\";\nimport {\n  CONTENT_TYPE_TO_EXT,\n  getKeys,\n  isDefined,\n  isObject,\n} from \"prostgles-types\";\nimport React, { useEffect } from \"react\";\nimport ButtonGroup from \"@components/ButtonGroup\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport type { FileTableConfigReferences } from \"./FileColumnConfigControls\";\n\nconst CONTENT_MODES = [\n  { key: \"Data types\", subLabel: \"image, video, audio ...\" },\n  { key: \"MIME types\", subLabel: \"image/jpeg, text/plain ...\" },\n  { key: \"Extensions\", subLabel: \".pdf, .svg, .mp4 ...\" },\n] as const;\n\ntype FileColumnConfigProps = {\n  tableName: string;\n  columnName: string;\n  refsConfig: FileTableConfigReferences;\n  onChange: (newConfig: FileTableConfigReferences) => void;\n  onSetError: (error?: any) => void;\n};\nexport const FileColumnConfigEditor = ({\n  tableName,\n  columnName,\n  refsConfig,\n  onChange,\n  onSetError,\n}: FileColumnConfigProps) => {\n  let contentMode: (typeof CONTENT_MODES)[number][\"key\"] = CONTENT_MODES[0].key;\n\n  const isChecked = (col: string, fieldFilter: FieldFilter): boolean => {\n    return (\n      fieldFilter === \"*\" ||\n      (Array.isArray(fieldFilter) && fieldFilter.includes(col)) ||\n      (isObject(fieldFilter) && fieldFilter[col])\n    );\n  };\n  const colConfig: FileColumnConfig = refsConfig[tableName]?.referenceColumns[\n    columnName\n  ] ?? { acceptedContent: \"*\" };\n\n  let fullOptions: readonly { key: string; checked: boolean }[] = [];\n\n  const updateMergeColConfig = (colConfig: FileColumnConfig) => {\n    const newConfig = getMergedRefFileColConfig({\n      colConfig,\n      columnName,\n      tableName,\n      refsConfig,\n    });\n    onChange(newConfig);\n  };\n\n  let onChangeOpts = (opts: string[]) => {};\n  if (\n    getKeys(colConfig).every((k) => (k as any) === \"maxFileSizeMB\") ||\n    (\"acceptedContent\" in colConfig && colConfig.acceptedContent)\n  ) {\n    contentMode = \"Data types\";\n    const CONTENT_OPTIONS = [\n      \"audio\",\n      \"video\",\n      \"image\",\n      \"text\",\n      \"application\",\n    ] as const;\n    fullOptions = CONTENT_OPTIONS.map((key) => ({\n      key,\n      checked: isChecked(\n        key,\n        \"acceptedContent\" in colConfig ? colConfig.acceptedContent : \"*\",\n      ),\n    }));\n    onChangeOpts = (opts) => {\n      updateMergeColConfig({\n        acceptedContent: opts.length === fullOptions.length ? \"*\" : opts,\n        //@ts-ignore\n        acceptedFileTypes: undefined,\n        acceptedContentType: undefined,\n      });\n    };\n  } else if (\n    \"acceptedContentType\" in colConfig &&\n    colConfig.acceptedContentType\n  ) {\n    contentMode = \"MIME types\";\n    fullOptions = getKeys(CONTENT_TYPE_TO_EXT).map((key) => ({\n      key,\n      checked: isChecked(key, colConfig.acceptedContentType),\n    }));\n    onChangeOpts = (opts) => {\n      const acceptedContentType: Extract<\n        FileColumnConfig,\n        { acceptedContentType: any }\n      > = {\n        acceptedContentType:\n          opts.length === fullOptions.length ? \"*\" : (opts as any),\n      };\n      updateMergeColConfig({\n        acceptedContentType,\n        acceptedContent: undefined,\n        //@ts-ignore\n        acceptedFileTypes: undefined,\n      });\n    };\n  } else if (\"acceptedFileTypes\" in colConfig && colConfig.acceptedFileTypes) {\n    contentMode = \"Extensions\";\n    fullOptions = Object.values(CONTENT_TYPE_TO_EXT)\n      .flat()\n      .flatMap((key) => ({\n        key,\n        subLabel: getKeys(CONTENT_TYPE_TO_EXT).find((cType) =>\n          CONTENT_TYPE_TO_EXT[cType].includes(key as never),\n        ),\n        checked: isChecked(key, colConfig.acceptedFileTypes),\n      }));\n    onChangeOpts = (opts) => {\n      updateMergeColConfig({\n        //@ts-ignore\n        acceptedFileTypes: opts.length === fullOptions.length ? \"*\" : opts,\n        acceptedContent: undefined,\n        acceptedContentType: undefined,\n      });\n    };\n  }\n\n  const selectedOpts = fullOptions.filter((o) => o.checked);\n\n  const error =\n    fullOptions.length && fullOptions.every((v) => !v.checked) ?\n      \"Must select at least one option\"\n    : undefined;\n  useEffect(() => {\n    onSetError(error);\n  }, [error, onSetError]);\n\n  return (\n    <FlexCol\n      className=\"f-1 min-h-0 gap-02\"\n      data-command=\"FileColumnConfigEditor\"\n    >\n      <FormField\n        type=\"number\"\n        label=\"Maximum file size in megabytes\"\n        data-command=\"FileColumnConfigEditor.maxFileSizeMB\"\n        value={colConfig.maxFileSizeMB ?? 1}\n        onChange={(e) => {\n          updateMergeColConfig({ ...colConfig, maxFileSizeMB: +e });\n        }}\n      />\n      <ButtonGroup\n        label={{\n          label: `Allowed types (${selectedOpts.length}/${fullOptions.length})`,\n          variant: \"normal\",\n        }}\n        className=\"mt-1 mb-1\"\n        options={CONTENT_MODES.map((cm) => cm.key)}\n        value={contentMode}\n        data-command=\"FileColumnConfigEditor.contentMode\"\n        onChange={(newMode) => {\n          updateMergeColConfig(\n            newMode === \"Data types\" ?\n              {\n                acceptedContent: \"*\",\n                acceptedFileTypes: undefined,\n                acceptedContentType: undefined,\n              }\n            : newMode === \"MIME types\" ?\n              {\n                acceptedContentType: \"*\",\n                acceptedFileTypes: undefined,\n                acceptedContent: undefined,\n              }\n            : {\n                acceptedFileTypes: \"*\",\n                acceptedContent: undefined,\n                acceptedContentType: undefined,\n              },\n          );\n        }}\n      />\n      <SearchList\n        className=\"w-full f-1 min-h-0\"\n        style={{ maxHeight: \"30vh\", minHeight: \"300px\" }}\n        items={fullOptions.map((o) => ({\n          ...o,\n          onPress: () => {\n            const newItems = fullOptions\n              .filter((d) => (d.key === o.key ? !d.checked : d.checked))\n              .map((d) => d.key)\n              .filter(isDefined);\n            onChangeOpts(newItems);\n          },\n        }))}\n        onMultiToggle={(items) => {\n          const newItems = items\n            .filter((d) => d.checked)\n            .map((d) => d.key)\n            .filter(isDefined);\n          onChangeOpts(newItems as string[]);\n        }}\n      />\n      <InfoRow\n        variant=\"filled\"\n        color=\"danger\"\n        /** To prevent layout shift we must reserve space within the popup */\n        style={{ opacity: error ? 1 : 0 }}\n      >\n        {error}\n      </InfoRow>\n    </FlexCol>\n  );\n};\n\nexport const getMergedRefFileColConfig = ({\n  colConfig,\n  columnName,\n  refsConfig,\n  tableName,\n}: {\n  refsConfig: FileTableConfigReferences;\n  tableName: string;\n  columnName: string;\n  colConfig: FileColumnConfig;\n}) => {\n  const _refTableConfig = refsConfig[tableName];\n  const refTableConfig: {\n    referenceColumns: Record<string, FileColumnConfig>;\n  } = isObject(_refTableConfig) ? _refTableConfig : { referenceColumns: {} };\n\n  return {\n    ...refsConfig,\n    [tableName]: {\n      ...refTableConfig,\n      referenceColumns: {\n        ...refTableConfig.referenceColumns,\n        [columnName]: {\n          ...(refTableConfig.referenceColumns[columnName] ?? {}),\n          ...colConfig,\n        },\n      },\n    },\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/FileTableControls/FileStorageControls.tsx",
    "content": "import { mdiContentSaveCogOutline } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useEffect, useState } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Select } from \"@components/Select/Select\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { pickKeys } from \"prostgles-types\";\nimport { CloudStorageCredentialSelector } from \"../BackupAndRestore/CloudStorageCredentialSelector\";\nimport { FileStorageDelete } from \"./FileStorageDelete\";\nimport type { FullExtraProps } from \"../../pages/ProjectConnection/ProjectConnection\";\n\nconst STORAGE_TYPES = [\n  {\n    key: \"local\",\n    label: \"Local\",\n    subLabel: \"Files stored within the docker volume\",\n  },\n  { key: \"S3\", label: \"Amazon S3\", subLabel: \"Files stored in the cloud\" },\n] as const;\n\nexport type FileStorageControlsProps = Pick<\n  FullExtraProps,\n  \"dbsMethods\" | \"dbTables\" | \"dbs\" | \"dbsTables\" | \"dbProject\"\n> & {\n  connection: DBSSchema[\"connections\"];\n  database_config: DBSSchema[\"database_configs\"];\n  canCreateTables?: boolean;\n};\n\nexport const FileStorageControls = (props: FileStorageControlsProps) => {\n  const {\n    canCreateTables,\n    connection,\n    dbTables,\n    dbs,\n    dbsTables,\n    dbsMethods,\n    dbProject,\n    database_config,\n  } = props;\n  const [showDelete, setShowDelete] = useState(false);\n\n  const fileSizes = usePromise(\n    async () => ({\n      projectFolderSize:\n        (\n          (await database_config.file_table_config?.storageType.type) ===\n          \"local\"\n        ) ?\n          (dbsMethods.getFileFolderSizeInBytes?.(connection.id) as any)\n        : 0,\n      totalFileFolderSize:\n        (\n          (await database_config.file_table_config?.storageType.type) ===\n          \"local\"\n        ) ?\n          (dbsMethods.getFileFolderSizeInBytes?.() as any)\n        : 0,\n    }),\n    [database_config, connection, dbsMethods],\n  );\n\n  const { projectFolderSize = 0, totalFileFolderSize = 0 } = fileSizes ?? {};\n\n  const fileConfig = database_config.file_table_config;\n  const [fileTable, setFileTable] = useState(fileConfig?.fileTable);\n\n  useEffect(() => {\n    setFileTable(fileConfig?.fileTable);\n  }, [fileConfig?.fileTable]);\n\n  const [storageType, setStorageType] = useState(fileConfig?.storageType.type);\n  const [credentialId, setCredentialId] = useState(\n    fileConfig?.storageType && \"credential_id\" in fileConfig.storageType ?\n      fileConfig.storageType.credential_id\n    : undefined,\n  );\n\n  const fileTableNameClash = dbTables.some(\n    (t) =>\n      t.name === fileTable &&\n      !t.columns.some((c) => c.name === \"signed_url_expires\"),\n  );\n\n  const canEnable =\n    !fileConfig?.fileTable &&\n    fileTable &&\n    storageType &&\n    (storageType === \"local\" || !!credentialId);\n\n  const error =\n    canCreateTables ? undefined : (\n      `Cannot use this feature: Your account needs CREATE TABLE privilege`\n    );\n  const [enablingError, setEnablingError] = useState<any>();\n\n  return (\n    <>\n      {!!showDelete && (\n        <FileStorageDelete\n          {...pickKeys(props, [\"connection\", \"dbsMethods\", \"database_config\"])}\n          db={dbProject}\n          onClose={() => setShowDelete(false)}\n        />\n      )}\n\n      <div className=\" \">\n        <p className=\"mt-0 pt-0\">\n          Files can be uploaded and viewed by configuring a local or remote\n          (Amazon S3) storage and designating a table within this database to\n          store file urls and metadata\n        </p>\n        <p className=\"mt-3\">Access to the files is controlled through: </p>\n        <ul className=\"no-ddecor\">\n          <li className=\"py-p25\">\n            <strong>file table</strong> - users that are allowed to\n            view/insert/delete the data within the file table can interact with\n            the files\n          </li>\n          <li className=\"py-p25\">\n            <strong>tables that reference the file table</strong> - users that\n            are allowed to view/insert/update the reference column are also\n            allowed to view/insert/update the related records from the file\n            table (and associated files)\n          </li>\n        </ul>\n        {error && <InfoRow color=\"danger\">{error}</InfoRow>}\n      </div>\n\n      <SwitchToggle\n        label={\n          !fileTable ? \"Enable\"\n          : !fileConfig?.fileTable ?\n            \"Enable\"\n          : \"Enabled\"\n        }\n        checked={!!fileTable}\n        className=\"\"\n        data-command=\"config.files.toggle\"\n        disabledInfo={error}\n        onChange={(val) => {\n          if (val) {\n            setFileTable(\"files\");\n            setStorageType(\"local\");\n          } else {\n            if (fileConfig?.fileTable) {\n              setShowDelete(true);\n            } else {\n              setFileTable(undefined);\n              setStorageType(undefined);\n            }\n          }\n        }}\n      />\n      <FlexRowWrap className=\" gap-1p5 \">\n        {!!fileTable && (\n          <>\n            <FormField\n              type=\"text\"\n              label={{\n                label: \"File table name\",\n                info:\n                  fileConfig?.fileTable ?\n                    \"Table that contains file metadata\"\n                  : \"Used for file metadata. Table created in the current database\",\n              }}\n              readOnly={!!fileConfig?.fileTable}\n              title={fileConfig?.fileTable ? \"Cannot be updated\" : \"\"}\n              value={fileTable}\n              onChange={setFileTable}\n              error={\n                fileTableNameClash ?\n                  \"There is a table with this name in the database. Choose another name\"\n                : undefined\n              }\n            />\n          </>\n        )}\n\n        {!!fileTable && (\n          <>\n            {fileConfig?.fileTable ?\n              <FormField\n                readOnly={true}\n                label=\"Storage type\"\n                value={storageType}\n              />\n            : <Select\n                fullOptions={STORAGE_TYPES}\n                label=\"Storage type\"\n                className=\"\"\n                value={storageType}\n                onChange={setStorageType}\n              />\n            }\n          </>\n        )}\n\n        {storageType === \"S3\" ?\n          <div className=\"flex-row-wrap gap-2 h-fit\">\n            <CloudStorageCredentialSelector\n              dbs={dbs}\n              dbsMethods={dbsMethods}\n              dbsTables={dbsTables}\n              selectedId={credentialId}\n              pickFirst={true}\n              onChange={(val) => {\n                setStorageType(\"S3\");\n                setCredentialId(val);\n              }}\n            />\n          </div>\n        : storageType === \"local\" ?\n          <>\n            {!!fileConfig?.fileTable && (\n              <>\n                <Chip\n                  variant=\"header\"\n                  label=\"This file folder size\"\n                  value={\n                    Math.round(\n                      (projectFolderSize ?? 0) / 1e6,\n                    ).toLocaleString() + \" MB\"\n                  }\n                />\n                <Chip\n                  variant=\"header\"\n                  label=\"All file folders size\"\n                  value={\n                    Math.round(\n                      (totalFileFolderSize ?? 0) / 1e6,\n                    ).toLocaleString() + \" MB\"\n                  }\n                />\n              </>\n            )}\n          </>\n        : null}\n      </FlexRowWrap>\n\n      {enablingError && (\n        <ErrorComponent\n          variant=\"outlined\"\n          findMsg={true}\n          error={enablingError}\n        />\n      )}\n\n      {canEnable && (\n        <div className=\"flex-col gap-1 mt-2 \">\n          <Btn\n            color=\"action\"\n            variant=\"filled\"\n            data-command=\"config.files.toggle.confirm\"\n            iconPath={mdiContentSaveCogOutline}\n            onClickMessage={async (_, setMsg) => {\n              try {\n                setMsg({ loading: 1, duration: 10000 });\n                if (storageType === \"S3\" && !credentialId) {\n                  throw \"storageType missing\";\n                }\n                await dbsMethods.setFileStorage!(connection.id, {\n                  fileTable,\n                  storageType:\n                    storageType === \"local\" ?\n                      {\n                        type: storageType,\n                      }\n                    : {\n                        type: storageType,\n                        credential_id: credentialId!,\n                      },\n                });\n              } catch (err) {\n                setEnablingError(err);\n              }\n            }}\n          >\n            Enable file storage\n          </Btn>\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/FileTableControls/FileStorageDelete.tsx",
    "content": "import React, { useState } from \"react\";\nimport type { PrglCore } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { CodeChecker } from \"../BackupAndRestore/CodeConfirmation\";\nimport { useEffectAsync } from \"../DashboardMenu/DashboardMenuSettings\";\nimport type { FileStorageControlsProps } from \"./FileStorageControls\";\n\ntype P = Pick<\n  FileStorageControlsProps,\n  \"connection\" | \"dbsMethods\" | \"database_config\"\n> & {\n  onClose: VoidFunction;\n  db: PrglCore[\"db\"];\n};\n\nexport const FileStorageDelete = ({\n  dbsMethods,\n  connection,\n  db,\n  onClose,\n  database_config,\n}: P) => {\n  const [keepS3Data, setkeepS3Data] = useState(false);\n  const [keepFileTable, setkeepFileTable] = useState(false);\n  const [hasConfirmed, setHasConfirmed] = useState(false);\n  const [error, setError] = useState<any>();\n\n  const [hasFiles, setHasFiles] = useState(false);\n  useEffectAsync(async () => {\n    const ftable = database_config.file_table_config?.fileTable;\n    const hasFiles =\n      ftable && db[ftable] ? Boolean(await db[ftable].count?.()) : false;\n    setHasFiles(hasFiles);\n  }, [database_config.file_table_config?.fileTable, db]);\n\n  const isLocalType =\n    database_config.file_table_config?.storageType.type === \"local\";\n\n  return (\n    <Popup\n      title=\"Disable file storage\"\n      onClose={onClose}\n      footerButtons={[\n        { label: \"Cancel\", onClick: onClose, variant: \"outline\" },\n        {\n          node: (\n            <Btn\n              color=\"danger\"\n              variant=\"filled\"\n              disabledInfo={\n                hasConfirmed ? undefined : \"Must code confirm first\"\n              }\n              onClickMessage={async (_, setMsg) => {\n                try {\n                  setMsg({ loading: 1 });\n                  await dbsMethods.setFileStorage!(connection.id, undefined, {\n                    keepS3Data,\n                    keepFileTable,\n                  });\n                  setMsg({ ok: \"Disabled!\" }, onClose);\n                } catch (error) {\n                  setError(error);\n                }\n              }}\n            >\n              Disable File storage\n            </Btn>\n          ),\n        },\n      ]}\n      contentClassName=\"flex-col gap-1  p-1\"\n    >\n      <SwitchToggle\n        label={`Keep the ${database_config.file_table_config?.fileTable} table`}\n        checked={keepFileTable}\n        onChange={setkeepFileTable}\n        disabledInfo={hasFiles ? undefined : \"No files\"}\n      />\n\n      <SwitchToggle\n        label=\"Keep existing files\"\n        checked={keepS3Data && !isLocalType}\n        onChange={isLocalType ? () => {} : setkeepS3Data}\n        disabledInfo={\n          !hasFiles ? \"No files\"\n          : isLocalType ?\n            \"Local files cannot be kept\"\n          : undefined\n        }\n      />\n\n      <InfoRow color=\"warning\" variant=\"filled\">\n        File storage will be disabled{\" \"}\n      </InfoRow>\n\n      <CodeChecker className=\"ai-start pl-p25\" onChange={setHasConfirmed} />\n      {error && <ErrorComponent error={error} />}\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/FileTableControls/FileStorageReferencedTablesConfig.tsx",
    "content": "import React from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Prgl, PrglCore } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { CreateFileColumn } from \"./CreateFileColumn\";\nimport { FileColumnConfigControls } from \"./FileColumnConfigControls\";\nimport type { useFileTableConfigControls } from \"./useFileTableConfigControls\";\nimport { pageReload } from \"@components/Loader/Loading\";\n\ntype FileStorageReferencedTablesConfigProps = Pick<PrglCore, \"tables\" | \"db\"> &\n  Pick<\n    ReturnType<typeof useFileTableConfigControls>,\n    | \"canCreateTables\"\n    | \"canUpdateRefColumns\"\n    | \"setRefsConfig\"\n    | \"updateRefsConfig\"\n    | \"refsConfig\"\n  > & {\n    file_table_config: DBSSchema[\"database_configs\"][\"file_table_config\"];\n  };\n\nexport const FileStorageReferencedTablesConfig = ({\n  file_table_config,\n  tables,\n  db,\n  setRefsConfig,\n  refsConfig,\n  updateRefsConfig,\n  canUpdateRefColumns,\n}: FileStorageReferencedTablesConfigProps) => {\n  const tc = file_table_config;\n  if (!tc?.fileTable) return null;\n  return (\n    <FlexCol className=\"f-1 mt-2\">\n      <h3 className=\"m-0 p-0\">Referenced column limits</h3>\n      <div>\n        <p className=\"p-0 m-0\">\n          The following tables have columns that reference the file table{\" \"}\n          <strong>{tc.fileTable}</strong>\n        </p>\n        <p className=\"p-0 m-0\">\n          Specify allowed file types and sizes as desired. By default any file\n          type is allowed{\" \"}\n        </p>\n      </div>\n      <FileColumnConfigControls\n        tables={tables}\n        refsConfig={refsConfig}\n        onChange={setRefsConfig}\n      />\n      <CreateFileColumn\n        db={db}\n        tables={tables}\n        fileTable={file_table_config?.fileTable}\n      />\n\n      {canUpdateRefColumns && (\n        <div className=\"my-1\">\n          <Btn\n            variant=\"filled\"\n            color=\"action\"\n            onClickMessage={async (_, setMsg) => {\n              setMsg({ loading: 1 });\n              try {\n                await updateRefsConfig();\n                setMsg({ ok: \"Updated!\" });\n                setTimeout(() => {\n                  pageReload(\n                    \"FileStorageReferencedTablesConfig updateRefsConfig\",\n                  );\n                }, 500);\n              } catch (err) {\n                setMsg({ err });\n              }\n            }}\n          >\n            Update column configurations\n          </Btn>\n        </div>\n      )}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/FileTableControls/FileTableConfigControls.tsx",
    "content": "import { useIsMounted, usePromise } from \"prostgles-client\";\nimport type { SQLHandler } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { type Prgl } from \"../../App\";\nimport Loading from \"@components/Loader/Loading\";\nimport type { FileTableConfigReferences } from \"./FileColumnConfigControls\";\nimport { FileStorageControls } from \"./FileStorageControls\";\nimport { FileStorageReferencedTablesConfig } from \"./FileStorageReferencedTablesConfig\";\nimport { useFileTableConfigControls } from \"./useFileTableConfigControls\";\n\ntype FileTableConfigControlsProps = {\n  prgl: Prgl;\n  connectionId?: string;\n  className?: string;\n};\n\nexport type ConnectionTableConfig =\n  DBSSchema[\"database_configs\"][\"file_table_config\"] & {\n    referencedTables?: FileTableConfigReferences[\"referencedTables\"];\n  };\n\nexport const FileTableConfigControls = ({\n  prgl,\n}: FileTableConfigControlsProps) => {\n  const { tables, db, dbs, dbsTables, dbsMethods } = prgl;\n  const {\n    connection,\n    database_config,\n    canCreateTables,\n    setRefsConfig,\n    updateRefsConfig,\n    refsConfig,\n    canUpdateRefColumns: canUpdate,\n  } = useFileTableConfigControls(prgl);\n  if (!connection || !database_config) {\n    return <Loading />;\n  }\n\n  return (\n    <div className=\"flex-col gap-1 f-1 min-h-0 o-auto \">\n      <FileStorageControls\n        canCreateTables={canCreateTables}\n        connection={connection}\n        database_config={database_config}\n        dbTables={tables}\n        dbsMethods={dbsMethods}\n        dbs={dbs}\n        dbsTables={dbsTables}\n        dbProject={db}\n      />\n\n      <FileStorageReferencedTablesConfig\n        setRefsConfig={setRefsConfig}\n        updateRefsConfig={updateRefsConfig}\n        canCreateTables={!!canCreateTables}\n        canUpdateRefColumns={canUpdate}\n        refsConfig={refsConfig}\n        file_table_config={database_config.file_table_config}\n        tables={tables}\n        db={db}\n      />\n    </div>\n  );\n};\n\nexport const getCanCreateTables = (sql: SQLHandler): Promise<boolean> => {\n  return sql(\n    `SELECT has_database_privilege(current_database(), 'create') as yes`,\n    {},\n    { returnType: \"value\" },\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/FileTableControls/useFileTableConfigControls.ts",
    "content": "import { useIsMounted, usePromise } from \"prostgles-client\";\nimport type { Prgl } from \"../../App\";\nimport { getCanCreateTables } from \"./FileTableConfigControls\";\nimport { useState } from \"react\";\nimport type { FileTableConfigReferences } from \"./FileColumnConfigControls\";\n\nexport type UseFileTableConfigControlsArgs = Pick<\n  Prgl,\n  \"dbs\" | \"db\" | \"connectionId\" | \"dbsMethods\"\n>;\nexport const useFileTableConfigControls = ({\n  dbs,\n  db,\n  dbsMethods,\n  connectionId,\n}: UseFileTableConfigControlsArgs) => {\n  const connectionFilter = { id: connectionId };\n  const { data: connection } =\n    dbs.connections.useSubscribeOne(connectionFilter);\n  const { data: database_config } = dbs.database_configs.useSubscribeOne({\n    $existsJoined: { connections: connectionFilter },\n  });\n\n  const canCreateTables = usePromise(() => getCanCreateTables(db.sql!));\n  const savedRefsConfig: FileTableConfigReferences =\n    database_config?.file_table_config?.referencedTables ?? {};\n\n  const [localRefsConfig, setRefsConfig] =\n    useState<FileTableConfigReferences>();\n  const refsConfig = localRefsConfig ?? savedRefsConfig;\n  const getIsMounted = useIsMounted();\n  const updateRefsConfig = async (newRefs?: FileTableConfigReferences) => {\n    await dbsMethods.setFileStorage!(connectionId, {\n      referencedTables: newRefs ?? refsConfig,\n    });\n    if (!getIsMounted()) return;\n    setRefsConfig(undefined);\n  };\n  const canUpdateRefColumns =\n    JSON.stringify(savedRefsConfig) !== JSON.stringify(refsConfig);\n\n  return {\n    connection,\n    database_config,\n    canCreateTables,\n    refsConfig,\n    updateRefsConfig,\n    canUpdateRefColumns,\n    setRefsConfig,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/JSONBColumnEditor.tsx",
    "content": "import React from \"react\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport { getJSONBSchemaAsJSONSchema } from \"prostgles-types\";\nimport type { CodeEditorProps } from \"./CodeEditor/CodeEditor\";\nimport { CodeEditor } from \"./CodeEditor/CodeEditor\";\nimport { appTheme, useReactiveState } from \"../App\";\nimport ErrorComponent from \"@components/ErrorComponent\";\n\ntype P = {\n  style?: React.CSSProperties;\n  className?: string;\n  value: any;\n  tableName: string;\n  column: ValidatedColumnInfo;\n  onChange: (v: any) => void;\n};\nexport const JSONBColumnEditor = ({\n  value,\n  column,\n  tableName,\n  onChange,\n  style,\n  className,\n}: P) => {\n  if (!column.jsonbSchema) {\n    return (\n      <ErrorComponent error={\"Provided column is not of jsonbSchema type\"} />\n    );\n  }\n\n  const jsonSchema = getJSONBSchemaAsJSONSchema(\n    tableName,\n    column.name,\n    column.jsonbSchema,\n  );\n  const codeEditorProps: CodeEditorProps = {\n    style,\n    className,\n    language: {\n      lang: \"json\",\n      jsonSchemas: [\n        {\n          id: `${tableName}_${column.name}`,\n          schema: jsonSchema,\n        },\n      ],\n    },\n    value:\n      (typeof value !== \"string\" && value ?\n        JSON.stringify(value, null, 2)\n      : value?.toString()) ?? \"\",\n  };\n\n  return (\n    <CodeEditor\n      {...codeEditorProps}\n      style={{ minWidth: \"500px\", flex: 1 }}\n      onChange={onChange}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/LinkMenu.tsx",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { ParsedJoinPath } from \"prostgles-types\";\nimport React from \"react\";\nimport Popup from \"@components/Popup/Popup\";\nimport { Chart } from \"./Charts\";\nimport type { CanvasChart, Shape } from \"./Charts/CanvasChart\";\nimport type { DBS } from \"./Dashboard/DBS\";\nimport type { CommonWindowProps } from \"./Dashboard/Dashboard\";\nimport type {\n  Link,\n  LinkSyncItem,\n  WindowData,\n  WindowSyncItem,\n} from \"./Dashboard/dashboardUtils\";\nimport RTComp from \"./RTComp\";\nimport { JoinPathSelectorV2 } from \"./W_Table/ColumnMenu/JoinPathSelectorV2\";\nimport { getLinkColorV2 } from \"./W_Map/fetchData/getMapLayerQueries\";\n\ntype P = {\n  db: DBHandlerClient;\n  dbs: DBS;\n  onClose: VoidFunction;\n\n  style?: object;\n  className?: string;\n  w: WindowSyncItem;\n  windows: WindowSyncItem[];\n  links: LinkSyncItem[];\n  onLinkTable: (tblName: string, path: ParsedJoinPath[]) => any;\n  anchorEl: Element;\n  gridRef: Element;\n\n  tables: CommonWindowProps[\"tables\"];\n};\n\ntype S = {\n  loading: boolean;\n  shapes?: Shape[];\n  chartRef?: CanvasChart;\n};\n\nexport class LinkMenu extends RTComp<P, S> {\n  state: S = {\n    loading: true,\n    chartRef: undefined,\n  };\n\n  static getMyLinks = (\n    _links: Link[],\n    w: WindowData,\n    windows: WindowData[],\n  ) => {\n    const getLinks = (links: Link[], allLinks: Link[]) => {\n      return allLinks.filter((al) =>\n        [al.w1_id, al.w2_id].some((alid) =>\n          links.some((l) => [l.w1_id, l.w2_id].includes(alid)),\n        ),\n      );\n    };\n    const links = _links.filter((l) =>\n      [l.w1_id, l.w2_id].every((wid) =>\n        windows.some((w) => wid === w.id && !w.closed && !w.deleted),\n      ),\n    );\n\n    let currLinks = links.filter((l) => [l.w1_id, l.w2_id].includes(w.id));\n    let prevLinks = currLinks;\n    let myLinks = [...currLinks];\n    do {\n      currLinks = getLinks(\n        prevLinks,\n        links.filter((l) => !myLinks.find((ml) => ml.id === l.id)),\n      );\n      prevLinks = currLinks;\n      myLinks = myLinks.concat(currLinks);\n    } while (currLinks.length);\n\n    return myLinks;\n  };\n\n  loadShapes = () => {\n    const { w, links, gridRef, windows, tables } = this.props;\n\n    if (w.table_name && this.rootRef) {\n      const shapes: Shape[] = [];\n\n      const myLinks = LinkMenu.getMyLinks(links, w, windows); // links.filter(l => [l.w1_id, l.w2_id].includes(w.id));\n      // const myWindows = windows.filter(w => !w.closed && !w.deleted && myLinks.some(l =>  [l.w1_id, l.w2_id].includes(w.id)));\n\n      /**\n       * If table then show directly linked tables (or maybe tree view of all possible joins?!)\n       */\n\n      const boxes: {\n        id: string; //.dataset.boxId\n        rect: DOMRect;\n        w?: WindowData;\n      }[] = [];\n\n      gridRef\n        .querySelectorAll<HTMLDivElement>(\"[data-box-id][data-box-type='item']\")\n        .forEach((n) => {\n          const id = n.dataset.boxId!;\n          const rect = n.getBoundingClientRect();\n          const w = windows.find((w) => w.id === id);\n          boxes.push({ id, rect, w });\n        });\n\n      const { colorStr } = getLinkColorV2(\n        myLinks.find((l) => l.options.type !== \"table\"),\n      );\n      const { x, y } = this.rootRef.getBoundingClientRect();\n      myLinks.forEach((l) => {\n        const box1 = boxes.find((b) => b.id === l.w1_id),\n          box2 = boxes.find((b) => b.id === l.w2_id),\n          offsetCoord = (box: (typeof boxes)[number]): [number, number] => {\n            return [\n              box.rect.x + box.rect.width / 2 - x,\n              box.rect.y + box.rect.height / 2 - y,\n            ];\n          };\n\n        if (box1 && box2) {\n          const getText = (\n            w: WindowData | undefined,\n            otherW: WindowData | undefined,\n          ) => {\n            if (!w) return \"\";\n            if (l.options.type === \"table\") {\n              return w.table_name!;\n            } else {\n              const { dataSource } = l.options;\n              const chartedColumnsLabel = l.options.columns.map((c) => c.name);\n              const chartedTableLabel =\n                dataSource?.type === \"table\" ?\n                  (dataSource.joinPath?.at(-1)?.table ?? dataSource.tableName)\n                : (w.table_name ?? otherW?.table_name);\n              return `${chartedTableLabel} (${chartedColumnsLabel})`;\n            }\n          };\n          const textStyle = {\n            font: \"20px Arial\",\n            textAlign: \"center\" as const,\n            elevation: 12,\n            background: {\n              fillStyle: \"white\",\n              borderRadius: 12,\n              padding: 12,\n            },\n          };\n          shapes.push({\n            id: 22,\n            type: \"multiline\",\n            coords: [offsetCoord(box1), offsetCoord(box2)],\n            lineWidth: 6,\n            strokeStyle: colorStr,\n          });\n          shapes.push({\n            id: 1,\n            type: \"text\",\n            coords: offsetCoord(box1),\n            fillStyle: \"black\",\n            text: getText(box1.w, box2.w),\n            ...textStyle,\n          });\n          shapes.push({\n            id: 3,\n            type: \"text\",\n            coords: offsetCoord(box2),\n            fillStyle: \"black\",\n            text: getText(box2.w, box1.w),\n            ...textStyle,\n          });\n\n          let joinTextLines: { color: string; text: string }[] = [];\n          const w1 = windows.find((w) => w.id === box1.id);\n          const w2 = windows.find((w) => w.id === box2.id);\n          if (!w1 || !w2) return;\n\n          const w1r = window.document.body\n            .querySelector(`[data-box-id='${w1.id}']`)\n            ?.getBoundingClientRect();\n          const w2r = window.document.body\n            .querySelector(`[data-box-id='${w2.id}']`)\n            ?.getBoundingClientRect();\n\n          if (!w1r || !w2r) return;\n\n          const w1Above = Boolean(w1r.y < w2r.y);\n          const w1Left = Boolean(w1r.x > w2r.x);\n          const dominant =\n            Math.abs(w1r.y - w2r.y) > Math.abs(w1r.x - w2r.x) ? \"y\" : \"x\";\n\n          const parsedPath =\n            l.options.type === \"table\" ? l.options.tablePath\n            : l.options.dataSource?.type === \"table\" ?\n              l.options.dataSource.joinPath\n            : undefined;\n          joinTextLines =\n            parsedPath?.flatMap((p) => [\n              {\n                text: `(${Object.entries(p.on[0]!)\n                  .map(([l, r]) => `${l} = ${r}`)\n                  .join(\" AND \")})`,\n                color: \"blue\",\n              },\n              { text: p.table, color: \"black\" },\n            ]) ?? [];\n\n          let flip = false;\n          if (!w1Above && dominant === \"y\") {\n            flip = true;\n          } else if (w1Left && dominant === \"x\") {\n            joinTextLines = joinTextLines.slice(0).reverse();\n          }\n          if (flip) {\n            joinTextLines = joinTextLines.slice(0).reverse();\n          }\n\n          if (joinTextLines.length) {\n            const c1 = offsetCoord(box1),\n              c2 = offsetCoord(box2),\n              txtSize = 16;\n\n            joinTextLines.map(({ text, color }, i) => {\n              shapes.push({\n                id: 22 + i,\n                type: \"text\",\n                coords: [\n                  Math.round((c1[0] + c2[0]) / 2),\n                  Math.round((c1[1] + c2[1]) / 2 + i * 2.5 * txtSize),\n                ],\n                fillStyle: color,\n                font: txtSize + \"px Arial\",\n                textAlign: \"center\",\n                elevation: 12,\n                background: {\n                  fillStyle: \"white\",\n                  borderRadius: 12,\n                  padding: 10,\n                  strokeStyle: color || \"gray\",\n                  lineWidth: 1,\n                },\n                text,\n              });\n            });\n          }\n        }\n      });\n\n      this.setState({ loading: false, shapes });\n      this.chartRef?.render(shapes);\n    }\n  };\n\n  onDelta = (dP, dS, dD) => {\n    if (!this.state.shapes) {\n      this.loadShapes();\n    }\n    this.state.chartRef?.render(this.state.shapes || []);\n  };\n\n  chartRef?: CanvasChart;\n  rootRef?: HTMLDivElement;\n  render() {\n    const { onClose, anchorEl, onLinkTable, tables, w, links, windows } =\n      this.props;\n\n    const currentJoinedTables = windows\n      .filter(\n        ({ id, type, table_name }) =>\n          table_name !== w.table_name &&\n          type === \"table\" &&\n          links.some((l) => [l.w1_id, l.w2_id].includes(id)),\n      )\n      .map((w) => w.table_name!);\n    const canJoin = tables.some(\n      (t) => t.name === w.table_name && t.joinsV2.length,\n    );\n\n    return (\n      <div\n        className=\"LinkMenu absolute inset-0\"\n        ref={(e) => {\n          if (e) this.rootRef = e;\n        }}\n      >\n        <Popup\n          onClose={onClose}\n          title={canJoin ? \"Add joined table\" : undefined}\n          collapsible={{ defaultValue: Boolean(links.length) }}\n          anchorEl={anchorEl}\n          positioning=\"beneath-left\"\n          clickCatchStyle={{ opacity: 0, zIndex: 14 }}\n          contentStyle={canJoin ? { padding: \"1em\" } : { display: \"none\" }}\n          rootStyle={{ zIndex: 14, maxWidth: \"500px\" }}\n        >\n          {canJoin && (\n            <JoinPathSelectorV2\n              tableName={w.table_name!}\n              value={undefined}\n              tables={tables}\n              getFullOption={(path) =>\n                (\n                  path.length === 1 &&\n                  currentJoinedTables.includes(path.at(-1)!.table)\n                ) ?\n                  { disabledInfo: \"Already joined\" }\n                : undefined\n              }\n              variant=\"expanded\"\n              onChange={(targetPath) => {\n                onLinkTable(targetPath.table.name, targetPath.path);\n                onClose();\n              }}\n            />\n          )}\n        </Popup>\n\n        <Chart\n          className=\" h-full w-full\"\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            zIndex: 14,\n            backdropFilter: \"blur(1px)\",\n          }}\n          setRef={(chart) => {\n            this.chartRef = chart;\n            this.setState({ chartRef: chart });\n          }}\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/Map/DeckGLFeatureEditor.tsx",
    "content": "import { mdiPencil, mdiPlus } from \"@mdi/js\";\nimport { scaleLinear } from \"d3\";\nimport type { GeoJsonLayer } from \"deck.gl\";\nimport type { Feature } from \"geojson\";\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Select } from \"@components/Select/Select\";\nimport type { FullExtraProps } from \"../../pages/ProjectConnection/ProjectConnection\";\nimport { isDefined } from \"../../utils/utils\";\nimport { SmartForm } from \"../SmartForm/SmartForm\";\nimport type { LayerTable, W_MapProps } from \"../W_Map/W_Map\";\nimport type { GeoJSONFeature, GeoJsonLayerProps } from \"./DeckGLMap\";\nimport type { DeckGlLibs, DeckWrapped } from \"./DeckGLWrapped\";\nimport type { AllDrawModes } from \"./mapDrawUtils\";\nimport { DrawModes, geometryToGeoEWKT } from \"./mapDrawUtils\";\n\nexport type DeckGLFeatureEditorProps = {\n  deckW: DeckWrapped;\n  edit: Pick<W_MapProps, \"layerQueries\"> &\n    Pick<FullExtraProps, \"dbProject\" | \"dbTables\" | \"dbMethods\" | \"theme\"> & {\n      feature:\n        | undefined\n        | (Feature & {\n            properties: GeoJSONFeature[\"properties\"] & {\n              geomColumn: string;\n              tableName: string;\n            };\n          });\n      onStartEdit: VoidFunction;\n      onInsertOrUpdate: VoidFunction;\n    };\n  onRenderLayer: (layer: GeoJsonLayer | undefined) => void;\n  deckGlLibs: DeckGlLibs;\n  /**\n   * Used for snapping\n   */\n  geoJsonLayers?: GeoJsonLayerProps[];\n};\n\ntype DrawnShape =\n  | { type: \"Point\"; coordinates: [number, number] }\n  | { type: \"LineString\"; coordinates: [number, number][] }\n  | { type: \"Polygon\"; coordinates: [number, number][][] };\n\nexport const DeckGLFeatureEditor = ({\n  onRenderLayer,\n  edit,\n  deckGlLibs,\n  deckW,\n}: DeckGLFeatureEditorProps) => {\n  const {\n    dbProject,\n    dbTables,\n    feature,\n    layerQueries,\n    dbMethods,\n    onInsertOrUpdate,\n  } = edit;\n\n  const [editMode, setEditMode] = useState<{\n    modeKey: keyof AllDrawModes;\n    geometry: DrawnShape | undefined;\n    initialGeometry: Feature[\"geometry\"] | undefined;\n    tableName: string;\n    geomColumn: string;\n    $rowhash: string | undefined;\n    finished?: boolean;\n    error?: any;\n  }>();\n  const modeKey = editMode?.modeKey ?? \"ViewMode\";\n  const isUpdate = !!editMode?.$rowhash;\n\n  const clearEditMode = useCallback(\n    (dataWasEdited = false) => {\n      setEditMode(undefined);\n      if (dataWasEdited) {\n        onInsertOrUpdate();\n      }\n    },\n    [onInsertOrUpdate, setEditMode],\n  );\n\n  const defaultData = useMemo(\n    () =>\n      !editMode?.geometry ?\n        undefined\n      : {\n          [editMode.geomColumn]: geometryToGeoEWKT(editMode.geometry),\n        },\n    [editMode],\n  );\n\n  const finishEditMode = useCallback(\n    async (insert = false) => {\n      const layer =\n        editMode?.geometry &&\n        getDrawnLayer({ deckGlLibs, shapes: [editMode.geometry], deckW });\n      onRenderLayer(layer);\n      if (!(editMode && !editMode.finished)) {\n        return;\n      }\n      if (insert && defaultData) {\n        try {\n          if (isUpdate) {\n            await dbProject[editMode.tableName]!.update!(\n              { $rowhash: editMode.$rowhash },\n              defaultData,\n            );\n          } else {\n            await dbProject[editMode.tableName]!.insert!(defaultData);\n          }\n          onInsertOrUpdate();\n          setEditMode(undefined);\n        } catch (error) {\n          setEditMode({ ...editMode, error, finished: true });\n        }\n      } else {\n        setEditMode({ ...editMode, finished: true });\n      }\n    },\n    [\n      editMode,\n      defaultData,\n      isUpdate,\n      dbProject,\n      onInsertOrUpdate,\n      deckGlLibs,\n      onRenderLayer,\n      deckW,\n    ],\n  );\n  const renderShapes = useCallback(\n    (cursorCoords: [number, number] | undefined) => {\n      if (!editMode) {\n        onRenderLayer(undefined);\n        return;\n      }\n      const { geometry } = editMode;\n      const renderedShapes: DrawnShape[] = [];\n      if (geometry?.type === \"LineString\") {\n        const extendedCoords =\n          cursorCoords ?\n            [...geometry.coordinates, cursorCoords]\n          : geometry.coordinates;\n        renderedShapes.push({\n          type: \"LineString\",\n          coordinates: extendedCoords,\n        });\n      } else if (geometry?.type === \"Polygon\") {\n        const currPoints = geometry.coordinates[0];\n        const [firstPoint, ...otherPoints] = currPoints ?? [];\n        if (firstPoint && otherPoints.length) {\n          const extendedCoords =\n            cursorCoords ?\n              [...currPoints!, cursorCoords, firstPoint]\n            : [...currPoints!, firstPoint];\n          // if(invalidPolygon(extendedCoords)){\n\n          // }\n          renderedShapes.push({\n            type: \"Polygon\",\n            coordinates: [extendedCoords],\n          });\n        } else if (firstPoint && cursorCoords) {\n          renderedShapes.push({\n            type: \"LineString\",\n            coordinates: [firstPoint, cursorCoords],\n          });\n        }\n      }\n      /* Shows cursor position */\n      renderedShapes.push({\n        type: \"Point\",\n        coordinates: cursorCoords as [number, number],\n      });\n\n      const layer = getDrawnLayer({\n        deckGlLibs,\n        shapes: renderedShapes,\n        deckW,\n      });\n      onRenderLayer(layer);\n    },\n    [onRenderLayer, deckGlLibs, editMode, deckW],\n  );\n\n  useEffect(() => {\n    if (!editMode || editMode.finished) {\n      return;\n    }\n    const onPointerMove = (e: PointerEvent) => {\n      const cursorCoords = deckW.currHover?.coordinate;\n      if (!cursorCoords) return;\n      renderShapes(cursorCoords as [number, number]);\n    };\n    const onPointerDown = (e: PointerEvent) => {\n      const deleteLastPoint = e.ctrlKey;\n      const cursorCoords = deckW.currHover?.coordinate;\n      if (!cursorCoords) return;\n      const { geometry } = editMode;\n      if (editMode.modeKey === \"DrawPointMode\") {\n        setEditMode({\n          ...editMode,\n          geometry: {\n            type: \"Point\",\n            coordinates: cursorCoords as [number, number],\n          },\n          finished: true,\n        });\n        return false;\n      } else if (editMode.modeKey === \"DrawLineStringMode\") {\n        const newCoordinates =\n          deleteLastPoint ?\n            [\n              ...(geometry?.type === \"LineString\" ?\n                geometry.coordinates.slice(0, -1)\n              : []),\n            ]\n          : [\n              ...(geometry?.type === \"LineString\" ? geometry.coordinates : []),\n              cursorCoords,\n            ];\n        setEditMode({\n          ...editMode,\n          geometry: {\n            type: \"LineString\",\n            coordinates: newCoordinates as [number, number][],\n          },\n        });\n        return false;\n      } else if (editMode.modeKey === \"DrawPolygonMode\") {\n        const newCoordinates =\n          deleteLastPoint ?\n            [\n              ...(geometry?.type === \"Polygon\" ?\n                geometry.coordinates[0]!.slice(0, -1)\n              : []),\n            ]\n          : [\n              ...(geometry?.type === \"Polygon\" ? geometry.coordinates[0]! : []),\n              cursorCoords,\n            ];\n        setEditMode({\n          ...editMode,\n          geometry: {\n            type: \"Polygon\",\n            coordinates: [newCoordinates as [number, number][]],\n          },\n        });\n        return false;\n      }\n      setEditMode({\n        ...editMode,\n        geometry: {\n          type: \"Point\",\n          coordinates: deckW.currHover?.coordinate as [number, number],\n        },\n      });\n    };\n\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Enter\") {\n        /* Many shapes have no geometry until Enter is pressed */\n        if (!editMode.geometry) {\n          return;\n        }\n        finishEditMode();\n      } else if (e.key === \"Escape\") {\n        clearEditMode();\n      }\n    };\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    window.addEventListener(\"pointermove\", onPointerMove);\n    window.addEventListener(\"pointerdown\", onPointerDown);\n\n    return () => {\n      window.removeEventListener(\"pointermove\", onPointerMove);\n      window.removeEventListener(\"pointerdown\", onPointerDown);\n      window.removeEventListener(\"keydown\", onKeyDown);\n    };\n  }, [\n    renderShapes,\n    deckW,\n    deckGlLibs,\n    editMode,\n    clearEditMode,\n    finishEditMode,\n  ]);\n\n  useEffect(() => {\n    renderShapes(undefined);\n  }, [editMode?.geometry, renderShapes]);\n\n  const wasUpdated = useMemo(() => {\n    return (\n      (isUpdate &&\n        JSON.stringify(editMode.initialGeometry) !==\n          JSON.stringify(editMode.geometry)) ||\n      editMode?.geometry\n    );\n  }, [editMode?.geometry, editMode?.initialGeometry, isUpdate]);\n\n  const layerTables: LayerTable[] = (\n    layerQueries?.filter((l) => \"tableName\" in l) as LayerTable[]\n  )\n    .map((l) => ({ ...l, rootTable: l.path?.at(-1) ?? l.tableName }))\n    .filter((l) => dbProject[l.tableName]?.update);\n\n  const closeEditMode = useCallback(() => {\n    clearEditMode(true);\n  }, [clearEditMode]);\n\n  const editModeFilter = useMemo(() => {\n    const filter =\n      editMode?.$rowhash ?\n        [{ fieldName: \"$rowhash\", value: editMode.$rowhash }]\n      : undefined;\n    return filter;\n  }, [editMode?.$rowhash]);\n\n  if (!layerTables.length) {\n    return null;\n  }\n\n  if (editMode) {\n    if (editMode.finished && editMode.geometry) {\n      return (\n        <SmartForm\n          asPopup={true}\n          tableName={editMode.tableName}\n          rowFilter={editModeFilter}\n          db={dbProject}\n          tables={dbTables}\n          methods={dbMethods}\n          defaultData={defaultData}\n          onSuccess={closeEditMode}\n          onClose={clearEditMode}\n          confirmUpdates={true}\n        />\n      );\n    }\n\n    const pressEnterHint = \"Press Enter to finish drawing.\";\n    const drawingIconPath = DrawModes[editMode.modeKey].iconPath;\n    const hintText =\n      (editMode.modeKey === \"DrawPolygonMode\" ?\n        `Click to place polygon points. Ctrl+Click to delete last point. ${pressEnterHint}`\n      : editMode.modeKey === \"DrawLineStringMode\" ?\n        `Click to place line points. Ctrl+Click to delete last point. ${pressEnterHint}`\n      : editMode.modeKey === \"DrawPolygonByDraggingMode\" ?\n        `Click and drag to draw polygon. Release to finish.`\n      : editMode.modeKey === \"DrawRectangleMode\" ?\n        `Click top left and then bottom right rectangle corner.`\n      : editMode.modeKey === \"DrawSquareMode\" ?\n        `Click top left and then bottom right square corner.`\n      : editMode.modeKey === \"DrawEllipseByBoundingBoxMode\" ?\n        `Click top left and then bottom right ellipse edge.`\n      : \"Click to place point.\") + \" Press Escape to cancel.\";\n    return (\n      <div className=\"flex-row gap-1 f-1 bg-color-1 rounded shadow p-1 ai-center\">\n        {!editMode.geometry ?\n          <>\n            <Btn\n              className=\"shadow\"\n              variant=\"faded\"\n              onClick={() => setEditMode(undefined)}\n            >\n              Cancel\n            </Btn>\n            <InfoRow\n              className=\"h-fit \"\n              color=\"action\"\n              variant=\"naked\"\n              iconPath={drawingIconPath}\n            >\n              {hintText}\n            </InfoRow>\n          </>\n        : <div className=\"flex-row gap-1 f-1\">\n            <Btn\n              className=\"shadow\"\n              variant=\"faded\"\n              onClick={() => clearEditMode(false)}\n            >\n              Cancel\n            </Btn>\n            {wasUpdated && (\n              <>\n                <Btn\n                  variant=\"filled\"\n                  color=\"action\"\n                  onClick={() => finishEditMode()}\n                >\n                  Done\n                </Btn>\n                <Btn\n                  variant=\"filled\"\n                  color=\"action\"\n                  onClick={() => finishEditMode(true)}\n                >\n                  Done and {isUpdate ? \"update\" : \"insert\"}\n                </Btn>\n                {layerTables.length > 1 && (\n                  <Select\n                    value={editMode.tableName}\n                    fullOptions={layerTables.map((l) => ({ key: l.tableName }))}\n                    onChange={(tableName) =>\n                      setEditMode({ ...editMode, tableName })\n                    }\n                    btnProps={{\n                      className: \"shadow\",\n                    }}\n                  />\n                )}\n              </>\n            )}\n            {isUpdate && (\n              <Btn\n                className=\"ml-auto\"\n                variant=\"faded\"\n                color=\"danger\"\n                onClick={async () => {\n                  try {\n                    await dbProject[editMode.tableName]?.delete!({\n                      $rowhash: editMode.$rowhash,\n                    });\n                    setEditMode(undefined);\n                    onInsertOrUpdate();\n                  } catch (error) {\n                    setEditMode({ ...editMode, error });\n                  }\n                }}\n              >\n                Delete\n              </Btn>\n            )}\n          </div>\n        }\n        {!!editMode.error && <ErrorComponent error={editMode.error} />}\n      </div>\n    );\n  }\n\n  const firstTable = layerTables[0];\n  if (edit.feature) {\n    return (\n      <>\n        <Btn\n          iconPath={mdiPencil}\n          color=\"action\"\n          variant=\"filled\"\n          size=\"small\"\n          title={\"Edit selected feature\"}\n          onClick={() => {\n            if (feature) {\n              const { geomColumn, tableName, $rowhash } = feature.properties;\n              const supportedGeometryTypes = [\n                \"Point\",\n                \"LineString\",\n                \"Polygon\",\n              ] satisfies DrawnShape[\"type\"][];\n              if (\n                !supportedGeometryTypes.includes(feature.geometry.type as any)\n              ) {\n                alert(\n                  `Unsopperted geometry type: ${feature.geometry.type}. Supported geometries: ${supportedGeometryTypes}`,\n                );\n                return;\n              }\n              setEditMode({\n                tableName,\n                geomColumn,\n                geometry: feature.geometry as any,\n                modeKey: \"ModifyMode\",\n                $rowhash,\n                initialGeometry: feature.geometry,\n              });\n            }\n          }}\n        >\n          Edit feature\n        </Btn>\n      </>\n    );\n  }\n\n  if (firstTable) {\n    return (\n      <Select\n        data-command=\"DeckGLFeatureEditor\"\n        title=\"Select shape type\"\n        fullOptions={Object.entries(DrawModes).map(\n          ([key, { label, iconPath }]) => ({\n            key: key as keyof AllDrawModes,\n            label,\n            iconPath,\n          }),\n        )}\n        iconPath={mdiPlus}\n        showIconOnly={true}\n        value={modeKey}\n        onChange={(modeKey) => {\n          setEditMode({\n            $rowhash: undefined,\n            initialGeometry: undefined,\n            error: undefined,\n            tableName: firstTable.tableName,\n            geomColumn: firstTable.geomColumn,\n            modeKey,\n            geometry: undefined,\n          });\n        }}\n        btnProps={{\n          color: \"action\",\n          variant: \"filled\",\n          iconStyle: {\n            color: \"inherit\",\n          },\n        }}\n      />\n    );\n  }\n\n  return <>Something went wrong</>;\n};\n\ntype GetDrawnLayerArgs = {\n  shapes: DrawnShape[];\n  deckGlLibs: DeckGlLibs;\n  deckW: DeckWrapped;\n};\nconst getDrawnLayer = ({ deckGlLibs, shapes, deckW }: GetDrawnLayerArgs) => {\n  const zoom = deckW.deck.getViewports()[0]?.zoom ?? 1;\n  const radiusScale = scaleLinear().domain([0, 20]).range([10, 0.01]);\n  const radius = radiusScale(zoom);\n  const layer = new deckGlLibs.lib.GeoJsonLayer({\n    id: \"prostgles-geojson-editor\",\n    data: {\n      type: \"FeatureCollection\",\n      features: shapes.map((geometry, i) => ({\n        id: i,\n        type: \"Feature\",\n        geometry,\n        properties: {},\n      })),\n    },\n    filled: true,\n    pointRadiusMinPixels: 5,\n    pointRadiusMaxPixels: 10,\n    pointRadiusScale: 1,\n    // pointType: 'circle',\n    getPointRadius: radius,\n    extruded: false,\n    getElevation: 0,\n    getFillColor: (f) =>\n      f.geometry.type === \"Polygon\" ? [200, 0, 80, 55] : [0, 129, 167, 255],\n    getLineColor: (f) =>\n      f.geometry.type === \"Polygon\" ? [200, 0, 80, 255] : [0, 129, 167, 255],\n    lineWidthMinPixels: 2,\n    widthScale: 22,\n    lineWidth: (f) => f.properties?.lineWidth ?? 1,\n    pickable: true,\n    pickingRadius: 10,\n    autoHighlight: true,\n    onClick: console.log,\n  });\n\n  return layer;\n};\nconst invalidPolygon = (extendedCoords: [number, number][]) => {\n  const lines = extendedCoords\n    .map((p, i) => {\n      const p2 = extendedCoords[i + 1];\n      if (!p2) return;\n      return [...p, ...p2] as [number, number, number, number];\n    })\n    .filter(isDefined);\n  let intersection;\n  lines.find((l, i) =>\n    lines.some((l2, i2) => {\n      /** Start and end */\n      if (i === i2) return false;\n      if (i === 0 && i2 === lines.length - 1) return false;\n      if (i2 === 0 && i === lines.length - 1) return false;\n      /** One after the other */\n      if (i >= i2 - 1 && i <= i2 + 1) return false;\n      if (i2 >= i - 1 && i2 <= i + 1) return false;\n\n      const inters = intersect(...l, ...l2);\n      if (!inters) return false;\n      intersection ??= {\n        ...inters,\n        l,\n        l2,\n      };\n      return inters;\n    }),\n  );\n  // console.log(intersection);\n  // if(intersection){\n  //   renderedShapes.push({\n  //     type: \"Point\",\n  //     coordinates: [intersection.x, intersection.y]\n  //   })\n  //   renderedShapes.push({\n  //     type: \"LineString\",\n  //     coordinates: [\n  //       intersection.l[0],\n  //       intersection.l[1],\n  //       intersection.l2[0],\n  //       intersection.l2[1],\n  //     ]\n  //   })\n  // }\n};\nconst intersect = (x1, y1, x2, y2, x3, y3, x4, y4) => {\n  // // Ensure lines overlap 1d\n  // if(!(x1 < x3 && x2 > x4 || x3 < x1 && x4 > x2 || y1 < y3 && y2 > y4 || y3 < y1 && y4 > y2)){\n  //   return false\n  // }\n\n  // Check if none of the lines are of length 0\n  if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {\n    return false;\n  }\n\n  const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);\n\n  // Lines are parallel\n  if (denominator === 0) {\n    return false;\n  }\n\n  const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;\n  const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;\n\n  // is the intersection along the segments\n  if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {\n    return false;\n  }\n\n  // Return a object with the x and y coordinates of the intersection\n  const x = x1 + ua * (x2 - x1);\n  const y = y1 + ua * (y2 - y1);\n\n  return { x, y };\n};\n"
  },
  {
    "path": "client/src/dashboard/Map/DeckGLMap.css",
    "content": ".map-controls-right > * {\n  transform: translate(calc(100% + 20px), 0px);\n}\n\n.map-controls-right > *:nth-child(1) {\n  transition: 0.3s transform;\n}\n.map-controls-right > *:nth-child(2) {\n  transition: 0.6s transform;\n}\n.map-controls-right > *:nth-child(3) {\n  transition: 0.9s transform;\n}\n\n.map-controls-right {\n  padding: 1em;\n  transition: 0.3s all;\n  background-color: transparent;\n  transform: translate(calc(100% - 30px), 0px);\n}\n.map-controls-right:hover {\n  background-color: white;\n  transform: translate(0, 0px);\n  overflow: auto;\n\n  box-shadow:\n    0 1px 3px 0 rgba(0, 0, 0, 0.1),\n    0 1px 2px 0 rgba(0, 0, 0, 0.06);\n}\n\n.dark-theme .map-controls-right:hover {\n  background-color: var(--gray-900);\n  /* background-color: transparent;\n  backdrop-filter: blur(4px); */\n}\n.map-controls-right:hover > * {\n  transform: translate(0, 0);\n}\n\n.in-map-hover-control {\n  opacity: 0.45;\n}\n.in-map-hover-control:hover {\n  opacity: 1;\n}\n"
  },
  {
    "path": "client/src/dashboard/Map/DeckGLMap.tsx",
    "content": "import React from \"react\";\nimport RTComp from \"../RTComp\";\n\nimport { isDefined } from \"prostgles-types\";\nimport type { MAP_PROJECTIONS } from \"../W_Map/W_MapMenu\";\nimport type { DeckGLFeatureEditorProps } from \"./DeckGLFeatureEditor\";\nimport \"./DeckGLMap.css\";\nimport type { Bounds, DeckGlLibs } from \"./DeckGLWrapped\";\nimport { DeckWrapped, getDeckLibs, getViewState } from \"./DeckGLWrapped\";\nimport { makeImageLayer, makeTileLayer } from \"./mapUtils\";\n\nexport type Extent = [number, number, number, number];\n\ntype OnClickEvent = {\n  coordinate?: [number, number];\n  devicePixel: any;\n  index: number;\n  layer: any;\n  picked: boolean;\n  pixel: [number, number];\n  pixelRatio: undefined;\n  viewport: any;\n  x: number;\n  y: number;\n  object?: Record<string, any>;\n};\n\n// import {_SunLight as SunLight, LightingEffect } from 'deck.gl';\n// const le = new LightingEffect({\n//   sunlight: new SunLight({\n//     timestamp: 1610455406000,\n//     color: [255, 0, 0],\n//     intensity: 1,\n//     _shadow: true\n//   })\n// });\n\n/* global window */\n// const devicePixelRatio = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;\n\n// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz\n// const COUNTRIES = 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson'; //eslint-disable-line\n// const AIR_PORTS = 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson';\n\nexport type Point = [number, number];\n\nimport type { GeoJsonLayer } from \"deck.gl\";\nimport type { Feature } from \"geojson\";\nimport type { MapExtent } from \"../Dashboard/dashboardUtils\";\nimport type { MAP_SELECT_COLUMNS } from \"../W_Map/fetchData/getMapData\";\nimport { InMapControls } from \"./InMapControls\";\nimport type { SmartFormProps } from \"../SmartForm/SmartForm\";\n\nexport type DeckGlColor =\n  | [number, number, number]\n  | [number, number, number, number];\n\nexport type GeoJSONFeature = Omit<Feature, \"properties\"> & {\n  properties: (\n    | {\n        type?: undefined;\n        _is_from_osm?: boolean;\n      }\n    | {\n        type: \"table\";\n        [MAP_SELECT_COLUMNS.idObj]: any;\n        [MAP_SELECT_COLUMNS.geoJson]: any;\n      }\n    | {\n        type: \"sql\";\n        $rowhash: string;\n      }\n  ) & {\n    radius?: number;\n  };\n};\n\nexport type GeoJsonLayerProps = {\n  id: string;\n  features: GeoJSONFeature[];\n  filled: boolean;\n  getFillColor: (f: GeoJSONFeature) => DeckGlColor;\n  getLineColor: (f: GeoJSONFeature) => DeckGlColor;\n  getText?: (f: GeoJSONFeature) => string;\n  getTextSize?: number | ((f: GeoJSONFeature) => number);\n  getIcon?: (f: GeoJSONFeature) => {\n    id: string;\n    url: string;\n    width: number;\n    height: number;\n  };\n  display: \"icon\" | \"icon+circle\" | undefined;\n  elevation?: number;\n  pickable?: boolean;\n  stroked?: boolean;\n  onClick?: (info: any) => any;\n  dataSignature: string;\n  getLineWidth?: (f: any) => number;\n  lineWidth?: number;\n  layerColor?: DeckGlColor;\n};\n\nexport type MapState = {\n  latitude: number;\n  longitude: number;\n  zoom: number;\n  extent: Extent;\n  target?: [number, number, number];\n  basemap: {\n    opacity: number;\n    tileURLs: string[];\n  };\n  dataOpacity: number;\n  pitch: number;\n  bearing: number;\n  mouseDown: boolean;\n};\n\nexport type MapHandler = {\n  fitBounds: (ext: MapExtent) => any;\n  getExtent: () => MapExtent;\n  deck: DeckWrapped;\n};\n\nexport type HoverCoords = {\n  x: number;\n  y: number;\n  screenCoordinates?: [number, number];\n  coordinates?: [number, number];\n};\nexport const MapExtentBehavior = [\n  {\n    key: \"autoZoomToData\",\n    label: \"Follow data\",\n    subLabel: \"Will zoom to data extent on data change\",\n  },\n  {\n    key: \"filterToMapBounds\",\n    label: \"Follow map\",\n    subLabel: \"Filters data to map bounds\",\n  },\n  {\n    key: \"freeRoam\",\n    label: \"Free roam\",\n    subLabel: \"Map bounds filter not applied\",\n  },\n] as const;\n\nexport type MapExtentBehavior = (typeof MapExtentBehavior)[number][\"key\"];\n\nexport type DecKGLMapProps = {\n  basemapOpacity: number;\n  basemapDesaturate: number;\n  dataOpacity: number;\n  initialState?: MapState;\n  geoJsonLayers?: GeoJsonLayerProps[];\n  /**\n   * Used to identify when auto-zoom needs to be triggered\n   */\n  geoJsonLayersDataFilterSignature: string;\n  onLoad?: (map: MapHandler) => void;\n  onHover?: (object?: any, coords?: HoverCoords) => any;\n  onPointerMove?: (coords?: HoverCoords) => any;\n  onMapStateChange?: (state: MapState) => any;\n  mapStateChangeDebounce?: number;\n\n  /**\n   * Must return:\n   * [\n   *    [minLng, minLat],\n   *    [maxLng, maxLat]\n   * ]\n   */\n  onGetFullExtent: (fromUserClick?: boolean) => Promise<MapExtent | undefined>;\n\n  onClick?: (args: OnClickEvent) => any;\n  tileURLs?: string[];\n  tileSize?: number;\n  tileAttribution?: {\n    title: string;\n    url: string;\n  };\n\n  options: {\n    extentBehavior?: \"autoZoomToData\" | \"filterToMapBounds\" | \"freeRoam\";\n  };\n\n  onOptionsChange: (newOpts: Partial<DecKGLMapProps[\"options\"]>) => any;\n  projection?: (typeof MAP_PROJECTIONS)[number];\n  topLeftContent?: React.ReactNode;\n  basemapImage?: {\n    url: string;\n    bounds: Extent;\n  };\n\n  edit: undefined | DeckGLFeatureEditorProps[\"edit\"];\n};\n\nconst vstateDebounce = 300;\n\nexport type DeckGLMapState = {\n  initialView: {\n    target: number[];\n    latitude: number;\n    longitude: number;\n    zoom: number;\n    bearing: number;\n    pitch: number;\n  };\n  mouseDown: boolean;\n  cursorCoords?: string;\n\n  editFeature?: GeoJSONFeature;\n};\n\ntype D = {\n  editedFeaturesLayer?: GeoJsonLayer;\n};\n\nexport type DeckGLMapDivDemoControls = HTMLDivElement & {\n  getLatLngXY: (place: { latitude: number; longitude: number }) => {\n    x: number;\n    y: number;\n  };\n  zoomTo: (place: {\n    latitude: number;\n    longitude: number;\n    zoom: number;\n  }) => void;\n};\n\nconst setDemoHandles = (node: HTMLDivElement, dmap: DeckGLMap) => {\n  (node as DeckGLMapDivDemoControls).getLatLngXY = (place: {\n    latitude: number;\n    longitude: number;\n  }) => {\n    const [x = 0, y = 0] = dmap\n      .deckW!.deck.getViewports()[0]!\n      .project([place.longitude, place.latitude]);\n    const bbox = node.getBoundingClientRect();\n    return {\n      x: bbox.x + x, //   -.1456\n      y: bbox.y + y, //   51.526\n    };\n  };\n  (node as DeckGLMapDivDemoControls).zoomTo = (place: {\n    latitude: number;\n    longitude: number;\n    zoom: number;\n  }) => {\n    dmap.deckW?.zoomTo({ type: \"point\", ...place });\n  };\n  node.classList.toggle(\"DeckGLMapDiv\", true);\n};\n\nexport class DeckGLMap extends RTComp<DecKGLMapProps, DeckGLMapState, D> {\n  state: DeckGLMapState = {\n    initialView: {\n      target: [1, 1, 0],\n      latitude: 51.47,\n      longitude: 0.45,\n      zoom: 4,\n      bearing: 0,\n      pitch: 0,\n    },\n    mouseDown: false,\n  };\n\n  ref?: typeof import(\"deck.gl\");\n  refRoot?: HTMLDivElement;\n  refCursor?: HTMLDivElement;\n  rootResizeObserver?: ResizeObserver;\n\n  dataExtent?: [[number, number], [number, number]];\n  deckGlLibs?: DeckGlLibs;\n  onDelta = async (\n    dP: Partial<DecKGLMapProps> | undefined,\n    dS: Partial<DeckGLMapState> = {},\n    dD: Partial<D> = {},\n  ) => {\n    if (\n      dP?.geoJsonLayersDataFilterSignature &&\n      this.props.options.extentBehavior === \"autoZoomToData\" &&\n      this.deckW\n    ) {\n      this.fitBounds();\n    }\n\n    /** Init */\n    if (this.refRoot && !this.rootResizeObserver) {\n      this.rootResizeObserver = new ResizeObserver(() => {\n        if (this._rootSizeKey !== this.rootSizeKey && this.mounted) {\n          this.forceUpdate();\n          this._rootSizeKey = this.rootSizeKey;\n        }\n      });\n      setDemoHandles(this.refRoot, this);\n      this.rootResizeObserver.observe(this.refRoot);\n\n      const { projection = \"mercator\", initialState: _initialState } =\n        this.props;\n      this.deckGlLibs = await getDeckLibs();\n      this.deckW = new DeckWrapped(\n        this.refRoot,\n        {\n          initialViewState: _initialState as any,\n          type: projection,\n          onLoad: () => {\n            this.props.onLoad?.({\n              deck: this.deckW!,\n              fitBounds: this.fitBounds,\n              getExtent: () => {\n                const ext = this.deckW!.getExtent();\n                if (!ext) return;\n                return [ext.slice(0, 2), ext.slice(2)] as any;\n              },\n            });\n          },\n          onViewStateChange: (viewState, bounds) => {\n            const extent = bounds.flat();\n            const parsedViewState = getViewState(projection, viewState);\n            const newMapState = { ...parsedViewState, extent };\n\n            this.props.onMapStateChange?.(newMapState as any);\n          },\n          onClickQuick: (e) => this.props.onClick?.(e as any),\n          onHoverItem: (obj, coords) => {\n            this.props.onHover?.(obj, coords);\n          },\n          onHover: (e) => {\n            this.props.onPointerMove?.({\n              ...e,\n              coordinates: e.coordinate as any,\n              screenCoordinates: e.pixel,\n            });\n          },\n          layers: this.getLayers().layers,\n        },\n        this.deckGlLibs.lib,\n      );\n    }\n\n    if (\n      this.deckW &&\n      ((dP?.geoJsonLayers && this.props.geoJsonLayers) ||\n        dP?.dataOpacity ||\n        dP?.basemapDesaturate ||\n        dP?.basemapOpacity ||\n        \"editedFeaturesLayer\" in dD)\n    ) {\n      this.deckW.render({\n        layers: this.getLayers().layers,\n      });\n    }\n  };\n\n  _rootSizeKey = \"\";\n  get rootSizeKey(): string {\n    return `${this.refRoot?.offsetWidth}.${this.refRoot?.offsetHeight}`;\n  }\n\n  onUnmount(): void {\n    this.deckW?.deck.finalize();\n    this.rootResizeObserver?.unobserve(this.refRoot!);\n  }\n\n  fitBounds = async () => {\n    const { projection } = this.props;\n    const dataExtent = await this.props.onGetFullExtent();\n    if (!dataExtent) {\n      return;\n    }\n\n    const limitedExtent: Bounds =\n      projection === \"orthographic\" ? dataExtent : (\n        [\n          [\n            Math.max(dataExtent[0][0], -179.9),\n            Math.max(dataExtent[0][1], -89.9),\n          ],\n          [Math.min(dataExtent[1][0], 179.9), Math.min(dataExtent[1][1], 89.9)],\n        ]\n      );\n\n    this.deckW?.zoomTo(limitedExtent);\n  };\n\n  viewStateDebounce: any;\n  onViewStateChange(viewState) {\n    const {\n      onMapStateChange,\n      mapStateChangeDebounce = vstateDebounce,\n      projection,\n    } = this.props;\n    const deckGlLibs = this.deckGlLibs;\n    if (!this.transitioning && onMapStateChange && viewState && deckGlLibs) {\n      if (this.viewStateDebounce) window.clearTimeout(this.viewStateDebounce);\n      this.viewStateDebounce = setTimeout(() => {\n        if (projection === \"orthographic\") {\n          onMapStateChange({\n            ...viewState,\n          });\n        } else {\n          const viewport = new deckGlLibs.lib.WebMercatorViewport(viewState);\n\n          const nw = viewport.unproject([0, 0]);\n          const se = viewport.unproject([viewport.width, viewport.height]);\n\n          const sw = [nw[0], se[1]],\n            ne = [se[0], nw[1]];\n\n          onMapStateChange({\n            ...viewState,\n            extent: [...sw, ...ne],\n          });\n        }\n\n        this.viewStateDebounce = null;\n      }, mapStateChangeDebounce);\n    }\n  }\n  transitioning?: boolean;\n  currHoverObject: any;\n  isHovering = false;\n  mouseDown = false;\n\n  getLayers = () => {\n    // const { basemap, dataOpacity = 1 } = this.state;\n    const {\n      geoJsonLayers = [],\n      tileURLs,\n      tileSize,\n      projection = \"mercator\",\n      basemapImage,\n      dataOpacity,\n      basemapDesaturate,\n      basemapOpacity,\n    } = this.props;\n    const { deckGlLibs } = this;\n    if (!deckGlLibs) return { layers: [], dataLayers: [], tileLayers: [] };\n\n    const dataLayers = geoJsonLayers.map(\n      (g) =>\n        new deckGlLibs.lib.GeoJsonLayer<GeoJSONFeature[\"properties\"]>({\n          id: g.id,\n          data: {\n            type: \"FeatureCollection\",\n            features: g.features,\n          },\n          /** Disabled due to bad experience (features missing) */\n          // extensions: [new deckGlLibs.extensions.CollisionFilterExtension()],\n          filled: true,\n\n          /**\n           * Radius of the circle in meters. If radiusUnits is not meters, this is converted from meters.\n           */\n          getPointRadius: (f) => (g.getIcon ? 0.1 : (f.properties.radius ?? 1)),\n          pointRadiusMinPixels: 2,\n          pointRadiusScale: 1,\n\n          extruded: Boolean(g.elevation),\n          getElevation: g.elevation || 0,\n\n          getFillColor: g.getFillColor, // ?? [200, 0, 80, 255],\n          getLineColor: g.getLineColor, // ?? [200, 0, 80, 255],\n          pointType: [\n            ...(g.getIcon ? (g.display ?? \"icon\").split(\"+\") : [\"circle\"]),\n            g.getText ? \"text\" : undefined,\n          ]\n            .filter(isDefined)\n            .join(\"+\"),\n          getText: g.getText,\n          getTextAlignmentBaseline: \"top\",\n          getTextPixelOffset: (f) => [0, 5],\n          getTextSize: g.getTextSize,\n          textCharacterSet: \"auto\",\n          /** For example, maxWidth: 10.0 used with getSize: 12 is roughly the equivalent of max-width: 120px in CSS. */\n          textMaxWidth: 10,\n\n          getIconColor: g.getFillColor,\n          getIcon: g.getIcon,\n          getIconPixelOffset: (f) => [0, -10],\n          getIconSize: g.getIcon && ((f) => g.getIcon!(f).width),\n          lineWidthMinPixels: 2,\n          //@ts-ignore\n          widthScale: 22,\n          lineWidth: (f) => f.properties?.lineWidth ?? 1,\n\n          pickable: true,\n          pickingRadius: 10,\n          autoHighlight: true,\n          onClick: g.onClick,\n          opacity: dataOpacity,\n          // material: {\n          //   ambient: 0.35,\n          //   diffuse: 0.6,\n          //   shininess: 32,\n          //   specularColor: [30, 30, 30]\n          // }\n        }),\n    );\n\n    const tileLayers =\n      !this.deckGlLibs ? []\n      : projection === \"mercator\" ?\n        [\n          makeTileLayer(\n            {\n              opacity: basemapOpacity,\n              desaturate: basemapDesaturate,\n              tileURLs,\n              tileSize,\n            },\n            this.deckGlLibs,\n          ),\n        ]\n      : basemapImage ?\n        [makeImageLayer({ ...basemapImage, deckGlLibs: this.deckGlLibs })]\n      : [];\n\n    const layers = [\n      ...tileLayers,\n      ...dataLayers,\n      ...(this.d.editedFeaturesLayer ? [this.d.editedFeaturesLayer] : []),\n    ];\n\n    return {\n      layers,\n      tileLayers,\n      dataLayers,\n      geoJsonLayers,\n    };\n  };\n  onRenderLayer = (editedFeaturesLayer: GeoJsonLayer<any, {}> | undefined) => {\n    this.setData({ editedFeaturesLayer });\n  };\n  deckW?: DeckWrapped;\n  render() {\n    const { deckW, deckGlLibs } = this;\n    return (\n      <div\n        className=\"relative flex-row f-1\"\n        style={{\n          overscrollBehavior: \"contain\",\n        }}\n        onMouseDown={() => {\n          this.mouseDown = true;\n        }}\n        onMouseUp={() => {\n          this.mouseDown = false;\n        }}\n      >\n        {deckW && deckGlLibs && (\n          <InMapControls\n            {...this.props}\n            fitBounds={this.fitBounds}\n            deckGlLibs={deckGlLibs}\n            deckW={deckW}\n            onRenderLayer={this.onRenderLayer}\n          />\n        )}\n        <div\n          ref={(e) => {\n            if (e) this.refRoot = e;\n          }}\n        ></div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/Map/DeckGLWrapped.ts",
    "content": "import type {\n  Deck,\n  DeckProps,\n  MapView,\n  MapViewState,\n  OrthographicView,\n  OrthographicViewState,\n  PickingInfo,\n  WebMercatorViewport,\n} from \"deck.gl\";\nimport { isDefined, omitKeys, pickKeys } from \"prostgles-types\";\nimport { createReactiveState } from \"../../appUtils\";\nimport type { HoverCoords } from \"./DeckGLMap\";\nimport { fitBounds } from \"./fitBounds\";\nexport const getDeckLibs = async () => {\n  const lib = await import(/* webpackChunkName: \"deckgl\" */ \"deck.gl\");\n  const mvtLoader = await import(\n    /* webpackChunkName: \"mvtLoader\" */ \"@loaders.gl/mvt\"\n  );\n  // const extensions = await import(/* webpackChunkName: \"deckglExtensions\" */ \"@deck.gl/extensions\");\n\n  return {\n    lib,\n    MVTLoader: mvtLoader.MVTLoader,\n    // extensions\n  };\n};\nexport type DeckGlLib = Awaited<ReturnType<typeof getDeckLibs>>[\"lib\"];\nexport type DeckGlLibs = Awaited<ReturnType<typeof getDeckLibs>>;\n\nexport type ViewState = MapViewState | OrthographicViewState;\ntype DeckWrappedOpts = (\n  | {\n      type: \"orthographic\";\n      initialViewState: OrthographicViewState | undefined;\n    }\n  | {\n      type: \"mercator\";\n      initialViewState: MapViewState | undefined;\n    }\n) &\n  Pick<DeckProps, \"onClick\" | \"onHover\" | \"layers\" | \"onLoad\"> & {\n    // | \"onViewStateChange\"\n    onViewStateChange: (params: ViewState, extent: Bounds) => any;\n    onHoverItem?: (\n      row: Record<string, any> | undefined,\n      coords: HoverCoords,\n    ) => void;\n    onClickQuick?: (info: PickingInfo) => void;\n  };\n\nexport class DeckWrapped {\n  readonly opts: DeckWrappedOpts;\n  lib: DeckGlLib;\n  private currHoverObjectStr?: string;\n\n  currHover?: PickingInfo;\n  currHoverRState = createReactiveState(this.currHover);\n  private transitioning = false;\n  node: HTMLDivElement;\n  deck: Deck<OrthographicView[] | MapView[]>;\n  constructor(node: HTMLDivElement, opts: DeckWrappedOpts, lib: DeckGlLib) {\n    this.lib = lib;\n    this.opts = opts;\n    this.node = node;\n    const { type, initialViewState } = this.opts;\n\n    this.deck = new lib.Deck({\n      ...(getViews({ type, lib, initialViewState }) as any),\n      parent: node,\n      controller: true,\n      ...omitKeys(opts, [\n        \"type\",\n        \"onHoverItem\",\n        \"onHover\",\n        \"onClickQuick\",\n        \"onViewStateChange\",\n        \"initialViewState\",\n      ]),\n      onHover:\n        opts.onHover || opts.onHoverItem || opts.onClickQuick ?\n          (info: PickingInfo, event) => {\n            this.currHover = info;\n            this.currHoverRState.set(info);\n            if (\n              opts.onHoverItem &&\n              JSON.stringify(info.object ?? {}) !== this.currHoverObjectStr\n            ) {\n              this.currHoverObjectStr = JSON.stringify(info.object ?? {});\n              opts.onHoverItem(info.object, {\n                x: info.x,\n                y: info.y,\n                coordinates: info.coordinate as [number, number],\n                screenCoordinates: info.pixel,\n              });\n            }\n            opts.onHover?.(info, event);\n          }\n        : undefined,\n      onViewStateChange: ({ viewState }) => {\n        if (this.transitioning) return;\n        this.onViewStateChangeDebounced(viewState);\n      },\n    });\n\n    if (opts.onClickQuick) {\n      node.addEventListener(\"pointerdown\", (ev) => {\n        if (!ev.target) return;\n        const bbox = (ev.target as HTMLDivElement).getBoundingClientRect(),\n          x = ev.clientX - bbox.x,\n          y = ev.clientY - bbox.y,\n          maxDist = 4;\n        if (\n          this.currHover &&\n          this.currHover.x - x < maxDist &&\n          this.currHover.y - y < maxDist\n        ) {\n          opts.onClickQuick?.(this.currHover);\n        }\n      });\n    }\n\n    this.onViewStateChangeDebounced = debounce(\n      this.onViewStateChangeDebounced.bind(this),\n      200,\n    );\n  }\n\n  onViewStateChangeDebounced = (initialViewState: ViewState) => {\n    const b = this.getExtent();\n    if (!b) return;\n    this.opts.onViewStateChange(initialViewState, b);\n  };\n\n  zoomTo = (\n    bounds:\n      | Bounds\n      | { type: \"point\"; latitude: number; longitude: number; zoom: number },\n  ) => {\n    const viewport = this.deck.getViewports()[0];\n    if (!viewport) return;\n\n    try {\n      let viewState: OrthographicViewState | MapViewState | undefined;\n      const OPTS = {\n        padding: 20,\n      };\n      if (this.opts.type !== \"orthographic\") {\n        const { longitude, latitude, zoom } =\n          !Array.isArray(bounds) ? bounds\n          : \"fitBounds\" in viewport ?\n            ((viewport as WebMercatorViewport).fitBounds(bounds, OPTS) as any)\n          : new this.lib.WebMercatorViewport({}).fitBounds(bounds, OPTS);\n        viewState = getViewState(this.opts.type, {\n          longitude,\n          latitude,\n          zoom: Math.min(zoom, 15),\n        });\n      } else {\n        if (!Array.isArray(bounds)) {\n          throw \"Unexpected\";\n        }\n        viewState = {\n          ...fitBounds({\n            padding: 50,\n            bounds,\n            width: viewport.width,\n            height: viewport.height,\n          }),\n        };\n      }\n\n      const initialViewState: MapViewState | OrthographicViewState = {\n        ...viewState,\n        transitionInterpolator:\n          this.opts.type !== \"orthographic\" ?\n            new this.lib.FlyToInterpolator({ speed: 2 })\n          : new this.lib.LinearInterpolator([\"target\", \"zoom\"]),\n        transitionDuration: 800,\n        onTransitionStart: () => {\n          this.transitioning = true;\n        },\n        onTransitionEnd: () => {\n          this.transitioning = false;\n          this.onViewStateChangeDebounced(initialViewState);\n        },\n      };\n\n      this.deck.setProps({ initialViewState } as any);\n    } catch (err) {\n      this.transitioning = false;\n      console.log(err);\n    }\n  };\n\n  /** Used in getting data */\n  getExtent = (): Bounds | undefined => {\n    const b = this.deck.getViewports()[0]?.getBounds();\n    if (!b) return undefined;\n    return [b.slice(0, 2) as any, b.slice(2) as any];\n  };\n\n  render(props: DeckProps<OrthographicView[] | MapView[]>) {\n    if (!this.deck.isInitialized) {\n      return;\n    }\n    const canvas = this.node.querySelector(\"canvas\");\n    if (canvas) {\n      canvas._deckgl = this.deck;\n    }\n    const currViews = this.deck.getViews().map((l) => l.constructor.name);\n    const curr2D = currViews.includes(\"OrthographicView\");\n    const nextViews = props.layers\n      ?.map((l) => l?.constructor.name)\n      .filter(isDefined);\n    const next2D = nextViews?.includes(\"BitmapLayer\");\n    if (currViews.length && curr2D !== next2D) {\n      const type = next2D ? \"orthographic\" : \"mercator\";\n      this.deck.setProps({\n        ...props,\n        ...getViews({\n          type,\n          lib: this.lib,\n          initialViewState: this.opts.initialViewState,\n        }),\n        layers: props.layers,\n      } as any);\n    } else {\n      this.deck.setProps(props);\n    }\n  }\n}\n\nexport type Bounds = [[number, number], [number, number]];\n\nexport function debounce<Params extends any[]>(\n  func: (...args: Params) => any,\n  timeout: number,\n): (...args: Params) => void {\n  let timer: NodeJS.Timeout;\n  return (...args: Params) => {\n    clearTimeout(timer);\n    timer = setTimeout(() => {\n      func(...args);\n    }, timeout);\n  };\n}\n\nexport const getViewState = <Type extends ViewType>(\n  type: Type,\n  state?: Partial<DeckWrappedOpts[\"initialViewState\"]>,\n): Type extends \"orthographic\" ? OrthographicViewState : MapViewState => {\n  if (type === \"orthographic\") {\n    const initialViewState: OrthographicViewState = {\n      target: state?.[\"target\"] ?? [1, 1, 0],\n      zoom: state?.[\"zoom\"] ?? 0,\n    };\n    return initialViewState as any;\n  }\n\n  const initialViewState: MapViewState = {\n    latitude: state?.[\"latitude\"] ?? 0,\n    longitude: state?.[\"longitude\"] ?? 51,\n    zoom: typeof state?.[\"zoom\"] === \"number\" ? state[\"zoom\"] : 0,\n    ...(state && pickKeys(state as any, [\"bearing\", \"pitch\", \"extent\"], true)),\n  };\n  return initialViewState as any;\n};\n\ntype ViewType = DeckWrappedOpts[\"type\"];\ntype GetViewsResult<T extends ViewType> = {\n  views: T extends \"orthographic\" ? OrthographicView[] : MapView[];\n  initialViewState: T extends \"orthographic\" ? OrthographicViewState\n  : MapViewState;\n};\n\nconst getViews = <T extends ViewType>({\n  type,\n  initialViewState: ivs,\n  lib,\n}: {\n  type: T;\n  lib: DeckGlLib;\n  initialViewState: OrthographicViewState | MapViewState | undefined;\n}): GetViewsResult<T> => {\n  if (type === \"orthographic\") {\n    const views = [\n      new lib.OrthographicView({\n        id: \"2d-scene\",\n        controller: true,\n        flipY: false,\n      }),\n    ];\n\n    const initialViewState = getViewState(type, ivs);\n    return { views, initialViewState } as GetViewsResult<T>;\n  }\n\n  const initialViewState = getViewState(type, ivs);\n  const views = [new lib.MapView({})];\n  return { views, initialViewState } as GetViewsResult<T>;\n};\n"
  },
  {
    "path": "client/src/dashboard/Map/InMapControls.tsx",
    "content": "import React, { useCallback, useEffect, useState } from \"react\";\n\nimport { mdiImageFilterCenterFocus, mdiTargetVariant } from \"@mdi/js\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport { MapExtentBehavior, type DecKGLMapProps } from \"./DeckGLMap\";\nimport { DeckGLFeatureEditor } from \"./DeckGLFeatureEditor\";\nimport type { DeckGlLibs, DeckWrapped } from \"./DeckGLWrapped\";\nimport type { GeoJsonLayer } from \"deck.gl\";\n\ntype P = Pick<\n  DecKGLMapProps,\n  \"tileAttribution\" | \"onOptionsChange\" | \"topLeftContent\" | \"options\" | \"edit\"\n> & {\n  deckGlLibs: DeckGlLibs;\n  fitBounds: VoidFunction;\n  deckW: DeckWrapped;\n  onRenderLayer: (layer: GeoJsonLayer<any, {}> | undefined) => void;\n};\nexport const InMapControls = ({\n  tileAttribution,\n  topLeftContent,\n  onOptionsChange,\n  options,\n  deckGlLibs,\n  edit,\n  fitBounds,\n  deckW,\n  onRenderLayer,\n}: P) => {\n  const [isDrawing, setIsDrawing] = useState(false);\n  const [showCursorCoords, setShowCursorCoords] = useState(false);\n  const refCursor = React.useRef<HTMLDivElement | null>(null);\n  useEffect(() => {\n    if (!showCursorCoords) return;\n    const sub = deckW.currHoverRState.subscribe((hover) => {\n      if (!refCursor.current) return;\n      refCursor.current.innerText =\n        !hover?.coordinate ?\n          \"\"\n        : `${hover.coordinate.map((v) => v.toString().padStart(3, \"0\")).join(\"\\n\")}`;\n    });\n    return sub.unsubscribe;\n  }, [showCursorCoords, deckW.currHoverRState]);\n\n  const onRenderDrawnLayer = useCallback(\n    (editedFeaturesLayer: GeoJsonLayer<any, {}> | undefined) => {\n      onRenderLayer(editedFeaturesLayer);\n      setIsDrawing(!!editedFeaturesLayer);\n    },\n    [onRenderLayer],\n  );\n\n  return (\n    <>\n      {showCursorCoords && (\n        <div\n          ref={refCursor}\n          className=\"absolute bg-color-0 rounded p-p25\"\n          style={{ bottom: 0, left: 0, zIndex: 1 }}\n        />\n      )}\n\n      {tileAttribution?.title && (\n        <div\n          className=\"text-ellipsis noselect rounded font-14\"\n          style={{\n            position: \"absolute\",\n            right: 0,\n            bottom: 0,\n            maxHeight: \"1.5em\",\n            zIndex: 2, // Must be on top of autoZoom button\n            backdropFilter: \"blur(6px)\",\n            padding: \"4px\",\n          }}\n        >\n          <a href={tileAttribution.url} target=\"_blank\" rel=\"noreferrer\">\n            {tileAttribution.title}\n          </a>\n        </div>\n      )}\n\n      <FlexCol\n        className=\"MapTopLeftControls ai-start flex-col gap-1 absolute ai-center jc-center\"\n        style={{ top: \"1em\", left: \"1em\", zIndex: 1 }}\n      >\n        {topLeftContent}\n\n        <Btn\n          data-command=\"InMapControls.showCursorCoords\"\n          title=\"Show cursor coords\"\n          iconPath={mdiTargetVariant}\n          color={showCursorCoords ? \"action\" : undefined}\n          variant=\"faded\"\n          size=\"small\"\n          className=\"shadow\"\n          onClick={() => {\n            setShowCursorCoords(!showCursorCoords);\n          }}\n        />\n      </FlexCol>\n\n      <FlexRow\n        className=\"MapTopControls absolute jc-center\"\n        style={{ top: \"1em\", left: \"5em\", right: \"1em\", zIndex: 1 }}\n      >\n        {!isDrawing && (\n          <FlexRow className=\"in-map-hover-control mx-auto\">\n            <Select\n              title=\"Map extent behavior\"\n              data-command=\"MapExtentBehavior\"\n              fullOptions={MapExtentBehavior}\n              value={options.extentBehavior}\n              onChange={(extentBehavior) => {\n                onOptionsChange({ extentBehavior });\n              }}\n            />\n            <Btn\n              data-command=\"InMapControls.goToDataBounds\"\n              title=\"Zoom to data\"\n              iconPath={mdiImageFilterCenterFocus}\n              onClick={fitBounds}\n              className=\"shadow \"\n              variant=\"faded\"\n            />\n          </FlexRow>\n        )}\n\n        {edit && (\n          <DeckGLFeatureEditor\n            edit={edit}\n            deckW={deckW}\n            onRenderLayer={onRenderDrawnLayer}\n            deckGlLibs={deckGlLibs}\n          />\n        )}\n      </FlexRow>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/Map/fitBounds.ts",
    "content": "import type { OrthographicViewState } from \"deck.gl\";\nimport type { Bounds } from \"./DeckGLWrapped\";\n\nexport function fitBounds(options: {\n  width: number;\n  height: number;\n  padding: number;\n  bounds: Bounds;\n  minExtent?: number;\n  maxZoom?: number;\n  offset?: [number, number];\n}): OrthographicViewState {\n  const {\n    width = 1,\n    height = 1,\n    bounds,\n    minExtent = 0,\n    maxZoom = 24,\n    offset = [0, 0],\n  } = options;\n  const [[west, south], [east, north]] = bounds;\n  const padding = getPaddingObject(options.padding);\n  const nw = [west, north] as const; // lngLatToWorld([west, clamp(north, -MAX_LATITUDE, MAX_LATITUDE)]);\n  const se = [east, south] as const; // lngLatToWorld([east, clamp(south, -MAX_LATITUDE, MAX_LATITUDE)]);\n  const size = [\n    Math.max(Math.abs(se[0] - nw[0]), minExtent),\n    Math.max(Math.abs(se[1] - nw[1]), minExtent),\n  ] as const;\n  const targetSize = [\n    width - padding.left - padding.right - Math.abs(offset[0]) * 2,\n    height - padding.top - padding.bottom - Math.abs(offset[1]) * 2,\n  ] as const;\n\n  const scaleX = targetSize[0] / size[0];\n  const scaleY = targetSize[1] / size[1];\n  const offsetX = (padding.right - padding.left) / 2 / scaleX;\n  const offsetY = (padding.top - padding.bottom) / 2 / scaleY;\n  const center: [number, number] = [\n    (se[0] + nw[0]) / 2 + offsetX,\n    (se[1] + nw[1]) / 2 + offsetY,\n  ];\n\n  const zoom = Math.min(maxZoom, Math.log2(Math.abs(Math.min(scaleX, scaleY))));\n\n  return {\n    target: center,\n    zoom,\n    // extent: bounds\n  };\n}\n\nfunction getPaddingObject(padding = 0) {\n  if (typeof padding === \"number\") {\n    return {\n      top: padding,\n      bottom: padding,\n      left: padding,\n      right: padding,\n    };\n  }\n\n  return padding;\n}\n"
  },
  {
    "path": "client/src/dashboard/Map/mapDrawUtils.ts",
    "content": "import { mdiMapMarker, mdiShapePolygonPlus, mdiVectorPolyline } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport { pickKeys } from \"prostgles-types\";\nimport type { GeoJSONFeature } from \"./DeckGLMap\";\n// export type  { Feature } from \"@deck.gl-community/editable-layers/dist/geojson-types\";\n// export type { EditableGeojsonLayerProps } from \"@deck.gl-community/editable-layers/dist/editable-layers/editable-geojson-layer\";\n// import type { FeatureCollection, Geometry } from \"@deck.gl-community/editable-layers\";\n// export type { EditableGeoJsonLayer } from \"@deck.gl-community/editable-layers\";\n// export type { FeatureCollection, Geometry } from \"@deck.gl-community/editable-layers\";\n// export type { EditableGeojsonLayerProps } from \"./editable-layers/editable-layers/editable-geojson-layer\";\n// export type { FeatureCollection, Geometry, EditableGeoJsonLayer } from \"./editable-layers/index\";\n// export type { Feature } from \"./editable-layers/geojson-types\";\n// import type { FeatureCollection, Geometry } from \"./editable-layers/index\";\n\nconst ld = {\n  ModifyMode: 1,\n  EditableGeoJsonLayer: 1,\n  DrawPointMode: 1,\n  DrawLineStringMode: 1,\n  DrawPolygonMode: 1,\n  DrawPolygonByDraggingMode: 1,\n  DrawRectangleMode: 1,\n  DrawSquareMode: 1,\n  DrawEllipseByBoundingBoxMode: 1,\n};\n\nconst getNebulaLib = () => {\n  // const lib = await import(/* webpackChunkName: \"editable_layers\" */  \"@deck.gl-community/editable-layers\");\n  const lib = ld; // await import(/* webpackChunkName: \"editable_layers\" */  \"./editable-layers/index\");\n  return lib;\n};\n\nexport type NebulaLib = Awaited<ReturnType<typeof getNebulaLib>>;\n\ntype DrawModes = NebulaLib;\nconst MODE_KEYS = [\n  \"EditableGeoJsonLayer\",\n  \"DrawPointMode\",\n  \"DrawLineStringMode\",\n  \"DrawPolygonMode\",\n  \"DrawRectangleMode\",\n  \"DrawSquareMode\",\n  \"DrawPolygonByDraggingMode\",\n  \"DrawEllipseByBoundingBoxMode\",\n  \"ModifyMode\",\n] as const satisfies (keyof Partial<DrawModes>)[];\nexport type AllDrawModes = Pick<DrawModes, (typeof MODE_KEYS)[number]>;\n\nexport const DrawModes = {\n  DrawPointMode: { label: \"Point\", iconPath: mdiMapMarker },\n  DrawLineStringMode: { label: \"LineString\", iconPath: mdiVectorPolyline },\n  DrawPolygonMode: { label: \"Polygon\", iconPath: mdiShapePolygonPlus },\n  // DrawPolygonByDraggingMode: { label: \"Polygon Dragged\", iconPath: mdiShapePolygonPlus },\n  // DrawRectangleMode: { label: \"Rectangle\", iconPath: mdiRectangleOutline },\n  // DrawSquareMode: { label: \"Square\", iconPath: mdiSquareOutline },\n  // DrawEllipseByBoundingBoxMode: { label: \"Ellipse\", iconPath: mdiEllipseOutline },\n} as const satisfies Partial<\n  Record<\n    keyof AllDrawModes,\n    {\n      label: string;\n      iconPath: string;\n    }\n  >\n>;\n\nexport const useDrawModes = () => {\n  const res = usePromise(async () => {\n    // const lib = await import(\"@deck.gl-community/editable-layers\");\n    const lib = await getNebulaLib();\n\n    const modes = pickKeys(lib, MODE_KEYS);\n\n    return { DrawModes, modes };\n  }, []);\n\n  return res;\n};\n\nexport const NEBULA_GL_EDIT_TYPES = [\n  \"updateTentativeFeature\",\n  \"movePosition\", //: A position was moved.\n  \"addPosition\", //: A position was added (either at the beginning, middle, or end of a feature's coordinates).\n  \"removePosition\", //: A position was removed. Note: it may result in multiple positions being removed in order to maintain valid GeoJSON (e.g. removing a point from a triangular hole will remove the hole entirely).\n  \"addFeature\", //: A new feature was added. Its index is reflected in featureIndexes\n  \"finishMovePosition\", //: A position finished moving (e.g. user finished dragging).\n  \"scaling\", //: A feature is being scaled.\n  \"scaled\", //: A feature finished scaling (increase/decrease) (e.g. user finished dragging).\n  \"rotating\", //: A feature is being rotated.\n  \"rotated\", //: A feature finished rotating (e.g. user finished dragging).\n  \"translating\", //: A feature is being translated.\n  \"translated\", //: A feature finished translating (e.g. user finished dragging).\n  \"startExtruding\", //: An edge started extruding (e.g. user started dragging).\n  \"extruding\", //: An edge is extruding.\n  \"extruded\", //: An edge finished extruding (e.g. user finished dragging).\n  \"split\", //: A feature finished splitting.\n  \"cancelFeature\",\n] as const;\n\nexport const geometryToGeoEWKT = (\n  f: GeoJSONFeature[\"geometry\"],\n  srid?: number,\n) => {\n  if (f.type === \"GeometryCollection\") {\n    return {\n      $ST_GeomFromEWKT: [\n        `${srid ? `SRID=${srid};` : \"\"}GEOMETRYCOLLECTION(${f.geometries.map((g) => geometryToGeoEWKT(g, srid))})`,\n      ],\n    };\n  }\n  const { type, coordinates } = f;\n  const coordsToStr = (point: number[]) => point.join(\" \");\n  const coordListToStr = (line: number[][]) => line.map(coordsToStr).join(\", \");\n  const coordListsToStr = (lines: number[][][]) =>\n    lines.map((l) => `( ${coordListToStr(l)} )`).join(\", \");\n\n  let str = \"\";\n  if (type === \"LineString\") {\n    str = `${type}(${coordListToStr(coordinates)})`;\n  } else if (type === \"MultiLineString\" || type === \"Polygon\") {\n    str = `${type}(${coordListsToStr(coordinates)})`;\n  } else if (type === \"Point\") {\n    str = `${type}(${coordinates.join(\" \")})`;\n  }\n  if (type === \"MultiPolygon\") {\n    str = `${type}((${coordinates.map((c0) => coordListsToStr(c0))}))`;\n  }\n  return { $ST_GeomFromEWKT: [`${srid ? `SRID=${srid};` : \"\"}${str}`] };\n};\n"
  },
  {
    "path": "client/src/dashboard/Map/mapUtils.ts",
    "content": "import type { Extent } from \"./DeckGLMap\";\nimport type { DeckGlLibs } from \"./DeckGLWrapped\";\nimport type { TileLayer, TileLayerProps } from \"deck.gl\";\n\nexport const DEFAULT_TILE_URLS = [\n  // 'http://{s}.tile.stamen.com/watercolor/{z}/{x}/{y}.jpg'\n  // http://stamen-tiles-c.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg\n\n  \"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png\",\n  \"https://b.tile.openstreetmap.org/{z}/{x}/{y}.png\",\n  \"https://c.tile.openstreetmap.org/{z}/{x}/{y}.png\",\n];\n\nexport function makeTileLayer(\n  {\n    opacity = 1,\n    desaturate = 22,\n    tileURLs = DEFAULT_TILE_URLS,\n    onTilesLoad = undefined,\n    showBorder = false,\n    tileSize = 256, // 256 / devicePixelRatio; // or 512\n\n    asMVT = false,\n  } = {},\n  deckGlLibs: DeckGlLibs,\n): TileLayer {\n  if (\n    !Array.isArray(tileURLs) ||\n    !tileURLs.find((url) => typeof url === \"string\")\n  ) {\n    tileURLs = DEFAULT_TILE_URLS;\n  }\n\n  if (tileURLs.some((url) => url.endsWith(\".pbf\") || url.endsWith(\".mvt\"))) {\n    return new deckGlLibs.lib.MVTLayer({\n      id: \"basemap\",\n      data: tileURLs,\n      loaders: [deckGlLibs.MVTLoader],\n      loadOptions: {\n        mvt: {\n          // cp node_modules/@loaders.gl/mvt/dist/mvt-worker.js static/mvt-worker.js\n          // Added file to express static\n          workerUrl: \"/mvt-worker.js\",\n        },\n      },\n      minZoom: 0,\n      maxZoom: 14,\n      getFillColor: (f) => {\n        switch (f.properties.layerName) {\n          case \"poi\":\n            return [255, 0, 0];\n          case \"water\":\n            return [120, 150, 180];\n          case \"building\":\n            return [218, 218, 218];\n          default:\n            return [240, 240, 240];\n        }\n      },\n      getLineWidth: (f) => {\n        switch (f.properties.class) {\n          case \"street\":\n            return 6;\n          case \"motorway\":\n            return 10;\n          default:\n            return 1;\n        }\n      },\n      getLineColor: [192, 192, 192],\n      getPointRadius: 2,\n      pointRadiusUnits: \"pixels\",\n      stroked: false,\n      // picking: true\n    });\n  }\n\n  return new deckGlLibs.lib.TileLayer({\n    id: \"basemap\",\n    TilesetClass: deckGlLibs.lib._Tileset2D,\n    opacity,\n\n    desaturate,\n    transparentColor: [255, 255, 255, 255],\n\n    /**\n     * https://www.trailnotes.org/FetchMap/TileServeSource.html\n     * \n        https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{Z}/{Y}/{X}.jpg\n     */\n\n    // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers\n    data: tileURLs,\n\n    // Since these OSM tiles support HTTP/2, we can make many concurrent requests\n    // and we aren't limited by the browser to a certain number per domain.\n    maxRequests: 20,\n\n    pickable: true,\n    onViewportLoad: onTilesLoad,\n    autoHighlight: showBorder,\n    highlightColor: [60, 60, 60, 40],\n    // https://wiki.openstreetmap.org/wiki/Zoom_levels\n    minZoom: 0,\n    maxZoom: 19,\n    tileSize,\n\n    renderSubLayers: (props) => {\n      const {\n        bbox: { west, south, east, north },\n      } = props.tile as { bbox: any };\n\n      return [\n        new deckGlLibs.lib.BitmapLayer(props as any, {\n          data: undefined, // null\n          image: props.data as any,\n          bounds: [west, south, east, north],\n        }),\n        showBorder &&\n          new deckGlLibs.lib.PathLayer({\n            id: `${props.id}-border`,\n            visible: props.visible,\n            data: [\n              [\n                [west, north],\n                [west, south],\n                [east, south],\n                [east, north],\n                [west, north],\n              ],\n            ],\n            getPath: (d) => d,\n            getColor: [255, 0, 0],\n            widthMinPixels: 4,\n          }),\n      ];\n    },\n  } as TileLayerProps);\n}\n\ntype MakeImageLayerArgs = {\n  url: string;\n  bounds: Extent;\n  opacity?: number;\n  desaturate?: number;\n  sharpImage?: boolean;\n  deckGlLibs: DeckGlLibs;\n};\nexport function makeImageLayer({\n  bounds,\n  url,\n  opacity = 1,\n  desaturate = 0,\n  sharpImage = false,\n  deckGlLibs,\n}: MakeImageLayerArgs) {\n  //@ts-ignore\n  const { GL } = deckGlLibs.luma;\n  return new deckGlLibs.lib.BitmapLayer({\n    opacity,\n    transparentColor: [255, 255, 255, 255],\n    desaturate,\n    id: \"bitmap-layer\",\n    bounds,\n    image: url,\n\n    /** To remove smoothing and achieve a pixelated appearance: */\n    textureParameters:\n      !sharpImage ? undefined : (\n        {\n          [GL.TEXTURE_MIN_FILTER]: GL.NEAREST,\n          [GL.TEXTURE_MAG_FILTER]: GL.NEAREST,\n        }\n      ),\n\n    coordinateSystem: deckGlLibs.lib.COORDINATE_SYSTEM.CARTESIAN,\n  });\n}\n"
  },
  {
    "path": "client/src/dashboard/RTComp.tsx",
    "content": "import React from \"react\";\n\nexport type DeepPartial<T> =\n  T extends any[] ? T\n  : T extends Record<string, any> ?\n    {\n      [P in keyof T]?: DeepPartial<T[P]>;\n    }\n  : T;\n\ntype CombinedPartial<P, S, D> = Partial<\n  DeltaOf<P> & DeltaOf<S> & DeltaOfData<D>\n>;\n\nexport type DeltaOfData<T> = T | DeepPartial<T> | undefined;\nexport type DeltaOf<T> = Partial<T> | undefined;\n\nexport default class RTComp<\n  P = {},\n  S = {},\n  D = Record<string, any>,\n> extends React.Component<P, S, D> {\n  mounted = false;\n  // private syncs: { [key: string]: any };\n  state: S = {} as S;\n\n  private p?: P;\n  s?: S;\n  d: Partial<D> = {} as D;\n\n  async componentDidMount() {\n    this.mounted = true;\n    try {\n      await this.onMount();\n    } catch (e) {\n      this.logAsyncErr(\"onMount\", e);\n    }\n    try {\n      this.setDeltaPS({ ...this.props }, { ...this.state });\n    } catch (e) {\n      this.logAsyncErr(\"setDeltaPS\", e);\n    }\n  }\n  onMount() {\n    //empty\n  }\n\n  componentWillUnmount = () => {\n    this.mounted = false;\n    this.onUnmount();\n  };\n\n  /**\n   * Should be used instead componentWillUnmount ensure this.mounted works as expected\n   */\n  onUnmount() {\n    //empty\n  }\n  componentDidUpdate = async (prevProps, prevState) => {\n    try {\n      let deltaP, deltaS;\n      Object.keys({ ...prevProps, ...this.props }).map((key) => {\n        if (prevProps[key] !== this.props[key]) {\n          deltaP = { ...deltaP, ...{ [key]: this.props[key] } };\n        }\n      });\n      Object.keys({ ...prevState, ...this.state }).map((key) => {\n        if (prevState[key] !== this.state[key]) {\n          deltaS = { ...deltaS, ...{ [key]: this.state[key] } };\n        }\n      });\n\n      if (deltaS || deltaP) {\n        await this.setDeltaPS(deltaP, deltaS);\n      }\n      await this.onUpdated(prevProps, prevState);\n    } catch (e) {\n      this.logAsyncErr(\"onUpdated\", e);\n    }\n  };\n  onUpdated(prevProps: P, prevState: S) {\n    //empty\n  }\n\n  setData = (deltaD: DeepPartial<D>, deepDeltaD?: DeepPartial<D>) => {\n    this.d = { ...this.d, ...deltaD };\n    try {\n      this._onDelta(undefined, undefined, deepDeltaD || deltaD);\n    } catch (e) {\n      this.logAsyncErr(\"setData\", e);\n    }\n  };\n  private setDeltaPS = (deltaP?: Partial<P>, deltaS?: Partial<S>) => {\n    // this.p = { ...this.p, ...deltaP };\n    // this.s = { ...this.s, ...deltaS };\n    this._onDelta(deltaP, deltaS, undefined);\n  };\n\n  private logAsyncErr = (methodName: string, e: any) => {\n    console.error(\n      `Uncaught promise within ${methodName} of <${this.constructor.name}/> component`,\n      e,\n    );\n  };\n\n  private _onDelta = (\n    deltaP?: DeltaOf<P>,\n    deltaS?: DeltaOf<S>,\n    deltaD?: DeltaOfData<D>,\n  ) => {\n    (async () => {\n      try {\n        await this.onDelta(deltaP, deltaS, deltaD);\n        const combinedDeltas: CombinedPartial<P, S, D> = {\n          ...deltaP,\n          ...deltaS,\n          ...deltaD,\n        } as any;\n        await this.onDeltaCombined(\n          combinedDeltas,\n          Object.keys(combinedDeltas as any) as (keyof CombinedPartial<\n            P,\n            S,\n            D\n          >)[],\n        );\n      } catch (e) {\n        const error = e instanceof Error ? e : new Error(e as any);\n        this.logAsyncErr(\"onDelta\", e);\n        throw error;\n      }\n    })();\n  };\n  /**\n   * Here we can use setState for anything that should trigger a render\n   */\n  onDelta(deltaP: DeltaOf<P>, deltaS: DeltaOf<S>, deltaD: DeltaOfData<D>) {\n    //empty\n  }\n  /** Helper func */\n  onDeltaCombined(\n    delta: CombinedPartial<P, S, D>,\n    deltaKeys: (keyof CombinedPartial<P, S, D>)[],\n  ) {\n    //empty\n  }\n\n  setState<K extends keyof S>(\n    state:\n      | ((prevState: Readonly<S>, props: Readonly<P>) => Pick<S, K> | null)\n      | (Pick<S, K> | null),\n    callback?: () => void,\n  ): void {\n    if (this.mounted) {\n      try {\n        return super.setState(state, callback);\n      } catch (e) {\n        console.error(\"setState error within \" + this.constructor.name, e);\n        throw e;\n      }\n    } else {\n      // console.error(\"setState called before mounting\")\n    }\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/RenderFilter.tsx",
    "content": "import type {\n  DetailedFilter,\n  GroupedDetailedFilter,\n} from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { mdiFilter } from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport type {\n  ContextDataSchema,\n  ForcedFilterControlProps,\n  SingleGroupFilter,\n} from \"./AccessControl/OptionControllers/FilterControl\";\nimport { SmartFilter, type SmartFilterProps } from \"./SmartFilter/SmartFilter\";\nimport type { ColumnConfig } from \"./W_Table/ColumnMenu/ColumnMenu\";\n\nexport type RenderFilterProps = {\n  filter: SingleGroupFilter | undefined;\n  onChange: (filter: SingleGroupFilter) => void;\n  contextData: ContextDataSchema | undefined;\n  title?: string;\n  mode?: \"micro\" | \"compact\" | \"minimised\";\n  itemName: \"filter\" | \"condition\";\n  selectedColumns: ColumnConfig[] | undefined;\n  hideOperand?: boolean;\n} & Pick<ForcedFilterControlProps, \"db\" | \"tableName\" | \"tables\">;\n\nexport const RenderFilter = (props: RenderFilterProps) => {\n  const {\n    filter: f = { $and: [] },\n    onChange,\n    contextData,\n    mode,\n    title = `Edit ${props.itemName}s`,\n    itemName,\n    db,\n    tableName,\n    tables,\n    selectedColumns,\n    hideOperand,\n  } = props;\n  const isAndOrFilter = \"$and\" in f || \"$or\" in f;\n  const minimised = mode && mode === \"minimised\";\n  const { filters, ...filterProps } = useMemo(() => {\n    const isAnd = \"$and\" in f;\n    const filters = isAnd ? f.$and : f.$or;\n    const simpleFilters = filters.filter(isSimpleFilter);\n    const groupFilters = filters.filter(isNotSimpleFilter);\n    return {\n      filters,\n      filterClassName:\n        /** Where was this needed? Both cases look better with the classes applied */\n        // (minimised ?? filters.some((f) => f.minimised)) ?\n        //   \" \"\n        // :\n        \" rounded  b b-action\",\n      operand: isAnd ? \"AND\" : \"OR\",\n      detailedFilter: simpleFilters,\n      onOperandChange: (operand) => {\n        onChange(operand === \"AND\" ? { $and: filters } : { $or: filters });\n      },\n      onChange: (newF) => {\n        const newFilters = [...newF, ...groupFilters];\n        if (isAnd) f.$and = newFilters;\n        else f.$or = newFilters;\n        onChange(f);\n      },\n    } satisfies Pick<\n      SmartFilterProps,\n      | \"filterClassName\"\n      | \"operand\"\n      | \"detailedFilter\"\n      | \"onOperandChange\"\n      | \"onChange\"\n    > & {\n      filters: DetailedFilter[];\n    };\n  }, [f, onChange]);\n\n  if (!isAndOrFilter) {\n    return <>Unexpected {itemName}. Expecting $and / $or</>;\n  }\n\n  const content = (showAddFilter?: boolean) => (\n    <>\n      <SmartFilter\n        type=\"where\"\n        itemName={itemName}\n        contextData={contextData}\n        variant={\n          minimised ? \"row\"\n          : window.isMobileDevice ?\n            undefined\n          : \"row\"\n        }\n        db={db}\n        tableName={tableName}\n        tables={tables}\n        selectedColumns={selectedColumns}\n        hideOperand={hideOperand}\n        {...filterProps}\n        newFilterType={contextData ? \"=\" : undefined}\n        hideToggle={true}\n        minimised={minimised}\n        showAddFilter={showAddFilter}\n        extraFilters={undefined}\n        showNoFilterInfoRow={true}\n      />\n    </>\n  );\n\n  if (!mode) {\n    return content();\n  }\n\n  if (mode === \"compact\") {\n    return content(true);\n  }\n\n  if (mode === \"minimised\") {\n    return content(false);\n  }\n\n  const filterIsNotEmpty = filters.some((f) => !f.disabled);\n\n  return (\n    <PopupMenu\n      title={title}\n      positioning=\"center\"\n      onClickClose={false}\n      button={\n        <Btn\n          title={title}\n          iconPath={mdiFilter}\n          variant=\"icon\"\n          data-command=\"RenderFilter.edit\"\n          color={filterIsNotEmpty ? \"action\" : undefined}\n        />\n      }\n      contentStyle={{\n        minWidth: \"400px\",\n      }}\n      fixedTopLeft={true}\n      clickCatchStyle={{ opacity: 0.5 }}\n      footerButtons={[\n        {\n          onClickClose: true,\n          color: \"action\",\n          variant: \"filled\",\n          label: \"Done\",\n          \"data-command\": \"RenderFilter.done\",\n          disabledInfo:\n            filters.some((f) => f.disabled) ?\n              `Some ${itemName}s are incomplete/disabled`\n            : undefined,\n        },\n      ]}\n    >\n      {content(true)}\n    </PopupMenu>\n  );\n};\n\nconst isSimpleFilter = (\n  f: DetailedFilter | GroupedDetailedFilter,\n): f is DetailedFilter => {\n  return !(\"$and\" in f || \"$or\" in f);\n};\nconst isNotSimpleFilter = (\n  f: DetailedFilter | GroupedDetailedFilter,\n): f is GroupedDetailedFilter => {\n  return !isSimpleFilter(f);\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/CommonMatchImports.ts",
    "content": "import { getKeys, isObject } from \"prostgles-types\";\nimport type { CodeBlock } from \"./completionUtils/getCodeBlock\";\nimport type {\n  MonacoSuggestion,\n  ParsedSQLSuggestion,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport type { IRange } from \"../../W_SQL/monacoEditorTypes\";\nimport type { TokenInfo } from \"./completionUtils/getTokens\";\nimport type { SQLSuggestion } from \"../W_SQLEditor\";\nconst asSQL = (v: string, lang = \"sql\") => \"```\" + lang + \"\\n\" + v + \"\\n```\";\n\nexport type MinimalSnippet = {\n  label: MonacoSuggestion[\"label\"];\n  insertText?: string;\n  sortText?: string;\n  docs?: MonacoSuggestion[\"documentation\"];\n  kind?: MonacoSuggestion[\"kind\"];\n  insertTextRules?: MonacoSuggestion[\"insertTextRules\"];\n  commitCharacters?: string[];\n};\n\n/**\n * Takes into account that non double-quoted names are case-insensitive\n */\nexport const nameMatches = (\n  s: Pick<SQLSuggestion, \"name\" | \"escapedName\">,\n  token: TokenInfo,\n) => {\n  const name = s.escapedName ?? s.name;\n  return (\n    name === token.text ||\n    (!name.startsWith(`\"`) && name.toLowerCase() === token.textLC)\n  );\n};\n\nexport const suggestSnippets = (\n  mSnippets: MinimalSnippet[],\n  opts?: { range?: IRange; cb?: CodeBlock },\n): { suggestions: ParsedSQLSuggestion[] } => {\n  const { range } = opts ?? {};\n  return {\n    suggestions: mSnippets.map(\n      ({\n        label,\n        insertText,\n        insertTextRules = 4,\n        docs,\n        sortText,\n        kind,\n        commitCharacters,\n      }) => {\n        const labelText = isObject(label) ? label.label : label;\n\n        return {\n          label,\n          name: labelText,\n          range: range ?? (undefined as unknown as IRange),\n          insertText: insertText ?? labelText, //(cb?.prevLC && !cb.prevText.endsWith(\" \")? \" \" : \"\") + (insertText ?? (labelText + \" \")),\n          documentation: typeof docs === \"string\" ? { value: docs } : docs,\n          kind: kind ?? 27, // 27 = snippet\n          insertTextRules,\n          ...(sortText && { sortText }),\n          commitCharacters,\n          type: \"snippet\",\n        };\n      },\n    ),\n  };\n};\n\n/* Known command-starting keywords. */\nexport const STARTING_KEYWORDS = [\n  \"ABORT\",\n  \"ALTER\",\n  \"ANALYZE\",\n  \"BEGIN\",\n  \"CALL\",\n  \"CHECKPOINT\",\n  \"CLOSE\",\n  \"CLUSTER\",\n  \"COMMENT\",\n  \"COMMIT\",\n  \"COPY\",\n  \"CREATE\",\n  \"DEALLOCATE\",\n  \"DECLARE\",\n  \"DELETE FROM\",\n  \"DISCARD\",\n  \"DO\",\n  \"DROP\",\n  \"END\",\n  \"EXECUTE\",\n  \"EXPLAIN\",\n  \"FETCH\",\n  \"GRANT\",\n  \"IMPORT FOREIGN SCHEMA\",\n  \"INSERT INTO\",\n  \"LISTEN\",\n  \"LOAD\",\n  \"LOCK\",\n  \"MERGE INTO\",\n  \"MOVE\",\n  \"NOTIFY\",\n  \"PREPARE\",\n  \"REASSIGN\",\n  \"REFRESH MATERIALIZED VIEW\",\n  \"REINDEX\",\n  \"RELEASE\",\n  \"RESET\",\n  \"REVOKE\",\n  \"ROLLBACK\",\n  \"SAVEPOINT\",\n  \"SECURITY LABEL\",\n  \"SELECT\",\n  \"SET\",\n  \"SHOW\",\n  \"START\",\n  \"TABLE\",\n  \"TRUNCATE\",\n  \"UNLISTEN\",\n  \"UPDATE\",\n  \"VACUUM\",\n  \"VALUES\",\n  \"WITH\",\n] as const;\n\nexport const PG_OBJECTS = [\n  \"ACCESS METHOD\", // NULL, NULL, NULL, NULL, THING_NO_ALTER},\n  \"AGGREGATE\", //  NULL, NULL, Query_for_list_of_aggregates},\n  \"CAST\", //  NULL, NULL, NULL}, /* Casts have complex structures for names, so * skip it */\n  \"COLLATION\", // NULL, NULL, &Query_for_list_of_collations},\n  \"PARAMETER\", // NULL, NULL, &Query_for_list_of_collations},\n\n  /*\n   * CREATE CONSTRAINT TRIGGER is not supported here because it is designed\n   * to be used only by pg_dump.\n   */\n  \"CONFIGURATION\", // NULL, NULL, &Query_for_list_of_ts_configurations, NULL, THING_NO_SHOW},\n  \"CONVERSION\", // \"SELECT conname FROM pg_catalog.pg_conversion WHERE conname LIKE '%s'\"},\n  \"DATABASE\", //  Query_for_list_of_databases},\n  \"DEFAULT PRIVILEGES\", // NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},\n  \"DICTIONARY\", //  NULL, NULL, &Query_for_list_of_ts_dictionaries, NULL, THING_NO_SHOW},\n  \"DOMAIN\", //  NULL, NULL, &Query_for_list_of_domains},\n  \"EVENT TRIGGER\", //  NULL, NULL, NULL},\n  \"EXTENSION\", //  Query_for_list_of_extensions},\n  \"FOREIGN DATA WRAPPER\", // NULL, NULL, NULL},\n  \"FOREIGN TABLE\", //  NULL, NULL, NULL},\n  \"FUNCTION\", // NULL, NULL, Query_for_list_of_functions},\n  \"GROUP\", //  Query_for_list_of_roles},\n  \"INDEX\", // NULL, NULL, &Query_for_list_of_indexes},\n  \"LANGUAGE\", //  Query_for_list_of_languages},\n  \"LARGE OBJECT\", //  NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},\n  \"MATERIALIZED VIEW\", // NULL, NULL, &Query_for_list_of_matviews},\n  \"OPERATOR\", // NULL, NULL, NULL}, /* Querying for this is probably not such * a good idea. */\n  \"OR REPLACE\", //  NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},\n  \"OWNED\", // NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_ALTER},\t/* for DROP OWNED BY ... */\n  \"PARSER\", // NULL, NULL, &Query_for_list_of_ts_parsers, NULL, THING_NO_SHOW},\n  \"POLICY\", //  NULL, NULL, NULL},\n  \"PROCEDURE\", //  NULL, NULL, Query_for_list_of_procedures},\n  \"PUBLICATION\", // NULL, Query_for_list_of_publications},\n  \"ROLE\", //  Query_for_list_of_roles},\n  \"ROUTINE\", // NULL, NULL, &Query_for_list_of_routines, NULL, THING_NO_CREATE},\n  \"RULE\", // \"SELECT rulename FROM pg_catalog.pg_rules WHERE rulename LIKE '%s'\"},\n  \"SCHEMA\", //  Query_for_list_of_schemas},\n  \"SEQUENCE\", // NULL, NULL, &Query_for_list_of_sequences},\n  \"SERVER\", // Query_for_list_of_servers},\n  \"STATISTICS\", // NULL, NULL, &Query_for_list_of_statistics},\n  \"SUBSCRIPTION\", //  NULL, Query_for_list_of_subscriptions},\n  \"SYSTEM\", // NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP},\n  \"TABLE\", // NULL, NULL, &Query_for_list_of_tables},\n  \"TABLESPACE\", // Query_for_list_of_tablespaces},\n  \"TEMP\", // NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},\t/* for CREATE TEMP TABLE * ... */\n  \"TEMPLATE\", // NULL, NULL, &Query_for_list_of_ts_templates, NULL, THING_NO_SHOW},\n  \"TEMPORARY\", //  NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},\t/* for CREATE TEMPORARY  * TABLE ... */\n  \"TEXT SEARCH\", //  NULL, NULL, NULL},\n  \"TRANSFORM\", //  NULL, NULL, NULL, NULL, THING_NO_ALTER},\n  \"TRIGGER\", // \"SELECT tgname FROM pg_catalog.pg_trigger WHERE tgname LIKE '%s' AND NOT tgisinternal\"},\n  \"TYPE\", // NULL, NULL, &Query_for_list_of_datatypes},\n  \"UNIQUE\", // NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE * INDEX ... */\n  \"UNLOGGED\", //  NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER},\t/* for CREATE UNLOGGED * TABLE ... */\n  \"USER\", // Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing},\n  \"USER MAPPING FOR\", //  NULL, NULL, NULL},\n  \"VIEW\", //  NULL, NULL, &Query_for_list_of_views},\n  \"FOREIGN SERVER\",\n] as const;\n\nexport const CREATE_OR_REPLACE = [\n  \"FUNCTION\",\n  \"PROCEDURE\",\n  \"LANGUAGE\",\n  \"VIEW\",\n  \"AGGREGATE\",\n  \"TRANSFORM\",\n  \"TRIGGER\",\n] as const;\n\nconst CREATE_POLICY_DOCS = `Define how a user interacts with rows within a given table\\n\\nNote that row-level security must be enabled on the table (using ALTER TABLE ... ENABLE ROW LEVEL SECURITY) in order for created policies to be applied.`;\n\nconst getVariations = (variations: string[], end: string): MinimalSnippet[] =>\n  variations.flatMap((v) => ({ label: v + \" ...\", insertText: v + \" \" + end }));\n\nexport const createStatements = {\n  TABLE: \"${1:table_name} (\\n  $0\\n);\",\n  VIEW: \"${1:view_name} AS\\n  SELECT \",\n  FUNCTION:\n    \"${1:new_func_name} (${2:input_arguments})\\n RETURNS VOID AS $$\\nBEGIN\\n\\n $3\\n\\nEND;\\n$$ LANGUAGE plpgsql;\",\n  TRIGGER: [\n    \"${1:trigger_name}\",\n    \"${2|AFTER,BEFORE,INSTEAD OF|} ${3|INSERT,UPDATE,DELETE|}\",\n    \"ON ${4:table_name}\",\n    \"FOR EACH ${5|ROW,STATEMENT|}\",\n    \"EXECUTE PROCEDURE ${6:trigger_func_name}();\",\n    \" \",\n    \"/* Example trigger function \",\n    \"\",\n    \"CREATE FUNCTION trigger_func_name()\",\n    \"  RETURNS TRIGGER AS $$\",\n    \"BEGIN\",\n    \"\",\n    \"  IF NEW.col <> OLD.col THEN \",\n    \"    ...\",\n    \"  END IF;\",\n    \"\",\n    \"  RETURN NEW;\",\n    \"\",\n    \"END;\",\n    \"$$ LANGUAGE plpgsql;\",\n    \"\",\n    \"*/\",\n  ].join(\"\\n\"),\n  \"EVENT TRIGGER\": [\n    \"${1:trigger_name} \\nON ${2|ddl_command_start,ddl_command_end,table_rewrite,sql_drop|}\",\n    \"EXECUTE FUNCTION ${3:function_name}();\",\n    \"\",\n    \"/* Example event trigger function:\",\n    \"CREATE OR REPLACE FUNCTION abort_any_command()\",\n    \"RETURNS event_trigger\",\n    \"LANGUAGE plpgsql\",\n    \"AS $$\",\n    \"BEGIN\",\n    \"  RAISE EXCEPTION 'command % is disabled', tg_tag;\",\n    \"END;\",\n    \"$$;\",\n    \"*/\",\n  ].join(\"\\n\"),\n};\n\nconst orReplace = <T extends { label: string }>(t: T): T[] => {\n  return [\n    t,\n    {\n      ...t,\n      label: `OR REPLACE ${t.label}`,\n    },\n  ];\n};\n\nexport const CREATE_SNIPPETS = [\n  ...getVariations([\"TABLE\", \"TABLE IF NOT EXISTS\"], createStatements.TABLE),\n\n  ...getVariations([\"VIEW\", \"MATERIALIZED VIEW\"], createStatements.VIEW),\n\n  ...CREATE_OR_REPLACE.flatMap((what) => `OR REPLACE ${what}`).map((label) => ({\n    label,\n    insertText: label + \" \",\n  })),\n\n  ...getVariations([\"FUNCTION\"], createStatements.FUNCTION),\n\n  ...getVariations([\"TRIGGER\"], createStatements.TRIGGER),\n\n  ...getVariations([\"EVENT TRIGGER\"], createStatements[\"EVENT TRIGGER\"]),\n\n  {\n    label: \"INDEX\",\n    insertText: \"INDEX\",\n    docs: `Constructs an index on the specified column(s) of the specified relation, which can be a table or a materialized view. Indexes are primarily used to enhance database performance (though inappropriate use can result in slower performance).`,\n  },\n\n  ...getVariations([\"EXTENSION\", \"EXTENSION IF NOT EXISTS\"], \"\"),\n\n  // {\n  //   label: \"PUBLICATION ...\",\n  //   insertText: \"PUBLICATION ${1:publication_name} \\nFOR TABLE ${2:table_name};\",\n  //   docs: \"Adds a new publication into the current database. The publication name must be distinct from the name of any existing publication in the current database. A publication is essentially a group of tables whose data changes are intended to be replicated through logical replication.\"\n  // },\n  {\n    label: \"SUBSCRIPTION ...\",\n    insertText:\n      \"CREATE SUBSCRIPTION ${1:sub_name}\\nCONNECTION 'host=localhost dbname=test_pub user=user password=password  application_name=sub1' \\nPUBLICATION ${2:pub_name};\",\n    docs: \"Adds a new logical-replication subscription. The subscription name must be distinct from the name of any existing subscription in the current database. A subscription represents a replication connection to the publisher. Hence, in addition to adding definitions in the local catalogs, this command normally creates a replication slot on the publisher. A logical replication worker will be started to replicate data for the new subscription at the commit of the transaction where this command is run, unless the subscription is initially disabled.\",\n  },\n  {\n    label: \"POLICY ...\",\n    insertText:\n      \"POLICY ${1:policy_name}\\nON$2\\nFOR$3\\nUSING($4 = CURRENT_USER)\",\n    docs: CREATE_POLICY_DOCS,\n  },\n  {\n    label: \"POLICY\",\n    docs: CREATE_POLICY_DOCS,\n  },\n  ...orReplace({\n    label: \"RULE\",\n    docs:\n      `A rule causes additional commands to be executed when a given command on a given table is executed.\\n\\n` +\n      asSQL(\n        `CREATE RULE \"_soft_delete\" \nAS ON DELETE \nTO \"users\" \nDO INSTEAD (\n  UPDATE users \n  SET deleted = true \n  WHERE id = OLD.id \n  AND NOT deleted\n);\n`,\n      ),\n  }),\n\n  {\n    label: \"ROLE\",\n    docs: `PostgreSQL manages database access permissions using the concept of roles. A role can be thought of as either a database user, or a group of database users, depending on how the role is set up. Roles can own database objects (for example, tables and functions) and can assign privileges on those objects to other roles to control who has access to which objects. Furthermore, it is possible to grant membership in a role to another role, thus allowing the member role to use privileges assigned to another role.`,\n  },\n  {\n    label: \"USER\",\n    docs: `A role with the LOGIN attribute`,\n  },\n\n  ...PG_OBJECTS.filter(\n    (v) =>\n      ![\n        \"POLICY\",\n        \"EXTENSION\",\n        \"MATERIALIZED VIEW\",\n        \"UNIQUE\",\n        \"INDEX\",\n        \"ROLE\",\n        \"USER\",\n        ...getKeys(createStatements),\n      ].includes(v),\n  ).map((label) => ({ label, insertText: label })),\n] satisfies MinimalSnippet[];\n\nexport const POLICY_FOR = [\n  {\n    label: \"ALL\",\n    docs: `Using ALL for a policy means that it will apply to all commands, regardless of the type of command. If an ALL policy exists and more specific policies exist, then both the ALL policy and the more specific policy (or policies) will be applied. Additionally, ALL policies will be applied to both the selection side of a query and the modification side, using the USING expression for both cases if only a USING expression has been defined.  As an example, if an UPDATE is issued, then the ALL policy will be applicable both to what the UPDATE will be able to select as rows to be updated (applying the USING expression), and to the resulting updated rows, to check if they are permitted to be added to the table (applying the WITH CHECK expression, if defined, and the USING expression otherwise). If an INSERT or UPDATE command attempts to add rows to the table that do not pass the ALL policy's WITH CHECK expression, the entire command will be aborted.`,\n  },\n  {\n    label: \"SELECT\",\n    docs: `Using SELECT for a policy means that it will apply to SELECT queries and whenever SELECT permissions are required on the relation the policy is defined for. The result is that only those records from the relation that pass the SELECT policy will be returned during a SELECT query, and that queries that require SELECT permissions, such as UPDATE, will also only see those records that are allowed by the SELECT policy. A SELECT policy cannot have a WITH CHECK expression, as it only applies in cases where records are being retrieved from the relation.`,\n  },\n  {\n    label: \"INSERT\",\n    docs: `Using INSERT for a policy means that it will apply to INSERT commands and MERGE commands that contain INSERT actions. Rows being inserted that do not pass this policy will result in a policy violation error, and the entire INSERT command will be aborted. An INSERT policy cannot have a USING expression, as it only applies in cases where records are being added to the relation. Note that INSERT with ON CONFLICT DO UPDATE checks INSERT policies' WITH CHECK expressions only for rows appended to the relation by the INSERT path.`,\n  },\n  {\n    label: \"UPDATE\",\n    docs: `Using UPDATE for a policy means that it will apply to UPDATE, SELECT FOR UPDATE and SELECT FOR SHARE commands, as well as auxiliary ON CONFLICT DO UPDATE clauses of INSERT commands. MERGE commands containing UPDATE actions are affected as well. Since UPDATE involves pulling an existing record and replacing it with a new modified record, UPDATE policies accept both a USING expression and a WITH CHECK expression. The USING expression determines which records the UPDATE command will see to operate against, while the WITH CHECK expression defines which modified rows are allowed to be stored back into the relation. Any rows whose updated values do not pass the WITH CHECK expression will cause an error, and the entire command will be aborted. If only a USING clause is specified, then that clause will be used for both USING and WITH CHECK cases. Typically an UPDATE command also needs to read data from columns in the relation being updated (e.g., in a WHERE clause or a RETURNING clause, or in an expression on the right hand side of the SET clause). In this case, SELECT rights are also required on the relation being updated, and the appropriate SELECT or ALL policies will be applied in addition to the UPDATE policies. Thus the user must have access to the row(s) being updated through a SELECT or ALL policy in addition to being granted permission to update the row(s) via an UPDATE or ALL policy. When an INSERT command has an auxiliary ON CONFLICT DO UPDATE clause, if the UPDATE path is taken, the row to be updated is first checked against the USING expressions of any UPDATE policies, and then the new updated row is checked against the WITH CHECK expressions. Note, however, that unlike a standalone UPDATE command, if the existing row does not pass the USING expressions, an error will be thrown (the UPDATE path will never be silently avoided).`,\n  },\n  {\n    label: \"DELETE\",\n    docs: `Using DELETE for a policy means that it will apply to DELETE commands. Only rows that pass this policy will be seen by a DELETE command. There can be rows that are visible through a SELECT that are not available for deletion, if they do not pass the USING expression for the DELETE policy. In most cases a DELETE command also needs to read data from columns in the relation that it is deleting from (e.g., in a WHERE clause or a RETURNING clause). In this case, SELECT rights are also required on the relation, and the appropriate SELECT or ALL policies will be applied in addition to the DELETE policies. Thus the user must have access to the row(s) being deleted through a SELECT or ALL policy in addition to being granted permission to delete the row(s) via a DELETE or ALL policy. A DELETE policy cannot have a WITH CHECK expression, as it only applies in cases where records are being deleted from the relation, so that there is no new row to check.`,\n  },\n];\n\nconst CommonCreateTales = {\n  users: [\n    \"id uuid not null primary key default gen_random_uuid()\",\n    \"email text not null unique\",\n    \"password text not null\",\n    \"created_at timestamp not null default now()\",\n    \"updated_at timestamp not null default now()\",\n  ],\n\n  sessions: [\n    \"id uuid not null primary key default gen_random_uuid()\",\n    \"user_id uuid not null references users(id) on delete cascade\",\n    \"token text not null\",\n    \"created_at timestamp not null default now()\",\n    \"updated_at timestamp not null default now()\",\n  ],\n\n  items: [\n    \"id uuid not null primary key default gen_random_uuid()\",\n    \"name text not null\",\n    \"description text not null\",\n    \"price int not null\",\n    \"image_url text not null\",\n    \"user_id uuid not null references users(id) on delete cascade\",\n    \"created_at timestamp not null default now()\",\n    \"updated_at timestamp not null default now()\",\n  ],\n\n  orders: [\n    \"id uuid not null primary key default gen_random_uuid()\",\n    \"user_id uuid not null references users on delete cascade\",\n    \"created_at timestamp not null default now()\",\n    \"updated_at timestamp not null default now()\",\n  ],\n\n  order_items: [\n    \"id uuid not null primary key default gen_random_uuid()\",\n    \"order_id uuid not null references orders(id) on delete cascade\",\n    \"item_id uuid not null references items(id) on delete cascade\",\n    \"quantity int not null\",\n    \"created_at timestamp not null default now()\",\n    \"updated_at timestamp not null default now()\",\n  ],\n\n  tags: [\n    \"id uuid not null primary key default gen_random_uuid()\",\n    \"name text not null\",\n    \"created_at timestamp not null default now()\",\n    \"updated_at timestamp not null default now()\",\n  ],\n\n  item_tags: [\n    \"id uuid not null primary key default gen_random_uuid()\",\n    \"item_id uuid not null references items(id) on delete cascade\",\n    \"tag_id uuid not null references tags(id) on delete cascade\",\n    \"created_at timestamp not null default now()\",\n    \"updated_at timestamp not null default now()\",\n  ],\n\n  stripe_customers: [\n    \"id uuid not null primary key default gen_random_uuid()\",\n    \"user_id uuid not null references users(id) on delete cascade\",\n    \"customer_id text not null\",\n    \"created_at timestamp not null default now()\",\n    \"updated_at timestamp not null default now()\",\n  ],\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/KEYWORDS.ts",
    "content": "import type { missingKeywordDocumentation } from \"../SQLEditorSuggestions\";\nimport { STARTING_KEYWORDS } from \"./CommonMatchImports\";\n\nexport type TopKeyword = {\n  label: string;\n  info?: string;\n  start_kwd: boolean;\n  priority?: number;\n  insertText: string | undefined;\n};\n\n/**\n * https://www.postgresql.org/docs/16/sql-commands.html\n */\nexport const STARTING_KWDS = [\n  \"SELECT\",\n  \"REVOKE\",\n  \"GRANT\",\n  \"VACUUM\",\n  \"ANALYZE\",\n  \"EXPLAIN\",\n  \"COPY\",\n  \"REINDEX\",\n  \"ROLLBACK\",\n  \"WITH\",\n  \"ALTER\",\n  \"SET\",\n  \"DO\",\n  \"BEGIN\",\n  \"CALL\",\n  \"COMMENT\",\n  \"DROP\",\n  \"CREATE\",\n  \"UPDATE\",\n  \"INSERT INTO\",\n  \"DELETE FROM\",\n  \"NOTIFY\",\n  \"LISTEN\",\n  \"SHOW\",\n  \"TRUNCATE\",\n  \"REASSIGN\",\n  \"CLUSTER\",\n  \"DELETE\",\n  \"INSERT\",\n  \"PREPARE\",\n  \"EXECUTE\",\n  \"RESET\",\n] as const;\n\nexport const asSQL = (v: string, lang = \"sql\") =>\n  \"```\" + lang + \"\\n\" + v + \"\\n```\";\nexport function getTopKeywords(): TopKeyword[] {\n  return (\n    STARTING_KEYWORDS\n  )\n    .map((label) => {\n      let info;\n      let start_kwd = STARTING_KWDS.includes(label as any);\n      let priority = 99;\n      let insertText: string | undefined;\n\n      if (label === \"REASSIGN\") {\n        priority = 0;\n        info = `REASSIGN OWNED instructs the system to change the ownership of database objects owned by any of the old_roles to new_role.\nhttps://www.postgresql.org/docs/current/sql-reassign-owned.html\n\n${asSQL(`REASSIGN OWNED BY CURRENT_ROLE TO new_role;`)}`;\n      } else if (label === \"TRUNCATE\") {\n        priority = 14;\n\n        info = `TRUNCATE quickly removes all rows from a set of tables. It has the same effect as an unqualified DELETE on each table, but since it does not actually scan the tables it is faster. \nFurthermore, it reclaims disk space immediately, rather than requiring a subsequent VACUUM operation. This is most useful on large tables.\n\nhttps://www.postgresql.org/docs/current/sql-truncate.html\n`;\n      } else if (label === \"CLUSTER\") {\n        priority = 16;\n\n        info = `CLUSTER instructs PostgreSQL to cluster the table specified by table_name based on the index specified by index_name. The index must already have been defined on table_name.\n\nWhen a table is clustered, it is physically reordered based on the index information. Clustering is a one-time operation: when the table is subsequently updated, the changes are not clustered. That is, no attempt is made to store new or updated rows according to their index order. (If one wishes, one can periodically recluster by issuing the command again. Also, setting the table's fillfactor storage parameter to less than 100% can aid in preserving cluster ordering during updates, since updated rows are kept on the same page if enough space is available there.)\n\nWhen a table is clustered, PostgreSQL remembers which index it was clustered by. The form CLUSTER table_name reclusters the table using the same index as before. You can also use the CLUSTER or SET WITHOUT CLUSTER forms of ALTER TABLE to set the index to be used for future cluster operations, or to clear any previous setting.\n\nCLUSTER without a table_name reclusters all the previously-clustered tables in the current database that the calling user owns, or all such tables if called by a superuser. This form of CLUSTER cannot be executed inside a transaction block.\n\nWhen a table is being clustered, an ACCESS EXCLUSIVE lock is acquired on it. This prevents any other database operations (both reads and writes) from operating on the table until the CLUSTER is finished.\nhttps://www.postgresql.org/docs/current/sql-cluster.html\n`;\n      } else if (label === \"PREPARE\") {\n        priority = 14;\n\n        info = `PREPARE creates a prepared statement. A prepared statement is a server-side object that can be used to optimize performance. When the PREPARE statement is executed, the specified statement is parsed, analyzed, and rewritten. When an EXECUTE command is subsequently issued, the prepared statement is planned and executed. This division of labor avoids repetitive parse analysis work, while allowing the execution plan to depend on the specific parameter values supplied.\n\nhttps://www.postgresql.org/docs/current/sql-prepare.html\n`;\n      } else if (label === \"EXECUTE\") {\n        priority = 14;\n\n        info = `EXECUTE is used to execute a previously prepared statement. Since prepared statements only exist for the duration of a session, the prepared statement must have been created by a PREPARE statement executed earlier in the current session.\n\nhttps://www.postgresql.org/docs/current/sql-execute.html\n`;\n      } else if (label === \"SELECT\") {\n        priority = 0;\n\n        info = `Retrieves data from tables, functions or other objects   \nhttps://www.postgresql.org/docs/current/sql-select.html\n\\`\\`\\`sql\nSELECT * FROM users;\n\nSELECT version();\n\nSELECT  f1, f2, MAX(f12)\nFROM table_name \nWHERE f1 = 2 \nGROUP BY col2\nHAVING MAX(f12) < 10\nORDER BY f1 DESC\nLIMIT 1\nOFFSET 2\n\n-- Create table from select\nCREATE TABLE archived_users AS\nSELECT * FROM users \n\\`\\`\\`\n`;\n      } else if (label === \"SHOW\") {\n        info = `Show the value of a run-time parameter. These variables can be set using the SET statement\n  https://www.postgresql.org/docs/current/sql-show.html\n\\`\\`\\`sql\nSHOW datestyle\n\nSHOW ALL\n\\`\\`\\`\n`;\n      } else if (label === \"TABLE\") {\n        start_kwd = true;\n        info = `\n\\`\\`\\`sql\n\\`\\`\\`\n`;\n      } else if (label === \"LISTEN\") {\n        info = `Listen to NOTIFY notifications on a channel\nhttps://www.postgresql.org/docs/current/sql-listen.html\n\\`\\`\\`sql\nLISTEN my_channel;\n\n/* To push a message */\nNOTIFY my_channel, 'hello';\n\\`\\`\\`\n`;\n      } else if (label === \"NOTIFY\") {\n        info = `Send a notification to a channel\nhttps://www.postgresql.org/docs/current/sql-notify.html\n\\`\\`\\`sql\nNOTIFY my_channel, 'hello';\n\n\n/* To listen to this channel */\nLISTEN my_channel;\n\\`\\`\\`\n`;\n      } else if (label === \"DELETE FROM\") {\n        priority = 4;\n\n        info = `Delete data from a table    \nhttps://www.postgresql.org/docs/current/sql-delete.html\n\\`\\`\\`sql\nDELETE FROM table_name;\n\nDELETE FROM users\nWHERE last_active < now() - interval'1 month'\nRETURNING *\n\\`\\`\\`\n`;\n      } else if (label === \"INSERT INTO\") {\n        priority = 1;\n\n        info = `Insert data into a table  \nhttps://www.postgresql.org/docs/current/sql-insert.html\n\\`\\`\\`sql\nINSERT INTO some_table \nDEFAULT VALUES;\n\nINSERT INTO users(age, name, added)\nVALUES(112, 'a', now())\nRETURNING *;\n\nINSERT INTO users(age, name, added)\nSELECT age, name, added\nFROM archived_users\n\\`\\`\\`\n`;\n      } else if (label === \"UPDATE\") {\n        priority = 2;\n\n        info = `Updates data in a table\nhttps://www.postgresql.org/docs/current/sql-update.html\n\\`\\`\\`sql\nUPDATE users SET status = 'active';\n\nUPDATE books \nSET title = books || 'active'\nWHERE title NOT ILIKE 'title%';\n\n\\`\\`\\`\n`;\n      } else if (label === \"CREATE\") {\n        priority = 3;\n\n        info = `Create an object\n\\`\\`\\`sql\nCREATE TABLE users(\n  age INTEGER,  \n  name TEXT,  \n  type TEXT,  \n  added TIMESTAMP  \n);\n\nCREATE OR REPLACE VIEW admin_users AS \n  SELECT * FROM users WHERE type = 'admin';\n\nCREATE OR REPLACE FUNCTION log_message(VARIADIC args TEXT[]) RETURNS VOID AS $func$    \nBEGIN\n\n  IF to_regclass('logs') IS NULL THEN\n    CREATE TABLE IF NOT EXISTS logs(m TEXT);\n  END IF;\n\n  INSERT INTO logs(m) \n  VALUES(concat_ws(' ', args));\n\nEND;\n$func$ LANGUAGE plpgsql;\n\n\\`\\`\\`\n`;\n      } else if (label === \"DROP\") {\n        priority = 5;\n\n        info = `Drop an object  \n\\`\\`\\`sql\nDROP TABLE users;\n\nDROP TABLE IF EXISTS books;\n\nDROP VIEW admin_users;\n\\`\\`\\`\n`;\n      } else if (label === \"COMMENT\") {\n        info = `Adds a comment to an object\nhttps://www.postgresql.org/docs/current/sql-comment.html\n\\`\\`\\`sql\nCOMMENT ON FUNCTION log_message \nIS 'Concat and insert arguments into logs table';\n\n--To view a table comment\nSELECT obj_description('table_name'::REGCLASS)\n\n--To view a column comment (1 is col position)\nSELECT col_description('public.dwadwad'::REGCLASS, 1)\n\n\\`\\`\\`\n`;\n      } else if (label === \"BEGIN\") {\n        info = `Initiates a transaction block where all statements will be executed in a single transaction until an explicit COMMIT or ROLLBACK is given\nhttps://www.postgresql.org/docs/current/sql-begin.html\n\\`\\`\\`sql\nBEGIN;\nUPDATE orders SET price = 2;\nCOMMIT;\n\nBEGIN ISOLATION LEVEL SERIALIZABLE;\nUPDATE orders SET price = 2;\nCOMMIT;\n\\`\\`\\`\n`;\n      } else if (label === \"CALL\") {\n        info = `invoke a procedure\n  https://www.postgresql.org/docs/current/sql-call.html\n  \\`\\`\\`sql\n  CALL my_procedure();\n  \\`\\`\\`\n  `;\n      } else if (label === \"DO\") {\n        insertText = `DO $$ \n  /* DECLARE some_var data_type; */\nBEGIN\n  $0\nEND $$;\n`;\n        info = `Execute an anonymous code block\nhttps://www.postgresql.org/docs/current/sql-do.html\n\\`\\`\\`sql\n${insertText}\n\\`\\`\\`\n`;\n      } else if (label === \"SET\") {\n        priority = 7;\n\n        info = `\n  \nChange a run-time parameter:\nhttps://www.postgresql.org/docs/current/sql-set.html  \n\n${asSQL(\"SET datestyle TO postgres, dmy;\")}  \n`;\n      } else if (label === \"RESET\") {\n        priority = 7;\n\n        info = `\nRestore the value of a run-time parameter to the default value \nhttps://www.postgresql.org/docs/current/sql-reset.html   \n\n${asSQL(\"RESET datestyle;\\n\\nRESET ALL;\")}  \n\n`;\n      } else if (label === \"ALTER\") {\n        priority = 7;\n\n        info = `Change an object\n\\`\\`\\`sql\nALTER TABLE users \nADD COLUMN deleted BOOLEAN;\n\nALTER DATABASE database_name \nSET datestyle TO \"ISO, DMY\";\n\n\\`\\`\\`\n`;\n      } else if (label === \"WITH\") {\n        priority = 6;\n\n        info = `Specifies a temporary named result set\nhttps://www.postgresql.org/docs/current/queries-with.html\n\\`\\`\\`sql\nWITH cte1 AS (SELECT * FROM orders)\n, cte2 AS (SELECT * FROM customers)\nSELECT *\nFROM cte1 c1\nINNER JOIN cte2 c2\n  ON ........\n\n\\`\\`\\`\n`;\n      } else if (label === \"ROLLBACK\") {\n        info = `rolls back the current transaction and causes all the updates made by the transaction to be discarded.\nhttps://www.postgresql.org/docs/9.4/sql-rollback.html\n\\`\\`\\`sql \nBEGIN;\nUPDATE orders SET price = 2;\nROLLBACK;\n\\`\\`\\`\n`;\n      } else if (label === \"REINDEX\") {\n        info = `Rebuild indexes\nhttps://www.postgresql.org/docs/current/sql-reindex.html\n\\`\\`\\`sql\nREINDEX TABLE CONCURRENTLY my_broken_table;\n\\`\\`\\`\n`;\n      } else if (label === \"COPY\") {\n        info = `Export or import data\nhttps://www.postgresql.org/docs/current/sql-copy.html\n\\`\\`\\`sql\n-- Export to csv\nCOPY my_table TO '/tmp/my_table.csv' \nWITH (FORMAT CSV, HEADER);\n\n-- Import from csv (Must create table beforehand)\nCOPY zip_codes \nFROM '/path/to/ZIP_CODES.txt' \nDELIMITER ',' CSV HEADER;\n\\`\\`\\`\n`;\n      } else if (label === \"VALUES\") {\n        info = `VALUES provides a way to generate a “constant table” that can be used in a query\nhttps://www.postgresql.org/docs/current/queries-values.html\n\\`\\`\\`sql\nINSERT INTO cities(name)\nVALUES ('London'), ('Vilnius');\n\\`\\`\\`\n`;\n      } else if (label === \"EXPLAIN\") {\n        priority = 10;\n        info = `Get query plan of a query\nhttps://www.postgresql.org/docs/current/sql-explain.html\n\\`\\`\\`sql\nEXPLAIN SELECT * FROM weather;\n\nEXPLAIN ANALYZE SELECT * FROM weather;\n\\`\\`\\`\n`;\n      } else if (label === \"ANALYZE\") {\n        priority = 14;\n\n        info = `\nCollects statistics about the contents of tables in the database, and stores the results in the pg_statistic system catalog. Subsequently, the query planner uses these statistics to help determine the most efficient execution plans for queries.\nhttps://www.postgresql.org/docs/current/sql-analyze.html\n\\`\\`\\`sql\nANALYZE (VERBOSE) my_table;\n\\`\\`\\``;\n      } else if (label === \"VACUUM\") {\n        priority = 11;\n\n        info = `\nGarbage-collect and optionally analyze a database\nhttps://www.postgresql.org/docs/current/sql-vacuum.html\n\\`\\`\\`sql\nVACUUM (VERBOSE, ANALYZE) my_database;\n\\`\\`\\``;\n      } else if (label === \"GRANT\") {\n        priority = 9;\n\n        info = `The GRANT command has two basic variants: \n  1. one that grants privileges on a database object (table, column, view, sequence, database, foreign-data wrapper, foreign server, function, procedural language, schema, or tablespace),\n  2. one that grants membership in a role\n  https://www.postgresql.org/docs/current/sql-grant.html\n\n\\`\\`\\`sql\nGRANT INSERT, UPDATE ON films TO PUBLIC;\n\\`\\`\\`\n`;\n      } else if (label === \"REVOKE\") {\n        priority = 10;\n\n        info = `The REVOKE command revokes previously granted privileges from one or more roles. The key word PUBLIC refers to the implicitly defined group of all roles.\nhttps://www.postgresql.org/docs/current/sql-revoke.html\n\n\\`\\`\\`sql\nREVOKE INSERT, UPDATE ON films TO PUBLIC;\n\n/* Revoke membership in role admins from user joe */\nREVOKE admins FROM joe;\n\n\\`\\`\\`\n`;\n      }\n\n      // garbage-collect and optionally analyze a database\n\n      return { label, info, start_kwd, priority, insertText };\n    })\n    .sort((a, b) => a.priority - b.priority);\n}\n\nexport const TOP_KEYWORDS = getTopKeywords();\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MacthCreate/MatchCreate.ts",
    "content": "import { getMonaco } from \"../../W_SQLEditor\";\nimport {\n  CREATE_OR_REPLACE,\n  CREATE_SNIPPETS,\n  PG_OBJECTS,\n  createStatements,\n  suggestSnippets,\n} from \"../CommonMatchImports\";\nimport {\n  getUserOpts,\n  matchCreateOrAlterUser,\n  ROLE_OPTIONS,\n} from \"../MatchAlter/matchCreateOrAlterUser\";\nimport { ENCODINGS } from \"../PSQL\";\nimport { getExpected } from \"../getExpected\";\nimport {\n  getKind,\n  type SQLMatcher,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport { withKWDs, type KWD } from \"../withKWDs\";\nimport { matchCreateView } from \"./MatchCreateView\";\nimport { matchCreateIndex } from \"./matchCreateIndex\";\nimport { matchCreatePolicy } from \"./matchCreatePolicy\";\nimport { matchCreateRule } from \"./matchCreateRule\";\nimport { getUserSchemaNames, matchCreateTable } from \"./matchCreateTable\";\nimport { matchCreateTrigger } from \"./matchCreateTrigger\";\n\nexport const MatchCreate: SQLMatcher = {\n  match: (cb) => {\n    return cb.textLC.startsWith(\"create\");\n  },\n  result: async (args) => {\n    const { cb, ss, setS, sql } = args;\n    if (\n      cb.textLC.startsWith(\"create trigger\") ||\n      cb.textLC === \"create or replace trigger\"\n    ) {\n      return matchCreateTrigger({ cb, ss, setS, sql });\n    }\n\n    if (\n      cb.textLC.startsWith(\"create index\") ||\n      cb.textLC === \"create or replace index\"\n    ) {\n      return matchCreateIndex({ cb, ss, setS, sql });\n    }\n\n    if (cb.textLC === \"create or\") {\n      return suggestSnippets([{ label: \"REPLACE\" }]);\n    }\n\n    const createViewStartingTexts = [\n      \"create view\",\n      \"create or replace view\",\n      \"create materialized view\",\n      \"create or replace materialized view\",\n      \"create recursive view\",\n      \"create or replace recursive view\",\n    ];\n    if (createViewStartingTexts.some((t) => cb.textLC.startsWith(t))) {\n      return matchCreateView(args);\n    }\n\n    const expect = cb.tokens[1]?.textLC;\n    const { ltoken, l1token, ftoken } = cb;\n    const f1token = cb.tokens[1];\n    const { prevLC, prevTokens } = cb;\n\n    if (expect === \"extension\") {\n      return {\n        suggestions: ss\n          .filter((s) => s.type === \"extension\")\n          .map((s) => ({\n            ...s,\n            sortText: !s.extensionInfo?.installed ? \"a\" : \"b\",\n          })),\n      };\n    }\n\n    if (ftoken?.textLC === \"create\" && f1token?.textLC === \"database\") {\n      if (ltoken?.textLC === \"database\") {\n        return suggestSnippets([{ label: \"$new_datebase_name\" }]);\n      }\n      if (l1token?.textLC === \"database\") {\n        return suggestSnippets([{ label: \"WITH\" }]);\n      }\n      const { getSuggestion } = withKWDs(CREATE_DB_KWDS, {\n        sql,\n        cb,\n        ss,\n        setS,\n        opts: { notOrdered: true },\n      });\n      return getSuggestion();\n    }\n\n    if (prevLC === \"create or replace\") {\n      return suggestSnippets(CREATE_OR_REPLACE.map((label) => ({ label })));\n    }\n\n    const monaco = await getMonaco();\n    const whatToReplace =\n      prevLC.startsWith(\"create or replace\") &&\n      PG_OBJECTS.filter((what) => prevLC.endsWith(what.toLowerCase())).sort(\n        (a, b) => b.length - a.length,\n      )[0];\n    if (whatToReplace) {\n      if ([\"FUNCTION\", \"PROCEDURE\"].includes(whatToReplace)) {\n        return {\n          suggestions: ss\n            .filter((s) => s.type === \"function\")\n            .map((s) => ({\n              ...s,\n              sortText: s.schema === \"public\" ? \"a\" : \"b\",\n              insertText: s.definition!.slice(27),\n              insertTextRules:\n                monaco.languages.CompletionItemInsertTextRule.None,\n            })),\n        };\n      } else if (whatToReplace === \"VIEW\") {\n        return {\n          suggestions: ss\n            .filter((s) => s.view && s.relkind === \"m\")\n            .map((s) => ({\n              ...s,\n              sortText: s.schema === \"public\" ? \"a\" : \"b\",\n              insertText: `${s.escapedIdentifier}  AS \\n ${s.view!.definition}`,\n              insertTextRules:\n                monaco.languages.CompletionItemInsertTextRule.None,\n            }))\n            .concat(\n              suggestSnippets([\n                { label: \"$name\", insertText: createStatements.VIEW },\n              ]).suggestions as any,\n            ),\n        };\n      } else {\n        whatToReplace;\n      }\n    }\n\n    if (prevLC === \"create function\" || prevLC === \"create procedure\") {\n      const userSchemas = getUserSchemaNames(ss);\n      const entity_name =\n        prevLC === \"create function\" ? \"$function_name\" : \"$procedure_name\";\n      return suggestSnippets(\n        [entity_name, ...userSchemas.map((s) => `${s}.${entity_name}`)].map(\n          (label) => ({ label }),\n        ),\n      );\n    }\n\n    if (\n      prevLC.startsWith(\"create rule\") ||\n      prevLC.startsWith(\"create or replace rule\")\n    ) {\n      return matchCreateRule(args);\n    }\n    if (prevLC.startsWith(\"create policy\")) {\n      return matchCreatePolicy(args);\n    }\n\n    const isInsideCreateTable = prevLC.startsWith(\"create table\");\n    if (isInsideCreateTable) {\n      return await matchCreateTable({ cb, setS, sql, ss });\n    }\n    if (prevLC.includes(\"create role\") || prevLC.includes(\"create user\")) {\n      return matchCreateOrAlterUser({ cb, ss, sql, setS });\n    }\n\n    if (cb.prevLC === \"create\") {\n      return suggestSnippets(\n        CREATE_SNIPPETS.map((s) => ({\n          ...s,\n          kind: getKind(\n            (s.label as string).split(\" \")[0]?.toLowerCase() as any,\n          ),\n        })),\n      );\n    } else if (\n      cb.prevTokens.length === 2 ||\n      (cb.prevLC.endsWith(\"exists\") && cb.ltoken?.type === \"operator.sql\")\n    ) {\n      return suggestSnippets([{ label: `$${expect}_name` }]);\n    } else if (ltoken?.type === \"identifier.sql\") {\n      const obj = ss.find((s) => s.name.includes(ltoken.text));\n\n      if (expect === \"function\" && obj?.definition) {\n        return suggestSnippets([\n          {\n            label: obj.definition.slice(\"CREATE OR REPLACE FUNCTION \".length),\n          },\n        ]);\n      }\n    }\n    const prevKwdToken = [...prevTokens]\n      .reverse()\n      .find((t) => t.type === \"keyword.sql\");\n    if (prevKwdToken?.textLC === \"where\") {\n      if (ltoken?.type === \"identifier.sql\")\n        return getExpected(\"operator\", cb, ss);\n\n      const identifiers = cb.tokens.filter((t) => t.type === \"identifier.sql\");\n      const cols = ss.filter(\n        (s) =>\n          s.type === \"column\" &&\n          identifiers.some((t) => t.text === s.parentName),\n      );\n      if (cols.length)\n        return {\n          suggestions: cols,\n        };\n      return getExpected(\"column\", cb, ss);\n    }\n\n    return { suggestions: [] };\n  },\n};\n\nconst LOCALES = [\"en_US.UTF-8\", \"C.UTF-8\"];\n\nconst CREATE_DB_KWDS = [\n  {\n    kwd: \"OWNER\",\n    expects: \"role\",\n    docs: \"The role name of the user who will own the new database, or DEFAULT to use the default (namely, the user executing the command). To create a database owned by another role, you must be a direct or indirect member of that role, or be a superuser.\",\n  },\n  {\n    kwd: \"TEMPLATE\",\n    expects: \"database\",\n    docs: \"The name of the template from which to create the new database, or DEFAULT to use the default template (template1).\",\n  },\n  {\n    kwd: \"ENCODING\",\n    options: ENCODINGS,\n    docs: `Character set encoding to use in the new database. Specify a string constant (e.g., 'SQL_ASCII'), or an integer encoding number, or DEFAULT to use the default encoding (namely, the encoding of the template database). The character sets supported by the PostgreSQL server are described in Section 24.3.1. See below for additional restrictions.`,\n  },\n  {\n    kwd: \"STRATEGY\",\n    options: [\"wal_log\", \"file_copy\"],\n    docs: `Strategy to be used in creating the new database. If the WAL_LOG strategy is used, the database will be copied block by block and each block will be separately written to the write-ahead log. This is the most efficient strategy in cases where the template database is small, and therefore it is the default. The older FILE_COPY strategy is also available. This strategy writes a small record to the write-ahead log for each tablespace used by the target database. Each such record represents copying an entire directory to a new location at the filesystem level. While this does reduce the write-ahead log volume substantially, especially if the template database is large, it also forces the system to perform a checkpoint both before and after the creation of the new database. In some situations, this may have a noticeable negative impact on overall system performance.`,\n  },\n  {\n    kwd: \"LOCALE\",\n    options: LOCALES,\n    docs: \"This is a shortcut for setting LC_COLLATE and LC_CTYPE at once.\",\n  },\n  {\n    kwd: \"LC_COLLATE\",\n    options: LOCALES,\n    docs: `Collation order (LC_COLLATE) to use in the new database. This affects the sort order applied to strings, e.g., in queries with ORDER BY, as well as the order used in indexes on text columns. The default is to use the collation order of the template database. See below for additional restrictions.`,\n  },\n  {\n    kwd: \"LC_CTYPE\",\n    options: LOCALES,\n    docs: \"Character classification (LC_CTYPE) to use in the new database. This affects the categorization of characters, e.g., lower, upper and digit. The default is to use the character classification of the template database. See below for additional restrictions.\",\n  },\n  {\n    kwd: \"ICU_LOCALE\",\n    options: LOCALES,\n    docs: \"Specifies the ICU locale ID if the ICU locale provider is used.\",\n  },\n  {\n    kwd: \"LOCALE_PROVIDER\",\n    options: [\"icu\", \"libc\"],\n    docs: `Specifies the provider to use for the default collation in this database. Possible values are: icu, libc. libc is the default. The available choices depend on the operating system and build options.`,\n  },\n  {\n    kwd: \"COLLATION_VERSION\",\n    expects: \"number\",\n    docs: `Specifies the collation version string to store with the database. Normally, this should be omitted, which will cause the version to be computed from the actual version of the database collation as provided by the operating system. This option is intended to be used by pg_upgrade for copying the version from an existing installation.`,\n  },\n  {\n    kwd: \"TABLESPACE\",\n    expects: \"schema\",\n    docs: `The name of the tablespace that will be associated with the new database, or DEFAULT to use the template database's tablespace. This tablespace will be the default tablespace used for objects created in this database. See CREATE TABLESPACE for more information.`,\n  },\n  {\n    kwd: \"ALLOW_CONNECTIONS\",\n    options: [\"10\", \"20\"],\n    docs: `If false then no one can connect to this database. The default is true, allowing connections (except as restricted by other mechanisms, such as GRANT/REVOKE CONNECT).`,\n  },\n  {\n    kwd: \"CONNECTION LIMIT\",\n    options: [\"10\", \"20\", \"100\", \"200\"],\n    docs: `How many concurrent connections can be made to this database. -1 (the default) means no limit.`,\n  },\n  {\n    kwd: \"IS_TEMPLATE\",\n    options: [\"TRUE\", \"FALSE\"],\n    docs: `If true, then this database can be cloned by any user with CREATEDB privileges; if false (the default), then only superusers or the owner of the database can clone it.`,\n  },\n  { kwd: \"OID\", expects: \"number\" },\n] as const;\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MacthCreate/MatchCreateView.ts",
    "content": "import { getMonaco } from \"../../W_SQLEditor\";\nimport { suggestSnippets, type MinimalSnippet } from \"../CommonMatchImports\";\nimport { MatchSelect } from \"../MatchSelect\";\nimport type { SQLMatcherResultArgs } from \"../monacoSQLSetup/registerSuggestions\";\nimport { withKWDs, type KWD } from \"../withKWDs\";\nimport { getUserSchemaNames } from \"./matchCreateTable\";\n\nexport const matchCreateView = async (args: SQLMatcherResultArgs) => {\n  const { cb, ss, setS, sql } = args;\n\n  if (cb.prevTokens.some((t) => t.textLC === \"select\")) {\n    return MatchSelect.result({ cb, ss, setS, sql });\n  }\n  if (cb.prevLC.endsWith(\" as\") && cb.prevTokens.length < 5) {\n    return suggestSnippets([{ label: \"SELECT\" }]);\n  }\n\n  const monaco = await getMonaco();\n  if (cb.ltoken?.textLC === \"view\") {\n    const userSchemas = getUserSchemaNames(ss);\n    const ms: MinimalSnippet[] = [\n      \"$new_or_existing_view_name\",\n      \"IF NOT EXISTS\",\n      ...userSchemas.map((s) => `${s}.$new_or_existing_view_name`),\n    ].map((label) => ({ label }));\n    if (cb.prevTokens.some((t) => t.textLC === \"replace\")) {\n      const views = await ss.filter((s) => s.type === \"view\");\n      return suggestSnippets([\n        ...ms,\n        ...views.map((v) => ({\n          ...v,\n          insertText: `${v.escapedIdentifier} AS \\n${v.view!.definition}`,\n          insertTextRules: monaco.languages.CompletionItemInsertTextRule.None,\n          sortText: v.schema === \"public\" ? \"0\" : \"1\",\n        })),\n      ]);\n    }\n    return suggestSnippets(ms);\n  }\n\n  const withOptions = [\n    {\n      label: \"check_option\",\n      options: [\n        {\n          label: \"cascaded\",\n          docs: `New rows are checked against the conditions of the view and all underlying base views. If the CHECK OPTION is specified, and neither LOCAL nor CASCADED is specified, then CASCADED is assumed.`,\n        },\n        {\n          label: \"local\",\n          docs: `New rows are only checked against the conditions defined directly in the view itself. Any conditions defined on underlying base views are not checked (unless they also specify the CHECK OPTION).`,\n        },\n      ],\n      docs: \"Specifies the level of checking to be done on data changes to the view. The options are LOCAL and CASCADED.\",\n    },\n    {\n      label: \"security_barrier\",\n      options: [{ label: \"true\" }, { label: \"false\" }],\n      docs: `This should be used if the view is intended to provide row-level security`,\n    },\n    {\n      label: \"security_invoker\",\n      options: [{ label: \"true\" }, { label: \"false\" }],\n      docs: `This option causes the underlying base relations to be checked against the privileges of the user of the view rather than the view owner. See the notes below for full details.`,\n    },\n  ] satisfies readonly (MinimalSnippet & { options: MinimalSnippet[] })[];\n\n  if (cb.currNestingFunc?.textLC === \"with\") {\n    return withKWDs(\n      withOptions.map(\n        (o) =>\n          ({\n            kwd: o.label,\n            options: o.options,\n            docs: o.docs,\n            canRepeat: false,\n            expects: \"=option\",\n          }) satisfies KWD,\n      ),\n      { sql, cb, ss, setS },\n    ).getSuggestion(\", \");\n  }\n  const res = await withKWDs(\n    [\n      {\n        kwd: \"AS\",\n        docs: \"Precedes the SELECT statement that will be used to create the view.\",\n      },\n      {\n        kwd: \"SELECT\",\n        dependsOn: \"AS\",\n        docs: \"The SELECT statement that will be used to create the view.\",\n      },\n      {\n        kwd: \"WITH\",\n        expects: \"(options)\",\n        docs: \"This clause specifies optional parameters for a view\",\n        optional: true,\n        excludeIf: () => cb.prevTokens.some((t) => t.textLC === \"as\"),\n        options: withOptions,\n      },\n    ] satisfies KWD[],\n    { sql, cb, ss, setS, opts: { notOrdered: true } },\n  ).getSuggestion();\n  return res;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MacthCreate/matchCreateIndex.ts",
    "content": "import { suggestSnippets } from \"../CommonMatchImports\";\nimport { getExpected } from \"../getExpected\";\nimport {\n  getKind,\n  type SQLMatchContext,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"../withKWDs\";\nimport { suggestKWD, withKWDs } from \"../withKWDs\";\n\nexport const matchCreateIndex = ({ cb, ss, setS, sql }: SQLMatchContext) => {\n  const getColsAndFuncs = (inParens = false) => {\n    const prevCols = cb.tokens\n      .filter((t) => t.nestingId.length === 1)\n      .map((t) => t.text);\n    const tableColumns = getExpected(\n      inParens ? \"(column)\" : \"column\",\n      cb,\n      ss,\n    ).suggestions.filter((s) => !prevCols.includes(s.insertText));\n    const immutableFuncs = ss.filter(\n      (s) => s.funcInfo?.provolatile === \"i\" && s.funcInfo.prokind === \"f\",\n    );\n    return {\n      suggestions: [\n        ...tableColumns,\n        ...immutableFuncs.map((s) => ({ ...s, sortText: \"zz\" })),\n      ],\n    };\n  };\n  if (cb.currNestingFunc && cb.currNestingFunc.textLC !== \"with\") {\n    if (\n      cb.ltoken?.text === \",\" ||\n      cb.ltoken?.text === \"(\" ||\n      cb.currToken?.text === \"(\"\n    ) {\n      return getColsAndFuncs();\n    }\n    if (cb.ltoken?.type === \"identifier.sql\") {\n      return suggestSnippets([\n        { label: \"ASC\" },\n        { label: \"DESC\" },\n        { label: \"NULLS LAST\" },\n        { label: \"NULLS FIRST\" },\n        { label: \",\" },\n      ]);\n    }\n  }\n\n  if (cb.prevLC.endsWith(\"not exists\")) {\n    return suggestKWD(getKind, [\"$index_name\"]);\n  }\n\n  if (cb.l1token?.textLC === \"using\") {\n    return getColsAndFuncs(true);\n  }\n\n  const concurrentlyOpt = {\n    label: \"CONCURRENTLY\",\n    docs: `When this option is used, PostgreSQL will build the index without taking any locks that prevent concurrent inserts, updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done. `,\n  } as const;\n  const crIdxOpts = [\n    { label: \"ON\" },\n    { label: \"$index_name\" },\n    { label: \"IF NOT EXISTS $index_name\" },\n    concurrentlyOpt,\n  ] as const;\n\n  const indexInfoUrl = `https://www.postgresql.org/docs/current/indexes-types.html`;\n  const kwds: KWD[] = [\n    {\n      kwd: \"INDEX\",\n      options: crIdxOpts,\n      docs: `Constructs an index on the specified column(s) of the specified relation, which can be a table or a materialized view. Indexes are primarily used to enhance database performance (though inappropriate use can result in slower performance).`,\n    },\n    {\n      kwd: \"ON\",\n      expects: \"table\",\n      docs: `The table for which the index will be created`,\n      include: () =>\n        cb.ltoken?.textLC === \"index\" ||\n        cb.l1token?.textLC === \"index\" ||\n        cb.l2token?.textLC === \"index\",\n    },\n    {\n      kwd: \"CONCURRENTLY\",\n      options: [{ label: \"$index_name\" }, { label: \"IF NOT EXISTS\" }],\n      include: () => cb.ltoken?.textLC === \"index\",\n      optional: true,\n      docs: concurrentlyOpt.docs,\n    },\n    {\n      kwd: \"USING\",\n      optional: true,\n      include: () => cb.l1token?.textLC === \"on\",\n      docs: `The name of the index method to be used. Choices are btree, hash, gist, spgist, gin, brin, or user-installed access methods like bloom. The default method is btree.`,\n      options: [\n        {\n          label: \"btree\",\n          docs: `B-trees can handle equality and range queries on data that can be sorted into some ordering. In particular, the PostgreSQL query planner will consider using a B-tree index whenever an indexed column is involved in a comparison using one of these operators: \n      \n      <   <=   =   >=   >\n  ${indexInfoUrl}`,\n        },\n        {\n          label: \"hash\",\n          docs: `Hash indexes store a 32-bit hash code derived from the value of the indexed column. Hence, such indexes can only handle simple equality comparisons. The query planner will consider using a hash index whenever an indexed column is involved in a comparison using the equal operator: \n        \n        =\n\n${indexInfoUrl}`,\n        },\n        {\n          label: \"gist\",\n          docs: `GiST indexes are not a single kind of index, but rather an infrastructure within which many different indexing strategies can be implemented. Accordingly, the particular operators with which a GiST index can be used vary depending on the indexing strategy (the operator class). As an example, the standard distribution of PostgreSQL includes GiST operator classes for several two-dimensional geometric data types, which support indexed queries using these operators:\n      \n      <<   &<   &>   >>   <<|   &<|   |&>   |>>   @>   <@   ~=   &&  <->\n      \n${indexInfoUrl}`,\n        },\n        {\n          label: \"spgist\",\n          docs: `SP-GiST indexes, like GiST indexes, offer an infrastructure that supports various kinds of searches. SP-GiST permits implementation of a wide range of different non-balanced disk-based data structures, such as quadtrees, k-d trees, and radix trees (tries). As an example, the standard distribution of PostgreSQL includes SP-GiST operator classes for two-dimensional points, which support indexed queries using these operators:\n\n      <<   >>   ~=   <@   <<|   |>>\n    \n${indexInfoUrl}`,\n        },\n        {\n          label: \"gin\",\n          docs: `GIN indexes are “inverted indexes” which are appropriate for data values that contain multiple component values, such as arrays. An inverted index contains a separate entry for each component value, and can efficiently handle queries that test for the presence of specific component values.\n\nLike GiST and SP-GiST, GIN can support many different user-defined indexing strategies, and the particular operators with which a GIN index can be used vary depending on the indexing strategy. As an example, the standard distribution of PostgreSQL includes a GIN operator class for arrays, which supports indexed queries using these operators:\n      \n      <@   @>   =   &&\n      \n${indexInfoUrl}`,\n        },\n        {\n          label: \"brin\",\n          docs: `BRIN indexes (a shorthand for Block Range INdexes) store summaries about the values stored in consecutive physical block ranges of a table. Thus, they are most effective for columns whose values are well-correlated with the physical order of the table rows. Like GiST, SP-GiST and GIN, BRIN can support many different indexing strategies, and the particular operators with which a BRIN index can be used vary depending on the indexing strategy. For data types that have a linear sort order, the indexed data corresponds to the minimum and maximum values of the values in the column for each block range. This supports indexed queries using these operators:\n\n      <   <=   =   >=   >\n      \n${indexInfoUrl}`,\n        },\n      ],\n    },\n    {\n      kwd: \"( $0 )\",\n      options: () => getColsAndFuncs(true).suggestions,\n      excludeIf: () =>\n        cb.prevTokens.some((t) => !t.nestingId && t.text === \")\"),\n    },\n    {\n      kwd: \"INCLUDE\",\n      expects: \"(column)\",\n      include: () => cb.prevTokens.some((t) => t.text === \")\"),\n      docs: `The optional INCLUDE clause specifies a list of columns which will be included in the index as non-key columns. A non-key column cannot be used in an index scan search qualification, and it is disregarded for purposes of any uniqueness or exclusion constraint enforced by the index. However, an index-only scan can return the contents of non-key columns without having to visit the index's table, since they are available directly from the index entry. Thus, addition of non-key columns allows index-only scans to be used for queries that otherwise could not use them.`,\n      optional: true,\n    },\n    {\n      kwd: \"NULLS\",\n      optional: true,\n      options: [\n        // { label: \"FIRST\", docs: `Specifies that nulls sort before non-nulls. This is the default when DESC is specified.` },\n        // { label: \"LAST\", docs: `Specifies that nulls sort after non-nulls. This is the default when DESC is not specified.` },\n        {\n          label: \"DISTINCT\",\n          docs: `Specifies whether for a unique index, null values should be considered distinct (not equal). The default is that they are distinct, so that a unique index could contain multiple null values in a column.`,\n        },\n        {\n          label: \"NOT DISTINCT\",\n          docs: `Specifies whether for a unique index, null values should be considered distinct (not equal). The default is that they are distinct, so that a unique index could contain multiple null values in a column.`,\n        },\n      ],\n    },\n    {\n      kwd: \"WITH\",\n      optional: true,\n      docs: `The optional WITH clause specifies storage parameters for the index. Each index method has its own set of allowed storage parameters. The B-tree, hash, GiST and SP-GiST index methods all accept this parameter:`,\n      expects: \"(options)\",\n      options: [\n        {\n          label: \"fillfactor = 70\",\n          docs: `The fillfactor for an index is a percentage that determines how full the index method will try to pack index pages. For B-trees, leaf pages are filled to this percentage during initial index builds, and also when extending the index at the right (adding new largest key values). If pages subsequently become completely full, they will be split, leading to fragmentation of the on-disk index structure. B-trees use a default fillfactor of 90, but any integer value from 10 to 100 can be selected.`,\n        },\n        {\n          label: \"fastupdate = off\",\n          docs: `This setting controls usage of the fast update technique described in Section 70.4.1. It is a Boolean parameter: ON enables fast update, OFF disables it. The default is ON.`,\n        },\n        {\n          label: \"deduplicate_items = off\",\n          docs: `Controls usage of the B-tree deduplication technique described in Section 67.4.3. Set to ON or OFF to enable or disable the optimization. (Alternative spellings of ON and OFF are allowed as described in Section 20.1.) The default is ON.`,\n        },\n        {\n          label: \"gin_pending_list_limit = 1024\",\n          docs: `Custom gin_pending_list_limit parameter. This value is specified in kilobytes.`,\n        },\n        {\n          label: \"pages_per_range = 23\",\n          docs: `Defines the number of table blocks that make up one block range for each entry of a BRIN index (see Section 71.1 for more details). The default is 128.`,\n        },\n        {\n          label: \"autosummarize = on\",\n          docs: `Defines whether a summarization run is queued for the previous page range whenever an insertion is detected on the next one. See Section 71.1.1 for more details. The default is off.`,\n        },\n      ],\n    },\n    {\n      kwd: \"WHERE\",\n      expects: \"condition\",\n      dependsOn: \"ON\",\n      docs: `When the WHERE clause is present, a partial index is created. A partial index is an index that contains entries for only a portion of a table, usually a portion that is more useful for indexing than the rest of the table. For example, if you have a table that contains both billed and unbilled orders where the unbilled orders take up a small fraction of the total table and yet that is an often used section, you can improve performance by creating an index on just that portion. Another possible application is to use WHERE with UNIQUE to enforce uniqueness over a subset of a table. See Section 11.8 for more discussion.\n\nThe expression used in the WHERE clause can refer only to columns of the underlying table, but it can use all columns, not just the ones being indexed. Presently, subqueries and aggregate expressions are also forbidden in WHERE. The same restrictions apply to index fields that are expressions.`,\n      optional: true,\n    },\n  ];\n  return withKWDs(kwds, { cb, ss, setS, sql }).getSuggestion();\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MacthCreate/matchCreatePolicy.ts",
    "content": "import { POLICY_FOR, suggestSnippets } from \"../CommonMatchImports\";\nimport { getExpected } from \"../getExpected\";\nimport { asSQL } from \"../KEYWORDS\";\nimport type {\n  SQLMatchContext,\n  SQLMatcher,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport { type KWD, suggestKWD, withKWDs } from \"../withKWDs\";\n\nexport const matchCreatePolicy: SQLMatcher[\"result\"] = async ({\n  cb,\n  ss,\n  setS,\n  sql,\n}: SQLMatchContext) => {\n  const _ss = ss.map((s) => ({\n    ...s,\n    ...(s.name === \"FOR\" ? { documentation: \"\" }\n    : s.name === \"USING\" ?\n      {\n        documentation: {\n          value: `Example:\\n${asSQL(\"(username = CURRENT_USER)\")} \\n\\nAny SQL conditional expression (returning boolean). The conditional expression cannot contain any aggregate or window functions. This expression will be added to queries that refer to the table if row-level security is enabled. Rows for which the expression returns true will be visible. Any rows for which the expression returns false or null will not be visible to the user (in a SELECT), and will not be available for modification (in an UPDATE or DELETE). Such rows are silently suppressed; no error is reported.`,\n        },\n      }\n    : {}),\n  }));\n\n  const { getSuggestion } = withKWDs(KwdPolicy, { cb, ss: _ss, setS, sql });\n\n  if (cb.thisLinePrevTokens[0]?.textLC === \"to\" && cb.ltoken?.textLC !== \"to\") {\n    if (cb.ltoken?.textLC === \",\") {\n      return getExpected(\"role\", cb, ss);\n    }\n    return suggestSnippets([\n      {\n        label: \",\",\n        docs: `The role(s) to which the policy is to be applied. The default is PUBLIC, which will apply the policy to all roles.`,\n      },\n    ]);\n  }\n\n  const s = getSuggestion();\n  return s;\n};\n\nexport const KwdPolicy = [\n  {\n    kwd: \"POLICY\",\n    options: [\n      {\n        label: \"$polciy_name\",\n        docs: `The name of the policy to be created. This must be distinct from the name of any other policy for the table.`,\n      },\n    ],\n  },\n  {\n    kwd: \"ON\",\n    expects: \"table\",\n    dependsOn: \"POLICY\",\n    docs: `The name (optionally schema-qualified) of the table the policy applies to.`,\n  },\n  {\n    kwd: \"AS\",\n    dependsOn: \"ON\",\n    optional: true,\n    options: [\n      {\n        label: \"PERMISSIVE\",\n        docs: `Specify that the policy is to be created as a permissive policy. All permissive policies which are applicable to a given query will be combined together using the Boolean “OR” operator. By creating permissive policies, administrators can add to the set of records which can be accessed. Policies are permissive by default.`,\n      },\n      {\n        label: \"RESTRICTIVE\",\n        docs: `Specify that the policy is to be created as a restrictive policy. All restrictive policies which are applicable to a given query will be combined together using the Boolean “AND” operator. By creating restrictive policies, administrators can reduce the set of records which can be accessed as all restrictive policies must be passed for each record.\n\nNote that there needs to be at least one permissive policy to grant access to records before restrictive policies can be usefully used to reduce that access. If only restrictive policies exist, then no records will be accessible. When a mix of permissive and restrictive policies are present, a record is only accessible if at least one of the permissive policies passes, in addition to all the restrictive policies.`,\n      },\n    ],\n    docs: `Specifies how multiple policies will be combined: either OR (for permissive policies, which are the default) or using AND (for restrictive policies). Policies are permissive by default.`,\n  },\n  {\n    kwd: \"FOR\",\n    options: POLICY_FOR,\n    dependsOn: \"ON\",\n    optional: true,\n    docs: `The command to which the policy applies. Valid options are ALL, SELECT, INSERT, UPDATE, and DELETE. ALL is the default. See below for specifics regarding how these are applied.`,\n  },\n  {\n    kwd: \"TO\",\n    expects: \"role\",\n    optional: true,\n    dependsOn: \"ON\",\n    docs: `The role(s) to which the policy is to be applied. The default is PUBLIC, which will apply the policy to all roles.`,\n  },\n  {\n    kwd: \"USING\",\n    expects: \"(condition)\",\n    dependsOn: \"ON\",\n    optional: true,\n    docs: `Any SQL conditional expression (returning boolean). The conditional expression cannot contain any aggregate or window functions. This expression will be added to queries that refer to the table if row-level security is enabled. Rows for which the expression returns true will be visible. Any rows for which the expression returns false or null will not be visible to the user (in a SELECT), and will not be available for modification (in an UPDATE or DELETE). Such rows are silently suppressed; no error is reported.`,\n  },\n  {\n    kwd: \"WITH CHECK\",\n    expects: \"(condition)\",\n    dependsOn: \"ON\",\n    optional: true,\n    docs: `Any SQL conditional expression (returning boolean). The conditional expression cannot contain any aggregate or window functions. This expression will be used in INSERT and UPDATE queries against the table if row-level security is enabled. Only rows for which the expression evaluates to true will be allowed. An error will be thrown if the expression evaluates to false or null for any of the records inserted or any of the records that result from the update. Note that the check_expression is evaluated against the proposed new contents of the row, not the original contents.`,\n  },\n] as const satisfies KWD[];\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MacthCreate/matchCreateRule.tsx",
    "content": "import { getCurrentCodeBlock } from \"../completionUtils/getCodeBlock\";\nimport { getMatch } from \"../getMatch\";\nimport type { SQLMatchContext } from \"../monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"../withKWDs\";\nimport { withKWDs } from \"../withKWDs\";\n\nexport const matchCreateRule = async ({\n  cb,\n  ss,\n  sql,\n  setS,\n}: SQLMatchContext) => {\n  const COMMANDS = [\"SELECT\", \"INSERT\", \"UPDATE\", \"DELETE\"];\n  if (cb.currNestingId) {\n    const startTkn = cb.tokens.findLast(\n      (t) =>\n        t.offset <= cb.offset &&\n        t.nestingId === cb.currNestingId &&\n        t.text === \"(\",\n    );\n    const endTkn = cb.tokens.find(\n      (t) =>\n        t.offset >= cb.offset &&\n        t.nestingId === cb.currNestingId &&\n        t.text === \")\",\n    );\n    const command = cb.tokens.find((t) =>\n      COMMANDS.includes(t.text.toUpperCase()),\n    )?.textLC;\n    if (startTkn && command) {\n      const nestedCb = await getCurrentCodeBlock(cb.model, cb.position, [\n        startTkn.offset,\n        endTkn?.offset ?? cb.offset + 1,\n      ]);\n      const { firstTry, match } = await getMatch({\n        cb: nestedCb,\n        ss,\n        sql,\n        setS,\n        filter: [\"MatchInsert\", \"MatchSelect\", \"MatchUpdate\", \"MatchDelete\"],\n      });\n\n      const res = firstTry ??\n        match?.result({ cb: nestedCb, ss, setS, sql }) ?? {\n          suggestions: ss.filter(\n            (s) =>\n              s.topKwd &&\n              COMMANDS.some((c) => s.name.toUpperCase().startsWith(c)),\n          ),\n        };\n      return res;\n    }\n  }\n  return withKWDs(\n    [\n      {\n        kwd: \"RULE\",\n        options: [\"$rule_name\"],\n        docs: `The PostgreSQL rule system allows one to define an alternative action to be performed on insertions, updates, or deletions in database tables. Roughly speaking, a rule causes additional commands to be executed when a given command on a given table is executed. Alternatively, an INSTEAD rule can replace a given command by another, or cause a command not to be executed at all. Rules are used to implement SQL views as well. It is important to realize that a rule is really a command transformation mechanism, or command macro. The transformation happens before the execution of the command starts. If you actually want an operation that fires independently for each physical row, you probably want to use a trigger, not a rule. More information about the rules system is in Chapter 41.`,\n      },\n      {\n        kwd: \"AS ON\",\n        dependsOn: \"RULE\",\n        options: COMMANDS,\n        docs: `The event is one of SELECT, INSERT, UPDATE, or DELETE. Note that an INSERT containing an ON CONFLICT clause cannot be used on tables that have either INSERT or UPDATE rules. Consider using an updatable view instead.`,\n      },\n      {\n        kwd: \"TO\",\n        expects: \"table\",\n        docs: `Expects the name (optionally schema-qualified) of the table or view the rule applies to.`,\n        dependsOn: \"AS ON\",\n      },\n      {\n        kwd: \"DO\",\n        options: [\n          {\n            label: \"()\",\n            insertText: `(\\n $0\\n)`,\n            docs: `ALSO indicates that the commands should be executed in addition to the original command.\\n\\nIf neither ALSO nor INSTEAD is specified, ALSO is the default.`,\n          },\n          {\n            label: \"ALSO\",\n            insertText: `ALSO (\\n $0\\n)`,\n            docs: `ALSO indicates that the commands should be executed in addition to the original command.\\n\\nIf neither ALSO nor INSTEAD is specified, ALSO is the default.`,\n          },\n          {\n            label: \"INSTEAD\",\n            insertText: `INSTEAD (\\n $0\\n)`,\n            docs: `INSTEAD indicates that the commands should be executed instead of the original command.`,\n          },\n        ],\n        optional: true,\n        docs: `Specifies if the original commands get executed as well. If neither ALSO nor INSTEAD is specified, ALSO is the default.`,\n        dependsOn: \"TO\",\n      },\n    ] satisfies KWD[],\n    { cb, ss, setS, sql },\n  ).getSuggestion();\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MacthCreate/matchCreateTable.ts",
    "content": "import type { MinimalSnippet } from \"../CommonMatchImports\";\nimport { suggestSnippets } from \"../CommonMatchImports\";\nimport { getExpected } from \"../getExpected\";\nimport { getParentFunction } from \"../MatchSelect\";\nimport {\n  getKind,\n  type ParsedSQLSuggestion,\n  type SQLMatchContext,\n  type SQLMatcherResultType,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport { suggestColumnLike } from \"../suggestColumnLike\";\nimport { suggestCondition } from \"../suggestCondition\";\nimport {\n  getNewColumnDefinitions,\n  PG_COLUMN_CONSTRAINTS,\n  REFERENCE_CONSTRAINT_OPTIONS_KWDS,\n} from \"../TableKWDs\";\nimport { type KWD, suggestKWD, withKWDs } from \"../withKWDs\";\n\nexport const getUserSchemaNames = (ss: ParsedSQLSuggestion[]) =>\n  ss\n    .filter(\n      (s) =>\n        s.type === \"schema\" &&\n        s.name !== \"information_schema\" &&\n        !s.name.startsWith(\"pg_\"),\n    )\n    .map((s) => s.escapedIdentifier ?? s.escapedName ?? s.name);\n\nexport const matchCreateTable = async ({\n  cb,\n  ss,\n  sql,\n  setS,\n}: SQLMatchContext): Promise<SQLMatcherResultType> => {\n  const { prevLC, l2token, l1token, ltoken, thisLineLC, prevTokens } = cb;\n\n  const insideFunc = getParentFunction(cb);\n  if (insideFunc?.prevTextLC?.endsWith(\"generated always as\")) {\n    return suggestColumnLike({ cb, ss, setS, sql });\n  }\n\n  if (cb.prevTokens.some((t) => !t.nestingId && t.textLC === \")\")) {\n    if (!cb.currNestingId) {\n      const partitionTypes = [\"RANGE\", \"LIST\", \"HASH\"];\n      return withKWDs(\n        [\n          {\n            kwd: \"PARTITION BY\",\n            optional: true,\n            options: partitionTypes,\n          },\n          ...[\"RANGE\", \"LIST\", \"HASH\"].map(\n            (kwd) =>\n              ({\n                kwd,\n                exactlyAfter: [\"PARTITION BY\"],\n                options: [\"($column_name)\"],\n                excludeIf: (cb) =>\n                  cb.prevTokens.some((t) =>\n                    partitionTypes.some(\n                      (pt) => t.nestingFuncToken?.textLC === pt.toLowerCase(),\n                    ),\n                  ),\n              }) satisfies KWD,\n          ),\n          {\n            kwd: \"WITH\",\n            optional: true,\n            expects: \"(options)\",\n            options: withOptions.map((o) => o.kwd),\n          },\n        ],\n        { cb, ss, sql, setS },\n      ).getSuggestion();\n    } else if (cb.currNestingFunc?.textLC === \"with\") {\n      return withKWDs(\n        withOptions.map((k) => ({\n          ...k,\n          expects: \"=option\",\n          options: k.options ?? [\" $number\"],\n        })),\n        { cb, ss, sql, setS },\n      ).getSuggestion();\n    }\n  }\n\n  if (prevLC.endsWith(\"references\")) {\n    return getExpected(\"table\", cb, ss);\n  }\n\n  if (ltoken?.textLC === \"table\") {\n    const userSchemas = getUserSchemaNames(ss);\n    return suggestKWD(getKind, [\n      \"$tableName\",\n      \"IF NOT EXISTS\",\n      ...userSchemas.map((s) => `${s}.$table_name`),\n    ]);\n  }\n\n  if (thisLineLC.includes(\"references\")) {\n    if (\n      l2token?.textLC === \"references\" &&\n      ltoken?.text &&\n      ltoken.text === \"(\"\n    ) {\n      const table_name = l1token?.text;\n      return {\n        suggestions: ss.filter(\n          (s) =>\n            s.type === \"column\" &&\n            table_name &&\n            s.escapedParentName === table_name,\n        ),\n      };\n    }\n\n    const refKWDs = [\n      {\n        kwd: \"REFERENCES\",\n        expects: \"table\",\n      },\n      ...REFERENCE_CONSTRAINT_OPTIONS_KWDS,\n    ] as const;\n\n    /** Table provided */\n    if (ltoken?.text !== \"references\") {\n      return withKWDs(refKWDs, { cb, ss, setS, sql }).getSuggestion();\n    }\n\n    if (\n      prevTokens.at(-2)?.textLC === \"on\" &&\n      [\"DELETE\", \"UPDATE\"].includes(ltoken.text.toUpperCase())\n    ) {\n      //  && REF_ACTIONS.map(a => a.label.split(\" \")[1]?.toLowerCase()).includes(_pwl!)\n\n      return withKWDs(refKWDs, { cb, ss, setS, sql }).getSuggestion();\n    }\n  }\n\n  const showFullColumnSamples =\n    ltoken?.textLC === \",\" || ltoken?.textLC === \"(\";\n  const showDataTypes =\n    cb.thisLinePrevTokens.length === 1 ||\n    (cb.thisLinePrevTokens.length === 2 &&\n      cb.currToken?.offset === cb.thisLinePrevTokens.at(-1)?.offset);\n  // console.log(cb.text, cb.offset, { showFullColumnSamples, showDataTypes, text: cb.text, offset: cb.offset, ltokentext: cb.ltoken?.text });\n  if (showFullColumnSamples) {\n    const snippetLines: MinimalSnippet[] = getNewColumnDefinitions(ss)\n      .filter(\n        ({ label: v }) => !prevTokens.some((t) => t.text === v.split(\" \")[0]),\n      ) // Exclude repeats\n      .concat([{ label: \"$column_name\" }])\n      .concat([\n        {\n          label:\n            \"${1:col_name} ${2:data_type} ${3:PRIMARY_KEY?} ${4:NOT NULL?} ${5:REFERENCES table_name?} ${6:CHECK(col1 > col2)?}\",\n        },\n      ])\n      .map((v) => ({ ...v, insertText: v.label, kind: getKind(\"column\") }));\n    return suggestSnippets(snippetLines);\n  }\n\n  if (showDataTypes) {\n    return getExpected(\"dataType\", cb, ss);\n  }\n\n  let snippetLines: { kwd: string; docs?: string }[] = [];\n  if (cb.thisLinePrevTokens.length) {\n    snippetLines = PG_COLUMN_CONSTRAINTS.filter(\n      (v) => !cb.thisLineLC.includes(v.kwd.toLowerCase()),\n    );\n    const res = (\n      await withKWDs(PG_COLUMN_CONSTRAINTS, {\n        cb,\n        ss,\n        setS,\n        sql,\n      }).getSuggestion()\n    ).suggestions;\n    return {\n      suggestions: [\n        ...res,\n        ...suggestSnippets(\n          snippetLines\n            .filter((s) => !res.some((r) => r.name === s.kwd))\n            .map((k) => ({\n              label: k.kwd,\n              docs: k.docs,\n              kind: getKind(\"keyword\"),\n            })),\n        ).suggestions,\n      ],\n    };\n  }\n\n  if (cb.prevLC.endsWith(\" default\")) {\n    const res = getExpected(\"function\", cb, ss);\n    return {\n      suggestions: res.suggestions.map((s) => ({\n        ...s,\n        sortText:\n          (\n            cb.thisLinePrevTokens.some((t) =>\n              s.funcInfo?.restype?.includes(t.textLC),\n            )\n          ) ?\n            !s.funcInfo?.args.length ?\n              \"a\"\n            : \"aa\"\n          : (s.sortText ?? \"b\"),\n      })),\n    };\n  }\n\n  return suggestSnippets(\n    snippetLines.map((k) => ({\n      label: k.kwd,\n      docs: k.docs,\n      kind: getKind(\"keyword\"),\n    })),\n  );\n};\n\nconst withOptions = [\n  {\n    kwd: \"fillfactor\",\n    docs: \"The fillfactor for a table is a percentage between 10 and 100. 100 (complete packing) is the default. When a smaller fillfactor is specified, INSERT operations pack table pages only to the indicated percentage; the remaining space on each page is reserved for updating rows on that page. This gives UPDATE a chance to place the updated copy of a row on the same page as the original, which is more efficient than placing it on a different page, and makes heap-only tuple updates more likely. For a table whose entries are never updated, complete packing is the best choice, but in heavily updated tables smaller fillfactors are appropriate. This parameter cannot be set for TOAST tables.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"toast_tuple_target\",\n    docs: \"The toast_tuple_target specifies the minimum tuple length required before we try to compress and/or move long column values into TOAST tables, and is also the target length we try to reduce the length below once toasting begins. This affects columns marked as External (for move), Main (for compression), or Extended (for both) and applies only to new tuples. There is no effect on existing rows. By default this parameter is set to allow at least 4 tuples per block, which with the default block size will be 2040 bytes. Valid values are between 128 bytes and the (block size - header), by default 8160 bytes. Changing this value may not be useful for very short or very long rows. Note that the default setting is often close to optimal, and it is possible that setting this parameter could have negative effects in some cases. This parameter cannot be set for TOAST tables.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"parallel_workers\",\n    docs: \"This sets the number of workers that should be used to assist a parallel scan of this table. If not set, the system will determine a value based on the relation size. The actual number of workers chosen by the planner or by utility statements that use parallel scans may be less, for example due to the setting of max_worker_processes.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_enabled\",\n    docs: \"Enables or disables the autovacuum daemon for a particular table. If true, the autovacuum daemon will perform automatic VACUUM and/or ANALYZE operations on this table following the rules discussed in Section 25.1.6. If false, this table will not be autovacuumed, except to prevent transaction ID wraparound. See Section 25.1.5 for more about wraparound prevention. Note that the autovacuum daemon does not run at all (except to prevent transaction ID wraparound) if the autovacuum parameter is false; setting individual tables' storage parameters does not override that. Therefore there is seldom much point in explicitly setting this storage parameter to true, only to false.\",\n    options: [\"true\", \"false\"],\n  },\n  {\n    kwd: \"vacuum_index_cleanup\",\n    docs: \"Forces or disables index cleanup when VACUUM is run on this table. The default value is AUTO. With OFF, index cleanup is disabled, with ON it is enabled, and with AUTO a decision is made dynamically, each time VACUUM runs. The dynamic behavior allows VACUUM to avoid needlessly scanning indexes to remove very few dead tuples. Forcibly disabling all index cleanup can speed up VACUUM very significantly, but may also lead to severely bloated indexes if table modifications are frequent. The INDEX_CLEANUP parameter of VACUUM, if specified, overrides the value of this option.\",\n    options: [\"AUTO\", \"ON\", \"OFF\"],\n  },\n  {\n    kwd: \"vacuum_truncate\",\n    docs: \"Enables or disables vacuum to try to truncate off any empty pages at the end of this table. The default value is true. If true, VACUUM and autovacuum do the truncation and the disk space for the truncated pages is returned to the operating system. Note that the truncation requires ACCESS EXCLUSIVE lock on the table. The TRUNCATE parameter of VACUUM, if specified, overrides the value of this option.\",\n    options: [\"true\", \"false\"],\n  },\n  {\n    kwd: \"autovacuum_vacuum_threshold\",\n    docs: \"Per-table value for autovacuum_vacuum_threshold parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_vacuum_scale_factor\",\n    docs: \"Per-table value for autovacuum_vacuum_scale_factor parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_vacuum_insert_threshold\",\n    docs: \"Per-table value for autovacuum_vacuum_insert_threshold parameter. The special value of -1 may be used to disable insert vacuums on the table.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_vacuum_insert_scale_factor\",\n    docs: \"Per-table value for autovacuum_vacuum_insert_scale_factor parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_analyze_threshold\",\n    docs: \"Per-table value for autovacuum_analyze_threshold parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_analyze_scale_factor\",\n    docs: \"Per-table value for autovacuum_analyze_scale_factor parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_vacuum_cost_delay\",\n    docs: \"Per-table value for autovacuum_vacuum_cost_delay parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_vacuum_cost_limit\",\n    docs: \"Per-table value for autovacuum_vacuum_cost_limit parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_freeze_min_age\",\n    docs: \"Per-table value for vacuum_freeze_min_age parameter. Note that autovacuum will ignore per-table autovacuum_freeze_min_age parameters that are larger than half the system-wide autovacuum_freeze_max_age setting.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_freeze_max_age\",\n    docs: \"Per-table value for autovacuum_freeze_max_age parameter. Note that autovacuum will ignore per-table autovacuum_freeze_max_age parameters that are larger than the system-wide setting (it can only be set smaller).\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_freeze_table_age\",\n    docs: \"Per-table value for vacuum_freeze_table_age parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_multixact_freeze_min_age\",\n    docs: \"Per-table value for vacuum_multixact_freeze_min_age parameter. Note that autovacuum will ignore per-table autovacuum_multixact_freeze_min_age parameters that are larger than half the system-wide autovacuum_multixact_freeze_max_age setting.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_multixact_freeze_max_age\",\n    docs: \"Per-table value for autovacuum_multixact_freeze_max_age parameter. Note that autovacuum will ignore per-table autovacuum_multixact_freeze_max_age parameters that are larger than the system-wide setting (it can only be set smaller).\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"autovacuum_multixact_freeze_table_age\",\n    docs: \"Per-table value for vacuum_multixact_freeze_table_age parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"log_autovacuum_min_duration\",\n    docs: \"Per-table value for log_autovacuum_min_duration parameter.\",\n    expects: \"number\",\n  },\n  {\n    kwd: \"user_catalog_table\",\n    docs: \"Declare the table as an additional catalog table for purposes of logical replication. See Section 49.6.2 for details. This parameter cannot be set for TOAST tables.\",\n    options: [\"true\", \"false\"],\n  },\n] satisfies KWD[];\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MacthCreate/matchCreateTrigger.ts",
    "content": "import type {\n  ParsedSQLSuggestion,\n  SQLMatchContext,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"../withKWDs\";\nimport { withKWDs } from \"../withKWDs\";\n\nexport const matchCreateTrigger = ({ cb, setS, sql, ss }: SQLMatchContext) => {\n  const getTriggerFuncs = (s: ParsedSQLSuggestion[]) => {\n    return s\n      .filter((s) => s.funcInfo?.restype?.toLowerCase() === \"trigger\")\n      .map((s) => ({\n        ...s,\n        sortText:\n          s.funcInfo!.schema === \"pg_catalog\" ? \"c\"\n          : s.funcInfo!.schema === \"public\" ? \"a\"\n          : \"b\",\n      }));\n    // .slice(0).sort((a, b) => a.sortText.localeCompare(b.sortText));\n  };\n\n  const types = [\"BEFORE\", \"AFTER\", \"INSTEAD OF\"];\n  const actions = [\"UPDATE\", \"DELETE\", \"INSERT\"];\n  return withKWDs(\n    [\n      { kwd: \"TRIGGER\", expects: \"string\", docs: \"Trigger name\" },\n      ...types.map(\n        (type) =>\n          ({\n            kwd: type,\n            docs: `Determines whether the function is called before, after, or instead of the event. A constraint trigger can only be specified as AFTER.\n        \nA common use of a BEFORE trigger is to set a timestamp column to \"now\" before the data has been inserted.\n\nA common use of an AFTER trigger is to populate an audit/history table with the changes.`,\n            options: [{ label: actions.join(\" OR \") }].concat(\n              actions.map((label) => ({ label })),\n            ),\n            excludeIf: types,\n          }) satisfies KWD,\n      ),\n      { kwd: \"ON\", expects: \"table\" },\n      {\n        kwd: \"OF\",\n        expects: \"column\",\n        docs: \"Only execute the function if the specified column was targeted\",\n      },\n      {\n        kwd: \"REFERENCING\",\n        options: [\"NEW TABLE AS new OLD TABLE AS old\"].map((label) => ({\n          label,\n        })),\n        dependsOn: \"AFTER\",\n        // excludeIf: [\"BEFORE\"],\n        docs: `The REFERENCING option enables collection of transition relations, which are row sets that include all of the rows inserted, deleted, or modified by the current SQL statement. This feature lets the trigger see a global view of what the statement did, not just one row at a time. This option is only allowed for an AFTER trigger that is not a constraint trigger; also, if the trigger is an UPDATE trigger, it must not specify a column_name list. OLD TABLE may only be specified once, and only for a trigger that can fire on UPDATE or DELETE; it creates a transition relation containing the before-images of all rows updated or deleted by the statement. Similarly, NEW TABLE may only be specified once, and only for a trigger that can fire on UPDATE or INSERT; it creates a transition relation containing the after-images of all rows updated or inserted by the statement.`,\n      },\n      {\n        kwd: \"FOR EACH\",\n        dependsOn: \"ON\",\n        options: [\"ROW\", \"STATEMENT\"].map((label) => ({ label })),\n        docs: `This specifies whether the trigger function should be fired once for every row affected by the trigger event, or just once per SQL statement. If neither is specified, FOR EACH STATEMENT is the default. Constraint triggers can only be specified FOR EACH ROW.`,\n      },\n      {\n        kwd: \"WHEN\",\n        expects: \"condition\",\n        dependsOn: \"ON\",\n        docs: `A Boolean expression that determines whether the trigger function will actually be executed. If WHEN is specified, the function will only be called if the condition returns true. In FOR EACH ROW triggers, the WHEN condition can refer to columns of the old and/or new row values by writing OLD.column_name or NEW.column_name respectively. Of course, INSERT triggers cannot refer to OLD and DELETE triggers cannot refer to NEW.`,\n      },\n      {\n        kwd: \"EXECUTE FUNCTION\",\n        options: getTriggerFuncs,\n        dependsOn: \"FOR EACH\",\n        docs: `A user-supplied function that is declared as taking no arguments and returning type trigger, which is executed when the trigger fires.`,\n      },\n      {\n        kwd: \"EXECUTE PROCEDURE\",\n        options: getTriggerFuncs,\n        dependsOn: \"FOR EACH\",\n        docs: `A user-supplied function that is declared as taking no arguments and returning type trigger, which is executed when the trigger fires.`,\n      },\n    ] as const,\n    { cb, ss, setS, sql },\n  ).getSuggestion();\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchAlter/MatchAlter.tsx",
    "content": "import type { MinimalSnippet } from \"../CommonMatchImports\";\nimport { PG_OBJECTS, suggestSnippets } from \"../CommonMatchImports\";\nimport { cleanExpect, getExpected } from \"../getExpected\";\nimport { getParentFunction } from \"../MatchSelect\";\nimport {\n  getKind,\n  type SQLMatcher,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport { suggestColumnLike } from \"../suggestColumnLike\";\nimport { getNewColumnDefinitions, PG_COLUMN_CONSTRAINTS } from \"../TableKWDs\";\nimport { withKWDs } from \"../withKWDs\";\nimport { matchAlterPolicy } from \"./matchAlterPolicy\";\nimport { matchAlterTable } from \"./matchAlterTable\";\nimport { matchCreateOrAlterUser } from \"./matchCreateOrAlterUser\";\n\nexport const MatchAlter: SQLMatcher = {\n  match: (cb) => cb.ftoken?.textLC === \"alter\",\n  result: async (args) => {\n    const { cb, ss, setS, sql } = args;\n    const { prevLC, prevTokens, ltoken } = cb;\n    const lltoken = prevTokens.at(-2);\n    const rawExpect =\n      prevTokens[prevTokens.findIndex((t) => t.textLC === \"alter\") + 1]\n        ?.textLC ?? \"\";\n    const expect = cleanExpect(rawExpect) ?? [];\n\n    if (cb.prevLC.endsWith(\"if exists\")) {\n      return getExpected(expect, cb, ss);\n    }\n\n    const lastToken = cb.tokens.at(-1);\n    const identifiers = cb.tokens\n      .filter((t) => t.type === \"identifier.sql\")\n      .map((t) => t.text);\n\n    if (prevLC.includes(\"add constraint\") && prevLC.includes(\"(\")) {\n      return getExpected(\"column\", cb, ss);\n    }\n\n    if (\n      ((lltoken?.textLC === \"alter\" || lltoken?.textLC === \"drop\") &&\n        ltoken?.textLC === \"constraint\") ||\n      cb.prevLC.includes(\" constraint \")\n    ) {\n      return {\n        suggestions: ss.filter((s) => {\n          const { escaped_table_name } = s.constraintInfo ?? {};\n          return (\n            (escaped_table_name && identifiers.includes(escaped_table_name)) ||\n            identifiers.includes([s.schema, escaped_table_name].join(\".\"))\n          );\n        }),\n      };\n    }\n\n    if (prevLC.endsWith(\"rename to\")) {\n      return suggestSnippets([\n        {\n          label: \"$new_name\",\n        },\n      ]);\n    }\n\n    if (cb.prevLC.startsWith(\"alter policy\")) {\n      return matchAlterPolicy(args);\n    }\n\n    const addColColumnTokenIdx = cb.prevTokens.findIndex(\n      (t, i, arr) => t.textLC === \"column\" && arr[i - 1]?.textLC === \"add\",\n    );\n    if (addColColumnTokenIdx > -1 && !cb.thisLineLC.includes(\"references\")) {\n      const tokensAfter = cb.prevTokens.slice(addColColumnTokenIdx + 1);\n      if (!tokensAfter.length) {\n        return suggestSnippets(\n          getNewColumnDefinitions(ss)\n            .concat([{ label: \"$new_column_name\" }])\n            .map((v) => ({ ...v, kind: getKind(\"column\") })),\n        );\n      }\n      if (tokensAfter.length === 1) {\n        return getExpected(\"dataType\", cb, ss);\n      }\n\n      const insideFun = getParentFunction(cb);\n      if (insideFun?.func.textLC === \"as\") {\n        if (\n          insideFun.prevTokens\n            ?.slice(-3)\n            .map((t) => t.textLC)\n            .join(\" \") === \"generated always as\"\n        ) {\n          return suggestColumnLike({ cb, ss, setS, sql });\n        }\n      }\n\n      return withKWDs(PG_COLUMN_CONSTRAINTS, {\n        cb,\n        ss,\n        setS,\n        sql,\n      }).getSuggestion();\n    }\n\n    if (prevLC.endsWith(\"set storage\")) {\n      return suggestSnippets(\n        [\"PLAIN\", \"EXTERNAL\", \"EXTENDED\", \"MAIN\"].map((label) => ({ label })),\n      );\n    }\n\n    if (prevTokens.some((t) => [\"reset\", \"set\"].includes(t.textLC))) {\n      if ([\"reset\", \"set\"].includes(cb.ltoken?.textLC ?? \"\")) {\n        return {\n          suggestions: setS.map((s) => ({\n            ...s,\n            insertText: `${s.insertText} TO ${s.settingInfo?.enumvals?.length ? `\\${1|${s.settingInfo.enumvals.join(\",\")}|}` : \"$1\"}`,\n            insertTextRules: 4,\n          })),\n        };\n      }\n\n      if (cb.l2token?.textLC === \"set\") {\n        const setting = setS.find((s) => s.name === cb.l1token?.text);\n        if (setting) {\n          if (setting.settingInfo?.enumvals) {\n            return suggestSnippets(\n              setting.settingInfo.enumvals.map((label) => ({ label })),\n            );\n          }\n\n          return suggestSnippets([\n            { label: \"$value\", docs: setting.settingInfo?.description },\n          ]);\n        }\n      }\n    }\n\n    const userFields = [\"role\", \"user\"];\n    const secondToken = cb.tokens[1];\n    if (secondToken && userFields.includes(secondToken.textLC)) {\n      return matchCreateOrAlterUser({ cb, ss, setS, sql });\n    }\n\n    if (prevLC.startsWith(\"alter database\")) {\n      if (prevLC.endsWith(\"alter database\")) {\n        return suggestSnippets(\n          ss\n            .filter((s) => s.type === \"database\")\n            .flatMap((s) =>\n              ALTED_DB_ACTIONS.map((a) => ({\n                label: `${s.escapedIdentifier} ${a}`,\n                kind: getKind(\"keyword\"),\n              })),\n            ),\n        );\n      }\n      return suggestSnippets(\n        ALTED_DB_ACTIONS.map((label) => ({ label, kind: getKind(\"keyword\") })),\n      );\n    } else if (prevLC.endsWith(\"data type\")) {\n      return {\n        suggestions: ss.filter((s) => s.type === \"dataType\"),\n      };\n    } else if (prevLC.endsWith(\"owner to\")) {\n      const users = ss.filter((s) => userFields.includes(s.type));\n      return suggestSnippets(users.map((s) => ({ label: s.name })));\n    } else if (prevLC.startsWith(\"alter table\")) {\n      return matchAlterTable({ cb, ss, setS, sql });\n    } else if (prevLC.startsWith(\"alter system\")) {\n      if (prevLC.includes(\"set\") && ltoken?.textLC !== \"set\") {\n        if (!prevTokens.some((t) => t.textLC === \"to\"))\n          return suggestSnippets([\"TO\"].map((label) => ({ label })));\n        return suggestSnippets([\"DEFAULT\"].map((label) => ({ label })));\n      }\n      return suggestSnippets(\n        [\"SET\", \"RESET\", \"RESET ALL\"].map((label) => ({ label })),\n      );\n    }\n\n    if (prevLC.endsWith(\"alter\")) {\n      return suggestSnippets(\n        PG_OBJECTS.flatMap((label) =>\n          [\" IF EXISTS\", \"\"].map<MinimalSnippet>((ifEx) => ({\n            label: `${label}${ifEx}`,\n            kind: getKind(label.toLowerCase() as any),\n          })),\n        ),\n      );\n    } else if (\n      cb.tokens.length === 2 ||\n      (cb.textLC.endsWith(\"exists\") &&\n        cb.tokens.at(-1)?.type === \"operator.sql\")\n    ) {\n      if (PG_OBJECTS.some((obj) => expect.includes(obj.toLowerCase() as any))) {\n        const res = {\n          suggestions: getExpected(expect, cb, ss).suggestions.map((s) => ({\n            ...s,\n            insertText: s.insertText || s.label,\n            ...(s.type === \"function\" &&\n              s.args && {\n                label: `${s.escapedIdentifier}(${s.args.map((a) => a.data_type).join(\", \")})`,\n                // insertText: `${s.escapedIdentifier}(${s.args.map(a => a.data_type).join(\", \")})`\n              }),\n          })) as any,\n        };\n\n        return res;\n      }\n    } else if (\n      cb.prevText.endsWith(\" \") &&\n      (lastToken?.type === \"identifier.sql\" ||\n        lastToken?.type === \"delimiter.parenthesis.sql\" ||\n        lastToken?.type === \"identifier.quote.sql\")\n    ) {\n      if (cb.prevText.includes(\"(\") && cb.prevLC.includes(\"constraint\")) {\n        const cols = ss.filter(\n          (s) => s.type === \"column\" && identifiers.includes(s.parentName!),\n        );\n        return {\n          suggestions:\n            cols.length ? cols : (\n              ss.filter((s) => s.type === \"column\" || s.type === \"operator\")\n            ),\n        };\n      }\n\n      // const obj = ss.find(s => s.name.includes(lastToken.text));\n      const r = suggestSnippets([]);\n\n      if (expect.includes(\"function\") || expect.includes(\"table\")) {\n        r.suggestions = r.suggestions.concat(\n          suggestSnippets(\n            [\n              `RENAME TO $new_${expect}_name`,\n              \"OWNER TO \",\n              `SET SCHEMA $1`,\n              `SET search_path = $1`,\n              `DEPENDS ON EXTENSION `,\n            ].map((label) => ({ label, kind: getKind(\"keyword\") })),\n          ).suggestions,\n        );\n      }\n\n      return r;\n    }\n\n    return getExpected(expect, cb, ss);\n  },\n};\n\nconst ALTED_DB_ACTIONS = [\n  \"RENAME TO $new_name\",\n  \"OWNER TO \",\n  \"SET TABLESPACE new_tablespace\",\n  \"REFRESH COLLATION VERSION\",\n  \"SET\",\n  \"RESET configuration_parameter\",\n  \"RESET ALL\",\n];\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchAlter/matchAlterPolicy.ts",
    "content": "import { asName } from \"prostgles-types\";\nimport { KwdPolicy } from \"../MacthCreate/matchCreatePolicy\";\nimport type {\n  ParsedSQLSuggestion,\n  SQLMatchContext,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"../withKWDs\";\nimport { withKWDs } from \"../withKWDs\";\n\nexport const matchAlterPolicy = async ({\n  cb,\n  ss,\n  sql,\n  setS,\n}: SQLMatchContext): Promise<{ suggestions: ParsedSQLSuggestion[] }> => {\n  if (cb.ltoken?.textLC === \"policy\") {\n    return {\n      suggestions: ss\n        .filter((s) => s.type === \"policy\")\n        .map((s) => ({\n          ...s,\n          insertText: `${s.policyInfo!.escaped_identifier} ON ${asName(s.policyInfo!.tablename)}`,\n        })),\n    };\n  }\n  const alterKwd = [\n    {\n      kwd: \"RENAME TO\",\n      docs: `Change policy name`,\n      dependsOn: \"ON\",\n      optional: true,\n    },\n    ...KwdPolicy.filter(\n      (k) =>\n        k.kwd !== \"POLICY\" &&\n        k.kwd !== \"AS\" &&\n        k.kwd !== \"FOR\" &&\n        k.kwd !== \"ON\",\n    ),\n    {\n      kwd: \";\",\n      dependsOn: \"ON\",\n      optional: true,\n    },\n  ] satisfies KWD[];\n  const { getSuggestion } = withKWDs(alterKwd, { cb, ss, setS, sql });\n\n  const s = await getSuggestion();\n  return s;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchAlter/matchAlterTable.ts",
    "content": "import { suggestSnippets } from \"../CommonMatchImports\";\nimport {\n  ALTER_COL_ACTIONS,\n  PG_TABLE_CONSTRAINTS,\n  REFERENCE_CONSTRAINT_OPTIONS_KWDS,\n  TABLE_CONS_TYPES,\n} from \"../TableKWDs\";\nimport {\n  type ParsedSQLSuggestion,\n  type SQLMatchContext,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"../withKWDs\";\nimport { withKWDs } from \"../withKWDs\";\n\nexport const matchAlterTable = async ({\n  cb,\n  ss,\n  sql,\n  setS,\n}: SQLMatchContext): Promise<{ suggestions: ParsedSQLSuggestion[] }> => {\n  if (cb.ltoken?.textLC === \"trigger\") {\n    const suggestions = ss.filter(\n      (s) =>\n        s.triggerInfo &&\n        cb.prevTokens.some((t) =>\n          t.text.includes(s.triggerInfo!.event_object_table),\n        ),\n    );\n    if (suggestions.length === 0) {\n      return suggestSnippets([\n        { label: \"No triggers found for this table\", insertText: \" \" },\n      ]);\n    }\n    return {\n      suggestions,\n    };\n  }\n\n  // if (cb.prevTokens.length === 2) {\n  //   return getExpected(\"table\", cb, ss);\n  // }\n\n  if (cb.prevTokens.length === 3 && cb.currToken?.textLC !== \".\") {\n    return withKWDs(\n      ALTER_TABLE_ACTIONS.map(({ label, docs }) => ({ kwd: label, docs })),\n      { cb, ss, setS, sql },\n    ).getSuggestion();\n  }\n\n  if (cb.prevLC.includes(\"alter column\") && !cb.prevLC.endsWith(\"column\")) {\n    return withKWDs(ALTER_COL_ACTIONS, { cb, ss, setS, sql }).getSuggestion();\n  }\n  if (cb.prevLC.includes(\"drop column\") && !cb.prevLC.endsWith(\"column\")) {\n    return suggestSnippets([\"CASCADE\", \"RESTRICT\"].map((label) => ({ label })));\n  }\n  if (cb.prevLC.includes(\"rename column\")) {\n    if (!cb.prevLC.endsWith(\"column\")) {\n      if (cb.l1token?.textLC == \"column\") {\n        return suggestSnippets(\n          [\"TO $new_col_name\"].map((label) => ({ label })),\n        );\n      }\n      if (cb.ltoken?.textLC == \"to\") {\n        return suggestSnippets([{ label: \"$new_col_name\" }]);\n      }\n    }\n  }\n\n  const k = withKWDs(ALTER_TABLE_KWD, { cb, ss, setS, sql });\n\n  const result = await k.getSuggestion();\n  return result;\n};\n\nconst AddOpts = [\n  {\n    label: \"COLUMN\",\n    docs: \"Creates a new column in this table\",\n  },\n  ...PG_TABLE_CONSTRAINTS.map((d) => ({ ...d, label: d.kwd, docs: d.docs })),\n];\n\nconst ALTER_TABLE_KWD = [\n  {\n    kwd: \"ALTER TABLE\",\n    docs: \"Alters a table\",\n    expects: \"table\",\n  },\n  {\n    kwd: \"ADD\",\n    justAfter: [\"TABLE\"],\n    options: AddOpts,\n    excludeIf: (cb) => !cb.prevTokens.some((t) => t.textLC === \"add\"),\n  },\n  ...AddOpts.map(\n    (opt) =>\n      ({\n        ...(opt as any),\n        kwd: `ADD ${opt.label}`,\n        excludeIf: (cb) => cb.prevTokens.some((t) => t.textLC === \"add\"),\n      }) satisfies KWD,\n  ),\n  {\n    kwd: \"DROP\",\n    docs: \"Drops a table related object\",\n    justAfter: [\"TABLE\"],\n    excludeIf: (cb) => cb.tokens.length > 3,\n    options: [{ label: \"CONSTRAINT\" }, { label: \"COLUMN\" }],\n  },\n  ...[\"RENAME\", \"ALTER\", \"DROP\"].map((kwd) => ({\n    kwd: `${kwd} COLUMN`,\n    expects: \"column\",\n    excludeIf: (cb) => cb.prevTokens.length > 3,\n  })),\n  {\n    kwd: \"COLUMN\",\n    expects: \"column\",\n    exactlyAfter: [\"ADD\", \"DROP\", \"ALTER\", \"RENAME\"],\n  },\n  {\n    kwd: \"DISABLE TRIGGER\",\n    expects: \"trigger\",\n    options: [{ label: \"ALL\" }],\n    excludeIf: (cb) => cb.prevTokens.length > 3,\n  },\n  {\n    kwd: \"ENABLE TRIGGER\",\n    expects: \"trigger\",\n    options: [{ label: \"ALL\" }],\n    excludeIf: (cb) => cb.prevTokens.length > 3,\n  },\n  {\n    kwd: \"ENABLE ALWAYS TRIGGER\",\n    expects: \"trigger\",\n    options: [{ label: \"ALL\" }],\n    excludeIf: (cb) => cb.prevTokens.length > 3,\n  },\n  {\n    kwd: \"RENAME TO\",\n    docs: \"Rename the table\",\n    excludeIf: (cb) => cb.prevTokens.length > 3,\n  },\n  {\n    kwd: \"ENABLE REPLICA TRIGGER\",\n    expects: \"trigger\",\n    options: [{ label: \"ALL\" }],\n    excludeIf: (cb) => cb.prevTokens.length > 3,\n  },\n  ...[\n    \"ENABLE ROW LEVEL SECURITY;\",\n    \"DISABLE ROW LEVEL SECURITY;\",\n    \"FORCE ROW LEVEL SECURITY;\",\n    \"NO FORCE ROW LEVEL SECURITY;\",\n  ].map((kwd) => ({\n    kwd,\n    docs: `These forms control the application of row security policies belonging to the table. If enabled and no policies exist for the table, then a default-deny policy is applied. Note that policies can exist for a table even if row-level security is disabled. In this case, the policies will not be applied and the policies will be ignored. See also CREATE POLICY.`,\n    excludeIf: (cb) => cb.prevTokens.length > 3,\n  })),\n  ...PG_TABLE_CONSTRAINTS.map((k) => ({\n    ...k,\n    exactlyAfter: [\"ADD\"],\n  })),\n  {\n    kwd: \"REFERENCES\",\n    expects: \"table\",\n    dependsOn: \"FOREIGN KEY\",\n  },\n  ...REFERENCE_CONSTRAINT_OPTIONS_KWDS,\n] as const satisfies readonly KWD[];\n\nexport const ALTER_TABLE_ACTIONS = [\n  ...PG_TABLE_CONSTRAINTS.map(({ kwd: ConsType }) => ({\n    label: `ADD ${ConsType}`,\n    docs: `Adds a constraint of type ${ConsType}`,\n  })),\n  {\n    label: \"ADD COLUMN\",\n    docs: `This form adds a new column to the table, using the same syntax as CREATE TABLE. If IF NOT EXISTS is specified and a column already exists with this name, no error is thrown.`,\n  },\n  {\n    label: \"DROP COLUMN\",\n    docs: `This form drops a column from a table. Indexes and table constraints involving the column will be automatically dropped as well. Multivariate statistics referencing the dropped column will also be removed if the removal of the column would cause the statistics to contain data for only a single column. You will need to say CASCADE if anything outside the table depends on the column, for example, foreign key references or views. If IF EXISTS is specified and the column does not exist, no error is thrown. In this case a notice is issued instead.`,\n  },\n  { label: \"ALTER COLUMN\", docs: \"This form alters a table column\" },\n  {\n    label: \"RENAME COLUMN $1 TO $2\",\n    docs: `The RENAME forms change the name of a table (or an index, sequence, view, materialized view, or foreign table), the name of an individual column in a table, or the name of a constraint of the table. When renaming a constraint that has an underlying index, the index is renamed as well. There is no effect on the stored data.`,\n  },\n  {\n    label: \"RENAME TO\",\n    docs: `The RENAME forms change the name of a table (or an index, sequence, view, materialized view, or foreign table), the name of an individual column in a table, or the name of a constraint of the table. When renaming a constraint that has an underlying index, the index is renamed as well. There is no effect on the stored data.`,\n  },\n  {\n    label: `ADD CONSTRAINT \\${1:constraint_name} \\${2|${TABLE_CONS_TYPES}|} ($0)`,\n    docs: `This form adds a new constraint to a table using the same constraint syntax as CREATE TABLE, plus the option NOT VALID, which is currently only allowed for foreign key and CHECK constraints.\n\nNormally, this form will cause a scan of the table to verify that all existing rows in the table satisfy the new constraint. But if the NOT VALID option is used, this potentially-lengthy scan is skipped. The constraint will still be enforced against subsequent inserts or updates (that is, they'll fail unless there is a matching row in the referenced table, in the case of foreign keys, or they'll fail unless the new row matches the specified check condition). But the database will not assume that the constraint holds for all rows in the table, until it is validated by using the VALIDATE CONSTRAINT option. See Notes below for more information about using the NOT VALID option.\n\nAlthough most forms of ADD table_constraint require an ACCESS EXCLUSIVE lock, ADD FOREIGN KEY requires only a SHARE ROW EXCLUSIVE lock. Note that ADD FOREIGN KEY also acquires a SHARE ROW EXCLUSIVE lock on the referenced table, in addition to the lock on the table on which the constraint is declared.\n\nAdditional restrictions apply when unique or primary key constraints are added to partitioned tables; see CREATE TABLE. Also, foreign key constraints on partitioned tables may not be declared NOT VALID at present.`,\n  },\n  {\n    label:\n      \"ALTER CONSTRAINT ${1:table_constraint} ${2|DEFERRABLE,NOT DEFERRABLE|} ${3|INITIALLY DEFERRED,INITIALLY IMMEDIATE|}\",\n    docs: `This form alters the attributes of a constraint that was previously created. Currently only foreign key constraints may be altered.`,\n  },\n  {\n    label: \"VALIDATE CONSTRAINT ${1:table_constraint}\",\n    docs: `This form validates a foreign key or check constraint that was previously created as NOT VALID, by scanning the table to ensure there are no rows for which the constraint is not satisfied. Nothing happens if the constraint is already marked valid. (See Notes below for an explanation of the usefulness of this command.)\n\nThis command acquires a SHARE UPDATE EXCLUSIVE lock.\n    `,\n  },\n  {\n    label: \"DROP CONSTRAINT\",\n    docs: `This form drops the specified constraint on a table, along with any index underlying the constraint. If IF EXISTS is specified and the constraint does not exist, no error is thrown. In this case a notice is issued instead.`,\n  },\n\n  ...[\n    \"DISABLE TRIGGER\",\n    \"ENABLE TRIGGER\",\n    \"ENABLE REPLICA TRIGGER\",\n    \"ENABLE ALWAYS TRIGGER\",\n  ].map((label) => ({\n    label,\n    docs: `These forms configure the firing of trigger(s) belonging to the table. A disabled trigger is still known to the system, but is not executed when its triggering event occurs. For a deferred trigger, the enable status is checked when the event occurs, not when the trigger function is actually executed. One can disable or enable a single trigger specified by name, or all triggers on the table, or only user triggers (this option excludes internally generated constraint triggers such as those that are used to implement foreign key constraints or deferrable uniqueness and exclusion constraints). Disabling or enabling internally generated constraint triggers requires superuser privileges; it should be done with caution since of course the integrity of the constraint cannot be guaranteed if the triggers are not executed.\n\nThe trigger firing mechanism is also affected by the configuration variable session_replication_role. Simply enabled triggers (the default) will fire when the replication role is “origin” (the default) or “local”. Triggers configured as ENABLE REPLICA will only fire if the session is in “replica” mode, and triggers configured as ENABLE ALWAYS will fire regardless of the current replication role.\n\nThe effect of this mechanism is that in the default configuration, triggers do not fire on replicas. This is useful because if a trigger is used on the origin to propagate data between tables, then the replication system will also replicate the propagated data, and the trigger should not fire a second time on the replica, because that would lead to duplication. However, if a trigger is used for another purpose such as creating external alerts, then it might be appropriate to set it to ENABLE ALWAYS so that it is also fired on replicas.\n\nThis command acquires a SHARE ROW EXCLUSIVE lock.`,\n  })),\n\n  ...[\n    \"DISABLE RULE $rewrite_rule_name;\",\n    \"ENABLE RULE $rewrite_rule_name;\",\n    \"ENABLE REPLICA RULE ${1:rewrite_rule_name}\",\n    \"ENABLE ALWAYS RULE ${1:rewrite_rule_name}\",\n  ].map((label) => ({\n    label,\n    docs: `These forms configure the firing of rewrite rules belonging to the table. A disabled rule is still known to the system, but is not applied during query rewriting. The semantics are as for disabled/enabled triggers. This configuration is ignored for ON SELECT rules, which are always applied in order to keep views working even if the current session is in a non-default replication role.\n\nThe rule firing mechanism is also affected by the configuration variable session_replication_role, analogous to triggers as described above.`,\n  })),\n  ...[\n    \"ENABLE ROW LEVEL SECURITY;\",\n    \"DISABLE ROW LEVEL SECURITY;\",\n    \"FORCE ROW LEVEL SECURITY;\",\n    \"NO FORCE ROW LEVEL SECURITY;\",\n  ].map((label) => ({\n    label,\n    docs: `These forms control the application of row security policies belonging to the table. If enabled and no policies exist for the table, then a default-deny policy is applied. Note that policies can exist for a table even if row-level security is disabled. In this case, the policies will not be applied and the policies will be ignored. See also CREATE POLICY.`,\n  })),\n  {\n    label: \"CLUSTER ON index_name\",\n    docs: `This form selects the default index for future CLUSTER operations. It does not actually re-cluster the table.\n\nChanging cluster options acquires a SHARE UPDATE EXCLUSIVE lock.`,\n  },\n  {\n    label: \"SET WITHOUT CLUSTER\",\n    docs: `This form removes the most recently used CLUSTER index specification from the table. This affects future cluster operations that don't specify an index.\n\nChanging cluster options acquires a SHARE UPDATE EXCLUSIVE lock.`,\n  },\n  {\n    label: \"SET WITHOUT OIDS\",\n    docs: `Backward-compatible syntax for removing the oid system column. As oid system columns cannot be added anymore, this never has an effect.`,\n  },\n  {\n    label: \"SET ACCESS METHOD ${1:new_access_method}\",\n    docs: `This form changes the access method of the table by rewriting it`,\n  },\n  {\n    label: \"SET TABLESPACE ${1:new_tablespace}\",\n    docs: `This form changes the table's tablespace to the specified tablespace and moves the data file(s) associated with the table to the new tablespace. Indexes on the table, if any, are not moved; but they can be moved separately with additional SET TABLESPACE commands. When applied to a partitioned table, nothing is moved, but any partitions created afterwards with CREATE TABLE PARTITION OF will use that tablespace, unless overridden by a TABLESPACE clause.\n\nAll tables in the current database in a tablespace can be moved by using the ALL IN TABLESPACE form, which will lock all tables to be moved first and then move each one. This form also supports OWNED BY, which will only move tables owned by the roles specified. If the NOWAIT option is specified then the command will fail if it is unable to acquire all of the locks required immediately. Note that system catalogs are not moved by this command; use ALTER DATABASE or explicit ALTER TABLE invocations instead if desired. The information_schema relations are not considered part of the system catalogs and will be moved. See also CREATE TABLESPACE.`,\n  },\n  {\n    label: \"SET ${1|LOGGED,UNLOGGED|}\",\n    docs: `This form changes the table from unlogged to logged or vice-versa (see UNLOGGED). It cannot be applied to a temporary table.\n\nThis also changes the persistence of any sequences linked to the table (for identity or serial columns). However, it is also possible to change the persistence of such sequences separately.`,\n  },\n  {\n    label: \"SET ( storage_parameter [= value] [, ... ] )\",\n    docs: `This form changes one or more storage parameters for the table. See Storage Parameters in the CREATE TABLE documentation for details on the available parameters. Note that the table contents will not be modified immediately by this command; depending on the parameter you might need to rewrite the table to get the desired effects. That can be done with VACUUM FULL, CLUSTER or one of the forms of ALTER TABLE that forces a table rewrite. For planner related parameters, changes will take effect from the next time the table is locked so currently executing queries will not be affected.\n\nSHARE UPDATE EXCLUSIVE lock will be taken for fillfactor, toast and autovacuum storage parameters, as well as the planner parameter parallel_workers.`,\n  },\n  {\n    label: \"RESET ( storage_parameter [, ... ] )\",\n    docs: \"This form resets one or more storage parameters to their defaults. As with SET, a table rewrite might be needed to update the table entirely.\",\n  },\n  {\n    label: \"INHERIT $parent_table\",\n    docs: `This form adds the target table as a new child of the specified parent table. Subsequently, queries against the parent will include records of the target table. To be added as a child, the target table must already contain all the same columns as the parent (it could have additional columns, too). The columns must have matching data types, and if they have NOT NULL constraints in the parent then they must also have NOT NULL constraints in the child.\n\nThere must also be matching child-table constraints for all CHECK constraints of the parent, except those marked non-inheritable (that is, created with ALTER TABLE ... ADD CONSTRAINT ... NO INHERIT) in the parent, which are ignored; all child-table constraints matched must not be marked non-inheritable. Currently UNIQUE, PRIMARY KEY, and FOREIGN KEY constraints are not considered, but this might change in the future.`,\n  },\n  {\n    label: \"NO INHERIT $parent_table\",\n    docs: \"This form removes the target table from the list of children of the specified parent table. Queries against the parent table will no longer include records drawn from the target table. \",\n  },\n  {\n    label: \"OF $type_name\",\n    docs: \"This form links the table to a composite type as though CREATE TABLE OF had formed it. The table's list of column names and types must precisely match that of the composite type. The table must not inherit from any other table. These restrictions ensure that CREATE TABLE OF would permit an equivalent table definition.\",\n  },\n  {\n    label: \"NOT OF\",\n    docs: \"This form dissociates a typed table from its type.\",\n  },\n  {\n    label: \"OWNER TO ${1|$new_owner,CURRENT_ROLE,CURRENT_USER,SESSION_USER|}\",\n    docs: \"\",\n  },\n  {\n    label:\n      \"REPLICA IDENTITY ${1|DEFAULT,USING INDEX $index_name,FULL,NOTHING|}\",\n    docs: \"\",\n  },\n];\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchAlter/matchCreateOrAlterUser.tsx",
    "content": "import { suggestSnippets } from \"../CommonMatchImports\";\nimport {\n  getKind,\n  type ParsedSQLSuggestion,\n  type SQLMatchContext,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport { type KWD, withKWDs } from \"../withKWDs\";\n\nexport const matchCreateOrAlterUser = async ({\n  cb,\n  ss,\n  sql,\n  setS,\n}: SQLMatchContext): Promise<{ suggestions: ParsedSQLSuggestion[] }> => {\n  const isCreate = cb.tokens[0]!.text.toUpperCase() === \"CREATE\";\n  const { tokens } = cb;\n  const userOrRoleToken = tokens[1]!.text.toUpperCase() as \"USER\" | \"ROLE\";\n\n  return withKWDs(\n    [\n      isCreate ?\n        {\n          kwd: \"CREATE \" + userOrRoleToken,\n          docs: \"Create a user\",\n          options: [\"$new_user_name\"],\n        }\n      : {\n          kwd: \"ALTER \" + userOrRoleToken,\n          docs: \"Alter a user\",\n          expects: userOrRoleToken.toLowerCase() as \"user\" | \"role\",\n        },\n      {\n        kwd: \"RENAME TO\",\n        docs: \"Rename user\",\n        options: [\"$new_user_name\"],\n        excludeIf: () => isCreate,\n      },\n      {\n        kwd: \"SET\",\n        docs: \"Changes user configuration parameter\",\n        expects: \"(setting)\",\n        excludeIf: () => isCreate,\n      },\n      {\n        kwd: \"WITH\",\n        docs: \"User options\",\n        options: ROLE_OPTIONS.flatMap((o) =>\n          o.params.map((label) => ({\n            label,\n            docs: o.docs,\n            kind: getKind(\"keyword\"),\n          })),\n        ),\n      },\n    ] satisfies KWD[],\n    { cb, ss, setS, sql },\n  ).getSuggestion();\n};\n\nexport const getUserOpts = (\n  prevLC: string,\n  ss: ParsedSQLSuggestion[],\n  kind: number,\n) => {\n  const prevWords = prevLC.split(\" \");\n\n  if (\n    prevLC.endsWith(\"role\") ||\n    prevLC.endsWith(\"admin\") ||\n    prevLC.endsWith(\"group\")\n  ) {\n    return {\n      suggestions: ss.filter((s) => s.type === \"role\"),\n    };\n  }\n\n  // const nonUsedOptions = ((prevLC.includes(\"create user\") || prevLC.includes(\"alter user\"))? USER_OPTIONS : ROLE_OPTIONS)\n  const nonUsedOptions = ROLE_OPTIONS.filter(\n    (o) =>\n      !prevWords.some((w) =>\n        (o.params as readonly string[]).includes(w.toUpperCase()),\n      ),\n  );\n  const labels = nonUsedOptions\n    .flatMap((o) =>\n      o.params.map((p) => ({\n        label: prevWords.includes(\"with\") ? `${p} ` : `WITH ${p} `,\n        docs: o.docs,\n      })),\n    )\n    .concat([{ label: \"SET\", docs: \"Set user setting\" as any }]);\n\n  return suggestSnippets(\n    labels.map((l, i) => ({\n      ...l,\n      kind,\n      sortText: i.toString().padStart(2, \"0\"),\n    })),\n  );\n};\n\n/**\n * https://www.postgresql.org/docs/current/sql-createrole.html\n * \n \nconst paramNode = Array.from(document.querySelectorAll('h2')).find(el => el.textContent === 'Parameters')?.parentElement;\nArray.from(paramNode.querySelectorAll(\"dt\")).map(d => ({ params: d.innerText.split(\"\\n\"), docs: d.nextElementSibling.innerText }));\n\n */\nexport const ROLE_OPTIONS = [\n  {\n    params: [\"SUPERUSER\", \"NOSUPERUSER\"],\n    docs: \"These clauses determine whether the new role is a “superuser”, who can override all access restrictions within the database. Superuser status is dangerous and should be used only when really needed. You must yourself be a superuser to create a new superuser. If not specified, NOSUPERUSER is the default.\",\n  },\n  {\n    params: [\"LOGIN\", \"NOLOGIN\"],\n    docs: \"These clauses determine whether a role is allowed to log in; that is, whether the role can be given as the initial session authorization name during client connection. A role having the LOGIN attribute can be thought of as a user. Roles without this attribute are useful for managing database privileges, but are not users in the usual sense of the word. If not specified, NOLOGIN is the default, except when CREATE ROLE is invoked through its alternative spelling CREATE USER.\",\n  },\n  {\n    params: [\"ENCRYPTED PASSWORD 'password'\"],\n    docs: \"Sets the role's password. (A password is only of use for roles having the LOGIN attribute, but you can nonetheless define one for roles without it.) If you do not plan to use password authentication you can omit this option. If no password is specified, the password will be set to null and password authentication will always fail for that user. A null password can optionally be written explicitly as PASSWORD NULL.\\n\\nNote\\n\\nSpecifying an empty string will also set the password to null, but that was not the case before PostgreSQL version 10. In earlier versions, an empty string could be used, or not, depending on the authentication method and the exact version, and libpq would refuse to use it in any case. To avoid the ambiguity, specifying an empty string should be avoided.\\n\\nThe password is always stored encrypted in the system catalogs. The ENCRYPTED keyword has no effect, but is accepted for backwards compatibility. The method of encryption is determined by the configuration parameter password_encryption. If the presented password string is already in MD5-encrypted or SCRAM-encrypted format, then it is stored as-is regardless of password_encryption (since the system cannot decrypt the specified encrypted password string, to encrypt it in a different format). This allows reloading of encrypted passwords during dump/restore.\",\n  },\n  {\n    params: [\"CREATEDB\", \"NOCREATEDB\"],\n    docs: \"These clauses define a role's ability to create databases. If CREATEDB is specified, the role being defined will be allowed to create new databases. Specifying NOCREATEDB will deny a role the ability to create databases. If not specified, NOCREATEDB is the default.\",\n  },\n  {\n    params: [\"CREATEROLE\", \"NOCREATEROLE\"],\n    docs: \"These clauses determine whether a role will be permitted to create new roles (that is, execute CREATE ROLE). A role with CREATEROLE privilege can also alter and drop other roles. If not specified, NOCREATEROLE is the default.\",\n  },\n  {\n    params: [\"INHERIT\", \"NOINHERIT\"],\n    docs: \"These clauses determine whether a role “inherits” the privileges of roles it is a member of. A role with the INHERIT attribute can automatically use whatever database privileges have been granted to all roles it is directly or indirectly a member of. Without INHERIT, membership in another role only grants the ability to SET ROLE to that other role; the privileges of the other role are only available after having done so. If not specified, INHERIT is the default.\",\n  },\n  {\n    params: [\"REPLICATION\", \"NOREPLICATION\"],\n    docs: \"These clauses determine whether a role is a replication role. A role must have this attribute (or be a superuser) in order to be able to connect to the server in replication mode (physical or logical replication) and in order to be able to create or drop replication slots. A role having the REPLICATION attribute is a very highly privileged role, and should only be used on roles actually used for replication. If not specified, NOREPLICATION is the default. You must be a superuser to create a new role having the REPLICATION attribute.\",\n  },\n  {\n    params: [\"BYPASSRLS\", \"NOBYPASSRLS\"],\n    docs: \"These clauses determine whether a role bypasses every row-level security (RLS) policy. NOBYPASSRLS is the default. You must be a superuser to create a new role having the BYPASSRLS attribute.\\n\\nNote that pg_dump will set row_security to OFF by default, to ensure all contents of a table are dumped out. If the user running pg_dump does not have appropriate permissions, an error will be returned. However, superusers and the owner of the table being dumped always bypass RLS.\",\n  },\n  {\n    params: [\"CONNECTION LIMIT $connlimit\"],\n    docs: \"If role can log in, this specifies how many concurrent connections the role can make. -1 (the default) means no limit. Note that only normal connections are counted towards this limit. Neither prepared transactions nor background worker connections are counted towards this limit.\",\n  },\n  {\n    params: [\"VALID UNTIL 'timestamp'\"],\n    docs: \"The VALID UNTIL clause sets a date and time after which the role's password is no longer valid. If this clause is omitted the password will be valid for all time.\",\n  },\n  {\n    params: [\"IN ROLE\"],\n    docs: \"The IN ROLE clause lists one or more existing roles to which the new role will be immediately added as a new member. (Note that there is no option to add the new role as an administrator; use a separate GRANT command to do that.)\",\n  },\n  {\n    params: [\"IN GROUP\"],\n    docs: \"IN GROUP is an obsolete spelling of IN ROLE.\",\n  },\n  {\n    params: [\"ROLE\"],\n    docs: \"The ROLE clause lists one or more existing roles which are automatically added as members of the new role. (This in effect makes the new role a “group”.)\",\n  },\n  {\n    params: [\"ADMIN\"],\n    docs: \"The ADMIN clause is like ROLE, but the named roles are added to the new role WITH ADMIN OPTION, giving them the right to grant membership in this role to others.\",\n  },\n  {\n    params: [\"USER\"],\n    docs: \"The USER clause is an obsolete spelling of the ROLE clause.\",\n  },\n  {\n    params: [\"SYSID uid\"],\n    docs: \"The SYSID clause is ignored, but is accepted for backwards compatibility.\",\n  },\n] as const;\n\nconst USER_OPTIONS = [\n  ...ROLE_OPTIONS.filter((r) =>\n    r.params.some(\n      (p: (typeof ROLE_OPTIONS)[number][\"params\"][number]) =>\n        p === \"CREATEDB\" ||\n        p === \"VALID UNTIL 'timestamp'\" ||\n        p === \"SYSID uid\" ||\n        p === \"ENCRYPTED PASSWORD 'password'\" ||\n        p === \"IN GROUP\",\n    ),\n  ),\n  {\n    params: [\"CREATEUSER\", \"NOCREATEUSER\"],\n    docs: \"These clauses determine whether a user will be permitted to create new users himself. CREATEUSER will also make the user a superuser, who can override all access restrictions. If not specified, NOCREATEUSER is the default.\",\n  },\n] as const;\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchComment.ts",
    "content": "import { suggestSnippets } from \"./CommonMatchImports\";\nimport { getExpected } from \"./getExpected\";\nimport type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\n\nexport const MatchComment: SQLMatcher = {\n  match: (cb) => cb.tokens[0]?.textLC === \"comment\",\n  result: ({ cb, ss }) => {\n    const { prevLC, ltoken } = cb;\n\n    if (\n      ltoken?.type === \"identifier.sql\" &&\n      ![\"comment\", \"on\", \"is\"].includes(ltoken.textLC)\n    ) {\n      return suggestSnippets([{ label: \"IS 'your comment$0'\" }]);\n    }\n\n    if (ltoken?.textLC === \"comment\") {\n      return suggestSnippets([{ label: \"ON \" }]);\n    } else if (ltoken?.textLC === \"on\") {\n      return suggestSnippets(COMMENT_ON.map((label) => ({ label })));\n    } else if (prevLC.startsWith(\"comment on\")) {\n      const expect = ltoken!.textLC;\n      if (expect === \"column\") {\n        return {\n          suggestions: ss\n            .filter((s) => s.type === \"column\")\n            .map((s) => ({\n              ...s,\n              insertText: `${s.escapedParentName}.${s.escapedIdentifier}`,\n            })),\n        };\n      }\n      return getExpected(expect, cb, ss);\n    }\n    return {\n      suggestions: [],\n    };\n  },\n};\n\nexport const COMMENT_ON = [\n  \"ACCESS METHOD ${1:object_name}\",\n  \"AGGREGATE ${1:aggregate_name}\",\n  \"CAST\",\n  \"COLUMN ${1:column_name}\",\n  // \"COLUMN ${1:table_name_dot_column_name}\",\n  \"CONSTRAINT ${1:constraint_name} ON ${2:table_name}\",\n  \"CONSTRAINT ${1:constraint_name} ON DOMAIN ${2:domain_name}\",\n  \"CONVERSION ${1:object_name}\",\n  \"COLLATION ${1:object_name}\",\n  \"DATABASE ${1:object_name}\",\n  \"DOMAIN ${1:object_name}\",\n  \"EXTENSION ${1:object_name}\",\n  \"EVENT TRIGGER ${1:object_name}\",\n  \"FOREIGN DATA WRAPPER ${1:object_name}\",\n  \"FOREIGN TABLE ${1:object_name}\",\n  \"FUNCTION ${1:function_name} \",\n  \"INDEX ${1:object_name}\",\n  \"LARGE OBJECT ${1:large_object_oid}\",\n  \"MATERIALIZED VIEW ${1:object_name}\",\n  \"OPERATOR ${1:operator_name} \",\n  \"OPERATOR CLASS ${1:object_name} USING ${2:index_method}\",\n  \"OPERATOR FAMILY ${1:object_name} USING ${2:index_method}\",\n  \"POLICY ${1:policy_name} ON ${2:table_name}\",\n  \"LANGUAGE ${1:object_name}\",\n  \"PROCEDURE ${1:procedure_name} \",\n  \"PUBLICATION ${1:object_name}\",\n  \"ROLE ${1:object_name}\",\n  \"ROUTINE ${1:routine_name} \",\n  \"RULE ${1:rule_name} ON ${2:table_name}\",\n  \"SCHEMA ${1:object_name}\",\n  \"SEQUENCE ${1:object_name}\",\n  \"SERVER ${1:object_name}\",\n  \"STATISTICS ${1:object_name}\",\n  \"SUBSCRIPTION ${1:object_name}\",\n  \"TABLE ${1:object_name}\",\n  \"TABLESPACE ${1:object_name}\",\n  \"TEXT SEARCH CONFIGURATION ${1:object_name}\",\n  \"TEXT SEARCH DICTIONARY ${1:object_name}\",\n  \"TEXT SEARCH PARSER ${1:object_name}\",\n  \"TEXT SEARCH TEMPLATE ${1:object_name}\",\n  \"TRANSFORM FOR ${1:type_name} LANGUAGE ${2:lang_name}\",\n  \"TRIGGER ${1:trigger_name} ON ${2:table_name}\",\n  \"TYPE ${1:object_name}\",\n  \"VIEW ${1:object_name}\",\n].map((v) => v + \" IS '${3:your_comment}' \");\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchCopy.ts",
    "content": "import type { SQLHandler } from \"prostgles-types\";\nimport { isObject } from \"@common/publishUtils\";\nimport { EXCLUDE_FROM_SCHEMA_WATCH } from \"@common/utils\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { ENCODINGS } from \"./PSQL\";\nimport {\n  getKind,\n  type GetKind,\n  type ParsedSQLSuggestion,\n  type SQLMatcher,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\nimport { withKWDs } from \"./withKWDs\";\nimport type { CodeBlock } from \"./completionUtils/getCodeBlock\";\n\nconst KWDOptions = [\n  {\n    kwd: \"FORMAT\",\n    options: [\"TEXT\", \"CSV\", \"BINARY\"].map((label) => ({ label })),\n    docs: \"Selects the data format to be read or written: text, csv (Comma Separated Values), or binary. The default is text.\",\n  },\n\n  {\n    kwd: \"FREEZE\",\n    options: [\"true\", \"false\"].map((label) => ({ label })),\n    docs: \"Requests copying the data with rows already frozen, just as they would be after running the VACUUM FREEZE command. This is intended as a performance option for initial data loading. Rows will be frozen only if the table being loaded has been created or truncated in the current subtransaction, there are no cursors open and there are no older snapshots held by this transaction. It is currently not possible to perform a COPY FREEZE on a partitioned table.\",\n  },\n\n  {\n    kwd: \"DELIMITER\",\n    options: [\"E'\\\\t'\", \"','\"].map((label) => ({ label })),\n    docs: \"Specifies the character that separates columns within each row (line) of the file. The default is a tab character in text format, a comma in CSV format. This must be a single one-byte character. This option is not allowed when using binary format.\",\n  },\n  {\n    kwd: \"NULL\",\n    options: [\"\"],\n    docs: \"Specifies the string that represents a null value. The default is \\\\N (backslash-N) in text format, and an unquoted empty string in CSV format. You might prefer an empty string even in text format for cases where you don't want to distinguish nulls from empty strings. This option is not allowed when using binary format.\",\n  },\n  {\n    kwd: \"HEADER\",\n    options: [\"TRUE\", \"FALSE\", \"MATCH\"].map((label) => ({ label })),\n    docs: \"Specifies that the file contains a header line with the names of each column in the file. On output, the first line contains the column names from the table. On input, the first line is discarded when this option is set to true (or equivalent Boolean value). If this option is set to MATCH, the number and names of the columns in the header line must match the actual column names of the table, in order; otherwise an error is raised. This option is not allowed when using binary format. The MATCH option is only valid for COPY FROM commands.\",\n  },\n  {\n    kwd: \"QUOTE\",\n    options: [`'\"'`, `''''`].map((label) => ({ label })),\n    docs: \"Specifies the quoting character to be used when a data value is quoted. The default is double-quote. This must be a single one-byte character. This option is allowed only when using CSV format.\",\n  },\n  {\n    kwd: \"ESCAPE\",\n    dependsOn: \"CSV\",\n    options: [`'\"'`, `''''`].map((label) => ({ label })),\n    docs: \"Specifies the character that should appear before a data character that matches the QUOTE value. The default is the same as the QUOTE value (so that the quoting character is doubled if it appears in the data). This must be a single one-byte character. This option is allowed only when using CSV format.\",\n  },\n  {\n    kwd: \"FORCE_QUOTE\",\n    options: [\"*\"],\n    dependsOn: \"CSV\",\n    docs: \"Forces quoting to be used for all non-NULL values in each specified column. NULL output is never quoted. If * is specified, non-NULL values will be quoted in all columns. This option is allowed only in COPY TO, and only when using CSV format.\",\n  },\n  {\n    kwd: \"FORCE_NOT_NULL\",\n    options: [\"*\"],\n    dependsOn: \"CSV\",\n    docs: \"Do not match the specified columns' values against the null string. In the default case where the null string is empty, this means that empty values will be read as zero-length strings rather than nulls, even when they are not quoted. This option is allowed only in COPY FROM, and only when using CSV format.\",\n  },\n  {\n    kwd: \"FORCE_NULL\",\n    options: [\"*\"],\n    dependsOn: \"CSV\",\n    docs: \"Match the specified columns' values against the null string, even if it has been quoted, and if a match is found set the value to NULL. In the default case where the null string is empty, this converts a quoted empty string into NULL. This option is allowed only in COPY FROM, and only when using CSV format.\",\n  },\n  {\n    kwd: \"ENCODING\",\n    options: ENCODINGS.map((label) => ({ label })),\n    docs: \"Specifies that the file is encoded in the encoding_name. If this option is omitted, the current client encoding is used. See the Notes below for more details.\",\n  },\n  {\n    kwd: \"WHERE\",\n    docs: \"where condition is any expression that evaluates to a result of type boolean. Any row that does not satisfy this condition will not be inserted to the table. A row satisfies the condition if it returns true when the actual row values are substituted for any variable references. \\\n    Currently, subqueries are not allowed in WHERE expressions, and the evaluation does not see any changes made by the COPY itself (this matters when the expression contains calls to VOLATILE functions).\",\n  },\n] as const satisfies readonly KWD[];\n\nexport const MatchCopy: SQLMatcher = {\n  match: (cb) => cb.prevLC.startsWith(\"copy\"),\n  result: async ({ cb, ss, setS, sql }) => {\n    const { prevLC, prevTokens, prevText } = cb;\n\n    if (\n      prevTokens.some(\n        (t) => t.type === \"string.sql\" || t.textLC.includes(\"$\"),\n      ) ||\n      prevText.endsWith(\"'\")\n    ) {\n      if (cb.ltoken?.type === \"string.sql\" || prevText.trim().endsWith(\"$\")) {\n        return suggestSnippets([\n          {\n            label: { label: \"( options... )\" },\n            docs: \"Import options\",\n            insertText: \"( $0 )\",\n          },\n        ]);\n      }\n      const { getSuggestion } = withKWDs(KWDOptions, { cb, ss, setS, sql });\n      return getSuggestion(\",\", [\"(\", \")\"]);\n    }\n\n    if (prevLC.endsWith(\"(\") || prevLC.endsWith(\",\")) {\n      const columns = prevText.split(\"(\")[1]?.split(\")\")[0] || \"\";\n\n      return {\n        suggestions: ss\n          .filter(\n            (s) =>\n              s.type === \"column\" &&\n              !columns.includes(s.name) &&\n              prevText.includes(s.escapedParentName as any),\n          )\n          .map((s) => ({\n            ...s,\n            sortText: `${s.colInfo?.ordinal_position}`.padStart(3, \"0\"),\n            insertText: s.insertText + \",\",\n          })),\n      };\n    }\n    if (cb.tokens.length === 2) {\n      const tableName = cb.tokens[1]?.text;\n      const colSuggestion = {\n        label: { label: \"( columns... )\" },\n        insertText: \"( $0 )\",\n      };\n      if (!prevText.includes(\"(\")) {\n        if (tableName) {\n          const cols = ss.filter(\n            (s) =>\n              s.type === \"column\" && s.escapedParentName?.includes(tableName),\n          );\n          if (cols.length) {\n            colSuggestion.insertText = `( ${cols.map((c) => c.insertText).join(\", \")} )`;\n          }\n        }\n      }\n      return suggestSnippets([\n        { label: \"FROM\" },\n        ...(prevText.includes(\"(\") ? [] : [colSuggestion]),\n      ]);\n    }\n\n    return withKWDs(\n      [\n        { kwd: \"COPY\", expects: \"table\" },\n        {\n          kwd: \"FROM\",\n          options: [\n            ...(cb.currToken?.type === \"string.sql\" ?\n              []\n            : [{ label: \"PROGRAM\", kind: getKind(\"keyword\") }]),\n            ...(cb.prevTokens.length > 2 ?\n              await getPathSuggestions(cb, sql, getKind)\n            : []),\n          ],\n        },\n        {\n          kwd: \"PROGRAM\",\n          exactlyAfter: [\"FROM\"],\n          expects: \"string\",\n        },\n      ] satisfies KWD[],\n      { cb, ss, setS, sql },\n    ).getSuggestion();\n  },\n};\n\nconst getPathSuggestions = async (\n  { currToken }: CodeBlock,\n  sql: SQLHandler | undefined,\n  getKind: GetKind,\n): Promise<ParsedSQLSuggestion[]> => {\n  if (!sql) return [];\n\n  const dirOrfiles = await suggestDirsAndFiles(sql, currToken?.text);\n\n  if (\"label\" in dirOrfiles) {\n    return suggestSnippets([\n      {\n        label: dirOrfiles.label,\n        docs: dirOrfiles.hint,\n        insertText: \"\",\n        kind: getKind(\"folder\"),\n      },\n    ]).suggestions;\n  }\n\n  return suggestSnippets(\n    dirOrfiles.map((d) => ({\n      label: d.path, // + (!d.info? \"\" : `    ${d.info?.size_pretty || \"\"}`),\n      insertText: d.insertText,\n      docs: d.documentation,\n      commitCharacters: d.commitCharacters,\n      kind: getKind(d.info?.isdir === false ? \"file\" : \"folder\"),\n    })),\n  ).suggestions;\n};\n\ntype DirOrFile = {\n  access: Date;\n  change: Date;\n  creation: Date;\n  isdir: boolean;\n  modification: Date;\n  size: string;\n  size_pretty: string;\n  firstRow?: string;\n};\ntype DirFilesResult = {\n  path: string;\n  documentation: string;\n  insertText: string;\n  info?: DirOrFile;\n  commitCharacters?: string[];\n};\nexport const suggestDirsAndFiles = async (\n  sql: SQLHandler,\n  lastWord = \"\",\n): Promise<DirFilesResult[] | { label: string; hint?: string }> => {\n  const hasPath = lastWord.includes(\"'\") || lastWord.startsWith(\"/\");\n  let baseDir = hasPath ? lastWord : \"/\";\n  if (baseDir.startsWith(\"'\")) baseDir = baseDir.slice(1);\n  if (baseDir.endsWith(\"'\")) baseDir = baseDir.slice(0, -1);\n\n  /* Remove partial folder name */\n  if (baseDir.length > 1 && baseDir.endsWith(\"/\")) {\n    const parts = baseDir.slice(1).split(\"/\");\n    if (parts.length > 2) {\n      if (parts.at(-1)?.length) {\n        baseDir = parts.slice(0, -1).join(\"/\");\n      }\n    }\n  }\n  let dirs: DirFilesResult[] = [];\n  let error;\n  try {\n    try {\n      dirs = (await sql(\n        \"set statement_timeout to 200; SELECT pg_ls_dir(${baseDir}) as path\",\n        { baseDir },\n        { returnType: \"rows\" },\n      )) as any;\n    } catch (err: any) {\n      dirs = [\n        {\n          path: \"\",\n          insertText: \"\",\n          documentation: \"\",\n        },\n      ];\n\n      return {\n        label: \"Error\",\n        hint: err.err_msg?.toString(),\n      };\n    }\n\n    dirs = await Promise.all(\n      dirs\n        /** Some queries take too long (SELECT * FROM pg_stat_file('/pagefile.sys'))  */\n        .filter(\n          (d) =>\n            ![\"swapfile.sys\", \"pagefile.sys\", \"DumpStack.log.tmp\"].includes(\n              d.path,\n            ) && !d.path.endsWith(\".sys\"),\n        )\n        .map(async (d) => {\n          const res = {\n            ...d,\n            insertText: hasPath ? d.path : `'${baseDir}${d.path}'`,\n          };\n          try {\n            const fileOrDirPath = `${baseDir}${d.path}`;\n            const f = (await sql(\n              \"set statement_timeout to 200; SELECT *, pg_size_pretty(size::bigint) as size_pretty from pg_stat_file(${baseDir})\",\n              { baseDir: fileOrDirPath },\n              { returnType: \"row\" },\n            )) as DirOrFile;\n            const isdir = f.isdir;\n            const nameParts = d.path.split(\".\");\n            const ext = nameParts.at(-1);\n            let firstRow = \"\";\n            if (!isdir && nameParts.length > 1 && ext!.length < 4) {\n              // isdir = false;\n\n              if (ext?.toLowerCase() === \"csv\") {\n                const q = `\n                /* ${EXCLUDE_FROM_SCHEMA_WATCH}  */\n                DROP TABLE IF EXISTS prostgles.temp_dir_suggestions; \n                CREATE table prostgles.temp_dir_suggestions(val text); \n                COPY prostgles.temp_dir_suggestions (val) FROM PROGRAM \\${command}; \n                SELECT * FROM prostgles.temp_dir_suggestions; \n              `;\n                const head = (await sql(\n                  q,\n                  { command: `head -n 1 ${baseDir}${d.path}` },\n                  { returnType: \"row\" },\n                )) as { val?: string };\n                if (head.val) {\n                  firstRow = head.val;\n                }\n              }\n            }\n            res.info = {\n              ...f,\n              isdir,\n              firstRow,\n              size: f.size_pretty,\n              access: new Date(f.access),\n              change: new Date(f.change),\n              creation: new Date(f.creation),\n              modification: new Date(f.modification),\n            };\n          } catch (err) {\n            console.warn(err);\n          }\n\n          return res;\n        }),\n    );\n    await sql(\"set statement_timeout to DEFAULT\");\n  } catch (errRaw) {\n    await sql(\"set statement_timeout to DEFAULT\");\n    error =\n      errRaw instanceof Error ? errRaw.message\n      : isObject(errRaw) ? errRaw.err_msg\n      : JSON.stringify(errRaw);\n    return { label: error };\n  }\n\n  if (!dirs.length)\n    return {\n      label: \"Directory is empty\",\n    };\n\n  dirs = dirs.map((d) => {\n    let documentation = \"\";\n    if (d.info) {\n      documentation += `  \\nType: \\`${d.info.isdir ? \"Folder\" : \"File\"}\\`    `;\n      documentation += `  \\nSize: \\`${d.info.size_pretty}\\`    `;\n      documentation += `  \\nModified: \\`${d.info.modification}\\`    `;\n    }\n    if (d.info?.firstRow) {\n      // if(d.path.includes(\"stateplane\")) debugger;\n      documentation +=\n        \"  \\n\\n**Headers:**  \\n\\n\" +\n        d.info.firstRow\n          .split(\",\")\n          .map((v) => `${v}   \\`TEXT\\``)\n          .join(\",    \\n\");\n    }\n    return {\n      ...d,\n      commitCharacters: d.info?.isdir ? [\"/\"] : undefined,\n      documentation,\n    };\n  });\n\n  return dirs;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchDrop.ts",
    "content": "import { isDefined } from \"prostgles-types\";\nimport { SUGGESTION_TYPE_DOCS } from \"../W_SQLEditor\";\nimport type { MinimalSnippet } from \"./CommonMatchImports\";\nimport { PG_OBJECTS, suggestSnippets } from \"./CommonMatchImports\";\nimport { cleanExpectFull } from \"./getExpected\";\nimport type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport { getKind } from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\nimport { withKWDs } from \"./withKWDs\";\n\nexport const MatchDrop: SQLMatcher = {\n  match: (cb) => cb.prevLC.startsWith(\"drop\"),\n  result: async ({ cb, ss, setS, sql }) => {\n    const { prevLC, ltoken, l1token } = cb;\n\n    const expect = cb.tokens[1]?.textLC;\n    const afterExpect = cb.tokens[2]?.textLC;\n\n    if (ltoken?.textLC === \"owned\") {\n      return suggestSnippets([{ label: \"BY\" }]);\n    }\n\n    if (\n      l1token?.textLC === \"database\" ||\n      (cb.prevTokens[1]?.textLC === \"database\" && l1token?.textLC === \"exists\")\n    ) {\n      return suggestSnippets([\n        {\n          label: \"WITH (FORCE)\",\n          kind: getKind(\"keyword\"),\n          docs: `Attempt to terminate all existing connections to the target database. It doesn't terminate if prepared transactions, active logical replication slots or subscriptions are present in the target database.\\n\\nThis will fail if the current user has no permissions to terminate other connections. Required permissions are the same as with pg_terminate_backend, described in Section 9.27.2. This will also fail if we are not able to terminate connections.`,\n        },\n      ]);\n    }\n\n    if (prevLC === \"drop\") {\n      const r = suggestSnippets(\n        PG_OBJECTS.flatMap<MinimalSnippet>((label) =>\n          [\" IF EXISTS\", \"\"].map((ifEx) => {\n            const labelUpper = label.toUpperCase();\n            return {\n              label: `${label}${ifEx}`,\n              docs: SUGGESTION_TYPE_DOCS[labelUpper],\n              kind: getKind((label.toLowerCase() as any) ?? \"keyword\"),\n            };\n          }),\n        ),\n      );\n      return {\n        suggestions: r.suggestions.concat(\n          suggestSnippets([\n            {\n              label: \"OWNED BY\",\n              insertText: \"OWNED BY\",\n              docs: \"DROP OWNED drops all the objects within the current database that are owned by one of the specified roles. Any privileges granted to the given roles on objects in the current database or on shared objects (databases, tablespaces, configuration parameters) will also be revoked.\",\n            },\n          ]).suggestions,\n        ),\n      };\n    } else if (expect) {\n      const kwds = getKWDS(expect, afterExpect);\n      if (kwds) {\n        const result = {\n          suggestions: (\n            await withKWDs(kwds as any, { cb, ss, setS, sql }).getSuggestion()\n          ).suggestions\n            .filter((s) => s.type !== \"extension\" || s.extensionInfo?.installed)\n            .map((s) => ({\n              ...s,\n              sortText:\n                s.insertText.trim() === \"IF EXISTS\" ? \"Zzz\" : s.sortText,\n              insertText:\n                s.funcInfo && s.funcInfo.args.length ?\n                  `${s.insertText}(${s.funcInfo.args.map((a) => a.data_type).join(\", \")})`\n                : s.type === \"policy\" ?\n                  `${s.policyInfo?.escaped_identifier} ON ${s.policyInfo?.tablename_escaped}`\n                : s.type === \"trigger\" ?\n                  `${s.insertText} ON ${s.triggerInfo?.event_object_table}`\n                : s.ruleInfo ?\n                  `${s.insertText} ON ${s.ruleInfo.tablename_escaped}`\n                  // s.type === \"database\"? `${s.insertText}\\n\\n${getDBInsertText(s.name)}` :\n                : s.insertText,\n            })),\n        };\n        return result;\n      }\n\n      // const expected = getExpected(expect, cb, ss).suggestions;\n\n      // const getDBInsertText = (v: string) => \"/* --If cannot drop due to it being in use then run this query from a different database on same server to close all connections \\n\" +\n      // \"SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity \\n\" +\n      // \"WHERE pg_stat_activity.datname = '\" + v + \"' AND pid <> pg_backend_pid();\\n\"+\n      // \"DROP DATABASE \" + v + \";\\n \\*/\"\n    }\n\n    return {\n      suggestions: [],\n    };\n  },\n};\n\nconst getKWDS = (rawExpect: string, afterExpect?: string  ) => {\n  if (rawExpect.toLowerCase() === \"owned\") {\n    const kwd = \"OWNED BY\";\n    return [\n      {\n        kwd,\n        expects: \"role\",\n        excludeIf: [rawExpect.toUpperCase()], // exclude a case where rawExpect=ROLE and cleanExpect=[USER] creates a bug that allows DROP ROLE USER ...\n        docs: `\nDROP OWNED is often used to prepare for the removal of one or more roles. Because DROP OWNED only affects the objects in the current database, it is usually necessary to execute this command in each database that contains objects owned by a role that is to be removed.\n\nUsing the CASCADE option might make the command recurse to objects owned by other users.\n\nThe REASSIGN OWNED command is an alternative that reassigns the ownership of all the database objects owned by one or more roles. However, REASSIGN OWNED does not deal with privileges for other objects.\n        `,\n      },\n      {\n        kwd: \"CASCADE\",\n        dependsOn: kwd,\n        docs: `Automatically drop objects that depend on the affected objects, and in turn all objects that depend on those objects. \\nThe default is to refuse to drop the role if any objects depend on it`,\n      },\n    ] as const satisfies readonly KWD[];\n  }\n\n  const cl = cleanExpectFull(rawExpect, afterExpect);\n  const expect = cl?.expect[0];\n  if (!expect) return undefined;\n\n  const objLabel = expect === \"mview\" ? \"materialized view\" : rawExpect;\n  const kwd = (cl.kwd ?? objLabel).toUpperCase();\n  const kwds: (KWD | undefined)[] = [\n    {\n      kwd,\n      expects: expect,\n      excludeIf: [rawExpect.toUpperCase()], // exclude a case where rawExpect=ROLE and cleanExpect=[USER] creates a bug that allows DROP ROLE USER ...\n      options: [\n        {\n          label: \"IF EXISTS\",\n          kind: getKind(\"keyword\"),\n          insertText: \"IF EXISTS \",\n          docs: `Do not throw an error if the ${objLabel} does not exist. A notice is issued in this case.`,\n        },\n      ],\n    },\n    expect === \"table\" ?\n      { kwd: \",\", expects: expect, canRepeat: true }\n    : undefined,\n    { kwd: \"IF EXISTS\", expects: expect, exactlyAfter: [kwd] },\n    expect === \"policy\" ?\n      ({ kwd: \"ON\", expects: \"table\", dependsOn: kwd } as const)\n    : undefined,\n    {\n      kwd: \"CASCADE\",\n      dependsOn: kwd,\n      docs: `Automatically drop objects that depend on the ${objLabel}, and in turn all objects that depend on those objects. \\nThe default is to refuse to drop the ${objLabel} if any objects depend on it`,\n    },\n  ];\n\n  return kwds.filter(isDefined);\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchFirst.ts",
    "content": "import PSQL_COMMANDS from \"@common/psql_queries.json\";\nimport {\n  getMonaco,\n  SUGGESTION_TYPE_DOCS,\n  SUGGESTION_TYPES,\n} from \"../W_SQLEditor\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { getExpected } from \"./getExpected\";\nimport { asSQL, TOP_KEYWORDS } from \"./KEYWORDS\";\nimport { getParentFunction } from \"./MatchSelect\";\nimport {\n  getKind,\n  type MonacoSuggestion,\n  type SQLMatchContext,\n  type SuggestionItem,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport { suggestColumnLike } from \"./suggestColumnLike\";\nimport { suggestCondition } from \"./suggestCondition\";\nimport { suggestTableLike } from \"./suggestTableLike\";\nimport { type KWD, suggestKWD, withKWDs } from \"./withKWDs\";\n\nexport const MatchFirst = async ({\n  cb,\n  ss,\n  setS,\n  sql,\n}: Pick<SQLMatchContext, \"cb\" | \"ss\" | \"setS\" | \"sql\">): Promise<\n  | undefined\n  | {\n      suggestions: SuggestionItem[];\n    }\n> => {\n  const languages = (await getMonaco()).languages;\n  const { isCommenting, prevText, currToken, l1token, ltoken, ftoken } = cb;\n\n  if (isCommenting) {\n    return { suggestions: [] };\n  }\n\n  if ([ltoken, currToken].some((t) => t?.text === \"::\")) {\n    return {\n      suggestions: ss\n        .filter((s) => s.type === \"dataType\")\n        .map((s) => ({ ...s, sortText: s.dataTypeInfo!.priority })),\n    };\n  }\n\n  const _suggestKWD = (vals: string[], sortText?: string) =>\n    suggestKWD(getKind, vals, sortText);\n\n  if (cb.ftoken?.textLC === \"cluster\") {\n    return withKWDs(\n      [\n        { kwd: \"CLUSTER\", expects: \"table\" },\n        { kwd: \"USING\", expects: \"index\" },\n      ],\n      { cb, ss, setS, sql },\n    ).getSuggestion();\n  }\n\n  /** Suggest format function string template content */\n  if (\n    currToken?.type === \"string.sql\" &&\n    cb.ltoken?.text === \"(\" &&\n    cb.l1token?.textLC === \"format\"\n  ) {\n    const opts = {\n      kind: languages.CompletionItemKind.Text,\n      insertTextRules: languages.CompletionItemInsertTextRule.KeepWhitespace,\n    };\n    return suggestSnippets([\n      {\n        label: \"%s\",\n        ...opts,\n        docs: asSQL(`SELECT FORMAT('Hello %s', 'world');\\n=>\\n'Hello world'`),\n      },\n      {\n        label: \"%I\",\n        ...opts,\n        docs: asSQL(\n          `SELECT FORMAT('Hello %I', 'select');\\n=>\\n'Hello \"select\"`,\n        ),\n      },\n      {\n        label: \"%L\",\n        ...opts,\n        docs: asSQL(`SELECT FORMAT('Hello %L', 'world');\\n=>\\n'Hello 'world'`),\n      },\n      {\n        label: \"%1$s\",\n        ...opts,\n        docs: asSQL(\n          `SELECT FORMAT('%1$s apple, %2$s orange, %1$s banana', 'small', 'big');\\n=>\\n' small apple, big orange, small banana'`,\n        ),\n      },\n    ]);\n  }\n  if (\n    cb.currNestingFunc?.textLC === \"to_char\" &&\n    currToken?.type === \"string.sql\"\n  ) {\n    const func = getParentFunction(cb);\n    if (func?.prevArgs.length) {\n      const opts = {\n        kind: languages.CompletionItemKind.Text,\n        insertTextRules: languages.CompletionItemInsertTextRule.KeepWhitespace,\n      };\n      return suggestSnippets(\n        TO_CHAR_PATTERNS.map((p) => ({\n          label: p.label,\n          ...opts,\n          docs: `**Timestamp Formatting**\\n\\n` + p.docs,\n        })),\n      );\n    }\n  }\n\n  if (\n    l1token?.text === \"?\" ||\n    ltoken?.text === \"?\" ||\n    currToken?.text === \"?\"\n  ) {\n    /** Wildcard search all suggestions */\n    if (\n      !ltoken ||\n      cb.tokens.length < 2 ||\n      (cb.currToken && cb.tokens.length <= 2)\n    ) {\n      return suggestSnippets(\n        SUGGESTION_TYPES.filter((t) => ![\"file\", \"folder\"].includes(t)).map(\n          (label) => ({\n            label,\n            kind: getKind(label),\n            insertText: label,\n            docs: SUGGESTION_TYPE_DOCS[label] || \"\",\n          }),\n        ),\n      );\n    }\n    const expect = ltoken.text;\n\n    if (expect === \"setting\") {\n      return { suggestions: setS };\n    }\n    return getExpected(expect, cb, ss);\n  }\n\n  if (ltoken?.type === \"keyword.block.sql\" && ltoken.textLC === \"case\") {\n    return _suggestKWD([\"WHEN\"]);\n  }\n\n  const condMatch = await suggestCondition({ cb, ss, sql, setS }, false);\n  if (condMatch) {\n    return condMatch;\n  }\n\n  if (ftoken?.textLC === \"table\") {\n    return suggestTableLike({ cb, ss, sql, parentCb: undefined });\n  }\n\n  if (ftoken?.textLC === \"explain\") {\n    if (cb.currNestingFunc?.textLC === \"explain\") {\n      const { getSuggestion } = withKWDs(ExplainOptions, {\n        cb,\n        ss,\n        setS,\n        sql,\n        opts: { notOrdered: true },\n      });\n      return getSuggestion(\",\", [\"(\", \")\"]);\n    }\n    if (cb.ltoken?.textLC === \"explain\") {\n      const hasNoStatement = !TOP_KEYWORDS.some(\n        (kwd) => !cb.text.toLowerCase().includes(kwd.label.toLowerCase()),\n      );\n      if (!hasNoStatement) {\n        return suggestSnippets([\n          { label: \"(...options)\", insertText: \"( $0 )\" },\n        ]);\n      }\n      return withKWDs(EXPLAIN_KWDS, { cb, ss, setS, sql }).getSuggestion();\n    }\n  }\n\n  if (ftoken?.textLC === \"truncate\") {\n    const d = withKWDs(\n      [\n        {\n          kwd: \"TRUNCATE\",\n          expects: \"table\",\n          options: [\n            {\n              label: \"ONLY\",\n              kind: getKind(\"keyword\"),\n              docs: \"If ONLY is specified before the table name, only that table is truncated. If ONLY is not specified, the table and all its descendant tables (if any) are truncated. Optionally, * can be specified after the table name to explicitly indicate that descendant tables are included.\",\n            },\n          ],\n        },\n        {\n          kwd: \"RESTART IDENTITY\",\n          dependsOn: \"TRUNCATE\",\n          docs: \"Automatically restart sequences owned by columns of the truncated table(s).\",\n        },\n        {\n          kwd: \"CONTINUE IDENTITY\",\n          dependsOn: \"TRUNCATE\",\n          docs: \"Do not change the values of sequences. This is the default.\",\n        },\n        {\n          kwd: \"CASCADE\",\n          dependsOn: \"TRUNCATE\",\n          docs: \"Automatically truncate all tables that have foreign-key references to any of the named tables, or to any tables added to the group due to CASCADE.\",\n        },\n        {\n          kwd: \"RESTRICT\",\n          dependsOn: \"TRUNCATE\",\n          docs: \"Refuse to truncate if any of the tables have foreign-key references from tables that are not listed in the command. This is the default.\",\n        },\n      ],\n      { cb, ss, setS: setS, sql },\n    );\n    return d.getSuggestion();\n  }\n\n  if (ftoken?.textLC === \"call\") {\n    if (cb.prevTokens.length === 1) {\n      return getExpected(\"function\", cb, ss);\n    }\n    if (cb.currNestingId) {\n      return suggestColumnLike({ cb, ss, sql, setS });\n    }\n  }\n\n  if (prevText.trim().startsWith(\"\\\\\")) {\n    return {\n      suggestions: PSQL_COMMANDS.map(\n        (c) =>\n          ({\n            // label: { label: c.cmd, description: c.desc },\n            label: { label: c.cmd + \"   \" + c.desc },\n            insertText: `/* psql ${c.cmd} --${c.desc} */${c.query}`,\n            kind: getKind(\"snippet\"),\n            range: {\n              startColumn: 0,\n              startLineNumber: cb.startLine,\n              endColumn: 132,\n              endLineNumber: cb.endLine,\n            },\n            filterText: `${c.cmd} ${c.desc}`,\n          }) as MonacoSuggestion,\n      ),\n    };\n  }\n\n  /** Refresh materialized view */\n  if (ftoken?.textLC === \"refresh\") {\n    if (cb.tokens.some((t) => t.textLC === \"view\")) {\n      return {\n        suggestions: ss.filter((s) => s.relkind === \"m\"),\n      };\n    }\n\n    return suggestSnippets([\n      {\n        label: \"MATERIALIZED VIEW\",\n        insertText: \"MATERIALIZED VIEW \",\n        docs: \"Replace the contents of a materialized view\",\n      },\n      {\n        label: \"REFRESH MATERIALIZED VIEW CONCURRENTLY\",\n        insertText: \"REFRESH MATERIALIZED VIEW CONCURRENTLY \",\n        docs: \"Replace the contents of a materialized view\",\n      },\n    ]);\n  }\n\n  if (\n    (ltoken?.textLC === \"inner\" ||\n      ltoken?.textLC === \"left\" ||\n      ltoken?.textLC === \"right\") &&\n    ltoken.type === \"identifier.sql\"\n  ) {\n    return suggestSnippets([\"join\"].map((label) => ({ label })));\n  }\n\n  return undefined;\n};\n\nconst EXPLAIN_KWDS = [\n  {\n    kwd: \"EXPLAIN\",\n    options: [\n      {\n        label: { label: \"( ...options )\" },\n        insertText: \"( $options )\",\n      },\n      {\n        label: { label: \"$statement\" },\n        insertText: \"$0\",\n      },\n    ],\n  },\n] satisfies KWD[];\n\nconst ExplainOptions = [\n  {\n    kwd: \"ANALYZE\",\n    docs: \"Carry out the command and show actual run times and other statistics. This parameter defaults to FALSE.\",\n  },\n  {\n    kwd: \"VERBOSE\",\n    docs: \"Display additional information regarding the plan. Specifically, include the output column list for each node in the plan tree, schema-qualify table and function names, always label variables in expressions with their range table alias, and always print the name of each trigger for which statistics are displayed. The query identifier will also be displayed if one has been computed, see compute_query_id for more details. This parameter defaults to FALSE.\",\n  },\n  {\n    kwd: \"COSTS\",\n    docs: \"Include information on the estimated startup and total cost of each plan node, as well as the estimated number of rows and the estimated width of each row. This parameter defaults to TRUE.\",\n  },\n  {\n    kwd: \"SETTINGS\",\n    docs: \"Include information on configuration parameters. Specifically, include options affecting query planning with value different from the built-in default value. This parameter defaults to FALSE.\",\n  },\n  {\n    kwd: \"GENERIC_PLAN\",\n    docs: \"Allow the statement to contain parameter placeholders like $1, and generate a generic plan that does not depend on the values of those parameters. See PREPARE for details about generic plans and the types of statement that support parameters. This parameter cannot be used together with ANALYZE. It defaults to FALSE.\",\n  },\n  {\n    kwd: \"BUFFERS\",\n    docs: \"Include information on buffer usage. Specifically, include the number of shared blocks hit, read, dirtied, and written, the number of local blocks hit, read, dirtied, and written, the number of temp blocks read and written, and the time spent reading and writing data file blocks and temporary file blocks (in milliseconds) if track_io_timing is enabled. A hit means that a read was avoided because the block was found already in cache when needed. Shared blocks contain data from regular tables and indexes; local blocks contain data from temporary tables and indexes; while temporary blocks contain short-term working data used in sorts, hashes, Materialize plan nodes, and similar cases. The number of blocks dirtied indicates the number of previously unmodified blocks that were changed by this query; while the number of blocks written indicates the number of previously-dirtied blocks evicted from cache by this backend during query processing. The number of blocks shown for an upper-level node includes those used by all its child nodes. In text format, only non-zero values are printed. This parameter defaults to FALSE.\",\n  },\n  {\n    kwd: \"WAL\",\n    docs: \"Include information on WAL record generation. Specifically, include the number of records, number of full page images (fpi) and the amount of WAL generated in bytes. In text format, only non-zero values are printed. This parameter may only be used when ANALYZE is also enabled. It defaults to FALSE.\",\n  },\n  {\n    kwd: \"TIMING\",\n    docs: \"Include actual startup time and time spent in each node in the output. The overhead of repeatedly reading the system clock can slow down the query significantly on some systems, so it may be useful to set this parameter to FALSE when only actual row counts, and not exact times, are needed. Run time of the entire statement is always measured, even when node-level timing is turned off with this option. This parameter may only be used when ANALYZE is also enabled. It defaults to TRUE.\",\n  },\n  {\n    kwd: \"SUMMARY\",\n    docs: \"Include summary information (e.g., totaled timing information) after the query plan. Summary information is included by default when ANALYZE is used but otherwise is not included by default, but can be enabled using this option. Planning time in EXPLAIN EXECUTE includes the time required to fetch the plan from the cache and the time required for re-planning, if necessary.\",\n  },\n  {\n    kwd: \"FORMAT\",\n    options: [\"JSON\", \"TEXT\", \"YAML\", \"XML\"],\n    docs: \"Specify the output format, which can be TEXT, XML, JSON, or YAML. Non-text output contains the same information as the text output format, but is easier for programs to parse. This parameter defaults to TEXT.\",\n  },\n] satisfies KWD[];\n\nconst TO_CHAR_PATTERNS: { label: string; docs: string }[] = [\n  { label: \"HH\", docs: `hour of day (01-12)` },\n  { label: \"HH12\", docs: `hour of day (01-12)` },\n  { label: \"HH24\", docs: `hour of day (00-23)` },\n  { label: \"MI\", docs: `minute (00-59)` },\n  { label: \"SS\", docs: `second (00-59)` },\n  { label: \"MS\", docs: `millisecond (000-999)` },\n  { label: \"US\", docs: `microsecond (000000-999999)` },\n  { label: \"FF1\", docs: `tenth of second (0-9)` },\n  { label: \"FF2\", docs: `hundredth of second (00-99)` },\n  { label: \"FF3\", docs: `millisecond (000-999)` },\n  { label: \"FF4\", docs: `tenth of a millisecond (0000-9999)` },\n  { label: \"FF5\", docs: `hundredth of a millisecond (00000-99999)` },\n  { label: \"FF6\", docs: `microsecond (000000-999999)` },\n  { label: \"SSSS, SSSSS\", docs: `seconds past midnight (0-86399)` },\n  { label: \"AM, am, PM or pm\t\", docs: `meridiem indicator (without periods)` },\n  {\n    label: \"A.M., a.m., P.M. or p.m.\t\",\n    docs: `meridiem indicator (with periods)`,\n  },\n  { label: \"Y,YYY\t\", docs: `year (4 or more digits) with comma` },\n  { label: \"YYYY\", docs: `year (4 or more digits)` },\n  { label: \"YYY\", docs: `last 3 digits of year` },\n  { label: \"YY\", docs: `last 2 digits of year` },\n  { label: \"Y\", docs: `last digit of year` },\n  { label: \"IYYY\", docs: `ISO 8601 week-numbering year (4 or more digits)` },\n  { label: \"IYY\", docs: `last 3 digits of ISO 8601 week-numbering year` },\n  { label: \"IY\", docs: `last 2 digits of ISO 8601 week-numbering year` },\n  { label: \"I\", docs: `last digit of ISO 8601 week-numbering year` },\n  { label: \"BC, bc, AD or ad\t\", docs: `era indicator (without periods)` },\n  { label: \"B.C., b.c., A.D. or a.d.\t\", docs: `era indicator (with periods)` },\n  {\n    label: \"MONTH\",\n    docs: `full upper case month name (blank-padded to 9 chars)`,\n  },\n  {\n    label: \"Month\",\n    docs: `full capitalized month name (blank-padded to 9 chars)`,\n  },\n  {\n    label: \"month\",\n    docs: `full lower case month name (blank-padded to 9 chars)`,\n  },\n  {\n    label: \"MON\",\n    docs: `abbreviated upper case month name (3 chars in English, localized lengths vary)`,\n  },\n  {\n    label: \"Mon\",\n    docs: `abbreviated capitalized month name (3 chars in English, localized lengths vary)`,\n  },\n  {\n    label: \"mon\",\n    docs: `abbreviated lower case month name (3 chars in English, localized lengths vary)`,\n  },\n  { label: \"MM\", docs: `month number (01-12)` },\n  { label: \"DAY\", docs: `full upper case day name (blank-padded to 9 chars)` },\n  { label: \"Day\", docs: `full capitalized day name (blank-padded to 9 chars)` },\n  { label: \"day\", docs: `full lower case day name (blank-padded to 9 chars)` },\n  {\n    label: \"DY\",\n    docs: `abbreviated upper case day name (3 chars in English, localized lengths vary)`,\n  },\n  {\n    label: \"Dy\",\n    docs: `abbreviated capitalized day name (3 chars in English, localized lengths vary)`,\n  },\n  {\n    label: \"dy\",\n    docs: `abbreviated lower case day name (3 chars in English, localized lengths vary)`,\n  },\n  { label: \"DDD\", docs: `day of year (001-366)` },\n  {\n    label: \"IDDD\",\n    docs: `day of ISO 8601 week-numbering year (001-371; day 1 of the year is Monday of the first ISO week)`,\n  },\n  { label: \"DD\", docs: `day of month (01-31)` },\n  { label: \"D\", docs: `day of the week, Sunday (1) to Saturday (7)` },\n  { label: \"ID\", docs: `ISO 8601 day of the week, Monday (1) to Sunday (7)` },\n  {\n    label: \"W\",\n    docs: `week of month (1-5) (the first week starts on the first day of the month)`,\n  },\n  {\n    label: \"WW\",\n    docs: `week number of year (1-53) (the first week starts on the first day of the year)`,\n  },\n  {\n    label: \"IW\",\n    docs: `week number of ISO 8601 week-numbering year (01-53; the first Thursday of the year is in week 1)`,\n  },\n  {\n    label: \"CC\",\n    docs: `century (2 digits) (the twenty-first century starts on 2001-01-01)`,\n  },\n  {\n    label: \"J\",\n    docs: `Julian Date (integer days since November 24, 4714 BC at local midnight; see Section B.7)`,\n  },\n  { label: \"Q\", docs: `quarter` },\n  {\n    label: \"RM\",\n    docs: `month in upper case Roman numerals (I-XII; I=January)`,\n  },\n  {\n    label: \"rm\",\n    docs: `month in lower case Roman numerals (i-xii; i=January)`,\n  },\n  {\n    label: \"TZ\",\n    docs: `upper case time-zone abbreviation (only supported in to_char)`,\n  },\n  {\n    label: \"tz\",\n    docs: `lower case time-zone abbreviation (only supported in to_char)`,\n  },\n  { label: \"TZH\", docs: `time-zone hours` },\n  { label: \"TZM\", docs: `time-zone minutes` },\n  {\n    label: \"OF\",\n    docs: `time-zone offset from UTC (only supported in to_char)`,\n  },\n];\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchGrant.ts",
    "content": "import { getKeys } from \"prostgles-types\";\nimport {\n  type MinimalSnippet,\n  PG_OBJECTS,\n  suggestSnippets,\n} from \"./CommonMatchImports\";\nimport { getParentFunction } from \"./MatchSelect\";\nimport { getExpected } from \"./getExpected\";\nimport { getKind, type SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\nimport { withKWDs } from \"./withKWDs\";\n\nexport const MatchGrant: SQLMatcher = {\n  match: (cb) => [\"grant\", \"revoke\"].includes(cb.ftoken?.textLC as string),\n  result: ({ cb, ss, setS, sql }) => {\n    const isGrant = cb.ftoken?.textLC === \"grant\";\n\n    if (cb.ltoken?.textLC === \"parameter\") {\n      return { suggestions: setS };\n    }\n\n    if (\n      cb.prevTokens.length === 2 &&\n      cb.prevTokens[1]?.type !== \"keyword.sql\" &&\n      cb.prevTokens[1]?.type !== \"operator.sql\"\n    ) {\n      return withKWDs(\n        [\n          {\n            kwd: isGrant ? \"GRANT\" : \"REVOKE\",\n            expects: \"role\",\n          },\n          {\n            kwd: \",\",\n            excludeIf: [\"TO\", \"FROM\"],\n          },\n          {\n            kwd: isGrant ? \"TO\" : \"FROM\",\n            expects: \"role\",\n          },\n        ] satisfies KWD[],\n        { cb, ss, setS, sql },\n      ).getSuggestion();\n    }\n\n    const columnKwds = [\"select\", \"update\", \"insert\", \"all\"];\n    const func = getParentFunction(cb);\n    if (func?.func && columnKwds.includes(func.func.textLC)) {\n      return getExpected(\"column\", cb, ss);\n    }\n    if (\n      !func?.func &&\n      cb.ltoken?.textLC === \",\" &&\n      !cb.textLC.includes(\" all \")\n    ) {\n      const additional: MinimalSnippet[] = Object.entries(PRIVILEGES)\n        .filter(([p, d]) => columnKwds.includes(p.toLowerCase()))\n        .map(([label, { docs }]) => ({\n          label,\n          docs,\n          kind: getKind(\"keyword\"),\n        }));\n      return suggestSnippets(additional);\n    }\n    if (columnKwds.includes(cb.ltoken?.textLC as string)) {\n      const { suggestions } = getExpected(\"(column)\", cb, ss);\n      return {\n        suggestions: [\n          ...suggestions.map((s) => ({\n            ...s,\n            insertText: `(${s.escapedIdentifier} ) ON ${s.escapedParentName}`,\n          })),\n          ...suggestSnippets([{ label: \"ON\" }]).suggestions.map((s) => ({\n            ...s,\n            sortText: \"!\",\n          })),\n        ],\n      };\n    }\n\n    const allPrivilegeTypes = getKeys(PRIVILEGES);\n    const addAll = (objects: readonly (typeof PG_OBJECTS)[number][]) => {\n      const allEntities: (typeof PG_OBJECTS)[number][] = [\n        \"TABLE\",\n        \"FUNCTION\",\n        \"SEQUENCE\",\n      ];\n      const matching = allEntities.filter((e) => objects.includes(e));\n      if (matching.length) {\n        return [...matching.map((obj) => `ALL ${obj}S IN SCHEMA`), ...objects];\n      }\n\n      return objects;\n    };\n    type ONOption = Extract<{ label: string }, MinimalSnippet>;\n    const ON_options: ONOption[] = [\n      { label: \"ALL TABLES IN SCHEMA\" },\n      { label: \"ALL FUNCTIONS IN SCHEMA\" },\n      { label: \"ALL SEQUENCES IN SCHEMA\" },\n      ...PG_OBJECTS.map(\n        (label) =>\n          ({\n            label,\n            kind: getKind(\n              (label as string).split(\" \")[0]?.toLowerCase() as any,\n            ),\n          }) as ONOption,\n      ).filter(({ label }) => label !== \"VIEW\"),\n    ].filter((opt) => {\n      const objMatch = Object.entries(PRIVILEGES).find(\n        ([privilege, { objects }]) => {\n          return (\n            cb.prevLC.includes(` ${privilege.toLowerCase()} `) &&\n            objects.some((objName) =>\n              opt.label.toLowerCase().includes(objName.toLowerCase()),\n            )\n          );\n        },\n      );\n      return objMatch;\n    });\n\n    if (\n      cb.ltoken?.textLC === \"on\" &&\n      cb.prevTokens.some((t) => columnKwds.includes(t.textLC))\n    ) {\n      const { suggestions } = getExpected(\"table\", cb, ss);\n      if (cb.l1token?.textLC === \")\") {\n        const tableSuggestions = suggestions.map((s) => {\n          const matchingColumnNames = s.tablesInfo?.cols.filter((c) =>\n            cb.prevTokens.some((t) => t.text === c.escaped_identifier),\n          ).length;\n          return {\n            ...s,\n            sortText:\n              (s.schema === \"public\" ? \"a\" : \"b\") +\n              (matchingColumnNames ? 100 - matchingColumnNames : \"a\"),\n          };\n        });\n        return {\n          suggestions: tableSuggestions,\n        };\n      }\n      return {\n        suggestions: [\n          ...suggestions,\n          ...suggestSnippets(ON_options).suggestions.map((s) => ({\n            ...s,\n            sortText: \"!\",\n            kind: getKind(\"keyword\"),\n          })),\n        ],\n      };\n    }\n    if (\n      cb.tokens.length <= 3 &&\n      (cb.prevLC.endsWith(\"all\") || cb.prevLC.endsWith(\"all privileges\"))\n    ) {\n      return suggestSnippets(\n        ON_options.map((p) => ({ label: `ON ${p.label}` })),\n      );\n    }\n\n    const excludePriviledge: Pick<KWD, \"excludeIf\"> = {\n      excludeIf: (cb) =>\n        allPrivilegeTypes.some((v) => cb.prevLC.includes(v.toLowerCase())),\n    };\n    const kwds = [\n      {\n        kwd: isGrant ? \"GRANT\" : \"REVOKE\",\n        expects: \"role\",\n        options: Object.entries(PRIVILEGES).flatMap(([label, { docs }]) => {\n          const item = {\n            label: `${label} ON`,\n            docs,\n            kind: getKind(\"keyword\"),\n          };\n          if (columnKwds.includes(label.toLowerCase())) {\n            return [item, { ...item, label }];\n          }\n          return item;\n        }),\n      },\n      {\n        kwd: \"EXECUTE\",\n        ...excludePriviledge,\n        options: [{ label: \"ON FUNCTION\" }],\n      },\n      {\n        kwd: \"FUNCTION\",\n        ...excludePriviledge,\n        expects: \"function\",\n        dependsOn: \"ON\",\n      },\n      {\n        kwd: \"ON\",\n        exactlyAfter: allPrivilegeTypes,\n        options: ON_options,\n      },\n      ...Object.entries(PRIVILEGES).map(\n        ([kwd, { objects }]) =>\n          ({\n            kwd: `${kwd} ON`,\n            ...excludePriviledge,\n            expects:\n              kwd.startsWith(\"ALL\") ? undefined : (kwd.toLowerCase() as any),\n            options: addAll(objects).map((label) => ({\n              label,\n              kind: getKind(\n                (label as string).split(\" \")[0]?.toLowerCase() as any,\n              ),\n            })),\n          }) satisfies KWD,\n      ),\n\n      ...PG_OBJECTS.map((kwd) => ({\n        kwd,\n        exactlyAfter: [\"ON\"],\n        expects:\n          kwd === \"TABLE\" ? [\"table\", \"view\"] : (kwd.toLowerCase() as any),\n      })),\n      {\n        kwd: \"IN SCHEMA\",\n        include: (cb) =>\n          !cb.prevLC.includes(` in schema `) && cb.prevLC.includes(` on all `),\n        expects: \"schema\",\n      },\n      {\n        kwd: isGrant ? \"TO\" : \"FROM\",\n        expects: \"role\",\n        docs: ``,\n        include: (cb) =>\n          cb.l1token?.textLC === \"on\" ||\n          (cb.prevTokens.some((t) => t.textLC === \"on\") &&\n            cb.ltoken?.textLC !== \"on\"),\n      },\n    ] satisfies KWD[];\n    return withKWDs(kwds, { cb, ss, setS, sql }).getSuggestion();\n  },\n};\n\n/** https://www.postgresql.org/docs/current/ddl-priv.html#PRIVILEGE-ABBREVS-TABLE */\nconst PRIVILEGES = {\n  ALL: {\n    docs: \"Allows all privileges\",\n    objects: PG_OBJECTS.slice(0),\n  },\n  \"ALL PRIVILEGES\": {\n    docs: \"Allows all privileges\",\n    objects: PG_OBJECTS.slice(0),\n  },\n  SELECT: {\n    docs: `Allows SELECT from any column, or specific column(s), of a table, view, materialized view, or other table-like object. Also allows use of COPY TO. This privilege is also needed to reference existing column values in UPDATE or DELETE. For sequences, this privilege also allows use of the currval function. For large objects, this privilege allows the object to be read.`,\n    objects: [\"LARGE OBJECT\", \"TABLE\"],\n  },\n  INSERT: {\n    docs: `Allows INSERT of a new row into a table, view, etc. Can be granted on specific column(s), in which case only those columns may be assigned to in the INSERT command (other columns will therefore receive default values). Also allows use of COPY FROM.`,\n    objects: [\"TABLE\"],\n  },\n  UPDATE: {\n    docs: `Allows UPDATE of any column, or specific column(s), of a table, view, etc. (In practice, any nontrivial UPDATE command will require SELECT privilege as well, since it must reference table columns to determine which rows to update, and/or to compute new values for columns.) SELECT ... FOR UPDATE and SELECT ... FOR SHARE also require this privilege on at least one column, in addition to the SELECT privilege. For sequences, this privilege allows use of the nextval and setval functions. For large objects, this privilege allows writing or truncating the object.`,\n    objects: [\"TABLE\"],\n  },\n  DELETE: {\n    docs: `Allows DELETE of a row from a table, view, etc. (In practice, any nontrivial DELETE command will require SELECT privilege as well, since it must reference table columns to determine which rows to delete.)`,\n    objects: [\"TABLE\"],\n  },\n  TRUNCATE: {\n    docs: `Allows TRUNCATE on a table.`,\n    objects: [\"TABLE\"],\n  },\n  REFERENCES: {\n    docs: `Allows creation of a foreign key constraint referencing a table, or specific column(s) of a table.`,\n    objects: [\"TABLE\"],\n  },\n  TRIGGER: {\n    docs: `Allows creation of a trigger on a table, view, etc.`,\n    objects: [\"TABLE\"],\n  },\n  CREATE: {\n    docs: `For databases, allows new schemas and publications to be created within the database, and allows trusted extensions to be installed within the database.\n      For schemas, allows new objects to be created within the schema. To rename an existing object, you must own the object and have this privilege for the containing schema.\n      For tablespaces, allows tables, indexes, and temporary files to be created within the tablespace, and allows databases to be created that have the tablespace as their default tablespace.\n      Note that revoking this privilege will not alter the existence or location of existing objects.`,\n    objects: [\"DATABASE\", \"SCHEMA\", \"TABLESPACE\"],\n  },\n  CONNECT: {\n    docs: `Allows the grantee to connect to the database. This privilege is checked at connection startup (in addition to checking any restrictions imposed by pg_hba.conf).`,\n    objects: [\"DATABASE\"],\n  },\n  TEMPORARY: {\n    docs: `Allows temporary tables to be created while using the database.`,\n    objects: [\"DATABASE\"],\n  },\n  EXECUTE: {\n    docs: `Allows calling a function or procedure, including use of any operators that are implemented on top of the function. This is the only type of privilege that is applicable to functions and procedures.`,\n    objects: [\"FUNCTION\", \"PROCEDURE\", \"ROUTINE\"],\n  },\n  USAGE: {\n    docs: `For procedural languages, allows use of the language for the creation of functions in that language. This is the only type of privilege that is applicable to procedural languages.\n    For schemas, allows access to objects contained in the schema (assuming that the objects' own privilege requirements are also met). Essentially this allows the grantee to “look up” objects within the schema. Without this permission, it is still possible to see the object names, e.g., by querying system catalogs. Also, after revoking this permission, existing sessions might have statements that have previously performed this lookup, so this is not a completely secure way to prevent object access.\n    For sequences, allows use of the currval and nextval functions.\n    For types and domains, allows use of the type or domain in the creation of tables, functions, and other schema objects. (Note that this privilege does not control all “usage” of the type, such as values of the type appearing in queries. It only prevents objects from being created that depend on the type. The main purpose of this privilege is controlling which users can create dependencies on a type, which could prevent the owner from changing the type later.)\n    For foreign-data wrappers, allows creation of new servers using the foreign-data wrapper.\n    For foreign servers, allows creation of foreign tables using the server. Grantees may also create, alter, or drop their own user mappings associated with that server.\n    `,\n    objects: [\n      \"DOMAIN\",\n      \"FOREIGN DATA WRAPPER\",\n      \"FOREIGN SERVER\",\n      \"LANGUAGE\",\n      \"SCHEMA\",\n      \"SEQUENCE\",\n      \"TYPE\",\n    ],\n  },\n  SET: {\n    docs: `Allows a server configuration parameter to be set to a new value within the current session. (While this privilege can be granted on any parameter, it is meaningless except for parameters that would normally require superuser privilege to set.)`,\n    objects: [\"PARAMETER\"],\n  },\n  \"ALTER SYSTEM\": {\n    docs: `Allows a server configuration parameter to be configured to a new value using the ALTER SYSTEM command.`,\n    objects: [\"PARAMETER\"],\n  },\n} as const satisfies Record<\n  string,\n  {\n    docs: string;\n    objects: readonly (typeof PG_OBJECTS)[number][];\n  }\n>;\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchInsert.tsx",
    "content": "import { isObject } from \"@common/publishUtils\";\nimport { withKWDs } from \"./withKWDs\";\nimport { getKind, type SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport { getParentFunction } from \"./MatchSelect\";\nimport { getExpected } from \"./getExpected\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { matchTableFromSuggestions } from \"./completionUtils/getTabularExpressions\";\n\nconst KWDS = [\n  { kwd: \"INTO\", justAfter: [\"INSERT\"], expects: \"keyword\" },\n  { kwd: \"VALUES\", justAfter: [\"INTO\"], expects: \"column\" },\n  {\n    kwd: \"DEFAULT VALUES\",\n    justAfter: [\"INTO\"],\n    expects: \"column\",\n    excludeIf: [\"(\"],\n  },\n  { kwd: \"SELECT\", justAfter: [\"INTO\"], expects: \"column\" },\n  { kwd: \"FROM\", justAfter: [\"SELECT\"], expects: \"table\" },\n  { kwd: \"WHERE\", justAfter: [\"FROM\"], expects: \"condition\" },\n  {\n    kwd: \"ON CONFLICT\",\n    justAfter: [\"VALUES\", \"SELECT\"],\n    options: [\"DO NOTHING\", \"DO UPDATE\"],\n    docs: `The optional ON CONFLICT clause specifies an alternative action to raising a unique violation or exclusion constraint violation error. For each individual row proposed for insertion, either the insertion proceeds, or, if an arbiter constraint or index specified by conflict_target is violated, the alternative conflict_action is taken. \n    \n    ON CONFLICT DO NOTHING \n    --simply avoids inserting a row as its alternative action. \n    \n    ON CONFLICT DO UPDATE \n    --updates the existing row that conflicts with the row proposed for insertion as its alternative action.`,\n  },\n\n  { kwd: \"RETURNING\", expects: \"column\", justAfter: [\"VALUES\"] },\n] as const;\n\nexport const MatchInsert: SQLMatcher = {\n  match: (cb) => {\n    return cb.tokens[0]?.textLC === \"insert\";\n  },\n  result: async ({ cb, ss, setS, sql }) => {\n    const { prevLC, prevIdentifiers } = cb;\n\n    /** Is inside func args */\n    const insideFunc = getParentFunction(cb);\n    if (insideFunc) {\n      if (insideFunc.prevToken?.textLC === \"into\") {\n        const cols = getExpected(\"column\", cb, ss).suggestions;\n        if (\n          cb.prevIdentifiers.length === 1 &&\n          cols.every(\n            (c) =>\n              c.colInfo &&\n              matchTableFromSuggestions(c as any, cb.prevIdentifiers[0]!),\n          )\n        ) {\n          return suggestSnippets([\n            {\n              label: \"...columns\",\n              insertText: cols\n                .sort(\n                  (a, b) =>\n                    a.colInfo!.ordinal_position - b.colInfo!.ordinal_position,\n                )\n                .map((c) => c.insertText)\n                .join(\", \"),\n            },\n          ]);\n        }\n        return {\n          suggestions: cols.filter(\n            (c) =>\n              !prevIdentifiers.some(\n                (pi) => pi.text === c.colInfo?.escaped_identifier,\n              ),\n          ),\n        };\n      }\n      const funcs = getExpected(\"function\", cb, ss).suggestions.filter(\n        (s) => !s.funcInfo?.args.length && s.funcInfo?.restype,\n      );\n      const def = suggestSnippets([{ label: \"DEFAULT\" }]);\n      return {\n        suggestions: [...def.suggestions, ...funcs],\n      };\n    }\n\n    if (prevLC.trim().toLowerCase().endsWith(\"insert into\")) {\n      const tables = ss.filter((s) => s.type === \"table\");\n\n      return {\n        suggestions: clone(tables)\n          .map((s) => {\n            (s.label = {\n              ...(isObject(s.label) && s.label),\n              label: s.name + \" (...\",\n            }),\n              (s.insertText += ` (${s.cols\n                ?.filter(\n                  (c) =>\n                    !(\n                      c.has_default &&\n                      [\"u\", \"p\"].includes(c.cConstraint?.contype as any)\n                    ),\n                )\n                .map((c) => c.escaped_identifier)\n                .join(\", \")})\\nVALUES`);\n            (s.kind = getKind(\"table\")),\n              (s.sortText = s.schema === \"public\" ? \"0a\" : \"a\");\n            return s;\n          })\n          .concat(tables),\n      };\n    }\n\n    const { getSuggestion } = withKWDs(KWDS, { cb, ss, setS, sql });\n    return getSuggestion();\n  },\n};\n\nexport const clone = <V,>(v: V): V => {\n  return structuredClone(v);\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchLast.ts",
    "content": "import type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\n\nexport const MatchLast: SQLMatcher = {\n  match: (cb) => cb.prevLC.endsWith(\"extension\"),\n  result: ({ cb, ss, setS }) => {\n    return {\n      suggestions: ss.filter((s) => s.type === \"extension\"),\n    };\n  },\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchPublication.ts",
    "content": "import { getParentFunction } from \"./MatchSelect\";\nimport { getExpected } from \"./getExpected\";\nimport type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\nimport { withKWDs } from \"./withKWDs\";\n\nexport const MatchPublication: SQLMatcher = {\n  match: (cb) => cb.tokens[1]?.textLC === \"publication\",\n  result: async ({ cb, ss, setS, sql }) => {\n    const command = cb.ftoken?.textLC;\n    const newPubName =\n      command === \"create\" &&\n      cb.tokens[1]?.textLC === \"publication\" &&\n      cb.tokens[2];\n\n    const isInfunc = getParentFunction(cb);\n    if (isInfunc?.func.textLC === \"with\") {\n      return withKWDs(\n        [\n          {\n            kwd: \"publish='insert, update, delete, truncate'\",\n            options: [``],\n            docs: `This parameter determines which DML operations will be published by the new publication to the subscribers. The value is comma-separated list of operations. The allowed operations are insert, update, delete, and truncate. The default is to publish all actions, and so the default value for this option is 'insert, update, delete, truncate'.`,\n          },\n          {\n            kwd: \"publish_via_partition_root=true\",\n            options: [``],\n            docs: `This parameter determines whether changes in a partitioned table (or on its partitions) contained in the publication will be published using the identity and schema of the partitioned table rather than that of the individual partitions that are actually changed; the latter is the default. Enabling this allows the changes to be replicated into a non-partitioned table or a partitioned table consisting of a different set of partitions.\\nIf this is enabled, TRUNCATE operations performed directly on partitions are not replicated.`,\n          },\n        ],\n        { cb, ss, setS, sql },\n      ).getSuggestion(\",\", [\"(\", \")\"]);\n    }\n\n    if (newPubName) {\n      return withKWDs(\n        [\n          ...Object.entries(options).map(\n            ([kwd, { docs, expects, optional }]) => ({\n              kwd,\n              docs,\n              expects,\n              optional,\n            }),\n          ),\n        ] satisfies KWD[],\n        { cb, ss, setS, sql },\n      ).getSuggestion();\n    }\n\n    if (cb.prevLC.endsWith(\"if exists\")) {\n      return getExpected(\"publication\", cb, ss);\n    }\n\n    return withKWDs(\n      [\n        {\n          kwd: \"PUBLICATION\",\n          expects: command === \"create\" ? undefined : \"publication\",\n          excludeIf: (cb) => cb.l1token?.textLC === \"publication\",\n          options:\n            command === \"drop\" ? [{ label: \"IF EXISTS\" }]\n            : command === \"create\" ? [{ label: \"$new_publication_name\" }]\n            : undefined,\n          docs: \"Adds a new publication into the current database. The publication name must be distinct from the name of any existing publication in the current database. A publication is essentially a group of tables whose data changes are intended to be replicated through logical replication.\",\n        },\n      ] satisfies KWD[],\n      { cb, ss, setS, sql },\n    ).getSuggestion();\n  },\n};\n\nconst options = {\n  \"FOR TABLE\": {\n    docs: `Specifies a list of tables to add to the publication. If ONLY is specified before the table name, only that table is added to the publication. If ONLY is not specified, the table and all its descendant tables (if any) are added. Optionally, * can be specified after the table name to explicitly indicate that descendant tables are included. This does not apply to a partitioned table, however. The partitions of a partitioned table are always implicitly considered part of the publication, so they are never explicitly added to the publication.\n\nIf the optional WHERE clause is specified, it defines a row filter expression. Rows for which the expression evaluates to false or null will not be published. Note that parentheses are required around the expression. It has no effect on TRUNCATE commands.\n\nWhen a column list is specified, only the named columns are replicated. If no column list is specified, all columns of the table are replicated through this publication, including any columns added later. It has no effect on TRUNCATE commands. See Section 31.4 for details about column lists.\n\nOnly persistent base tables and partitioned tables can be part of a publication. Temporary tables, unlogged tables, foreign tables, materialized views, and regular views cannot be part of a publication.\n\nSpecifying a column list when the publication also publishes FOR TABLES IN SCHEMA is not supported.\n\nWhen a partitioned table is added to a publication, all of its existing and future partitions are implicitly considered to be part of the publication. So, even operations that are performed directly on a partition are also published via publications that its ancestors are part of.`,\n    expects: \"table\",\n    optional: false,\n  },\n  \"FOR ALL TABLES\": {\n    docs: `Marks the publication as one that replicates changes for all tables in the database, including tables created in the future.`,\n    expects: undefined,\n    optional: false,\n  },\n  \"FOR TABLES IN SCHEMA\": {\n    docs: `Marks the publication as one that replicates changes for all tables in the specified list of schemas, including tables created in the future.\n\nSpecifying a schema when the publication also publishes a table with a column list is not supported.\n\nOnly persistent base tables and partitioned tables present in the schema will be included as part of the publication. Temporary tables, unlogged tables, foreign tables, materialized views, and regular views from the schema will not be part of the publication.\n\nWhen a partitioned table is published via schema level publication, all of its existing and future partitions are implicitly considered to be part of the publication, regardless of whether they are from the publication schema or not. So, even operations that are performed directly on a partition are also published via publications that its ancestors are part of.`,\n    expects: \"schema\",\n    optional: false,\n  },\n  WITH: {\n    optional: true,\n    docs: `This clause specifies optional parameters for a publication. The following parameters are supported:\n\npublish (string) \nThis parameter determines which DML operations will be published by the new publication to the subscribers. The value is comma-separated list of operations. The allowed operations are insert, update, delete, and truncate. The default is to publish all actions, and so the default value for this option is 'insert, update, delete, truncate'.\n\nThis parameter only affects DML operations. In particular, the initial data synchronization (see Section 31.7.1) for logical replication does not take this parameter into account when copying existing table data.\n\npublish_via_partition_root (boolean) \nThis parameter determines whether changes in a partitioned table (or on its partitions) contained in the publication will be published using the identity and schema of the partitioned table rather than that of the individual partitions that are actually changed; the latter is the default. Enabling this allows the changes to be replicated into a non-partitioned table or a partitioned table consisting of a different set of partitions.\n\nThere can be a case where a subscription combines multiple publications. If a partitioned table is published by any subscribed publications which set publish_via_partition_root = true, changes on this partitioned table (or on its partitions) will be published using the identity and schema of this partitioned table rather than that of the individual partitions.\n\nThis parameter also affects how row filters and column lists are chosen for partitions; see below for details.\n\nIf this is enabled, TRUNCATE operations performed directly on partitions are not replicated.\n\nWhen specifying a parameter of type boolean, the = value part can be omitted, which is equivalent to specifying TRUE.`,\n    expects: undefined,\n  },\n} as const;\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchReassign.ts",
    "content": "import type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport { withKWDs } from \"./withKWDs\";\n\nconst KWDS = [\n  { kwd: \"REASSIGN\" },\n  { kwd: \"OWNED\", justAfter: [\"REASSIGN\"] },\n  { kwd: \"BY\", expects: \"role\", justAfter: [\"OWNED\"] },\n  { kwd: \"TO\", expects: \"role\", dependsOn: \"BY\" },\n  { kwd: \";\", dependsOn: \"TO\" },\n] as const;\n\nexport const MatchReassign: SQLMatcher = {\n  match: (cb) => cb.prevLC.startsWith(\"reassign\"),\n  result: async ({ cb, ss, setS, sql }) => {\n    const { getSuggestion, prevKWD } = withKWDs(KWDS, { cb, ss, setS, sql });\n\n    const result = await getSuggestion();\n\n    if (prevKWD?.kwd === \"TO\" && result.suggestions.length) {\n      const prevUser = cb.tokens.find((t, i, arr) => {\n        return arr[i - 1]?.textLC === \"by\";\n      });\n      /** Exclude prev (BY) user from suggestions */\n      if (prevUser) {\n        return {\n          suggestions: result.suggestions.filter(\n            (t) => t.name !== prevUser.text,\n          ),\n        };\n      }\n    }\n\n    return result;\n  },\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchReindex.ts",
    "content": "import { getExpected } from \"./getExpected\";\nimport type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport { getKind } from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\nimport { withKWDs } from \"./withKWDs\";\n\nexport const MatchReindex: SQLMatcher = {\n  match: (cb) => cb.ftoken?.textLC === \"reindex\",\n  result: async ({ cb, ss, setS, sql }) => {\n    if (cb.tokens.length === 2) {\n      const what = cb.tokens[1]!.textLC.replace(\"system\", \"database\");\n      return getExpected(what, cb, ss);\n    }\n    return withKWDs(\n      [\n        {\n          kwd: \"REINDEX\",\n          options: Object.entries(targets).map(([label, docs]) => ({\n            label,\n            docs,\n            kind: getKind(\"keyword\"),\n          })),\n        },\n      ] satisfies KWD[],\n      { cb, ss, setS, sql },\n    ).getSuggestion();\n  },\n};\n\nconst targets = {\n  INDEX: `Recreate the specified index. This form of REINDEX cannot be executed inside a transaction block when used with a partitioned index.`,\n  TABLE: `Recreate all indexes of the specified table. If the table has a secondary “TOAST” table, that is reindexed as well. This form of REINDEX cannot be executed inside a transaction block when used with a partitioned table.`,\n  SCHEMA: `Recreate all indexes of the specified schema. If a table of this schema has a secondary “TOAST” table, that is reindexed as well. Indexes on shared system catalogs are also processed. This form of REINDEX cannot be executed inside a transaction block.`,\n  DATABASE: `Recreate all indexes within the current database, except system catalogs. Indexes on system catalogs are not processed. This form of REINDEX cannot be executed inside a transaction block.`,\n  SYSTEM: `Recreate all indexes on system catalogs within the current database. Indexes on shared system catalogs are included. Indexes on user tables are not processed. This form of REINDEX cannot be executed inside a transaction block.`,\n} as const;\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchSelect.ts",
    "content": "import { missingKeywordDocumentation } from \"../SQLEditorSuggestions\";\nimport {\n  nameMatches,\n  suggestSnippets,\n  type MinimalSnippet,\n} from \"./CommonMatchImports\";\nimport type { CodeBlock } from \"./completionUtils/getCodeBlock\";\nimport {\n  getCurrentCodeBlock,\n  getCurrentNestingOffsetLimits,\n} from \"./completionUtils/getCodeBlock\";\nimport { getExpected } from \"./getExpected\";\nimport { jsonbPathSuggest } from \"./jsonbPathSuggest\";\nimport {\n  getKind,\n  type ParsedSQLSuggestion,\n  type SQLMatcher,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport { suggestColumnLike } from \"./suggestColumnLike\";\nimport { suggestTableLike } from \"./suggestTableLike\";\nimport type { KWD } from \"./withKWDs\";\nimport { withKWDs } from \"./withKWDs\";\n\nexport const AGG_FUNC_NAMES = [\n  \"max\",\n  \"min\",\n  \"agg\",\n  \"sum\",\n  \"count\",\n  \"string_agg\",\n  \"json_object_agg\",\n  \"array_agg\",\n  \"jsonb_agg\",\n];\n\nexport const preSubQueryKwds = [\"in\", \"from\", \"join\", \"lateral\"];\nexport const MatchSelect: SQLMatcher = {\n  match: ({ prevTopKWDs, ftoken }) => {\n    return (\n      ftoken?.textLC === \"select\" ||\n      (prevTopKWDs[0]?.text === \"SELECT\" && ftoken?.textLC !== \"with\")\n    );\n  },\n  result: async (args) => {\n    const { cb, ss, setS, sql, options } = args;\n\n    const {\n      ltoken,\n      thisLineLC,\n      prevLC,\n      prevText,\n      currToken,\n      thisLinePrevTokens,\n      offset,\n    } = cb;\n    const { prevKWD, suggestKWD, remainingKWDS } = withKWDs(\n      getKWDSz(options?.MatchSelect?.excludeInto),\n      { cb, ss, setS, sql, opts: { topResetKwd: \"SELECT\" } },\n    );\n\n    /**\n     * Is inside IN (...)\n     * or FROM (...)\n     * or (SELECT ...)\n     * */\n    const insideFunc = getParentFunction(cb);\n    // const isSelectSubQuery = insideFunc?.prevArgs.at(-1)?.textLC === \"select\" && [\",\", \"select\"].includes(insideFunc.func.textLC);\n    const isSelectSubQuery =\n      insideFunc?.prevArgs.at(0)?.textLC === \"select\" &&\n      [\",\", \"select\"].includes(insideFunc.func.textLC);\n    if (\n      insideFunc?.func &&\n      (isSelectSubQuery ||\n        (preSubQueryKwds.includes(insideFunc.func.textLC) &&\n          insideFunc.func.textLC !== \"lateral\")) &&\n      !options?.MatchSelect\n    ) {\n      const nestedLimits = getCurrentNestingOffsetLimits(cb);\n      if (nestedLimits) {\n        const cbNested = await getCurrentCodeBlock(\n          cb.model,\n          cb.position,\n          nestedLimits.limits,\n        );\n        return MatchSelect.result({\n          parentCb: cb,\n          cb: cbNested,\n          ss,\n          setS,\n          sql,\n          options: { MatchSelect: { excludeInto: true } },\n        });\n      }\n    }\n\n    if (insideFunc?.func.textLC === \"over\") {\n      const frameDocs = `The default framing option is RANGE UNBOUNDED PRECEDING, which is the same as RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. With ORDER BY, this sets the frame to be all rows from the partition start up through the current row's last ORDER BY peer. Without ORDER BY, this means all rows of the partition are included in the window frame, since all rows become peers of the current row.`;\n\n      const offsetDocs = [\n        `In ROWS mode, the offset must yield a non-null, non-negative integer, and the option means that the frame starts or ends the specified number of rows before or after the current row`,\n        `In GROUPS mode, the offset again must yield a non-null, non-negative integer, and the option means that the frame starts or ends the specified number of peer groups before or after the current row's peer group, where a peer group is a set of rows that are equivalent in the ORDER BY ordering. (There must be an ORDER BY clause in the window definition to use GROUPS mode.)`,\n        `In RANGE mode, these options require that the ORDER BY clause specify exactly one column. The offset specifies the maximum difference between the value of that column in the current row and its value in preceding or following rows of the frame. The data type of the offset expression varies depending on the data type of the ordering column. For numeric ordering columns it is typically of the same type as the ordering column, but for datetime ordering columns it is an interval. For example, if the ordering column is of type date or timestamp, one could write RANGE BETWEEN '1 day' PRECEDING AND '10 days' FOLLOWING. The offset is still required to be non-null and non-negative, though the meaning of “non-negative” depends on its data type.`,\n      ].join(\"\\n\\n\");\n      const frameEdge = [\n        {\n          label: \"BETWEEN\",\n          docs: `used to specify a frame_start and frame_end`,\n          optional: true,\n        },\n        {\n          label: \"UNBOUNDED PRECEDING\",\n          docs: `means that the frame starts with the first row of the partition`,\n        },\n        { label: \"$offset PRECEDING\", docs: offsetDocs },\n        { label: \"CURRENT ROW\", docs: `` },\n        { label: \"$offset FOLLOWING\", docs: offsetDocs },\n        {\n          label: \"UNBOUNDED FOLLOWING\",\n          docs: `means that the frame ends with the last row of the partition`,\n        },\n      ];\n      const frameExclusion = [\n        {\n          label: \"EXCLUDE CURRENT ROW\",\n          docs: `excludes the current row from the frame`,\n        },\n        {\n          label: \"EXCLUDE GROUP\",\n          docs: `excludes the current row and its ordering peers from the frame`,\n        },\n        {\n          label: \"EXCLUDE TIES\",\n          docs: `excludes any peers of the current row from the frame, but not the current row itself`,\n        },\n        {\n          label: \"EXCLUDE NO OTHERS\",\n          docs: `simply specifies explicitly the default behavior of not excluding the current row or its peers`,\n        },\n      ];\n      return withKWDs(\n        [\n          {\n            kwd: \"PARTITION BY\",\n            expects: \"column\",\n            docs: `The PARTITION BY clause groups the rows of the query into partitions, which are processed separately by the window function. PARTITION BY works similarly to a query-level GROUP BY clause, except that its expressions are always just expressions and cannot be output-column names or numbers. Without PARTITION BY, all rows produced by the query are treated as a single partition`,\n          },\n          {\n            kwd: \"ORDER BY\",\n            expects: \"column\",\n            docs: `The ORDER BY clause determines the order in which the rows of a partition are processed by the window function. It works similarly to a query-level ORDER BY clause, but likewise cannot use output-column names or numbers. Without ORDER BY, rows are processed in an unspecified order`,\n          },\n          { kwd: \",\", expects: \"column\" },\n          {\n            kwd: \"ROWS\",\n            options: [...frameEdge, ...frameExclusion],\n            docs: frameDocs,\n          },\n          {\n            kwd: \"RANGE\",\n            options: [...frameEdge, ...frameExclusion],\n            docs: frameDocs,\n          },\n          {\n            kwd: \"GROUPS\",\n            options: [...frameEdge, ...frameExclusion],\n            docs: frameDocs,\n          },\n          { kwd: \"ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\" },\n          { kwd: \"UNBOUNDED\" },\n          { kwd: \"UNBOUNDED PRECEDING\" },\n        ],\n        { cb, ss, setS, sql },\n      ).getSuggestion();\n    }\n\n    if (\n      insideFunc?.func.textLC &&\n      AGG_FUNC_NAMES.includes(insideFunc.func.textLC)\n    ) {\n      if (cb.prevText.toLowerCase().trim().endsWith(\"order by\")) {\n        return suggestColumnLike({\n          cb,\n          ss,\n          setS,\n          parentCb: args.parentCb,\n          sql,\n        });\n      }\n      if (\n        cb.prevText.endsWith(\" \") &&\n        !cb.prevText.trim().endsWith(\",\") &&\n        !cb.prevText.trim().endsWith(\"(\")\n      ) {\n        return suggestSnippets([\n          { label: \"ORDER BY\", kind: getKind(\"keyword\") },\n        ]);\n      }\n    }\n\n    if (\n      currToken?.type === \"string.sql\" &&\n      currToken.offset <= offset &&\n      currToken.end >= offset\n    ) {\n      return {\n        suggestions: [],\n      };\n    }\n\n    const s = await jsonbPathSuggest(args);\n    if (s) {\n      return s;\n    }\n\n    const { l1token: lltoken } = cb;\n\n    if (\n      ltoken?.textLC === \"(\" &&\n      lltoken &&\n      preSubQueryKwds.includes(lltoken.textLC)\n    ) {\n      return suggestSnippets([{ label: \"SELECT\" }]);\n    }\n\n    const getColsAndFuncs = async () => {\n      const colLikeSuggestions = await suggestColumnLike({\n        cb,\n        ss,\n        setS,\n        parentCb: args.parentCb,\n        sql,\n      });\n      return colLikeSuggestions;\n    };\n\n    const expectsColumn =\n      prevLC.trim().endsWith(\" group by\") ||\n      prevLC.trim().endsWith(\" order by\") ||\n      prevLC.trim().endsWith(\" having\") ||\n      prevLC.trim().endsWith(\" where\") ||\n      prevLC.trim().endsWith(\" when\");\n    if (expectsColumn) {\n      const colsAndFuncs = await getColsAndFuncs();\n      return colsAndFuncs;\n    }\n\n    /** Is inside func args */\n    if (\n      insideFunc?.func &&\n      ![\"lateral\", \"from\"].includes(insideFunc.func.textLC)\n    ) {\n      const colsAndFuncs = await getColsAndFuncs();\n      const suggestions = colsAndFuncs.suggestions;\n\n      return {\n        suggestions,\n      };\n    }\n\n    const prevFunc =\n      cb.thisLineLC.endsWith(\")\") &&\n      cb.ltoken?.text === \")\" &&\n      cb.prevTokens\n        .slice(0)\n        .reverse()\n        .find((_, i, arr) => {\n          const t = arr[i - 1];\n          return t && t.text === \"(\" && t.nestingId === cb.ltoken?.nestingId;\n        });\n    if (prevFunc) {\n      const funcInfo = ss.find(\n        (s) => s.type === \"function\" && nameMatches(s, prevFunc),\n      );\n      if (funcInfo?.funcInfo?.proretset) {\n        return suggestSnippets([\n          {\n            label: `WITH ORDINALITY $0`,\n            docs: `If the WITH ORDINALITY clause is specified, an additional column of type bigint will be added to the function result columns. This column numbers the rows of the function result set, starting from 1.`,\n            kind: getKind(\"function\"),\n          },\n        ]);\n      }\n      if (funcInfo?.funcInfo?.prokind === \"a\") {\n        return suggestSnippets([\n          {\n            label: `FILTER ( WHERE $0 )`,\n            docs: missingKeywordDocumentation.FILTER,\n            kind: getKind(\"function\"),\n          },\n        ]);\n      }\n      if (funcInfo?.funcInfo?.prokind === \"w\") {\n        return suggestSnippets([\n          {\n            label: `OVER()`,\n            kind: getKind(\"function\"),\n            docs: rancDocs,\n          },\n        ]);\n      }\n    }\n\n    if (\n      prevKWD?.expects === \"number\" &&\n      ltoken?.text.toUpperCase() === prevKWD.kwd\n    ) {\n      return suggestSnippets([\"10\", \"50\", \"100\"].map((label) => ({ label })));\n    }\n\n    if (ltoken?.textLC === \"using\") {\n      return suggestSnippets([\n        { label: \"( join_column_list )\", insertText: \"( $0 )\" },\n      ]);\n    }\n\n    const SELKWDS = [\"SELECT\", \"DISTINCT\"] as const;\n    const selectIsComplete =\n      ltoken?.text !== \",\" &&\n      (ltoken?.text === \"*\" ||\n        (ltoken?.type !== \"operator.sql\" &&\n          ltoken?.text !== \".\" &&\n          currToken?.text !== \".\")) &&\n      (!thisLinePrevTokens.length ||\n        !SELKWDS.includes(ltoken?.text.toUpperCase() as any));\n    const isMaybeTypingSchemaDotTable = currToken?.text === \".\";\n    const ltokenIsIdentifier =\n      ltoken?.type === \"identifier.quote.sql\" ||\n      ltoken?.type === \"identifier.sql\" ||\n      ltoken?.textLC === \"*\" ||\n      ltoken?.textLC === \")\";\n\n    if (\n      prevKWD?.kwd === \"ORDER BY\" &&\n      ltokenIsIdentifier &&\n      cb.currToken?.text !== \".\" &&\n      cb.thisLinePrevTokens.length\n    ) {\n      return suggestSnippets([\n        { label: \"ASC\", kind: getKind(\"keyword\") },\n        { label: \"DESC\", kind: getKind(\"keyword\") },\n        { label: \",\", kind: getKind(\"keyword\") },\n        { label: \"NULLS LAST\", kind: getKind(\"keyword\") },\n        { label: \"NULLS FIRST\", kind: getKind(\"keyword\") },\n      ]);\n    }\n    const COND_KWDS = [\"WHERE\", \"HAVING\"] as const;\n    const conditionIsComplete =\n      ltoken &&\n      (ltoken.type !== \"operator.sql\" || ltoken.textLC === \"null\") && // for some reason null is an operator\n      !COND_KWDS.includes(ltoken.textLC as any);\n\n    const extraOptions: MinimalSnippet[] = [];\n    if (prevKWD?.kwd === \"WHERE\") {\n      extraOptions.push({ label: \"AND\", kind: getKind(\"keyword\") });\n      extraOptions.push({ label: \"OR\", kind: getKind(\"keyword\") });\n    }\n    const remainingKWDSOptions: MinimalSnippet[] = remainingKWDS.map((k) => ({\n      label: k.kwd,\n      kind: getKind(\"keyword\"),\n      docs: k.docs,\n      sortText: k.sortText,\n    }));\n    const remainingKWDSWithAndOr = [...remainingKWDSOptions, ...extraOptions];\n    if (\n      remainingKWDSWithAndOr.length &&\n      !isMaybeTypingSchemaDotTable &&\n      (!cb.text.trim() ||\n        (SELKWDS.includes(prevKWD?.kwd as any) && selectIsComplete) ||\n        (prevKWD?.kwd === \"INTO\" && ltokenIsIdentifier) ||\n        (prevKWD?.kwd === \"FROM\" && ltokenIsIdentifier) ||\n        (prevKWD?.kwd.endsWith(\"JOIN\") && ltokenIsIdentifier) ||\n        (prevKWD?.kwd === \"WHERE\" && conditionIsComplete) ||\n        (prevKWD?.kwd === \"LIMIT\" && ltoken?.type === \"number.sql\") ||\n        (prevKWD?.kwd === \"OFFSET\" && ltoken?.type === \"number.sql\") ||\n        ((prevKWD?.kwd === \"GROUP BY\" || prevKWD?.kwd === \"ORDER BY\") &&\n          ltoken?.text !== \",\" &&\n          ltoken?.textLC !== \"by\" &&\n          (cb.currToken?.text.length ?? 0) <= 1) ||\n        (prevKWD?.kwd === \"ON\" && !thisLinePrevTokens.length))\n    ) {\n      return suggestSnippets(remainingKWDSWithAndOr);\n    }\n\n    if (!thisLineLC && prevKWD?.kwd === \"FROM\" && ltoken?.textLC !== \"from\") {\n      const kwds = suggestKWD(\n        remainingKWDS.map((k) => k.kwd),\n        \"0\",\n      );\n      const tables = getExpected(\"tableOrView\", cb, ss).suggestions;\n\n      return {\n        suggestions: [...kwds.suggestions, ...tables],\n      };\n    }\n\n    if (prevKWD?.expects === \"table\") {\n      return suggestTableLike({ cb, ss, sql, parentCb: args.parentCb });\n    }\n\n    if (\n      cb.ftoken?.textLC === \"select\" &&\n      cb.nextTokens[0]?.textLC === \"select\"\n    ) {\n      return suggestSnippets([{ label: \"FROM\", kind: getKind(\"keyword\") }]);\n    }\n\n    return getColsAndFuncs();\n  },\n};\n\nconst joins = [\"JOIN\", \"INNER JOIN\", \"LEFT JOIN\", \"RIGHT JOIN\"] as const;\nconst getKWDSz = (excludeInto = false) =>\n  [\n    { kwd: \"SELECT\", expects: \"column\" },\n    { kwd: \"DISTINCT\", expects: \"column\", exactlyAfter: [\"SELECT\"] },\n    { kwd: \"WHEN\", expects: \"column\", justAfter: [\"CASE\"] },\n    ...(excludeInto ?\n      []\n    : [\n        {\n          kwd: \"INTO\",\n          expects: \"table\",\n          justAfter: [\"SELECT\"],\n          dependsOnAfter: \"FROM\",\n          docs: \"Creates a table from the result of the select statement\",\n        } as const,\n      ]),\n    {\n      kwd: \"FROM\",\n      expects: \"table\",\n      justAfter: [\"SELECT\"],\n      docs: \"Specifies a table/view or function which returns a table-like result\",\n    },\n    {\n      kwd: \"JOIN\",\n      expects: \"table\",\n      docs: \"Combine rows from one table with rows from a second table\",\n      canRepeat: true,\n    },\n    {\n      kwd: \"JOIN LATERAL\",\n      expects: \"table\",\n      docs: \"Lateral join subquery can reference columns provided by preceding FROM items\",\n      canRepeat: true,\n    },\n    {\n      kwd: \"INNER JOIN\",\n      dependsOn: \"FROM\",\n      expects: \"table\",\n      docs: \"Combine rows from one table with rows from a second table. Only matching records from both tables are returned\",\n      canRepeat: true,\n    },\n    {\n      kwd: \"LEFT JOIN\",\n      dependsOn: \"FROM\",\n      expects: \"table\",\n      docs: \"Combine rows from one table with rows from a second table. Records from first table AND matching records are returned\",\n      canRepeat: true,\n    },\n    {\n      kwd: \"RIGHT JOIN\",\n      dependsOn: \"FROM\",\n      expects: \"table\",\n      docs: \"Combine rows from one table with rows from a second table. Records from second table AND matching records are returned\",\n      canRepeat: true,\n    },\n    {\n      kwd: \"CROSS JOIN\",\n      dependsOn: \"FROM\",\n      expects: \"table\",\n      docs: \"Combine rows from one table with rows from a second table. Returns every possible combination of rows between the joined sets\",\n      canRepeat: true,\n    },\n    {\n      kwd: \"ON\",\n      expects: \"column\",\n      justAfter: joins,\n      docs: \"Join condition\",\n      canRepeat: true,\n    },\n    {\n      kwd: \"USING\",\n      expects: \"column\",\n      justAfter: joins,\n      docs: \"The USING clause is a shorthand that allows you to take advantage of the specific situation where both sides of the join use the same name for the joining column(s). It takes a comma-separated list of the shared column names and forms a join condition that includes an equality comparison for each one. For example, joining T1 and T2 with USING (a, b) produces the join condition ON T1.a = T2.a AND T1.b = T2.b.\",\n    },\n    {\n      kwd: \"WHERE\",\n      expects: \"column\",\n      docs: \"Condition/filter applied to the data\",\n    },\n    {\n      kwd: \"GROUP BY\",\n      expects: \"column\",\n      docs: \"Used with aggregate functions to split data into groups (or buckets).\\n\\nA group is defined by the combination of unique values of the columns from GROUP BY \",\n    },\n    {\n      kwd: \"HAVING\",\n      expects: \"column\",\n      docs: \"Allows filtering the aggregated results\",\n    },\n    {\n      kwd: \"ORDER BY\",\n      expects: \"column\",\n      docs: `The ORDER BY clause allows you to sort rows returned by a SELECT clause in ascending or descending order based on a sort expression.`,\n    },\n    {\n      kwd: \"LIMIT\",\n      expects: \"number\",\n      docs: \"If a limit count is given, no more than that many rows will be returned (but possibly fewer, if the query itself yields fewer rows\",\n    },\n    {\n      kwd: \"OFFSET\",\n      expects: \"number\",\n      docs: `OFFSET says to skip that many rows before beginning to return rows. OFFSET 0 is the same as omitting the OFFSET clause, as is OFFSET with a NULL argument.`,\n    },\n    {\n      kwd: \"UNION\",\n      expects: undefined,\n      options: [\"SELECT\"],\n      docs: `UNION effectively appends the result of query2 to the result of query1 (although there is no guarantee that this is the order in which the rows are actually returned). Furthermore, it eliminates duplicate rows from its result, in the same way as DISTINCT, unless UNION ALL is used.`,\n    },\n    {\n      kwd: \"UNION ALL\",\n      expects: undefined,\n      options: [\"SELECT\"],\n      docs: `UNION effectively appends the result of query2 to the result of query1 (although there is no guarantee that this is the order in which the rows are actually returned). Furthermore, it eliminates duplicate rows from its result, in the same way as DISTINCT, unless UNION ALL is used.`,\n    },\n  ] as const satisfies readonly KWD[];\n\nconst rancDocs = `\nrank function produces a numerical rank for each distinct ORDER BY value in the current row's partition, using the order defined by the ORDER BY clause. rank needs no explicit parameter, because its behavior is entirely determined by the OVER clause.`;\n\nconst getCurrentFunction = (\n  cb: Pick<\n    CodeBlock,\n    | \"currNestingId\"\n    | \"getPrevTokensNoParantheses\"\n    | \"currToken\"\n    | \"ltoken\"\n    | \"tokens\"\n    | \"currOffset\"\n    | \"currNestingFunc\"\n  >,\n) => {\n  if (!cb.currNestingId) return undefined;\n\n  /** prevTokens use end < currOffset */\n  const actuallyPrevTokens = cb.tokens\n    .slice(0)\n    .filter((t) => t.end <= cb.currOffset)\n    .sort((a, b) => a.offset - b.offset);\n  const f =\n    cb.currNestingFunc ??\n    actuallyPrevTokens.reverse().find((t, i) => {\n      const prevToken = actuallyPrevTokens[i - 1];\n      return (\n        prevToken?.text === \"(\" && t.nestingId === cb.currNestingId.slice(1)\n      );\n    });\n  if (!f) return undefined;\n  const prevArgsWithDelimiters = actuallyPrevTokens.filter((t) => {\n    return (\n      ![\"white.sql\", \"delimiter.parenthesis.sql\"].includes(t.type) &&\n      t.offset > f.offset + 1 &&\n      t.nestingId === cb.currNestingId &&\n      t.offset <= cb.currOffset\n    );\n  });\n  let prevArgs = prevArgsWithDelimiters.filter(\n    (t) => t.type !== \"delimiter.sql\",\n  );\n  const prevDelimiters = prevArgsWithDelimiters.filter(\n    (t) => t.type === \"delimiter.sql\",\n  );\n  const isWrittingDotColumn =\n    cb.currToken?.text === \".\" && cb.ltoken?.end === cb.currToken.offset;\n  if (isWrittingDotColumn) {\n    prevArgs = prevArgs.slice(1);\n  }\n  return { func: f, prevArgs, prevDelimiters };\n};\n\nexport const getLastFuncSuggestions = (\n  cb: CodeBlock,\n  ss: ParsedSQLSuggestion[],\n) => {\n  const funcName = getCurrentFunction(cb);\n  if (!funcName) return [];\n  return ss.filter(\n    (s) => s.type === \"function\" && s.escapedIdentifier === funcName.func.text,\n  );\n};\n\nexport const getParentFunction = (cb: CodeBlock) => {\n  const isInsidef = getCurrentFunction(cb);\n  /** Is inside func args */\n  if (isInsidef) {\n    const { func, prevArgs, prevDelimiters } = isInsidef;\n    const prevTokenIdx = cb.prevTokens.findIndex(\n      (t, i, arr) => arr[i + 1]?.offset === func.offset,\n    );\n    const prevToken = prevTokenIdx ? cb.prevTokens[prevTokenIdx] : undefined;\n    const prevTokens =\n      prevTokenIdx ? cb.prevTokens.slice(0, prevTokenIdx + 2) : undefined;\n    const prevTextLC = prevTokens?.map((t) => t.textLC).join(\" \");\n    /** Ignore this case:  \"WITH cte AS () ...\" */\n    // if(funcName?.type === \"keyword.sql\" && funcName.textLC === \"as\"){\n    //   return undefined;\n    // }\n\n    return {\n      func,\n      prevToken,\n      prevTokens,\n      prevArgs,\n      prevTextLC,\n      prevDelimiters,\n    };\n  }\n\n  return undefined;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchSet.ts",
    "content": "import { omitKeys } from \"prostgles-types\";\nimport { asListObject } from \"../SQLEditorSuggestions\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { getExpected } from \"./getExpected\";\nimport {\n  getKind,\n  KNDS as KindMap,\n  type SQLMatcher,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\nimport { suggestKWD, withKWDs } from \"./withKWDs\";\nimport type { SQLHandler } from \"prostgles-types\";\n\nexport const MatchSet: SQLMatcher = {\n  match: ({ ftoken }) =>\n    ftoken?.textLC === \"set\" ||\n    ftoken?.textLC === \"show\" ||\n    ftoken?.textLC === \"reset\",\n  result: async (args) => {\n    const { cb, ss, setS: settingSuggestions, sql } = args;\n    const { ltoken } = cb;\n    const _suggestKWD = (vals: string[], sortText?: string) =>\n      suggestKWD(getKind, vals, sortText);\n\n    if (cb.ftoken?.textLC === \"reset\") {\n      return withKWDs(\n        [\n          {\n            kwd: \"RESET\",\n            options: [\n              {\n                label: \"ALL\",\n                docs: \"Resets all parameters to their default values\",\n              },\n              ...settingSuggestions,\n            ],\n          },\n        ],\n        { cb, ss, setS: settingSuggestions, sql },\n      ).getSuggestion();\n    }\n\n    if (cb.prevLC.startsWith(\"set session authorization\")) {\n      return {\n        suggestions: getExpected(\"user\", cb, ss).suggestions.concat(\n          _suggestKWD([\"DEFAULT\"]).suggestions as any,\n        ),\n      };\n    }\n\n    if (ltoken?.textLC === \"to\") {\n      if (cb.l1token?.textLC === \"search_path\") {\n        return getExpected(\"schema\", cb, ss);\n      }\n\n      if ([\"timezone\", \"time zone\"].some((v) => cb.prevLC.includes(v)) && sql) {\n        const timeZones = await getTimeZones(sql);\n        return suggestSnippets(\n          timeZones.map((t) => ({\n            label: t.name,\n            insertText: `'${t.name}'`,\n            docs: asListObject(t),\n            kind: getKind(\"keyword\"),\n          })),\n        );\n      }\n\n      const settingInfo = settingSuggestions.find(\n        (s) => s.name === cb.identifiers.at(-1)?.text,\n      )?.settingInfo;\n      const docs = settingInfo?.description;\n      const getVal = (v = \"\") =>\n        settingInfo?.unit ? `'${v}${settingInfo.unit}'` : v;\n      const valueSuggestions = suggestSnippets([\n        { label: \"DEFAULT\", docs },\n        ...(settingInfo?.vartype === \"bool\" ?\n          [\n            { label: `ON`, docs },\n            { label: `OFF`, docs },\n          ]\n        : settingInfo?.vartype === \"string\" ?\n          [{ label: \"'$setting_string_value'\", docs }]\n        : settingInfo?.vartype === \"integer\" ?\n          [\n            {\n              label:\n                settingInfo.setting_pretty?.min_val ??\n                getVal(settingInfo.min_val),\n              kind: KindMap.Value,\n              docs: \"Minimum value\" + `\\n\\n${docs}`,\n            },\n            {\n              label:\n                settingInfo.setting_pretty?.max_val ??\n                getVal(settingInfo.max_val),\n              kind: KindMap.Value,\n              docs: \"Maximum value\" + `\\n\\n${docs}`,\n            },\n          ]\n        : settingInfo?.enumvals?.length ?\n          settingInfo.enumvals.map((label) => {\n            const isDefault = settingInfo.reset_val === label;\n            return {\n              label: { label, description: isDefault ? \"DEFAULT\" : undefined },\n              docs,\n            };\n          })\n        : []),\n      ]);\n\n      return valueSuggestions;\n    }\n    const extraSettings = [\n      {\n        label: \"SESSION\",\n        expects: \"setting\",\n        docs: `Specifies that the command takes effect for the current session. (This is the default if neither SESSION nor LOCAL appears.)`,\n      },\n      {\n        label: \"SESSION AUTHORIZATION\",\n        role: \"user\",\n        docs: \"Set the session user identifier and the current user identifier of the current session\",\n      },\n      {\n        label: \"LOCAL\",\n        docs: `Specifies that the command takes effect for only the current transaction. After COMMIT or ROLLBACK, the session-level setting takes effect again. Issuing this outside of a transaction block emits a warning and otherwise has no effect.`,\n      },\n      { label: \"ALL\" },\n      {\n        label: \"SCHEMA\",\n        options: (ss) =>\n          ss\n            .filter((s) => s.type === \"schema\")\n            .map((s) => ({ ...s, insertText: `'${s.name}'` })),\n        docs: `SET SCHEMA 'value' is an alias for SET search_path TO value. Only one schema can be specified using this syntax.`,\n      },\n      {\n        label: \"SEED\",\n        options: [\"-1\", \"1\"],\n        docs: `Sets the internal seed for the random number generator (the function random). Allowed values are floating-point numbers between -1 and 1 inclusive. The seed can also be set by invoking the function setseed`,\n      },\n      {\n        label: \"TIME ZONE\",\n        docs: `SET TIME ZONE 'value' is an alias for SET timezone TO 'value'. The syntax SET TIME ZONE allows special syntax for the time zone specification. Here are examples of valid values: 'PST8PDT', 'Europe/Rome', -7, INTERVAL '-08:00' HOUR TO MINUTE`,\n      },\n    ] as const;\n    return withKWDs(\n      [\n        { kwd: \"SHOW\", options: settingSuggestions },\n        {\n          kwd: \"SET\",\n          options: [...extraSettings, ...settingSuggestions].map((o) => ({\n            ...o,\n            kind: getKind(\"setting\"),\n          })),\n        },\n        { kwd: \"TO\", dependsOn: \"SET\" },\n        ...extraSettings.map((d) => ({\n          kwd: d.label,\n          dependsOn: d.label,\n          ...omitKeys(d, [\"label\"]),\n        })),\n      ] satisfies KWD[],\n      { cb, ss, setS: settingSuggestions, sql },\n    ).getSuggestion();\n  },\n};\n\nconst getTimeZones = async (sql: SQLHandler) => {\n  return (await sql(\n    `\n    SELECT *\n    FROM pg_catalog.pg_timezone_names\n  `,\n    {},\n    { returnType: \"rows\" },\n  )) as {\n    name: string;\n    abbrev: string;\n    utc_offset: string;\n    is_dst: boolean;\n  }[];\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchSubscription.ts",
    "content": "import { getParentFunction } from \"./MatchSelect\";\nimport { getExpected } from \"./getExpected\";\nimport type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\nimport { withKWDs } from \"./withKWDs\";\n\nexport const MatchSubscription: SQLMatcher = {\n  match: (cb) => cb.tokens[1]?.textLC === \"subscription\",\n  result: async ({ cb, ss, setS, sql }) => {\n    const command = cb.ftoken?.textLC;\n    const newSubscriptionName =\n      command === \"create\" &&\n      cb.tokens[1]?.textLC === \"subscription\" &&\n      cb.tokens[2];\n\n    const isInfunc = getParentFunction(cb);\n    if (isInfunc?.func.textLC === \"with\") {\n      return withKWDs(\n        Object.entries(withOptions).map(([kwd, { docs, options }]) => ({\n          kwd,\n          docs,\n          options,\n        })),\n        { cb, ss, setS, sql },\n      ).getSuggestion(\",\", [\"(\", \")\"]);\n    }\n\n    if (newSubscriptionName) {\n      return withKWDs(\n        [\n          {\n            kwd: \"CONNECTION\",\n            docs: `The libpq connection string defining how to connect to the publisher database`,\n            options: [\n              \"'host=192.168.1.50 port=5432 user=foo dbname=foodb password=mypassword'\",\n            ],\n          },\n          {\n            kwd: \"PUBLICATION\",\n            docs: `Names of the publications on the publisher to subscribe to.`,\n            options: [\"$publication_name\"],\n          },\n          {\n            kwd: \"WITH\",\n            optional: true,\n            docs: `Optional parameters for a subscription`,\n          },\n        ] satisfies KWD[],\n        { cb, ss, setS, sql },\n      ).getSuggestion();\n    }\n\n    if (cb.prevLC.endsWith(\"if exists\")) {\n      return getExpected(\"subscription\", cb, ss);\n    }\n\n    return withKWDs(\n      [\n        {\n          kwd: \"SUBSCRIPTION\",\n          expects: command === \"create\" ? undefined : \"subscription\",\n          excludeIf: (cb) => cb.l1token?.textLC === \"subscription\",\n          options:\n            command === \"drop\" ? [{ label: \"IF EXISTS\" }]\n            : command === \"create\" ? [{ label: \"$new_subscription_name\" }]\n            : undefined,\n          docs: \"Adds a new logical-replication subscription. The user that creates a subscription becomes the owner of the subscription. The subscription name must be distinct from the name of any existing subscription in the current database.\",\n        },\n      ] satisfies KWD[],\n      { cb, ss, setS, sql },\n    ).getSuggestion();\n  },\n};\n\nconst options = {\n  \"FOR TABLE\": {\n    docs: `Specifies a list of tables to add to the publication. If ONLY is specified before the table name, only that table is added to the publication. If ONLY is not specified, the table and all its descendant tables (if any) are added. Optionally, * can be specified after the table name to explicitly indicate that descendant tables are included. This does not apply to a partitioned table, however. The partitions of a partitioned table are always implicitly considered part of the publication, so they are never explicitly added to the publication.\n\nIf the optional WHERE clause is specified, it defines a row filter expression. Rows for which the expression evaluates to false or null will not be published. Note that parentheses are required around the expression. It has no effect on TRUNCATE commands.\n\nWhen a column list is specified, only the named columns are replicated. If no column list is specified, all columns of the table are replicated through this publication, including any columns added later. It has no effect on TRUNCATE commands. See Section 31.4 for details about column lists.\n\nOnly persistent base tables and partitioned tables can be part of a publication. Temporary tables, unlogged tables, foreign tables, materialized views, and regular views cannot be part of a publication.\n\nSpecifying a column list when the publication also publishes FOR TABLES IN SCHEMA is not supported.\n\nWhen a partitioned table is added to a publication, all of its existing and future partitions are implicitly considered to be part of the publication. So, even operations that are performed directly on a partition are also published via publications that its ancestors are part of.`,\n    expects: \"table\",\n  },\n  \"FOR ALL TABLES\": {\n    docs: `Marks the publication as one that replicates changes for all tables in the database, including tables created in the future.`,\n    expects: undefined,\n  },\n  \"FOR TABLES IN SCHEMA\": {\n    docs: `Marks the publication as one that replicates changes for all tables in the specified list of schemas, including tables created in the future.\n\nSpecifying a schema when the publication also publishes a table with a column list is not supported.\n\nOnly persistent base tables and partitioned tables present in the schema will be included as part of the publication. Temporary tables, unlogged tables, foreign tables, materialized views, and regular views from the schema will not be part of the publication.\n\nWhen a partitioned table is published via schema level publication, all of its existing and future partitions are implicitly considered to be part of the publication, regardless of whether they are from the publication schema or not. So, even operations that are performed directly on a partition are also published via publications that its ancestors are part of.`,\n    expects: \"schema\",\n  },\n  WITH: {\n    docs: `This clause specifies optional parameters for a subscription`,\n    expects: undefined,\n  },\n} as const;\n\nconst withOptions = {\n  connect: {\n    options: [\"=true\", \"=false\"],\n    docs: `(boolean) \nSpecifies whether the CREATE SUBSCRIPTION command should connect to the publisher at all. The default is true. Setting this to false will force the values of create_slot, enabled and copy_data to false. (You cannot combine setting connect to false with setting create_slot, enabled, or copy_data to true.)\n\nSince no connection is made when this option is false, no tables are subscribed. To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. See Section 31.2.3 for examples. \n`,\n  },\n  create_slot: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (boolean) \nSpecifies whether the command should create the replication slot on the publisher. The default is true.\n\nIf set to false, you are responsible for creating the publisher's slot in some other way. See Section 31.2.3 for examples.\n\n`,\n  },\n  enabled: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (boolean) \nSpecifies whether the subscription should be actively replicating or whether it should just be set up but not started yet. The default is true.\n\n`,\n  },\n  slot_name: {\n    options: [\"$slot_name\"],\n    docs: ` (string) \nName of the publisher's replication slot to use. The default is to use the name of the subscription for the slot name.\n\nSetting slot_name to NONE means there will be no replication slot associated with the subscription. Such subscriptions must also have both enabled and create_slot set to false. Use this when you will be creating the replication slot later manually. See Section 31.2.3 for examples.\n\nThe following parameters control the subscription's replication behavior after it has been created:\n\n`,\n  },\n  binary: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (boolean) \nSpecifies whether the subscription will request the publisher to send the data in binary format (as opposed to text). The default is false. Any initial table synchronization copy (see copy_data) also uses the same format. Binary format can be faster than the text format, but it is less portable across machine architectures and PostgreSQL versions. Binary format is very data type specific; for example, it will not allow copying from a smallint column to an integer column, even though that would work fine in text format. Even when this option is enabled, only data types having binary send and receive functions will be transferred in binary. Note that the initial synchronization requires all data types to have binary send and receive functions, otherwise the synchronization will fail (see CREATE TYPE for more about send/receive functions).\n\nWhen doing cross-version replication, it could be that the publisher has a binary send function for some data type, but the subscriber lacks a binary receive function for that type. In such a case, data transfer will fail, and the binary option cannot be used.\n\nIf the publisher is a PostgreSQL version before 16, then any initial table synchronization will use text format even if binary = true.\n\n`,\n  },\n  copy_data: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (boolean) \nSpecifies whether to copy pre-existing data in the publications that are being subscribed to when the replication starts. The default is true.\n\nIf the publications contain WHERE clauses, it will affect what data is copied. Refer to the Notes for details.\n\nSee Notes for details of how copy_data = true can interact with the origin parameter.\n\n`,\n  },\n  streaming: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (enum) \nSpecifies whether to enable streaming of in-progress transactions for this subscription. The default value is off, meaning all transactions are fully decoded on the publisher and only then sent to the subscriber as a whole.\n\nIf set to on, the incoming changes are written to temporary files and then applied only after the transaction is committed on the publisher and received by the subscriber.\n\nIf set to parallel, incoming changes are directly applied via one of the parallel apply workers, if available. If no parallel apply worker is free to handle streaming transactions then the changes are written to temporary files and applied after the transaction is committed. Note that if an error happens in a parallel apply worker, the finish LSN of the remote transaction might not be reported in the server log.\n\n`,\n  },\n  synchronous_commit: {\n    options: [\"=off\", \"=on\"],\n    docs: ` (enum) \nThe value of this parameter overrides the synchronous_commit setting within this subscription's apply worker processes. The default value is off.\n\nIt is safe to use off for logical replication: If the subscriber loses transactions because of missing synchronization, the data will be sent again from the publisher.\n\nA different setting might be appropriate when doing synchronous logical replication. The logical replication workers report the positions of writes and flushes to the publisher, and when using synchronous replication, the publisher will wait for the actual flush. This means that setting synchronous_commit for the subscriber to off when the subscription is used for synchronous replication might increase the latency for COMMIT on the publisher. In this scenario, it can be advantageous to set synchronous_commit to local or higher.\n\n`,\n  },\n  two_phase: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (boolean) \nSpecifies whether two-phase commit is enabled for this subscription. The default is false.\n\nWhen two-phase commit is enabled, prepared transactions are sent to the subscriber at the time of PREPARE TRANSACTION, and are processed as two-phase transactions on the subscriber too. Otherwise, prepared transactions are sent to the subscriber only when committed, and are then processed immediately by the subscriber.\n\nThe implementation of two-phase commit requires that replication has successfully finished the initial table synchronization phase. So even when two_phase is enabled for a subscription, the internal two-phase state remains temporarily “pending” until the initialization phase completes. See column subtwophasestate of pg_subscription to know the actual two-phase state.\n\n`,\n  },\n  disable_on_error: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (boolean) \nSpecifies whether the subscription should be automatically disabled if any errors are detected by subscription workers during data replication from the publisher. The default is false.\n\n`,\n  },\n  password_required: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (boolean) \nSpecifies whether connections to the publisher made as a result of this subscription must use password authentication. This setting is ignored when the subscription is owned by a superuser. The default is true. Only superusers can set this value to false.\n\n`,\n  },\n  run_as_owner: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (boolean) \nIf true, all replication actions are performed as the subscription owner. If false, replication workers will perform actions on each table as the owner of that table. The latter configuration is generally much more secure; for details, see Section 31.9. The default is false.\n\n`,\n  },\n  origin: {\n    options: [\"=true\", \"=false\"],\n    docs: ` (string) \nSpecifies whether the subscription will request the publisher to only send changes that don't have an origin or send changes regardless of origin. Setting origin to none means that the subscription will request the publisher to only send changes that don't have an origin. Setting origin to any means that the publisher sends changes regardless of their origin. The default is any.\n\nSee Notes for details of how copy_data = true can interact with the origin parameter.\n\n`,\n  },\n} as const;\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchUpdate.ts",
    "content": "import { suggestSnippets } from \"./CommonMatchImports\";\nimport { getExpected } from \"./getExpected\";\nimport type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport { suggestColumnLike } from \"./suggestColumnLike\";\nimport { suggestCondition } from \"./suggestCondition\";\nimport { type KWD, withKWDs, suggestKWD } from \"./withKWDs\";\n\nconst KWDs = [\n  { kwd: \"UPDATE\", expects: \"table\" },\n  { kwd: \"SET\", expects: \"column\", justAfter: [\"UPDATE\"] },\n  { kwd: \",\", expects: \"column\", canRepeat: true },\n  { kwd: \"FROM\", optional: true, expects: \"table\" },\n  { kwd: \"WHERE\", expects: \"condition\" },\n  { kwd: \"RETURNING\", expects: \"column\" },\n] as const satisfies KWD[];\n\nexport const MatchUpdate: SQLMatcher = {\n  match: ({ prevTopKWDs }) => {\n    return prevTopKWDs.slice(0, 2).some((t) => [\"UPDATE\"].includes(t.text));\n  },\n  result: async (args) => {\n    const { cb, ss } = args;\n    const { prevTokens, ltoken } = cb;\n    const kwds = withKWDs(KWDs, args);\n\n    const isSettingColumns =\n      !cb.currNestingId && cb.prevTopKWDs[0]?.textLC === \"set\";\n    if (\n      isSettingColumns &&\n      cb.ltoken?.type.startsWith(\"identifier\") &&\n      [\",\", \"set\"].includes(cb.l1token?.textLC ?? \"\")\n    ) {\n      return suggestSnippets([\n        {\n          label: \"=\",\n        },\n      ]);\n    }\n\n    if (\n      isSettingColumns &&\n      !cb.thisLinePrevTokens.length &&\n      ![\",\", \"set\"].includes(cb.ltoken?.textLC ?? \"\")\n    ) {\n      return withKWDs(KWDs.slice(3), args).getSuggestion();\n    }\n\n    if (prevTokens.length === 1) {\n      return {\n        suggestions: getExpected(\"table\", cb, ss).suggestions.map((s) => ({\n          ...s,\n          label: `${s.name}...`,\n          insertText: s.insertText + `\\nSET`,\n        })),\n      };\n    }\n\n    if (ltoken?.text === \",\") {\n      const cols = getExpected(\"column\", cb, ss);\n      return {\n        // space added after = to prevent bad suggestions (next kwd) showing up\n        suggestions: cols.suggestions.map((c) => ({\n          ...c,\n          insertText: `${c.insertText} = `,\n        })),\n      };\n    }\n    if (ltoken?.text === \"=\" || kwds.prevKWD?.expects === \"column\") {\n      return suggestColumnLike(args);\n    }\n\n    if (kwds.prevKWD?.expects === \"condition\") {\n      const cond = await suggestCondition(args, true);\n      if (cond) return cond;\n    }\n\n    return kwds.getSuggestion();\n  },\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchVacuum.ts",
    "content": "import { getParentFunction } from \"./MatchSelect\";\nimport { getExpected } from \"./getExpected\";\nimport type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport { getKind } from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\nimport { withKWDs } from \"./withKWDs\";\nimport { pickKeys } from \"prostgles-types\";\n\nexport const MatchVacuumOrAnalyze: SQLMatcher = {\n  match: (cb) => [\"vacuum\", \"analyze\"].includes(cb.ftoken?.textLC ?? \"\"),\n  result: async ({ cb, ss, setS, sql }) => {\n    const command = cb.ftoken?.textLC ?? \"vacuum\";\n    const func = getParentFunction(cb);\n    if (func?.func.textLC === command) {\n      return withKWDs(\n        Object.entries(options).map(([kwd, docs]) => ({ kwd, docs })),\n        { cb, ss, setS, sql, opts: { notOrdered: true } },\n      ).getSuggestion(\",\", [\"(\", \")\"]);\n    }\n\n    if (cb.ltoken?.textLC === \")\") {\n      return getExpected(\"table\", cb, ss);\n    }\n\n    return withKWDs(\n      command === \"analyze\" ?\n        [\n          {\n            kwd: \"ANALYZE\",\n            expects: [\"table\"],\n            options: [\n              {\n                label: \"(options...)\",\n                insertText: \"($0)\",\n                docs: Object.keys(\n                  pickKeys(options, [\n                    \"VERBOSE\",\n                    \"SKIP_LOCKED\",\n                    \"BUFFER_USAGE_LIMIT\",\n                  ]),\n                ).join(\"\\n\\n\"),\n                kind: getKind(\"snippet\"),\n              },\n            ],\n          },\n        ]\n      : ([\n          {\n            kwd: \"VACUUM\",\n            expects: [\"table\"],\n            options: [\n              {\n                label: \"(options...)\",\n                insertText: \"($0)\",\n                docs: Object.keys(options).join(\"\\n\\n\"),\n                kind: getKind(\"snippet\"),\n              },\n            ],\n          },\n          {\n            kwd: \"VACUUM FULL\",\n            expects: [\"table\"],\n            excludeIf: [\"VACUUM\"],\n          },\n        ] satisfies KWD[]),\n      { cb, ss, setS, sql },\n    ).getSuggestion();\n  },\n};\n\nconst options = {\n  FULL: `Selects “full” vacuum, which can reclaim more space, but takes much longer and exclusively locks the table. This method also requires extra disk space, since it writes a new copy of the table and doesn't release the old copy until the operation is complete. Usually this should only be used when a significant amount of space needs to be reclaimed from within the table.`,\n  FREEZE: `Selects aggressive “freezing” of tuples. Specifying FREEZE is equivalent to performing VACUUM with the vacuum_freeze_min_age and vacuum_freeze_table_age parameters set to zero. Aggressive freezing is always performed when the table is rewritten, so this option is redundant when FULL is specified.`,\n  VERBOSE: `Prints a detailed vacuum activity report for each table.`,\n  ANALYZE: `Updates statistics used by the planner to determine the most efficient way to execute a query.`,\n  DISABLE_PAGE_SKIPPING: `Normally, VACUUM will skip pages based on the visibility map. Pages where all tuples are known to be frozen can always be skipped, and those where all tuples are known to be visible to all transactions may be skipped except when performing an aggressive vacuum. Furthermore, except when performing an aggressive vacuum, some pages may be skipped in order to avoid waiting for other sessions to finish using them. This option disables all page-skipping behavior, and is intended to be used only when the contents of the visibility map are suspect, which should happen only if there is a hardware or software issue causing database corruption.`,\n  SKIP_LOCKED: `Specifies that VACUUM should not wait for any conflicting locks to be released when beginning work on a relation: if a relation cannot be locked immediately without waiting, the relation is skipped. Note that even with this option, VACUUM may still block when opening the relation's indexes. Additionally, VACUUM ANALYZE may still block when acquiring sample rows from partitions, table inheritance children, and some types of foreign tables. Also, while VACUUM ordinarily processes all partitions of specified partitioned tables, this option will cause VACUUM to skip all partitions if there is a conflicting lock on the partitioned table.`,\n  INDEX_CLEANUP: `Normally, VACUUM will skip index vacuuming when there are very few dead tuples in the table. The cost of processing all of the table's indexes is expected to greatly exceed the benefit of removing dead index tuples when this happens. This option can be used to force VACUUM to process indexes when there are more than zero dead tuples. The default is AUTO, which allows VACUUM to skip index vacuuming when appropriate. If INDEX_CLEANUP is set to ON, VACUUM will conservatively remove all dead tuples from indexes. This may be useful for backwards compatibility with earlier releases of PostgreSQL where this was the standard behavior.\\nINDEX_CLEANUP can also be set to OFF to force VACUUM to always skip index vacuuming, even when there are many dead tuples in the table. This may be useful when it is necessary to make VACUUM run as quickly as possible to avoid imminent transaction ID wraparound (see Section 25.1.5). However, the wraparound failsafe mechanism controlled by vacuum_failsafe_age will generally trigger automatically to avoid transaction ID wraparound failure, and should be preferred. If index cleanup is not performed regularly, performance may suffer, because as the table is modified indexes will accumulate dead tuples and the table itself will accumulate dead line pointers that cannot be removed until index cleanup is completed.\\nThis option has no effect for tables that have no index and is ignored if the FULL option is used. It also has no effect on the transaction ID wraparound failsafe mechanism. When triggered it will skip index vacuuming, even when INDEX_CLEANUP is set to ON.`,\n  PROCESS_MAIN: `Specifies that VACUUM should attempt to process the main relation. This is usually the desired behavior and is the default. Setting this option to false may be useful when it is only necessary to vacuum a relation's corresponding TOAST table.`,\n  PROCESS_TOAST: `Specifies that VACUUM should attempt to process the corresponding TOAST table for each relation, if one exists. This is usually the desired behavior and is the default. Setting this option to false may be useful when it is only necessary to vacuum the main relation. This option is required when the FULL option is used.`,\n  TRUNCATE: `Specifies that VACUUM should attempt to truncate off any empty pages at the end of the table and allow the disk space for the truncated pages to be returned to the operating system. This is normally the desired behavior and is the default unless the vacuum_truncate option has been set to false for the table to be vacuumed. Setting this option to false may be useful to avoid ACCESS EXCLUSIVE lock on the table that the truncation requires. This option is ignored if the FULL option is used.`,\n  PARALLEL: `Perform index vacuum and index cleanup phases of VACUUM in parallel using integer background workers (for the details of each vacuum phase, please refer to Table 28.45). The number of workers used to perform the operation is equal to the number of indexes on the relation that support parallel vacuum which is limited by the number of workers specified with PARALLEL option if any which is further limited by max_parallel_maintenance_workers. An index can participate in parallel vacuum if and only if the size of the index is more than min_parallel_index_scan_size. Please note that it is not guaranteed that the number of parallel workers specified in integer will be used during execution. It is possible for a vacuum to run with fewer workers than specified, or even with no workers at all. Only one worker can be used per index. So parallel workers are launched only when there are at least 2 indexes in the table. Workers for vacuum are launched before the start of each phase and exit at the end of the phase. These behaviors might change in a future release. This option can't be used with the FULL option.`,\n  SKIP_DATABASE_STATS: `Specifies that VACUUM should skip updating the database-wide statistics about oldest unfrozen XIDs. Normally VACUUM will update these statistics once at the end of the command. However, this can take awhile in a database with a very large number of tables, and it will accomplish nothing unless the table that had contained the oldest unfrozen XID was among those vacuumed. Moreover, if multiple VACUUM commands are issued in parallel, only one of them can update the database-wide statistics at a time. Therefore, if an application intends to issue a series of many VACUUM commands, it can be helpful to set this option in all but the last such command; or set it in all the commands and separately issue VACUUM (ONLY_DATABASE_STATS) afterwards.`,\n  ONLY_DATABASE_STATS: `Specifies that VACUUM should do nothing except update the database-wide statistics about oldest unfrozen XIDs. When this option is specified, the table_and_columns list must be empty, and no other option may be enabled except VERBOSE.`,\n  BUFFER_USAGE_LIMIT: `Specifies the Buffer Access Strategy ring buffer size for VACUUM. This size is used to calculate the number of shared buffers which will be reused as part of this strategy. 0 disables use of a Buffer Access Strategy. If ANALYZE is also specified, the BUFFER_USAGE_LIMIT value is used for both the vacuum and analyze stages. This option can't be used with the FULL option except if ANALYZE is also specified. When this option is not specified, VACUUM uses the value from vacuum_buffer_usage_limit. Higher settings can allow VACUUM to run more quickly, but having too large a setting may cause too many other useful pages to be evicted from shared buffers. The minimum value is 128 kB and the maximum value is 16 GB.`,\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MatchWith.ts",
    "content": "import {\n  getCurrentCodeBlock,\n  getCurrentNestingOffsetLimits,\n} from \"./completionUtils/getCodeBlock\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { MatchInsert } from \"./MatchInsert\";\nimport { MatchSelect } from \"./MatchSelect\";\nimport { MatchUpdate } from \"./MatchUpdate\";\nimport { MatchDelete } from \"./MathDelete\";\nimport {\n  getKind,\n  type SQLMatchContext,\n  type SQLMatcher,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport { suggestKWD } from \"./withKWDs\";\nimport { SQLMatchers } from \"./getMatch\";\nimport { isObject } from \"@common/publishUtils\";\nimport type { IMarkdownString } from \"../../W_SQL/monacoEditorTypes\";\n\nconst DATA_MODIF_INFO = `Trying to update the same row twice in a single statement is not supported. Only one of the modifications takes place, but it is not easy (and sometimes not possible) to reliably predict which one. This also applies to deleting a row that was already updated in the same statement: only the update is performed. Therefore you should generally avoid trying to modify a single row twice in a single statement. In particular avoid writing WITH sub-statements that could affect the same rows changed by the main statement or a sibling sub-statement. The effects of such a statement will not be predictable.`;\n\nexport const matchNested = async (\n  args: SQLMatchContext,\n  commands: (keyof typeof SQLMatchers)[],\n  nestingId: string | undefined,\n) => {\n  const { cb } = args;\n  const nestedLimits = getCurrentNestingOffsetLimits(cb, nestingId);\n  if (nestedLimits) {\n    const cbNested = await getCurrentCodeBlock(\n      cb.model,\n      cb.position,\n      nestedLimits.limits,\n    );\n    const matchedCommand = commands.find((command) =>\n      SQLMatchers[command].match(cbNested),\n    );\n    if (matchedCommand) {\n      return SQLMatchers[matchedCommand].result({\n        ...args,\n        parentCb: cb,\n        cb: cbNested,\n        options: { MatchSelect: { excludeInto: true } },\n      });\n    }\n  }\n  return undefined;\n};\n\nexport const MatchWith: SQLMatcher = {\n  match: ({ ftoken }) => ftoken?.textLC === \"with\",\n  result: async (args) => {\n    const { cb, ss } = args;\n\n    const { prevLC, ltoken, currNestingId, currNestingFunc } = cb;\n    const prevTokensNoParens = cb.getPrevTokensNoParantheses();\n    const ltokenNoParens = prevTokensNoParens.at(-1);\n    const isWrittingEndCommand =\n      !currNestingId &&\n      [\"select\", \"update\", \"delete\"].some((kwd) =>\n        prevTokensNoParens.some((ptn) => ptn.textLC.includes(kwd)),\n      );\n\n    if (prevLC.trim() === \"with\") {\n      return suggestSnippets([\n        { label: \"RECURSIVE\" },\n        {\n          label: \"RECURSIVE...\",\n          insertText:\n            \"RECURSIVE t(n) AS (\\n  VALUES (1)\\n  UNION ALL\\n  SELECT n+1\\n  FROM t\\n  WHERE n < 100 \\n) \\nSELECT sum(n) FROM t;\",\n        },\n        {\n          label: \"cte1\",\n          insertText: \"cte1 AS (\\n SELECT 1 \\n FROM $1\\n)\\nSELECT * FROM cte1;\",\n        },\n      ]);\n    }\n    if (cb.prevTokens.length === 2) {\n      return suggestSnippets([\n        {\n          label: \"AS...\",\n          insertText: ` AS (\\n  SELECT * FROM $1\\n)\\nSELECT * FROM ${cb.prevTokens.at(-1)?.text ?? \"cte\"}`,\n        },\n        {\n          label: \"cte1\",\n          insertText: \"\\n  cte1 AS (SELECT 1 FROM $1)\\nSELECT * FROM cte1;\",\n        },\n      ]);\n    }\n\n    if (currNestingId) {\n      const topNestingToken = cb.prevTokens\n        .slice(0)\n        .reverse()\n        .find((t) => {\n          return (\n            t.nestingFuncToken?.textLC === \"as\" && t.nestingId.length === 1\n          );\n        });\n      const asToken = [currNestingFunc, topNestingToken?.nestingFuncToken].find(\n        (t) => t?.textLC === \"as\",\n      );\n      if (asToken) {\n        if (\n          currNestingFunc?.textLC === \"as\" &&\n          currNestingId.length === 1 &&\n          cb.ltoken?.text === \"(\"\n        ) {\n          const getDocStr = (v: string | IMarkdownString | undefined) =>\n            isObject(v) ? v.value : (v ?? \"\");\n          const allowedCteCommands = ss\n            .filter(\n              (s) =>\n                s.topKwd &&\n                [\"SELECT\", \"UPDATE\", \"INSERT INTO\", \"DELETE FROM\"].includes(\n                  s.name.toUpperCase(),\n                ),\n            )\n            .map((s) => ({\n              ...s,\n              documentation: {\n                value:\n                  (\n                    [s.name.toLowerCase()].every((nameLc) =>\n                      [\"update\", \"delete\"].some((modif) =>\n                        nameLc.startsWith(modif),\n                      ),\n                    )\n                  ) ?\n                    `${DATA_MODIF_INFO}\\n\\n${getDocStr(s.documentation ?? \"\")}`\n                  : getDocStr(s.documentation),\n              },\n            }));\n          if (allowedCteCommands.length !== 4) {\n            console.error(\"Some allowedCteCommands missing\");\n          }\n          return {\n            suggestions: allowedCteCommands,\n          };\n        }\n        const res = await matchNested(\n          args,\n          [\"MatchSelect\", \"MatchInsert\", \"MatchUpdate\", \"MatchDelete\"],\n          topNestingToken?.nestingId,\n        );\n        if (res) return res;\n      }\n    }\n\n    if (!isWrittingEndCommand && !currNestingId) {\n      if (ltoken?.nestingId.length === 1 && ltoken.text === \")\") {\n        return suggestKWD(getKind, [\"$1 AS (\\n  $2\\n)\", \"SELECT\"]);\n      }\n      if (!ltoken?.nestingId) {\n        if (cb.l1token?.text === \",\") {\n          return suggestKWD(getKind, [\"AS (\\n  $1\\n)\"]);\n        }\n        if (ltoken?.text === \",\") {\n          return suggestKWD(getKind, [\"$1 AS (\\n  $2\\n)\"]);\n        }\n      }\n    }\n\n    /** Is inside AS ( ... ) */\n    const prevTokensReversed = cb.prevTokens.slice(0).reverse();\n    const topFuncNameIdx = prevTokensReversed.findIndex((t, i) => {\n      const prevToken = prevTokensReversed[i - 1];\n      return (\n        prevToken?.text === \"(\" &&\n        prevToken.nestingId.length === 1 &&\n        !t.nestingId\n      );\n    });\n    const topFuncName = prevTokensReversed[topFuncNameIdx];\n    if (topFuncName?.textLC === \"as\") {\n      const topNestedKwd = prevTokensReversed[topFuncNameIdx - 2];\n      const allowedCommands = [\"SELECT\", \"INSERT\", \"UPDATE\", \"DELETE\"];\n      if (!topNestedKwd) {\n        return suggestKWD(getKind, allowedCommands);\n      } else {\n        const firstParens = prevTokensReversed[topFuncNameIdx - 1];\n        const lastParens =\n          cb.tokens.find(\n            (t) =>\n              t.offset > topFuncName.offset && t.text === \")\" && !t.nestingId,\n          ) ?? cb.tokens.at(-1);\n        const nestedCb = await getCurrentCodeBlock(cb.model, cb.position, [\n          firstParens?.end ?? 0,\n          lastParens?.offset ?? cb.offset + 10,\n        ]);\n        const matcher = [\n          MatchSelect,\n          MatchInsert,\n          MatchUpdate,\n          MatchDelete,\n        ].find((m) => m.match(nestedCb));\n\n        const res = await matcher?.result({ ...args, cb: nestedCb });\n        return res ?? { suggestions: [] };\n      }\n    }\n\n    if (!isWrittingEndCommand && !currNestingId) {\n      if (cb.thisLineLC === \")\") {\n        return suggestKWD(getKind, [\", $1 AS (\\n  $2 \\n)\"]);\n      }\n      if (cb.ltoken?.text === \")\") {\n        return suggestKWD(getKind, [\n          \"SELECT\",\n          \"DELETE FROM\",\n          \"UPDATE\",\n          \", $1 AS (\\nSELECT * \\n  FROM $2 \\n)$3\",\n        ]);\n      }\n      if (ltokenNoParens?.text === \",\") {\n        return suggestKWD(getKind, [\" $cte_name AS (\\n$1 \\n)\"]);\n      }\n    }\n\n    return MatchSelect.result(args);\n  },\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/MathDelete.ts",
    "content": "import type { SQLMatcher } from \"./monacoSQLSetup/registerSuggestions\";\nimport { withKWDs } from \"./withKWDs\";\n\nconst KWDS = [\n  { kwd: \"DELETE\", expects: \"keyword\" },\n  { kwd: \"FROM\", expects: \"table\" },\n  { kwd: \"WHERE\", expects: \"column\", justAfter: [\"FROM\"] },\n  { kwd: \"RETURNING\", expects: \"column\", dependsOn: \"FROM\" },\n] as const;\n\nexport const MatchDelete: SQLMatcher = {\n  match: (cb) => cb.prevLC.startsWith(\"delete\"),\n  result: async ({ cb, ss, setS, sql }) => {\n    const { getSuggestion } = withKWDs(KWDS, { cb, ss, setS, sql });\n\n    return getSuggestion();\n  },\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/PSQL.ts",
    "content": "export const COMMANDS = [\n  { cmd: \"\\\\d\", opts: \"[S+]\", desc: \"list tables, views, and sequences\" },\n  {\n    cmd: \"\\\\d\",\n    opts: \"[S+]\",\n    desc: \"describe table, view, sequence, or index\",\n  },\n  { cmd: \"\\\\da\", opts: \"[S]\", desc: \"list aggregates\" },\n  { cmd: \"\\\\dA\", opts: \"[+]\", desc: \"list access methods\" },\n  { cmd: \"\\\\dAc\", opts: \"[+]\", desc: \"list operator classes\" },\n  { cmd: \"\\\\dAf\", opts: \"[+]\", desc: \"list operator families\" },\n  { cmd: \"\\\\dAo\", opts: \"[+]\", desc: \"list operators of operator families\" },\n  {\n    cmd: \"\\\\dAp\",\n    opts: \"[+]\",\n    desc: \"list support functions of operator families\",\n  },\n  { cmd: \"\\\\db\", opts: \"[+]\", desc: \"list tablespaces\" },\n  { cmd: \"\\\\dc\", opts: \"[S+]\", desc: \"list conversions\" },\n  { cmd: \"\\\\dC\", opts: \"[+]\", desc: \"list casts\" },\n  {\n    cmd: \"\\\\dd\",\n    opts: \"[S]\",\n    desc: \"show object descriptions not displayed elsewhere\",\n  },\n  { cmd: \"\\\\dD\", opts: \"[S+]\", desc: \"list domains\" },\n  { cmd: \"\\\\ddp\", desc: \"list default privileges\" },\n  { cmd: \"\\\\dE\", opts: \"[S+]\", desc: \"list foreign tables\" },\n  { cmd: \"\\\\des\", opts: \"[+]\", desc: \"list foreign servers\" },\n  { cmd: \"\\\\det\", opts: \"[+]\", desc: \"list foreign tables\" },\n  { cmd: \"\\\\deu\", opts: \"[+]\", desc: \"list user mappings\" },\n  { cmd: \"\\\\dew\", opts: \"[+]\", desc: \"list foreign-data wrappers\" },\n  {\n    cmd: \"\\\\df[anptw]\",\n    desc: \"list [only agg/normal/procedure/trigger/window]\",\n  },\n  { cmd: \"\\\\dF\", opts: \"[+]\", desc: \"list text search configurations\" },\n  { cmd: \"\\\\dFd\", opts: \"[+]\", desc: \"list text search dictionaries\" },\n  { cmd: \"\\\\dFp\", opts: \"[+]\", desc: \"list text search parsers\" },\n  { cmd: \"\\\\dFt\", opts: \"[+]\", desc: \"list text search templates\" },\n  { cmd: \"\\\\dg\", opts: \"[S+]\", desc: \"list roles\" },\n  { cmd: \"\\\\di\", opts: \"[S+]\", desc: \"list indexes\" },\n  { cmd: \"\\\\dl\", desc: \"list large objects, same as \\\\lo_list\" },\n  { cmd: \"\\\\dL\", opts: \"[S+]\", desc: \"list procedural languages\" },\n  { cmd: \"\\\\dm\", opts: \"[S+]\", desc: \"list materialized views\" },\n  { cmd: \"\\\\dn\", opts: \"[S+]\", desc: \"list schemas\" },\n  { cmd: \"\\\\do\", opts: \"[S+]\", desc: \"list operators\" },\n  { cmd: \"\\\\dO\", opts: \"[S+]\", desc: \"list collations\" },\n  { cmd: \"\\\\dp\", desc: \"list table, view, and sequence access privileges\" },\n  {\n    cmd: \"\\\\dP[itn+]\",\n    desc: \"list [only index/table] partitioned relations [n=nested]\",\n  },\n  { cmd: \"\\\\drds\", desc: \"list per-database role settings\" },\n  { cmd: \"\\\\dRp\", opts: \"[+]\", desc: \"list replication publications\" },\n  { cmd: \"\\\\dRs\", opts: \"[+]\", desc: \"list replication subscriptions\" },\n  { cmd: \"\\\\ds\", opts: \"[S+]\", desc: \"list sequences\" },\n  { cmd: \"\\\\dt\", opts: \"[S+]\", desc: \"list tables\" },\n  { cmd: \"\\\\dT\", opts: \"[S+]\", desc: \"list data types\" },\n  { cmd: \"\\\\du\", opts: \"[S+]\", desc: \"list roles\" },\n  { cmd: \"\\\\dv\", opts: \"[S+]\", desc: \"list views\" },\n  { cmd: \"\\\\dx\", opts: \"[+]\", desc: \"list extensions\" },\n  { cmd: \"\\\\dX\", desc: \"list extended statistics\" },\n  { cmd: \"\\\\dy\", opts: \"[+]\", desc: \"list event triggers\" },\n  { cmd: \"\\\\l\", opts: \"[+]\", desc: \"list databases\" },\n  { cmd: \"\\\\sf\", opts: \"[+]\", desc: \"show a function's definition\" },\n  { cmd: \"\\\\sv\", opts: \"[+]\", desc: \"show a view's definition\" },\n  { cmd: \"\\\\z\", desc: \"same as \\\\dp\" },\n];\n\n/*\n  https://www.postgresql.org/docs/current/multibyte.html\n*/\n\nexport const ENCODINGS = [\n  \"DEFAULT\",\n  \"BIG5\", //\tBig Five\tTraditional Chinese\tNo\tNo\t1–2\tWIN950, Windows950\n  \"EUC_CN\", //\tExtended UNIX Code-CN\tSimplified Chinese\tYes\tYes\t1–3\n  \"EUC_JP\", //\tExtended UNIX Code-JP\tJapanese\tYes\tYes\t1–3\n  \"EUC_JIS_2004\", //\tExtended UNIX Code-JP, JIS X 0213\tJapanese\tYes\tNo\t1–3\n  \"EUC_KR\", //\tExtended UNIX Code-KR\tKorean\tYes\tYes\t1–3\n  \"EUC_TW\", //\tExtended UNIX Code-TW\tTraditional Chinese, Taiwanese\tYes\tYes\t1–3\n  \"GB18030\", //\tNational Standard\tChinese\tNo\tNo\t1–4\n  \"GBK\", //\tExtended National Standard\tSimplified Chinese\tNo\tNo\t1–2\tWIN936, Windows936\n  \"ISO_8859_5\", //\tISO 8859-5, ECMA 113\tLatin/Cyrillic\tYes\tYes\t1\n  \"ISO_8859_6\", //\tISO 8859-6, ECMA 114\tLatin/Arabic\tYes\tYes\t1\n  \"ISO_8859_7\", //\tISO 8859-7, ECMA 118\tLatin/Greek\tYes\tYes\t1\n  \"ISO_8859_8\", //\tISO 8859-8, ECMA 121\tLatin/Hebrew\tYes\tYes\t1\n  \"JOHAB\", //\tJOHAB\tKorean (Hangul)\tNo\tNo\t1–3\n  \"KOI8R\", //\tKOI8-R\tCyrillic (Russian)\tYes\tYes\t1\tKOI8\n  \"KOI8U\", //\tKOI8-U\tCyrillic (Ukrainian)\tYes\tYes\t1\n  \"LATIN1\", //\tISO 8859-1, ECMA 94\tWestern European\tYes\tYes\t1\tISO88591\n  \"LATIN2\", //\tISO 8859-2, ECMA 94\tCentral European\tYes\tYes\t1\tISO88592\n  \"LATIN3\", //\tISO 8859-3, ECMA 94\tSouth European\tYes\tYes\t1\tISO88593\n  \"LATIN4\", //\tISO 8859-4, ECMA 94\tNorth European\tYes\tYes\t1\tISO88594\n  \"LATIN5\", //\tISO 8859-9, ECMA 128\tTurkish\tYes\tYes\t1\tISO88599\n  \"LATIN6\", //\tISO 8859-10, ECMA 144\tNordic\tYes\tYes\t1\tISO885910\n  \"LATIN7\", //\tISO 8859-13\tBaltic\tYes\tYes\t1\tISO885913\n  \"LATIN8\", //\tISO 8859-14\tCeltic\tYes\tYes\t1\tISO885914\n  \"LATIN9\", //\tISO 8859-15\tLATIN1 with Euro and accents\tYes\tYes\t1\tISO885915\n  \"LATIN10\", //\tISO 8859-16, ASRO SR 14111\tRomanian\tYes\tNo\t1\tISO885916\n  \"MULE_INTERNAL\", //\tMule internal code\tMultilingual Emacs\tYes\tNo\t1–4\n  \"SJIS\", //\tShift JIS\tJapanese\tNo\tNo\t1–2\tMskanji, ShiftJIS, WIN932, Windows932\n  \"SHIFT_JIS_2004\", //\tShift JIS, JIS X 0213\tJapanese\tNo\tNo\t1–2\n  \"SQL_ASCII\", //\tunspecified (see text)\tany\tYes\tNo\t1\n  \"UHC\", //\tUnified Hangul Code\tKorean\tNo\tNo\t1–2\tWIN949, Windows949\n  \"UTF8\", //\tUnicode, 8-bit\tall\tYes\tYes\t1–4\tUnicode\n  \"WIN866\", //\tWindows CP866\tCyrillic\tYes\tYes\t1\tALT\n  \"WIN874\", //\tWindows CP874\tThai\tYes\tNo\t1\n  \"WIN1250\", //\tWindows CP1250\tCentral European\tYes\tYes\t1\n  \"WIN1251\", //\tWindows CP1251\tCyrillic\tYes\tYes\t1\tWIN\n  \"WIN1252\", //\tWindows CP1252\tWestern European\tYes\tYes\t1\n  \"WIN1253\", //\tWindows CP1253\tGreek\tYes\tYes\t1\n  \"WIN1254\", //\tWindows CP1254\tTurkish\tYes\tYes\t1\n  \"WIN1255\", //\tWindows CP1255\tHebrew\tYes\tYes\t1\n  \"WIN1256\", //\tWindows CP1256\tArabic\tYes\tYes\t1\n  \"WIN1257\", //\tWindows CP1257\tBaltic\tYes\tYes\t1\n  \"WIN1258\", //\tWindows CP1258\tVietnamese\tYes\tYes\t1\tABC, TCVN, TCVN5712, VSCII\n];\n\n/*\n\nhttps://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW\ndocument.querySelectorAll(\"div.table\").forEach(n => {\n    const title = n.querySelector(\".structname\")\n    const tableName = title?.innerText;\n    if(!tableName) return;\n    const cols = Array.from(n.querySelectorAll(\"tbody tr\")).map(c => ({\n        name: c.querySelector(\".structname\")?.innerText,c,\n        desc: c.querySelector(\"td:nth-child(2)\")?.innerText,\n    }));\n    console.log(title, tableName, cols);\n});\n\n\n*/\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/STARTING_KEYWORDS.ts",
    "content": "/**\n * Starting keywords for SQL autocompletion\n * https://www.postgresql.org/docs/16/sql-commands.html\n \n\n  Array.from(document.querySelector(\"dl.toc\").querySelectorAll(\"dt\")).map(dt => {\n    const kwd = dt.querySelector(\".refentrytitle\").innerText;\n    const docs = dt.querySelector(\".refpurpose\").innerText.replace(\"— \", \"\");\n    const url = dt.querySelector(\".refentrytitle\").querySelector(\"a\").href;\n    return { kwd, docs, url };\n  });\n\n */\n\nexport const STARTING_KEYWORDS = [\n  {\n    kwd: \"ABORT\",\n    docs: \"abort the current transaction\",\n    url: \"https://www.postgresql.org/docs/16/sql-abort.html\",\n  },\n  {\n    kwd: \"ALTER AGGREGATE\",\n    docs: \"change the definition of an aggregate function\",\n    url: \"https://www.postgresql.org/docs/16/sql-alteraggregate.html\",\n  },\n  {\n    kwd: \"ALTER COLLATION\",\n    docs: \"change the definition of a collation\",\n    url: \"https://www.postgresql.org/docs/16/sql-altercollation.html\",\n  },\n  {\n    kwd: \"ALTER CONVERSION\",\n    docs: \"change the definition of a conversion\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterconversion.html\",\n  },\n  {\n    kwd: \"ALTER DATABASE\",\n    docs: \"change a database\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterdatabase.html\",\n  },\n  {\n    kwd: \"ALTER DEFAULT PRIVILEGES\",\n    docs: \"define default access privileges\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterdefaultprivileges.html\",\n  },\n  {\n    kwd: \"ALTER DOMAIN\",\n    docs: \"change the definition of a domain\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterdomain.html\",\n  },\n  {\n    kwd: \"ALTER EVENT TRIGGER\",\n    docs: \"change the definition of an event trigger\",\n    url: \"https://www.postgresql.org/docs/16/sql-altereventtrigger.html\",\n  },\n  {\n    kwd: \"ALTER EXTENSION\",\n    docs: \"change the definition of an extension\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterextension.html\",\n  },\n  {\n    kwd: \"ALTER FOREIGN DATA WRAPPER\",\n    docs: \"change the definition of a foreign-data wrapper\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterforeigndatawrapper.html\",\n  },\n  {\n    kwd: \"ALTER FOREIGN TABLE\",\n    docs: \"change the definition of a foreign table\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterforeigntable.html\",\n  },\n  {\n    kwd: \"ALTER FUNCTION\",\n    docs: \"change the definition of a function\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterfunction.html\",\n  },\n  {\n    kwd: \"ALTER GROUP\",\n    docs: \"change role name or membership\",\n    url: \"https://www.postgresql.org/docs/16/sql-altergroup.html\",\n  },\n  {\n    kwd: \"ALTER INDEX\",\n    docs: \"change the definition of an index\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterindex.html\",\n  },\n  {\n    kwd: \"ALTER LANGUAGE\",\n    docs: \"change the definition of a procedural language\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterlanguage.html\",\n  },\n  {\n    kwd: \"ALTER LARGE OBJECT\",\n    docs: \"change the definition of a large object\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterlargeobject.html\",\n  },\n  {\n    kwd: \"ALTER MATERIALIZED VIEW\",\n    docs: \"change the definition of a materialized view\",\n    url: \"https://www.postgresql.org/docs/16/sql-altermaterializedview.html\",\n  },\n  {\n    kwd: \"ALTER OPERATOR\",\n    docs: \"change the definition of an operator\",\n    url: \"https://www.postgresql.org/docs/16/sql-alteroperator.html\",\n  },\n  {\n    kwd: \"ALTER OPERATOR CLASS\",\n    docs: \"change the definition of an operator class\",\n    url: \"https://www.postgresql.org/docs/16/sql-alteropclass.html\",\n  },\n  {\n    kwd: \"ALTER OPERATOR FAMILY\",\n    docs: \"change the definition of an operator family\",\n    url: \"https://www.postgresql.org/docs/16/sql-alteropfamily.html\",\n  },\n  {\n    kwd: \"ALTER POLICY\",\n    docs: \"change the definition of a row-level security policy\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterpolicy.html\",\n  },\n  {\n    kwd: \"ALTER PROCEDURE\",\n    docs: \"change the definition of a procedure\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterprocedure.html\",\n  },\n  {\n    kwd: \"ALTER PUBLICATION\",\n    docs: \"change the definition of a publication\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterpublication.html\",\n  },\n  {\n    kwd: \"ALTER ROLE\",\n    docs: \"change a database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterrole.html\",\n  },\n  {\n    kwd: \"ALTER ROUTINE\",\n    docs: \"change the definition of a routine\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterroutine.html\",\n  },\n  {\n    kwd: \"ALTER RULE\",\n    docs: \"change the definition of a rule\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterrule.html\",\n  },\n  {\n    kwd: \"ALTER SCHEMA\",\n    docs: \"change the definition of a schema\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterschema.html\",\n  },\n  {\n    kwd: \"ALTER SEQUENCE\",\n    docs: \"change the definition of a sequence generator\",\n    url: \"https://www.postgresql.org/docs/16/sql-altersequence.html\",\n  },\n  {\n    kwd: \"ALTER SERVER\",\n    docs: \"change the definition of a foreign server\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterserver.html\",\n  },\n  {\n    kwd: \"ALTER STATISTICS\",\n    docs: \"change the definition of an extended statistics object\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterstatistics.html\",\n  },\n  {\n    kwd: \"ALTER SUBSCRIPTION\",\n    docs: \"change the definition of a subscription\",\n    url: \"https://www.postgresql.org/docs/16/sql-altersubscription.html\",\n  },\n  {\n    kwd: \"ALTER SYSTEM\",\n    docs: \"change a server configuration parameter\",\n    url: \"https://www.postgresql.org/docs/16/sql-altersystem.html\",\n  },\n  {\n    kwd: \"ALTER TABLE\",\n    docs: \"change the definition of a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-altertable.html\",\n  },\n  {\n    kwd: \"ALTER TABLESPACE\",\n    docs: \"change the definition of a tablespace\",\n    url: \"https://www.postgresql.org/docs/16/sql-altertablespace.html\",\n  },\n  {\n    kwd: \"ALTER TEXT SEARCH CONFIGURATION\",\n    docs: \"change the definition of a text search configuration\",\n    url: \"https://www.postgresql.org/docs/16/sql-altertsconfig.html\",\n  },\n  {\n    kwd: \"ALTER TEXT SEARCH DICTIONARY\",\n    docs: \"change the definition of a text search dictionary\",\n    url: \"https://www.postgresql.org/docs/16/sql-altertsdictionary.html\",\n  },\n  {\n    kwd: \"ALTER TEXT SEARCH PARSER\",\n    docs: \"change the definition of a text search parser\",\n    url: \"https://www.postgresql.org/docs/16/sql-altertsparser.html\",\n  },\n  {\n    kwd: \"ALTER TEXT SEARCH TEMPLATE\",\n    docs: \"change the definition of a text search template\",\n    url: \"https://www.postgresql.org/docs/16/sql-altertstemplate.html\",\n  },\n  {\n    kwd: \"ALTER TRIGGER\",\n    docs: \"change the definition of a trigger\",\n    url: \"https://www.postgresql.org/docs/16/sql-altertrigger.html\",\n  },\n  {\n    kwd: \"ALTER TYPE\",\n    docs: \"change the definition of a type\",\n    url: \"https://www.postgresql.org/docs/16/sql-altertype.html\",\n  },\n  {\n    kwd: \"ALTER USER\",\n    docs: \"change a database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-alteruser.html\",\n  },\n  {\n    kwd: \"ALTER USER MAPPING\",\n    docs: \"change the definition of a user mapping\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterusermapping.html\",\n  },\n  {\n    kwd: \"ALTER VIEW\",\n    docs: \"change the definition of a view\",\n    url: \"https://www.postgresql.org/docs/16/sql-alterview.html\",\n  },\n  {\n    kwd: \"ANALYZE\",\n    docs: \"collect statistics about a database\",\n    url: \"https://www.postgresql.org/docs/16/sql-analyze.html\",\n  },\n  {\n    kwd: \"BEGIN\",\n    docs: \"start a transaction block\",\n    url: \"https://www.postgresql.org/docs/16/sql-begin.html\",\n  },\n  {\n    kwd: \"CALL\",\n    docs: \"invoke a procedure\",\n    url: \"https://www.postgresql.org/docs/16/sql-call.html\",\n  },\n  {\n    kwd: \"CHECKPOINT\",\n    docs: \"force a write-ahead log checkpoint\",\n    url: \"https://www.postgresql.org/docs/16/sql-checkpoint.html\",\n  },\n  {\n    kwd: \"CLOSE\",\n    docs: \"close a cursor\",\n    url: \"https://www.postgresql.org/docs/16/sql-close.html\",\n  },\n  {\n    kwd: \"CLUSTER\",\n    docs: \"cluster a table according to an index\",\n    url: \"https://www.postgresql.org/docs/16/sql-cluster.html\",\n  },\n  {\n    kwd: \"COMMENT\",\n    docs: \"define or change the comment of an object\",\n    url: \"https://www.postgresql.org/docs/16/sql-comment.html\",\n  },\n  {\n    kwd: \"COMMIT\",\n    docs: \"commit the current transaction\",\n    url: \"https://www.postgresql.org/docs/16/sql-commit.html\",\n  },\n  {\n    kwd: \"COMMIT PREPARED\",\n    docs: \"commit a transaction that was earlier prepared for two-phase commit\",\n    url: \"https://www.postgresql.org/docs/16/sql-commit-prepared.html\",\n  },\n  {\n    kwd: \"COPY\",\n    docs: \"copy data between a file and a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-copy.html\",\n  },\n  {\n    kwd: \"CREATE ACCESS METHOD\",\n    docs: \"define a new access method\",\n    url: \"https://www.postgresql.org/docs/16/sql-create-access-method.html\",\n  },\n  {\n    kwd: \"CREATE AGGREGATE\",\n    docs: \"define a new aggregate function\",\n    url: \"https://www.postgresql.org/docs/16/sql-createaggregate.html\",\n  },\n  {\n    kwd: \"CREATE CAST\",\n    docs: \"define a new cast\",\n    url: \"https://www.postgresql.org/docs/16/sql-createcast.html\",\n  },\n  {\n    kwd: \"CREATE COLLATION\",\n    docs: \"define a new collation\",\n    url: \"https://www.postgresql.org/docs/16/sql-createcollation.html\",\n  },\n  {\n    kwd: \"CREATE CONVERSION\",\n    docs: \"define a new encoding conversion\",\n    url: \"https://www.postgresql.org/docs/16/sql-createconversion.html\",\n  },\n  {\n    kwd: \"CREATE DATABASE\",\n    docs: \"create a new database\",\n    url: \"https://www.postgresql.org/docs/16/sql-createdatabase.html\",\n  },\n  {\n    kwd: \"CREATE DOMAIN\",\n    docs: \"define a new domain\",\n    url: \"https://www.postgresql.org/docs/16/sql-createdomain.html\",\n  },\n  {\n    kwd: \"CREATE EVENT TRIGGER\",\n    docs: \"define a new event trigger\",\n    url: \"https://www.postgresql.org/docs/16/sql-createeventtrigger.html\",\n  },\n  {\n    kwd: \"CREATE EXTENSION\",\n    docs: \"install an extension\",\n    url: \"https://www.postgresql.org/docs/16/sql-createextension.html\",\n  },\n  {\n    kwd: \"CREATE FOREIGN DATA WRAPPER\",\n    docs: \"define a new foreign-data wrapper\",\n    url: \"https://www.postgresql.org/docs/16/sql-createforeigndatawrapper.html\",\n  },\n  {\n    kwd: \"CREATE FOREIGN TABLE\",\n    docs: \"define a new foreign table\",\n    url: \"https://www.postgresql.org/docs/16/sql-createforeigntable.html\",\n  },\n  {\n    kwd: \"CREATE FUNCTION\",\n    docs: \"define a new function\",\n    url: \"https://www.postgresql.org/docs/16/sql-createfunction.html\",\n  },\n  {\n    kwd: \"CREATE GROUP\",\n    docs: \"define a new database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-creategroup.html\",\n  },\n  {\n    kwd: \"CREATE INDEX\",\n    docs: \"define a new index\",\n    url: \"https://www.postgresql.org/docs/16/sql-createindex.html\",\n  },\n  {\n    kwd: \"CREATE LANGUAGE\",\n    docs: \"define a new procedural language\",\n    url: \"https://www.postgresql.org/docs/16/sql-createlanguage.html\",\n  },\n  {\n    kwd: \"CREATE MATERIALIZED VIEW\",\n    docs: \"define a new materialized view\",\n    url: \"https://www.postgresql.org/docs/16/sql-creatematerializedview.html\",\n  },\n  {\n    kwd: \"CREATE OPERATOR\",\n    docs: \"define a new operator\",\n    url: \"https://www.postgresql.org/docs/16/sql-createoperator.html\",\n  },\n  {\n    kwd: \"CREATE OPERATOR CLASS\",\n    docs: \"define a new operator class\",\n    url: \"https://www.postgresql.org/docs/16/sql-createopclass.html\",\n  },\n  {\n    kwd: \"CREATE OPERATOR FAMILY\",\n    docs: \"define a new operator family\",\n    url: \"https://www.postgresql.org/docs/16/sql-createopfamily.html\",\n  },\n  {\n    kwd: \"CREATE POLICY\",\n    docs: \"define a new row-level security policy for a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-createpolicy.html\",\n  },\n  {\n    kwd: \"CREATE PROCEDURE\",\n    docs: \"define a new procedure\",\n    url: \"https://www.postgresql.org/docs/16/sql-createprocedure.html\",\n  },\n  {\n    kwd: \"CREATE PUBLICATION\",\n    docs: \"define a new publication\",\n    url: \"https://www.postgresql.org/docs/16/sql-createpublication.html\",\n  },\n  {\n    kwd: \"CREATE ROLE\",\n    docs: \"define a new database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-createrole.html\",\n  },\n  {\n    kwd: \"CREATE RULE\",\n    docs: \"define a new rewrite rule\",\n    url: \"https://www.postgresql.org/docs/16/sql-createrule.html\",\n  },\n  {\n    kwd: \"CREATE SCHEMA\",\n    docs: \"define a new schema\",\n    url: \"https://www.postgresql.org/docs/16/sql-createschema.html\",\n  },\n  {\n    kwd: \"CREATE SEQUENCE\",\n    docs: \"define a new sequence generator\",\n    url: \"https://www.postgresql.org/docs/16/sql-createsequence.html\",\n  },\n  {\n    kwd: \"CREATE SERVER\",\n    docs: \"define a new foreign server\",\n    url: \"https://www.postgresql.org/docs/16/sql-createserver.html\",\n  },\n  {\n    kwd: \"CREATE STATISTICS\",\n    docs: \"define extended statistics\",\n    url: \"https://www.postgresql.org/docs/16/sql-createstatistics.html\",\n  },\n  {\n    kwd: \"CREATE SUBSCRIPTION\",\n    docs: \"define a new subscription\",\n    url: \"https://www.postgresql.org/docs/16/sql-createsubscription.html\",\n  },\n  {\n    kwd: \"CREATE TABLE\",\n    docs: \"define a new table\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtable.html\",\n  },\n  {\n    kwd: \"CREATE TABLE AS\",\n    docs: \"define a new table from the results of a query\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtableas.html\",\n  },\n  {\n    kwd: \"CREATE TABLESPACE\",\n    docs: \"define a new tablespace\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtablespace.html\",\n  },\n  {\n    kwd: \"CREATE TEXT SEARCH CONFIGURATION\",\n    docs: \"define a new text search configuration\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtsconfig.html\",\n  },\n  {\n    kwd: \"CREATE TEXT SEARCH DICTIONARY\",\n    docs: \"define a new text search dictionary\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtsdictionary.html\",\n  },\n  {\n    kwd: \"CREATE TEXT SEARCH PARSER\",\n    docs: \"define a new text search parser\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtsparser.html\",\n  },\n  {\n    kwd: \"CREATE TEXT SEARCH TEMPLATE\",\n    docs: \"define a new text search template\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtstemplate.html\",\n  },\n  {\n    kwd: \"CREATE TRANSFORM\",\n    docs: \"define a new transform\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtransform.html\",\n  },\n  {\n    kwd: \"CREATE TRIGGER\",\n    docs: \"define a new trigger\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtrigger.html\",\n  },\n  {\n    kwd: \"CREATE TYPE\",\n    docs: \"define a new data type\",\n    url: \"https://www.postgresql.org/docs/16/sql-createtype.html\",\n  },\n  {\n    kwd: \"CREATE USER\",\n    docs: \"define a new database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-createuser.html\",\n  },\n  {\n    kwd: \"CREATE USER MAPPING\",\n    docs: \"define a new mapping of a user to a foreign server\",\n    url: \"https://www.postgresql.org/docs/16/sql-createusermapping.html\",\n  },\n  {\n    kwd: \"CREATE VIEW\",\n    docs: \"define a new view\",\n    url: \"https://www.postgresql.org/docs/16/sql-createview.html\",\n  },\n  {\n    kwd: \"DEALLOCATE\",\n    docs: \"deallocate a prepared statement\",\n    url: \"https://www.postgresql.org/docs/16/sql-deallocate.html\",\n  },\n  {\n    kwd: \"DECLARE\",\n    docs: \"define a cursor\",\n    url: \"https://www.postgresql.org/docs/16/sql-declare.html\",\n  },\n  {\n    kwd: \"DELETE\",\n    docs: \"delete rows of a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-delete.html\",\n  },\n  {\n    kwd: \"DISCARD\",\n    docs: \"discard session state\",\n    url: \"https://www.postgresql.org/docs/16/sql-discard.html\",\n  },\n  {\n    kwd: \"DO\",\n    docs: \"execute an anonymous code block\",\n    url: \"https://www.postgresql.org/docs/16/sql-do.html\",\n  },\n  {\n    kwd: \"DROP ACCESS METHOD\",\n    docs: \"remove an access method\",\n    url: \"https://www.postgresql.org/docs/16/sql-drop-access-method.html\",\n  },\n  {\n    kwd: \"DROP AGGREGATE\",\n    docs: \"remove an aggregate function\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropaggregate.html\",\n  },\n  {\n    kwd: \"DROP CAST\",\n    docs: \"remove a cast\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropcast.html\",\n  },\n  {\n    kwd: \"DROP COLLATION\",\n    docs: \"remove a collation\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropcollation.html\",\n  },\n  {\n    kwd: \"DROP CONVERSION\",\n    docs: \"remove a conversion\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropconversion.html\",\n  },\n  {\n    kwd: \"DROP DATABASE\",\n    docs: \"remove a database\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropdatabase.html\",\n  },\n  {\n    kwd: \"DROP DOMAIN\",\n    docs: \"remove a domain\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropdomain.html\",\n  },\n  {\n    kwd: \"DROP EVENT TRIGGER\",\n    docs: \"remove an event trigger\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropeventtrigger.html\",\n  },\n  {\n    kwd: \"DROP EXTENSION\",\n    docs: \"remove an extension\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropextension.html\",\n  },\n  {\n    kwd: \"DROP FOREIGN DATA WRAPPER\",\n    docs: \"remove a foreign-data wrapper\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropforeigndatawrapper.html\",\n  },\n  {\n    kwd: \"DROP FOREIGN TABLE\",\n    docs: \"remove a foreign table\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropforeigntable.html\",\n  },\n  {\n    kwd: \"DROP FUNCTION\",\n    docs: \"remove a function\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropfunction.html\",\n  },\n  {\n    kwd: \"DROP GROUP\",\n    docs: \"remove a database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropgroup.html\",\n  },\n  {\n    kwd: \"DROP INDEX\",\n    docs: \"remove an index\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropindex.html\",\n  },\n  {\n    kwd: \"DROP LANGUAGE\",\n    docs: \"remove a procedural language\",\n    url: \"https://www.postgresql.org/docs/16/sql-droplanguage.html\",\n  },\n  {\n    kwd: \"DROP MATERIALIZED VIEW\",\n    docs: \"remove a materialized view\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropmaterializedview.html\",\n  },\n  {\n    kwd: \"DROP OPERATOR\",\n    docs: \"remove an operator\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropoperator.html\",\n  },\n  {\n    kwd: \"DROP OPERATOR CLASS\",\n    docs: \"remove an operator class\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropopclass.html\",\n  },\n  {\n    kwd: \"DROP OPERATOR FAMILY\",\n    docs: \"remove an operator family\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropopfamily.html\",\n  },\n  {\n    kwd: \"DROP OWNED\",\n    docs: \"remove database objects owned by a database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-drop-owned.html\",\n  },\n  {\n    kwd: \"DROP POLICY\",\n    docs: \"remove a row-level security policy from a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-droppolicy.html\",\n  },\n  {\n    kwd: \"DROP PROCEDURE\",\n    docs: \"remove a procedure\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropprocedure.html\",\n  },\n  {\n    kwd: \"DROP PUBLICATION\",\n    docs: \"remove a publication\",\n    url: \"https://www.postgresql.org/docs/16/sql-droppublication.html\",\n  },\n  {\n    kwd: \"DROP ROLE\",\n    docs: \"remove a database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-droprole.html\",\n  },\n  {\n    kwd: \"DROP ROUTINE\",\n    docs: \"remove a routine\",\n    url: \"https://www.postgresql.org/docs/16/sql-droproutine.html\",\n  },\n  {\n    kwd: \"DROP RULE\",\n    docs: \"remove a rewrite rule\",\n    url: \"https://www.postgresql.org/docs/16/sql-droprule.html\",\n  },\n  {\n    kwd: \"DROP SCHEMA\",\n    docs: \"remove a schema\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropschema.html\",\n  },\n  {\n    kwd: \"DROP SEQUENCE\",\n    docs: \"remove a sequence\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropsequence.html\",\n  },\n  {\n    kwd: \"DROP SERVER\",\n    docs: \"remove a foreign server descriptor\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropserver.html\",\n  },\n  {\n    kwd: \"DROP STATISTICS\",\n    docs: \"remove extended statistics\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropstatistics.html\",\n  },\n  {\n    kwd: \"DROP SUBSCRIPTION\",\n    docs: \"remove a subscription\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropsubscription.html\",\n  },\n  {\n    kwd: \"DROP TABLE\",\n    docs: \"remove a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptable.html\",\n  },\n  {\n    kwd: \"DROP TABLESPACE\",\n    docs: \"remove a tablespace\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptablespace.html\",\n  },\n  {\n    kwd: \"DROP TEXT SEARCH CONFIGURATION\",\n    docs: \"remove a text search configuration\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptsconfig.html\",\n  },\n  {\n    kwd: \"DROP TEXT SEARCH DICTIONARY\",\n    docs: \"remove a text search dictionary\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptsdictionary.html\",\n  },\n  {\n    kwd: \"DROP TEXT SEARCH PARSER\",\n    docs: \"remove a text search parser\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptsparser.html\",\n  },\n  {\n    kwd: \"DROP TEXT SEARCH TEMPLATE\",\n    docs: \"remove a text search template\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptstemplate.html\",\n  },\n  {\n    kwd: \"DROP TRANSFORM\",\n    docs: \"remove a transform\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptransform.html\",\n  },\n  {\n    kwd: \"DROP TRIGGER\",\n    docs: \"remove a trigger\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptrigger.html\",\n  },\n  {\n    kwd: \"DROP TYPE\",\n    docs: \"remove a data type\",\n    url: \"https://www.postgresql.org/docs/16/sql-droptype.html\",\n  },\n  {\n    kwd: \"DROP USER\",\n    docs: \"remove a database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropuser.html\",\n  },\n  {\n    kwd: \"DROP USER MAPPING\",\n    docs: \"remove a user mapping for a foreign server\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropusermapping.html\",\n  },\n  {\n    kwd: \"DROP VIEW\",\n    docs: \"remove a view\",\n    url: \"https://www.postgresql.org/docs/16/sql-dropview.html\",\n  },\n  {\n    kwd: \"END\",\n    docs: \"commit the current transaction\",\n    url: \"https://www.postgresql.org/docs/16/sql-end.html\",\n  },\n  {\n    kwd: \"EXECUTE\",\n    docs: \"execute a prepared statement\",\n    url: \"https://www.postgresql.org/docs/16/sql-execute.html\",\n  },\n  {\n    kwd: \"EXPLAIN\",\n    docs: \"show the execution plan of a statement\",\n    url: \"https://www.postgresql.org/docs/16/sql-explain.html\",\n  },\n  {\n    kwd: \"FETCH\",\n    docs: \"retrieve rows from a query using a cursor\",\n    url: \"https://www.postgresql.org/docs/16/sql-fetch.html\",\n  },\n  {\n    kwd: \"GRANT\",\n    docs: \"define access privileges\",\n    url: \"https://www.postgresql.org/docs/16/sql-grant.html\",\n  },\n  {\n    kwd: \"IMPORT FOREIGN SCHEMA\",\n    docs: \"import table definitions from a foreign server\",\n    url: \"https://www.postgresql.org/docs/16/sql-importforeignschema.html\",\n  },\n  {\n    kwd: \"INSERT\",\n    docs: \"create new rows in a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-insert.html\",\n  },\n  {\n    kwd: \"LISTEN\",\n    docs: \"listen for a notification\",\n    url: \"https://www.postgresql.org/docs/16/sql-listen.html\",\n  },\n  {\n    kwd: \"LOAD\",\n    docs: \"load a shared library file\",\n    url: \"https://www.postgresql.org/docs/16/sql-load.html\",\n  },\n  {\n    kwd: \"LOCK\",\n    docs: \"lock a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-lock.html\",\n  },\n  {\n    kwd: \"MERGE\",\n    docs: \"conditionally insert, update, or delete rows of a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-merge.html\",\n  },\n  {\n    kwd: \"MOVE\",\n    docs: \"position a cursor\",\n    url: \"https://www.postgresql.org/docs/16/sql-move.html\",\n  },\n  {\n    kwd: \"NOTIFY\",\n    docs: \"generate a notification\",\n    url: \"https://www.postgresql.org/docs/16/sql-notify.html\",\n  },\n  {\n    kwd: \"PREPARE\",\n    docs: \"prepare a statement for execution\",\n    url: \"https://www.postgresql.org/docs/16/sql-prepare.html\",\n  },\n  {\n    kwd: \"PREPARE TRANSACTION\",\n    docs: \"prepare the current transaction for two-phase commit\",\n    url: \"https://www.postgresql.org/docs/16/sql-prepare-transaction.html\",\n  },\n  {\n    kwd: \"REASSIGN OWNED\",\n    docs: \"change the ownership of database objects owned by a database role\",\n    url: \"https://www.postgresql.org/docs/16/sql-reassign-owned.html\",\n  },\n  {\n    kwd: \"REFRESH MATERIALIZED VIEW\",\n    docs: \"replace the contents of a materialized view\",\n    url: \"https://www.postgresql.org/docs/16/sql-refreshmaterializedview.html\",\n  },\n  {\n    kwd: \"REINDEX\",\n    docs: \"rebuild indexes\",\n    url: \"https://www.postgresql.org/docs/16/sql-reindex.html\",\n  },\n  {\n    kwd: \"RELEASE SAVEPOINT\",\n    docs: \"release a previously defined savepoint\",\n    url: \"https://www.postgresql.org/docs/16/sql-release-savepoint.html\",\n  },\n  {\n    kwd: \"RESET\",\n    docs: \"restore the value of a run-time parameter to the default value\",\n    url: \"https://www.postgresql.org/docs/16/sql-reset.html\",\n  },\n  {\n    kwd: \"REVOKE\",\n    docs: \"remove access privileges\",\n    url: \"https://www.postgresql.org/docs/16/sql-revoke.html\",\n  },\n  {\n    kwd: \"ROLLBACK\",\n    docs: \"abort the current transaction\",\n    url: \"https://www.postgresql.org/docs/16/sql-rollback.html\",\n  },\n  {\n    kwd: \"ROLLBACK PREPARED\",\n    docs: \"cancel a transaction that was earlier prepared for two-phase commit\",\n    url: \"https://www.postgresql.org/docs/16/sql-rollback-prepared.html\",\n  },\n  {\n    kwd: \"ROLLBACK TO SAVEPOINT\",\n    docs: \"roll back to a savepoint\",\n    url: \"https://www.postgresql.org/docs/16/sql-rollback-to.html\",\n  },\n  {\n    kwd: \"SAVEPOINT\",\n    docs: \"define a new savepoint within the current transaction\",\n    url: \"https://www.postgresql.org/docs/16/sql-savepoint.html\",\n  },\n  {\n    kwd: \"SECURITY LABEL\",\n    docs: \"define or change a security label applied to an object\",\n    url: \"https://www.postgresql.org/docs/16/sql-security-label.html\",\n  },\n  {\n    kwd: \"SELECT\",\n    docs: \"retrieve rows from a table or view\",\n    url: \"https://www.postgresql.org/docs/16/sql-select.html\",\n  },\n  {\n    kwd: \"SELECT INTO\",\n    docs: \"define a new table from the results of a query\",\n    url: \"https://www.postgresql.org/docs/16/sql-selectinto.html\",\n  },\n  {\n    kwd: \"SET\",\n    docs: \"change a run-time parameter\",\n    url: \"https://www.postgresql.org/docs/16/sql-set.html\",\n  },\n  {\n    kwd: \"SET CONSTRAINTS\",\n    docs: \"set constraint check timing for the current transaction\",\n    url: \"https://www.postgresql.org/docs/16/sql-set-constraints.html\",\n  },\n  {\n    kwd: \"SET ROLE\",\n    docs: \"set the current user identifier of the current session\",\n    url: \"https://www.postgresql.org/docs/16/sql-set-role.html\",\n  },\n  {\n    kwd: \"SET SESSION AUTHORIZATION\",\n    docs: \"set the session user identifier and the current user identifier of the current session\",\n    url: \"https://www.postgresql.org/docs/16/sql-set-session-authorization.html\",\n  },\n  {\n    kwd: \"SET TRANSACTION\",\n    docs: \"set the characteristics of the current transaction\",\n    url: \"https://www.postgresql.org/docs/16/sql-set-transaction.html\",\n  },\n  {\n    kwd: \"SHOW\",\n    docs: \"show the value of a run-time parameter\",\n    url: \"https://www.postgresql.org/docs/16/sql-show.html\",\n  },\n  {\n    kwd: \"START TRANSACTION\",\n    docs: \"start a transaction block\",\n    url: \"https://www.postgresql.org/docs/16/sql-start-transaction.html\",\n  },\n  {\n    kwd: \"TRUNCATE\",\n    docs: \"empty a table or set of tables\",\n    url: \"https://www.postgresql.org/docs/16/sql-truncate.html\",\n  },\n  {\n    kwd: \"UNLISTEN\",\n    docs: \"stop listening for a notification\",\n    url: \"https://www.postgresql.org/docs/16/sql-unlisten.html\",\n  },\n  {\n    kwd: \"UPDATE\",\n    docs: \"update rows of a table\",\n    url: \"https://www.postgresql.org/docs/16/sql-update.html\",\n  },\n  {\n    kwd: \"VACUUM\",\n    docs: \"garbage-collect and optionally analyze a database\",\n    url: \"https://www.postgresql.org/docs/16/sql-vacuum.html\",\n  },\n  {\n    kwd: \"VALUES\",\n    docs: \"compute a set of rows\",\n    url: \"https://www.postgresql.org/docs/16/sql-values.html\",\n  },\n] as const;\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/TableKWDs.ts",
    "content": "import { isDefined, pickKeys } from \"prostgles-types\";\nimport { asSQL } from \"./KEYWORDS\";\nimport type { ParsedSQLSuggestion } from \"./monacoSQLSetup/registerSuggestions\";\nimport type { KWD } from \"./withKWDs\";\n\nexport const PG_COLUMN_CONSTRAINTS = [\n  {\n    kwd: \"GENERATED\",\n    options: [\n      {\n        label: \"BY DEFAULT AS IDENTITY\",\n        docs: `Instructs PostgreSQL to generate a value for the identity column. However, if you supply a value for insert or update, PostgreSQL will use that value to insert into the identity column instead of using the system-generated value.`,\n      },\n      {\n        label: \"ALWAYS AS IDENTITY\",\n        docs: `Instructs PostgreSQL to always generate a value for the identity column. If you attempt to insert (or update) values into the GENERATED ALWAYS AS IDENTITY column, PostgreSQL will issue an error.`,\n      },\n      {\n        label: \"ALWAYS AS ($0 ) STORED\",\n        docs:\n          `A stored generated column is computed when it is written (inserted or updated) and occupies storage as if it were a normal column\\n\\n` +\n          asSQL(\n            [\n              `CREATE TABLE people (`,\n              `   ...,`,\n              `   height_cm numeric,`,\n              `   height_in numeric GENERATED ALWAYS AS (height_cm / 2.54) STORED`,\n              `);`,\n            ].join(\"\\n\"),\n          ),\n      },\n    ],\n    docs: `A generated column is a special column that is always computed from other columns. Thus, it is for columns what a view is for tables. There are two kinds of generated columns: stored and virtual. A stored generated column is computed when it is written (inserted or updated) and occupies storage as if it were a normal column. A virtual generated column occupies no storage and is computed when it is read. Thus, a virtual generated column is similar to a view and a stored generated column is similar to a materialized view (except that it is always updated automatically). PostgreSQL currently implements only stored generated columns.`,\n  },\n  {\n    kwd: \"NOT NULL\",\n    docs: \"Constraint ensuring this column cannot be NULL. By default columns can have NULL values if no constraints are specified.\",\n  },\n  {\n    kwd: \"REFERENCES\",\n    expects: \"table\",\n    docs:\n      \"A foreign key constraint specifies that the values in a column (or a group of columns) must match the values appearing in some row of another table. We say this maintains the referential integrity between two related tables. The referenced column/s must be unique.\\nhttps://www.postgresql.org/docs/current/tutorial-fk.html\\n\\n\" +\n      asSQL(\"product_id INTEGER REFERENCES products (id) \"),\n  },\n  {\n    kwd: \"PRIMARY KEY\",\n    docs: \"A primary key constraint indicates that a column, or group of columns, can be used as a unique identifier for rows in the table. This requires that the values be both unique and not null.\",\n  },\n  {\n    kwd: \"DEFAULT\",\n    expects: \"function\",\n    docs:\n      \"A column can be assigned a default value. When a new row is created and no values are specified for some of the columns, those columns will be filled with their respective default values. If no default value is declared explicitly, the default value is the null value. \\n\" +\n      asSQL(\"/* For example */\\ncreated_at TIMESTAMP DEFAULT now()\"),\n  },\n  {\n    kwd: \"CHECK\",\n    expects: \"(condition)\",\n    docs:\n      \"Specify that the value in a certain column must satisfy a Boolean (truth-value) expression. For instance, to require positive product prices, you could use: \\n\" +\n      asSQL(`CHECK (price > 0)`),\n  },\n  {\n    kwd: \"UNIQUE\",\n    expects: \"(column)\",\n    docs: \"The UNIQUE constraint specifies that a group of one or more columns of a table can contain only unique values. For the purpose of a unique constraint, null values are not considered equal, unless NULLS NOT DISTINCT is specified.Adding a unique constraint will automatically create a unique btree index on the column or group of columns used in the constraint.\",\n  },\n] as const satisfies readonly KWD[];\n\n/** Used in ADD CONSTRAINT. These exclude constraints that target single columns only. */\nexport const PG_TABLE_CONSTRAINTS = [\n  {\n    kwd: \"FOREIGN KEY\",\n    expects: \"(column)\",\n    docs: PG_COLUMN_CONSTRAINTS.find((c) => c.kwd === \"REFERENCES\")!.docs,\n  },\n  {\n    kwd: \"PRIMARY KEY\",\n    expects: \"(column)\",\n    docs: PG_COLUMN_CONSTRAINTS.find((c) => c.kwd === \"PRIMARY KEY\")!.docs,\n  },\n  ...PG_COLUMN_CONSTRAINTS.filter(\n    (c) =>\n      !([\"REFERENCES\", \"DEFAULT\", \"NOT NULL\", \"PRIMARY KEY\"] as const).some(\n        (v) => v === c.kwd,\n      ),\n  ),\n] as const satisfies readonly KWD[];\n\nexport const REFERENCES_COL_OPTS = [\n  {\n    kwd: \"ON DELETE CASCADE\",\n    docs: \"When a referenced row is deleted, row(s) referencing it should be automatically deleted as well\",\n  },\n  {\n    kwd: \"ON DELETE SET NULL\",\n    docs: \"When a referenced row is deleted referencing column(s) in the referencing row(s) to be set to NULL\",\n  },\n  {\n    kwd: \"ON DELETE SET DEFAULT\",\n    docs: \"When a referenced row is deleted referencing column(s) in the referencing row(s) to be set to their DEFAULT values\",\n  },\n  { kwd: \"ON DELETE RESTRICT\", docs: \"Prevents deletion of a referenced row\" },\n  {\n    kwd: \"ON DELETE NO ACTION\",\n    docs: \"If any referencing rows still exist when the constraint is checked, an error is raised; this is the default behavior if you do not specify anything\",\n  },\n  {\n    kwd: \"ON UPDATE CASCADE\",\n    docs: \"When a referenced row is updated referencing column(s) in the referencing row(s) should be automatically updated as well\",\n  },\n  {\n    kwd: \"ON UPDATE SET NULL\",\n    docs: \"When a referenced row is updated referencing column(s) in the referencing row(s) should be automatically set to NULL\",\n  },\n  {\n    kwd: \"ON UPDATE SET DEFAULT\",\n    docs: \"When a referenced row is updated referencing column(s) in the referencing row(s) should be automatically set to their DEFAULT values\",\n  },\n  { kwd: \"ON UPDATE RESTRICT\", docs: \"Prevents updating a referenced row\" },\n  {\n    kwd: \"ON UPDATE NO ACTION\",\n    docs: \"If any referencing rows still exist when the constraint is checked, an error is raised; this is the default behavior if you do not specify anything\",\n  },\n] as const;\n\nconst REF_OPTS = [\n  {\n    label: \"CASCADE\",\n    getInfo: (a?: \"DELETE\" | \"UPDATE\") => {\n      const d = \"DELETE any rows referencing the deleted row\";\n      const u =\n        \"UPDATE the values of the referencing column(s) to the new values of the referenced columns\";\n      if (!a) return [d, u].join(\" OR \");\n      if (a === \"DELETE\") {\n        return d;\n      }\n      return u;\n    },\n  },\n  {\n    label: \"SET NULL\",\n    getInfo: (a?: \"DELETE\" | \"UPDATE\") =>\n      \"Set the referencing column(s) to null\",\n  },\n  {\n    label: \"RESTRICT\",\n    getInfo: (a?: \"DELETE\" | \"UPDATE\") =>\n      \"Produce an error indicating that the deletion or update would create a foreign key constraint violation. This is the same as NO ACTION except that the check is not deferrable.\",\n  },\n  {\n    label: \"SET DEFAULT\",\n    getInfo: (a?: \"DELETE\" | \"UPDATE\") =>\n      \"Set the referencing column(s) to their default values. (There must be a row in the referenced table matching the default values, if they are not null, or the operation will fail.)\",\n  },\n] as const;\n\nexport const REFERENCE_CONSTRAINT_OPTIONS_KWDS = [\n  {\n    kwd: \"ON DELETE\",\n    docs: `When the data in the referenced columns is deleted: `,\n    options: REF_OPTS.map((d) => ({\n      label: d.label,\n      docs: d.getInfo(\"DELETE\"),\n    })),\n    dependsOn: \"REFERENCES\",\n  },\n  {\n    kwd: \"ON UPDATE\",\n    docs: `When the data in the referenced columns is updated: `,\n    options: REF_OPTS.map((d) => ({\n      label: d.label,\n      docs: d.getInfo(\"UPDATE\"),\n    })),\n    dependsOn: \"REFERENCES\",\n  },\n] as const satisfies readonly KWD[];\n\nexport const TABLE_CONS_TYPES = PG_TABLE_CONSTRAINTS.map((c) => c.kwd);\n\nexport const ALTER_COL_ACTIONS = [\n  {\n    kwd: \"SET DATA TYPE $data_type\",\n    expects: \"dataType\",\n    docs: `This form changes the type of a column of a table. \n  \nIndexes and simple table constraints involving the column will be automatically converted to use the new column type by reparsing the originally supplied expression. The optional COLLATE clause specifies a collation for the new column; if omitted, the collation is the default for the new column type. The optional USING clause specifies how to compute the new column value from the old; if omitted, the default conversion is the same as an assignment cast from old data type to new. A USING clause must be provided if there is no implicit or assignment cast from old to new type.\n\nWhen this form is used, the column's statistics are removed, so running ANALYZE on the table afterwards is recommended.`,\n  },\n  {\n    kwd: \"SET DEFAULT\",\n    options: (ss, cb) => {\n      const col = ss.find(\n        (s) =>\n          cb.prevIdentifiers.some((pi) => pi.text === s.escapedIdentifier) &&\n          cb.prevIdentifiers.some((pi) => pi.text === s.escapedParentName),\n      );\n      const prioritisedFuncNames = [\n        \"now\",\n        \"current_timestamp\",\n        `\"current_user\"`,\n        \"current_setting\",\n        \"gen_random_uuid\",\n        \"uuid_generate_v1\",\n        \"uuid_generate_v4\",\n      ];\n      const funcs = ss\n        .filter((s) => {\n          return (\n            !col ||\n            (s.funcInfo?.restype_udt_name?.startsWith(\n              col.colInfo?.udt_name ?? \"invalid\",\n            ) &&\n              !s.funcInfo.args.length)\n          );\n        })\n        .map((s) => ({\n          ...s,\n          sortText:\n            prioritisedFuncNames.includes(s.name) ? \"!\" : (\n              (s.sortText ?? s.name)\n            ),\n        }));\n      return [...funcs];\n    },\n    docs: PG_COLUMN_CONSTRAINTS.find((d) => d.kwd === \"DEFAULT\")?.docs,\n  },\n  {\n    kwd: \"DROP DEFAULT\",\n    options: [{ label: \";\" }],\n    docs: PG_COLUMN_CONSTRAINTS.find((d) => d.kwd === \"DEFAULT\")?.docs,\n  },\n  {\n    kwd: \"SET NOT NULL\",\n    docs: PG_COLUMN_CONSTRAINTS.find((d) => d.kwd === \"NOT NULL\")?.docs,\n  },\n  {\n    kwd: \"DROP NOT NULL\",\n    docs: PG_COLUMN_CONSTRAINTS.find((d) => d.kwd === \"NOT NULL\")?.docs,\n  },\n  {\n    kwd: \"DROP EXPRESSION $1\",\n    docs: `This form turns a stored generated column into a normal base column. Existing data in the columns is retained, but future changes will no longer apply the generation expression.\n\nIf DROP EXPRESSION IF EXISTS is specified and the column is not a stored generated column, no error is thrown. In this case a notice is issued instead.\n`,\n  },\n\n  ...[\n    \"ADD GENERATED ${1|ALWAYS,BY DEFAULT|} AS IDENTITY ( sequence_options )\",\n    \"DROP IDENTITY [ IF EXISTS ]\",\n  ].map((kwd) => ({\n    kwd,\n    docs: `These forms change whether a column is an identity column or change the generation attribute of an existing identity column. See CREATE TABLE for details. Like SET DEFAULT, these forms only affect the behavior of subsequent INSERT and UPDATE commands; they do not cause rows already in the table to change.\n\nIf DROP IDENTITY IF EXISTS is specified and the column is not an identity column, no error is thrown. In this case a notice is issued instead.`,\n  })),\n\n  {\n    kwd: \"SET STATISTICS $integer\",\n    docs: `This form sets the per-column statistics-gathering target for subsequent ANALYZE operations. The target can be set in the range 0 to 10000; alternatively, set it to -1 to revert to using the system default statistics target (default_statistics_target). For more information on the use of statistics by the PostgreSQL query planner, refer to Section 14.2.\n\nSET STATISTICS acquires a SHARE UPDATE EXCLUSIVE lock.`,\n  },\n  ...[\n    \"SET ( attribute_option = value [, ... ] )\",\n    \"RESET ( attribute_option [, ... ] )\",\n  ].map((kwd) => ({\n    kwd,\n    docs: `This form sets or resets per-attribute options. Currently, the only defined per-attribute options are n_distinct and n_distinct_inherited, which override the number-of-distinct-values estimates made by subsequent ANALYZE operations. n_distinct affects the statistics for the table itself, while n_distinct_inherited affects the statistics gathered for the table plus its inheritance children. When set to a positive value, ANALYZE will assume that the column contains exactly the specified number of distinct nonnull values. When set to a negative value, which must be greater than or equal to -1, ANALYZE will assume that the number of distinct nonnull values in the column is linear in the size of the table; the exact count is to be computed by multiplying the estimated table size by the absolute value of the given number. For example, a value of -1 implies that all values in the column are distinct, while a value of -0.5 implies that each value appears twice on the average. This can be useful when the size of the table changes over time, since the multiplication by the number of rows in the table is not performed until query planning time. Specify a value of 0 to revert to estimating the number of distinct values normally. For more information on the use of statistics by the PostgreSQL query planner, refer to Section 14.2.\n\nChanging per-attribute options acquires a SHARE UPDATE EXCLUSIVE lock.`,\n  })),\n  {\n    kwd: \"SET STORAGE ${1|PLAIN,EXTERNAL,EXTENDED,MAIN|}\",\n    options: [\n      { label: \"PLAIN\" },\n      { label: \"EXTERNAL\" },\n      { label: \"EXTENDED\" },\n      { label: \"MAIN\" },\n    ],\n    docs: `This form sets the storage mode for a column. This controls whether this column is held inline or in a secondary TOAST table, and whether the data should be compressed or not. PLAIN must be used for fixed-length values such as integer and is inline, uncompressed. MAIN is for inline, compressible data. EXTERNAL is for external, uncompressed data, and EXTENDED is for external, compressed data. EXTENDED is the default for most data types that support non-PLAIN storage. Use of EXTERNAL will make substring operations on very large text and bytea values run faster, at the penalty of increased storage space. Note that SET STORAGE doesn't itself change anything in the table, it just sets the strategy to be pursued during future table updates. See Section 73.2 for more information.`,\n  },\n  {\n    kwd: \"SET COMPRESSION\",\n    options: [{ label: \"default\" }, { label: \"pglz\" }, { label: \"lz4\" }],\n    docs: `This form sets the compression method for a column, determining how values inserted in future will be compressed (if the storage mode permits compression at all). This does not cause the table to be rewritten, so existing data may still be compressed with other compression methods. If the table is restored with pg_restore, then all values are rewritten with the configured compression method. However, when data is inserted from another relation (for example, by INSERT ... SELECT), values from the source table are not necessarily detoasted, so any previously compressed data may retain its existing compression method, rather than being recompressed with the compression method of the target column. The supported compression methods are pglz and lz4. (lz4 is available only if --with-lz4 was used when building PostgreSQL.) In addition, compression_method can be default, which selects the default behavior of consulting the default_toast_compression setting at the time of data insertion to determine the method to use.`,\n  },\n] satisfies readonly KWD[];\n\nconst commonTableNames = [\n  \"users\",\n  \"products\",\n  \"sessions\",\n  \"orders\",\n  \"bookings\",\n  \"locations\",\n  \"customers\",\n  \"subscriptions\",\n  \"plans\",\n  \"refunds\",\n  \"payments\",\n  \"transactions\",\n  \"logs\",\n  \"files\",\n  \"chats\",\n  \"messages\",\n  \"notifications\",\n  \"events\",\n] as const;\n\nexport const getNewColumnDefinitions = (ss: ParsedSQLSuggestion[]) => {\n  const getRefCols = (tName: string | undefined, colDataType: string) => {\n    if (tName?.includes(\"user\") || tName?.includes(\"customer\")) {\n      return [\n        \"created\",\n        \"updated\",\n        \"deleted\",\n        \"changed\",\n        \"viewed\",\n        \"approved\",\n        \"signed\",\n        \"requested\",\n        \"reviewed\",\n        \"assigned\",\n      ].flatMap((action) => {\n        return {\n          label: `${action}_by ${colDataType} NOT NULL REFERENCES ${tName}`,\n          docs: \"\",\n        };\n      });\n    }\n\n    return [];\n  };\n\n  const cols = [\n    ...commonTableNames.flatMap((tableName) =>\n      [\"INTEGER\", \"BIGINT\", \"UUID\"].flatMap((dataType) => {\n        const idRef = {\n          label: `${tableName.slice(0, -1)}_id  ${dataType}  REFERENCES ${tableName}`,\n          docs: \"\",\n        };\n        return getRefCols(tableName, dataType).concat([idRef]);\n      }),\n    ),\n    { label: \"username TEXT NOT NULL\", docs: \"\" },\n    { label: \"username  VARCHAR(50) UNIQUE NOT NULL\", docs: \"\" },\n    { label: \"first_name  VARCHAR(150) NOT NULL\", docs: \"\" },\n    { label: \"last_name  VARCHAR(150) NOT NULL\", docs: \"\" },\n    {\n      label: `age  INTEGER \\n CONSTRAINT \"is greater than 0\"\\n CHECK(age > 0)`,\n      docs: \"\",\n    },\n    { label: \"birthdate DATE NOT NULL CHECK(birthdate < now())\", docs: \"\" },\n    { label: \"dob  DATE NOT NULL CHECK(dob < now())\", docs: \"\" },\n    {\n      label: \"date_of_birth  DATE NOT NULL CHECK(date_of_birth < now())\",\n      docs: \"\",\n    },\n    { label: \"password  VARCHAR(50) NOT NULL\", docs: \"\" },\n    { label: `address_line1  VARCHAR(250)`, docs: \"\" },\n    { label: `address_line2  VARCHAR(250)`, docs: \"\" },\n    { label: \"postcode  VARCHAR(20) NOT NULL\", docs: \"\" },\n    { label: \"status  VARCHAR(50)\", docs: \"\" },\n    { label: \"phone  VARCHAR(50) NOT NULL\", docs: \"\" },\n    { label: \"country  VARCHAR(50) NOT NULL\", docs: \"\" },\n    { label: \"city  VARCHAR(150) NOT NULL\", docs: \"\" },\n    { label: \"organization  VARCHAR(150) NOT NULL\", docs: \"\" },\n    { label: \"zip_code  VARCHAR(20) NOT NULL\", docs: \"\" },\n    {\n      label: `email  VARCHAR(255) NOT NULL UNIQUE \\n CONSTRAINT \"prevent case and whitespace duplicates\"\\n CHECK (email = trim(lower(email)))`,\n      docs: \"\",\n    },\n    ...[\n      \"created_on\",\n      \"starts_at\",\n      \"ends_at\",\n      \"start\",\n      \"end\",\n      \"created_at\",\n      \"added_at\",\n      \"updated\",\n      \"updated_at\",\n      \"deleted\",\n      \"deleted_at\",\n    ].map((colName) => ({\n      label: `${colName} TIMESTAMP NOT NULL DEFAULT now()`,\n      docs: \"\",\n    })),\n    { label: \"last_login TIMESTAMP\", docs: \"\" },\n    { label: \"order_timestamp NOT NULL TIMESTAMP\", docs: \"\" },\n    { label: \"quantity INTEGER NOT NULL CHECK(quantity > 1)\", docs: \"\" },\n    { label: \"notes  VARCHAR(200)\", docs: \"\" },\n    {\n      label: \"id  INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY\",\n      docs: \"Cannot be updated\",\n    },\n    {\n      label: \"id  BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY\",\n      docs: \"Cannot be updated\",\n    },\n    { label: \"id  SERIAL PRIMARY KEY\", docs: \"\" },\n    { label: \"id  BIGSERIAL PRIMARY KEY\", docs: \"\" },\n    { label: \"id  UUID PRIMARY KEY DEFAULT gen_random_uuid()\", docs: \"\" },\n    { label: \"id  UUID PRIMARY KEY DEFAULT uuid_generate_v1()\", docs: \"\" },\n    { label: \"id  INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY\", docs: \"\" },\n    { label: \"name  VARCHAR(150) NOT NULL\", docs: \"\" },\n    { label: \"type  VARCHAR(150) NOT NULL\", docs: \"\" },\n    { label: \"url  TEXT\", docs: \"\" },\n    { label: \"description  VARCHAR(250)\", docs: \"\" },\n    { label: \"title  VARCHAR(250)\", docs: \"\" },\n    { label: \"label  VARCHAR(250)\", docs: \"\" },\n    { label: \"message  VARCHAR(450) NOT NULL\", docs: \"\" },\n    { label: \"price  DECIMAL(12,2) CHECK(price >= 0)\", docs: \"\" },\n\n    { label: \"geom GEOMETRY\", docs: \"\" },\n    { label: \"geog GEOGRAPHY\", docs: \"\" },\n\n    ...[\"GEOMETRY\", \"GEOGRAPHY\"].flatMap((type) => [\n      { label: `location ${type}(point, 4326)`, docs: \"\" },\n      { label: `latlong  ${type}(point, 4326)`, docs: \"\" },\n      { label: `route  ${type}(linestring, 4326)`, docs: \"\" },\n      { label: `path ${type}(linestring, 4326)`, docs: \"\" },\n      { label: `line ${type}(linestring, 4326)`, docs: \"\" },\n      { label: `track ${type}(linestring, 4326)`, docs: \"\" },\n      {\n        label: `geo${type[3]!.toLowerCase()} ${type}(linestring, 4326)`,\n        docs: \"\",\n      },\n      {\n        label: `geo${type[3]!.toLowerCase()} ${type}(polygon, 4326)`,\n        docs: \"\",\n      },\n      { label: `geo${type[3]!.toLowerCase()} ${type}(point, 4326)`, docs: \"\" },\n    ]),\n\n    { label: \"preferences JSONB default '{}'\", docs: \"\" },\n    { label: \"description  VARCHAR(450)\", docs: \"\" },\n  ];\n\n  const refCols = ss\n    .filter(\n      (s) =>\n        s.type === \"table\" &&\n        ![\"prostgles\", \"pg_catalog\", \"information_schema\"].includes(s.schema!),\n    )\n    .flatMap((t) => {\n      const tableNamePref =\n        t.escapedIdentifier!.endsWith(\"s\") ?\n          t.escapedIdentifier?.slice(0, -1)\n        : t.escapedIdentifier!;\n      /** Is unique/pkey */\n      return t.cols\n        ?.filter((c) =>\n          [\"u\", \"p\"].some((cns) => c.cConstraint?.contype === cns),\n        )\n        .flatMap((c) => {\n          const colDataType = c.data_type.toUpperCase();\n          const newColName = JSON.stringify(\n            `${tableNamePref?.endsWith('\"') ? tableNamePref.slice(1, -1) : tableNamePref}_${c.name}`,\n          );\n          const idRef = {\n            docs: \"\",\n            label: `${newColName} ${colDataType} NOT NULL REFERENCES ${t.escapedIdentifier}`,\n          };\n\n          return getRefCols(t.escapedIdentifier, colDataType).concat([idRef]);\n        });\n    })\n    .filter(isDefined);\n\n  return [\n    ...cols.filter((c) => !c.label.includes(\"REFERENCES \")), // || !refCols.some(rc => rc.label.split(\" \")[0] === c.label.split(\" \")[0])),\n    ...refCols,\n  ].map((v) => pickKeys(v, [\"label\"]));\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/completionUtils/checkIfInsideDollarFunctionDefinition.ts",
    "content": "import type { Monaco } from \"../../../W_SQL/monacoEditorTypes\";\nimport { type TokenInfo, getTokens } from \"./getTokens\";\n\nexport type LineInfo = {\n  v: string;\n  n: number;\n  isComment: boolean;\n};\n\ntype Args = {\n  allLines: LineInfo[];\n  lineNumber: number;\n  currOffset: number;\n  offsetLimits?: [number, number];\n  eol: string;\n  editor: Monaco[\"editor\"];\n  startLine: number;\n  endLine: number;\n};\n\nexport const checkIfInsideDollarFunctionDefinition = ({\n  allLines,\n  lineNumber,\n  currOffset,\n  offsetLimits,\n  editor,\n  eol,\n  ...limits\n}: Args) => {\n  /** If inside function select function body so must disregard \";\" */\n  if (allLines.some((l) => l.v.includes(\"$\"))) {\n    let { tokens: allTokens } = getTokens({\n      editor,\n      eol,\n      lines: allLines.map((l) => l.v),\n      currOffset,\n    });\n    if (offsetLimits) {\n      const [minOffset, maxOffset] = offsetLimits;\n      allTokens = allTokens.filter((t) => {\n        return t.offset >= minOffset && t.offset <= maxOffset;\n      });\n    }\n\n    const funcToken = allTokens\n      .filter((t, i) => {\n        const prevToken = allTokens[i - 1];\n        const nextToken = allTokens[i + 1];\n        return (\n          !t.nestingId &&\n          (t.offset <= currOffset || t.lineNumber === lineNumber) &&\n          ((t.textLC === \"do\" &&\n            nextToken &&\n            nextToken.textLC.startsWith(\"$\")) ||\n            ([\"function\", \"procedure\"].includes(t.textLC) &&\n              prevToken &&\n              [\"create\", \"replace\"].includes(prevToken.textLC)))\n        );\n      })\n      .at(-1);\n    if (funcToken) {\n      const firstDollarQuote = allTokens.find(\n        (t) =>\n          funcToken.offset <= t.offset &&\n          t.lineNumber <= limits.endLine &&\n          t.text.startsWith(\"$\") &&\n          t.text.endsWith(\"$\"),\n      );\n      const secondDollarQuote = allTokens.find(\n        (t) =>\n          firstDollarQuote &&\n          t.offset > firstDollarQuote.offset &&\n          t.text === firstDollarQuote.text,\n      );\n      if (\n        firstDollarQuote &&\n        secondDollarQuote &&\n        (secondDollarQuote.offset >= currOffset ||\n          secondDollarQuote.lineNumber === lineNumber)\n      ) {\n        return {\n          startLine: funcToken.lineNumber,\n          endLine: secondDollarQuote.lineNumber,\n        };\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/completionUtils/checkIfUnfinishedParenthesis.ts",
    "content": "import type { LineInfo } from \"./checkIfInsideDollarFunctionDefinition\";\n\nexport const checkIfUnfinishedParenthesis = ({\n  allLines,\n  startLineNumber,\n  endLineNumber,\n}: {\n  startLineNumber: number;\n  endLineNumber: number;\n  allLines: LineInfo[];\n}) => {\n  const initialText = allLines\n    .filter((l) => l.n >= startLineNumber && l.n <= endLineNumber)\n    .map((l) => l.v)\n    .join(\"\\n\");\n  const initialCounts = getCounts(initialText);\n\n  if (initialCounts.diff) {\n    let newLineNumber: number | undefined;\n    const allowedEmptyLines = 1;\n    let emptyLines = 0;\n    let { startCount, endCount } = getCounts(\"\");\n    const extendStart = initialCounts.extendTo === \"start\";\n    const remainingLines =\n      extendStart ?\n        allLines.slice(0, startLineNumber).reverse()\n      : allLines.slice(endLineNumber);\n    remainingLines.forEach((line) => {\n      if (emptyLines > allowedEmptyLines) {\n        return;\n      }\n      if (!line.v) {\n        emptyLines++;\n      }\n      const counts = getCounts(line.v);\n      startCount += counts.startCount;\n      endCount += counts.endCount;\n      if (\n        extendStart ?\n          startCount > 0 && startCount - endCount <= initialCounts.diff\n        : endCount > 0 && endCount - startCount <= initialCounts.diff\n      ) {\n        newLineNumber = line.n;\n      }\n    });\n    if (newLineNumber !== undefined) {\n      return extendStart ?\n          {\n            startLineNumber: newLineNumber,\n            endLineNumber,\n          }\n        : {\n            startLineNumber,\n            endLineNumber: newLineNumber,\n          };\n    }\n  }\n\n  return undefined;\n};\n\nconst findEnd = (allLines: LineInfo[]) => {};\n\nconst getCounts = (text: string) => {\n  const startCount = text.match(/\\(/g)?.length ?? 0;\n  const endCount = text.match(/\\)/g)?.length ?? 0;\n\n  return {\n    startCount,\n    endCount,\n    diff: Math.abs(startCount - endCount),\n    extendTo: startCount > endCount ? \"end\" : \"start\",\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/completionUtils/getCodeBlock.ts",
    "content": "import type { editor, Position } from \"../../../W_SQL/monacoEditorTypes\";\nimport { STARTING_KWDS } from \"../KEYWORDS\";\n\nimport { getPrevTokensNoParantheses } from \"../getPrevTokensNoParantheses\";\nimport type { TokenInfo } from \"./getTokens\";\nimport { getTokens } from \"./getTokens\";\nimport { isDefined } from \"../../../../utils/utils\";\nimport { getMonaco } from \"../../W_SQLEditor\";\nimport { checkIfInsideDollarFunctionDefinition } from \"./checkIfInsideDollarFunctionDefinition\";\nimport { checkIfUnfinishedParenthesis } from \"./checkIfUnfinishedParenthesis\";\n\n/**\n * Get block of uninterrupted text at cursor position\n * Type of interruptions:\n *    - At least one empty line\n *    - Stop keyword \";\"\n */\nexport type CodeBlock = {\n  startLine: number;\n  endLine: number;\n  lines: { v: string; n: number }[];\n  tokens: TokenInfo[];\n  prevTokens: TokenInfo[];\n  nextTokens: TokenInfo[];\n  thisLinePrevTokens: TokenInfo[];\n  currToken?: TokenInfo;\n  ltoken?: TokenInfo;\n  l1token?: TokenInfo;\n  l2token?: TokenInfo;\n  l3token?: TokenInfo;\n  l4token?: TokenInfo;\n  ftoken?: TokenInfo;\n  currNestingId: string;\n  currNestingFunc: TokenInfo | undefined;\n  text: string;\n  textLC: string;\n  prevLC: string;\n  prevText: string;\n  nextLC: string;\n  thisLine: string;\n  thisLineLC: string;\n  prevIdentifiers: TokenInfo[];\n  identifiers: TokenInfo[];\n  tableIdentifiers: TokenInfo[];\n  offset: number;\n  getPrevTokensNoParantheses: (excludeParantheses?: boolean) => TokenInfo[];\n  prevTopKWDs: (Omit<TokenInfo, \"text\"> & {\n    text: (typeof STARTING_KWDS)[number];\n  })[];\n  model: editor.ITextModel;\n  position: Position;\n  isCommenting: boolean;\n  currOffset: number;\n  /**\n   * Start offset of the block\n   */\n  blockStartOffset: number;\n};\n\n/** Disruptions within function body due to \";\"  */\n// const isInterrupted = (line: string) => line.trim().includes(\";\") || line.trim() === \"\";\n\ntype GetCurrentCodeBlockOpts = {\n  smallestBlock?: boolean;\n  expandFrom?: {\n    startLine: number;\n    endLine: number;\n  };\n};\n/**\n * Get block of uninterrupted sql code around cursor position\n */\nexport const getCurrentCodeBlock = async (\n  model: editor.ITextModel,\n  pos: Position,\n  offsetLimits?: [number, number],\n  { smallestBlock = false, expandFrom }: GetCurrentCodeBlockOpts = {},\n): Promise<CodeBlock> => {\n  const { lineNumber } = pos;\n  const currOffset = model.getOffsetAt(pos);\n  /** https://github.com/microsoft/monaco-editor/issues/1225 */\n  const eol = model.getEOL();\n  let allLines = model\n    .getLinesContent()\n    .map((v, i) => ({ v, n: i + 1, isComment: false }));\n  const isInterrupted = (\n    line: (typeof allLines)[number],\n    type: \"empty\" | \"ended\",\n  ) => {\n    const interrupted =\n      !line.isComment &&\n      (type === \"empty\" ? line.v.trim() === \"\" : line.v.trim().endsWith(\";\"));\n    return interrupted;\n  };\n  const editor = (await getMonaco()).editor;\n  const editorForModel = editor\n    .getEditors()\n    .find((e) => e.getModel() === model);\n  const editorSelection = editorForModel?.getSelection();\n\n  /** Ignore comments */\n  if (allLines.some((l) => l.v.includes(`/*`) || l.v.includes(`--`))) {\n    const { tokens: allTokens } = getTokens({\n      editor,\n      eol,\n      lines: allLines.map((l) => l.v),\n    });\n    const commentLineNumbers = Array.from(\n      new Set(\n        allTokens\n          .filter((t) => t.type.includes(\"comment\"))\n          .map((t) => t.lineNumber + 1),\n      ),\n    );\n    allLines = allLines.map((l) => {\n      const isComment = commentLineNumbers.includes(l.n);\n      return {\n        ...l,\n        /** If comment then maintain content length for offset but disregard content */\n        v:\n          isComment ?\n            l.v.length >= 2 ?\n              `--${\" \".repeat(l.v.length - 2)}`\n            : \"\"\n          : l.v,\n        isComment,\n      };\n    });\n  }\n\n  const isCodeBlockEdge = (\n    line: (typeof allLines)[number],\n    nextLine: (typeof allLines)[number] | undefined,\n    isEnd: boolean,\n  ) => {\n    const nextLineIsInterrupted = !nextLine || isInterrupted(nextLine, \"empty\");\n    if (!isEnd && line.n === lineNumber) {\n      return nextLineIsInterrupted || isInterrupted(nextLine, \"ended\");\n    }\n    const isCbEdge =\n      (isEnd ? line.n >= lineNumber : line.n <= lineNumber) &&\n      (isInterrupted(line, \"ended\") ||\n        (!isEnd && isInterrupted(line, \"empty\")) || // isEnd=false starts at next line\n        nextLineIsInterrupted);\n    return isCbEdge;\n  };\n  let startLineNumber =\n    allLines\n      .slice(0, lineNumber)\n      .reverse()\n      .find((l, i, arr) => isCodeBlockEdge(l, arr[i + 1], false))?.n ??\n    allLines.at(0)?.n ??\n    1;\n  let endLineNumber =\n    allLines\n      .slice(lineNumber - 1)\n      .find((l, i, arr) => isCodeBlockEdge(l, arr[i + 1], true))?.n ??\n    allLines.at(-1)?.n ??\n    1;\n\n  /** If inside function select function body so must disregard \";\" */\n  const funcLimits = checkIfInsideDollarFunctionDefinition({\n    lineNumber: pos.lineNumber,\n    allLines,\n    currOffset,\n    offsetLimits,\n    editor,\n    eol,\n    startLine: startLineNumber,\n    endLine: endLineNumber,\n  });\n  if (funcLimits) {\n    startLineNumber = funcLimits.startLine;\n    endLineNumber = funcLimits.endLine;\n  }\n\n  /** If inside create table parenthesis */\n  const nestingLimits = checkIfUnfinishedParenthesis({\n    allLines,\n    startLineNumber,\n    endLineNumber,\n  });\n  if (nestingLimits) {\n    startLineNumber = nestingLimits.startLineNumber;\n    endLineNumber = nestingLimits.endLineNumber;\n  }\n\n  /** Selected text becomes the active codeblock */\n  if (\n    editorSelection &&\n    editorSelection.startLineNumber < editorSelection.endLineNumber\n  ) {\n    startLineNumber = editorSelection.startLineNumber;\n    endLineNumber = editorSelection.endLineNumber;\n  }\n\n  const startLine = allLines.find((l) => l.n === startLineNumber);\n  const endLine = allLines.find((l) => l.n === endLineNumber);\n\n  /** Skip empty lines */\n  if (\n    startLine &&\n    (isInterrupted(startLine, \"empty\") || startLine.v.trim().endsWith(\";\")) &&\n    startLine.n < lineNumber\n  ) {\n    startLineNumber++;\n  }\n  if (endLine && !endLine.v.trim() && endLineNumber !== lineNumber) {\n    endLineNumber--;\n  }\n  endLineNumber = Math.max(startLineNumber, endLineNumber);\n\n  const lines = allLines.slice(\n    !startLineNumber ? 0 : startLineNumber - 1,\n    !endLineNumber ? 0 : endLineNumber,\n  );\n  const blockStartOffset = model.getOffsetAt({\n    column: 0,\n    lineNumber: startLineNumber,\n  });\n\n  const _tokens = getTokens({\n    editor,\n    eol,\n    lines: lines.map((l) => l.v),\n    startOffset: blockStartOffset,\n    startLine: startLineNumber,\n    currOffset,\n  });\n  let { text } = _tokens;\n  let tokens = _tokens.tokens.filter(\n    (t) => t.text,\n  ); /** Something is adding an empty token to the start */\n\n  if (offsetLimits) {\n    const [minOffset, maxOffset] = offsetLimits;\n    tokens = tokens.filter((t) => {\n      return t.offset >= minOffset && t.end <= maxOffset;\n    });\n\n    /** If is nested then remove root nesting */\n    const [firstToken] = tokens;\n    if (firstToken) {\n      if (tokens.every((t) => t.nestingId.startsWith(firstToken.nestingId))) {\n        tokens = tokens.map((t) => ({\n          ...t,\n          nestingId: t.nestingId.slice(firstToken.nestingId.length),\n        }));\n      }\n    }\n  }\n\n  const textLC = tokens\n    .map((t) => t.textLC)\n    .join(\" \")\n    .toLowerCase()\n    .trim();\n\n  // const cbStartOffset = model.getOffsetAt({ lineNumber: startLine, column: 1 })\n  // const offset = modelOffset - cbStartOffset;\n  const prevTokens = tokens\n    .filter((t) => t.end < currOffset)\n    .sort((a, b) => a.offset - b.offset);\n  const nextTokens = tokens\n    .filter((t) => t.offset >= currOffset)\n    .sort((a, b) => a.offset - b.offset);\n  const currToken = tokens.find(\n    (t) => t.offset < currOffset && t.end >= currOffset,\n  );\n  const thisEntireLineTokens = tokens\n    .filter((t) => t.lineNumber === pos.lineNumber)\n    .sort((a, b) => a.offset - b.offset);\n  const thisLinePrevTokens = thisEntireLineTokens.filter(\n    (t) => t.end < currOffset,\n  );\n\n  const prevLC = prevTokens\n    .map((t) => t.textLC)\n    .join(\" \")\n    .toLowerCase()\n    .trim();\n  const nextLC = nextTokens\n    .map((t) => t.textLC)\n    .join(\" \")\n    .toLowerCase()\n    .trim();\n  const thisLineLC = thisEntireLineTokens\n    .map((t) => t.textLC)\n    .join(\" \")\n    .toLowerCase()\n    .trim();\n\n  const ftoken = tokens.at(0);\n  const ltoken = prevTokens.at(-1);\n  const l1token = prevTokens.at(-2);\n  const l2token = prevTokens.at(-3);\n  const l3token = prevTokens.at(-4);\n  const l4token = prevTokens.at(-5);\n\n  let prevText = model.getValueInRange({\n    startColumn: 0,\n    startLineNumber: startLineNumber,\n    endLineNumber: pos.lineNumber,\n    endColumn: pos.column,\n  });\n  if (offsetLimits) {\n    const startPos = model.getPositionAt(offsetLimits[0]);\n    const endPos = model.getPositionAt(offsetLimits[1]);\n    startLineNumber = startPos.lineNumber;\n    endLineNumber = endPos.lineNumber;\n    text = model.getValueInRange({\n      startColumn: startPos.column,\n      startLineNumber: startPos.lineNumber,\n      endColumn: endPos.column,\n      endLineNumber: endPos.lineNumber,\n    });\n    prevText = model.getValueInRange({\n      startColumn: startPos.column,\n      startLineNumber: startPos.lineNumber,\n      endLineNumber: pos.lineNumber,\n      endColumn: pos.column,\n    });\n  }\n\n  const prevIdentifiers = prevTokens.filter((t) => t.type === \"identifier.sql\");\n\n  // const identifiers = tokens.filter(t => t.type === \"identifier.sql\").map(t => t.text);\n  const _identifiers = tokens\n    .map((t, i, arr) => {\n      if (t.type !== \"identifier.sql\") {\n        return undefined;\n      }\n      /* Ignore create statement new names */\n      if (\n        arr[0]?.textLC === \"create\" &&\n        (i === 2 || (arr[4]?.textLC === \"exists\" && i === 5))\n      ) {\n        return undefined;\n      }\n      const prevT = arr[i - 1];\n      const tableKeyords = [\n        \"from\",\n        \"join\",\n        \"table\",\n        \"on\",\n        \"update\",\n        \"truncate\",\n        \"insert into\",\n        \"analyze\",\n        \"copy\",\n        \"into\",\n        \"cluster\",\n      ];\n      return {\n        type:\n          tableKeyords.includes(prevT?.textLC as any) ?\n            (\"table\" as const)\n          : undefined,\n        t,\n      };\n    })\n    .filter(isDefined);\n  const identifiers = _identifiers.map((d) => d.t);\n  const tableIdentifiers = _identifiers\n    .filter((d) => d.type === \"table\")\n    .map((d) => d.t);\n\n  const currLtoken = currToken ?? ltoken;\n  const [nextToken] = nextTokens;\n\n  const currNestingId =\n    currLtoken?.text === \"(\" && nextToken?.text === \")\" ?\n      `${currLtoken.nestingId}1`\n    : nextToken?.text === \")\" && currLtoken ? currLtoken.nestingId\n    : currLtoken?.text === \"(\" ? `${currLtoken.nestingId}1`\n    : !nextToken ? \"\"\n    : (currToken?.nestingId ??\n      tokens.find((t) => [t.offset, t.end].includes(currOffset))?.nestingId ??\n      ltoken?.nestingId ??\n      \"\");\n  const currNestingFunc =\n    currLtoken?.text === \"(\" ?\n      tokens\n        .slice(0)\n        .reverse()\n        .find((_, i, arr) => arr[i - 1]?.offset === currLtoken.offset)\n    : currLtoken?.nestingFuncToken;\n\n  const { isCommenting } = getTokens({\n    editor,\n    eol,\n    lines: model.getLinesContent(),\n    includeComments: true,\n    currOffset,\n  });\n\n  // const thisLine = allLines[pos.lineNumber-1]?.v ?? model.getLineContent(pos.lineNumber)\n  const thisLine = model.getLineContent(pos.lineNumber);\n  const result = {\n    get prevTopKWDs() {\n      return getPrevTokensNoParantheses(prevTokens, true)\n        .slice(0)\n        .reverse()\n        .filter((t) =>\n          STARTING_KWDS.find(\n            (kwd) =>\n              [\"keyword.sql\", \"identifier.sql\", \"predefined.sql\"].includes(\n                t.type,\n              ) &&\n              !t.text.includes('\"') &&\n              t.textLC === kwd.toLowerCase(),\n          ),\n        )\n        .map((t) => ({\n          ...t,\n          text: t.text.toUpperCase() as (typeof STARTING_KWDS)[number],\n        }));\n    },\n    prevText,\n    blockStartOffset,\n    startLine: startLineNumber,\n    endLine: endLineNumber,\n    text,\n    textLC,\n    prevLC,\n    nextLC,\n    thisLine,\n    thisLineLC,\n    thisLinePrevTokens,\n    lines,\n    tokens,\n    prevTokens,\n    nextTokens,\n    currToken,\n    ftoken,\n    l4token,\n    l3token,\n    l2token,\n    l1token,\n    ltoken,\n    prevIdentifiers,\n    currNestingId,\n    currNestingFunc,\n    identifiers,\n    tableIdentifiers,\n    getPrevTokensNoParantheses: (excludeParantheses?: boolean) =>\n      getPrevTokensNoParantheses(prevTokens, excludeParantheses),\n    offset: currOffset,\n    model,\n    position: pos,\n    isCommenting,\n    currOffset,\n  };\n\n  if (smallestBlock && !offsetLimits && currNestingId) {\n    const cbCurrOffset =\n      expandFrom ?\n        model.getOffsetAt({ lineNumber: expandFrom.startLine, column: 1 })\n      : currOffset;\n    const cbNestingId = expandFrom ? currNestingId.slice(0, -1) : currNestingId;\n    const smallestBlockoffsetLimits = getCurrentNestingOffsetLimits({\n      currNestingId: cbNestingId,\n      currOffset: cbCurrOffset,\n      tokens,\n    });\n\n    if (smallestBlockoffsetLimits && !smallestBlockoffsetLimits.isEmpty) {\n      const smallestCb = await getCurrentCodeBlock(\n        model,\n        pos,\n        smallestBlockoffsetLimits.limits,\n      );\n      return smallestCb;\n    }\n  }\n  return result;\n};\n\nexport const getCurrentNestingOffsetLimits = (\n  {\n    currNestingId: cn,\n    currOffset,\n    tokens,\n  }: Pick<CodeBlock, \"currNestingId\" | \"tokens\" | \"currOffset\">,\n  nestingId?: string,\n): { limits: [number, number]; isEmpty: boolean } | undefined => {\n  const currNestingId = nestingId ?? cn;\n  const isInsideCurrentNestingId = (\n    t: TokenInfo,\n    i: number,\n    arr: TokenInfo[],\n  ) => {\n    const pToken = arr[i - 1];\n    const nToken = arr[i + 1];\n    const isParens = nToken?.type === \"delimiter.parenthesis.sql\";\n    const isExitingNesting =\n      !nToken || nToken.nestingId.length < currNestingId.length;\n    const isSameNesting =\n      t.nestingId.startsWith(currNestingId) &&\n      (!pToken || pToken.nestingId.startsWith(currNestingId));\n    const foundEnd = isParens && isSameNesting && isExitingNesting;\n    return foundEnd;\n  };\n  const startTokens = tokens\n    .slice(0)\n    .filter((t) => t.offset <= currOffset)\n    .reverse();\n  const startTokenIdx = startTokens.findIndex(isInsideCurrentNestingId);\n  let startToken = startTokens[startTokenIdx];\n  if (\n    startTokens[0]?.text === \"(\" &&\n    startTokens[0].nestingId.length < currNestingId.length\n  ) {\n    startToken = undefined;\n  }\n  const firstBefore = tokens.slice(0).findLast((t) => t.end <= currOffset);\n  const firstAfter = tokens.slice(0).find((t) => t.offset >= currOffset);\n  /** Is inside empty brackets */\n  if (\n    firstBefore?.text === \"(\" &&\n    firstAfter?.text === \")\" &&\n    firstBefore.end <= currOffset &&\n    firstAfter.end >= currOffset &&\n    firstBefore.nestingId.length < currNestingId.length &&\n    firstAfter.nestingId.length < currNestingId.length\n  ) {\n    return undefined;\n  }\n  const endTokens = tokens.filter(\n    (t) => t.offset >= currOffset || t.end === currOffset,\n  );\n  /** If cursor is near end parens then must find the token just before cursor */\n  if (\n    (!endTokens.length ||\n      (endTokens[0]?.text === \")\" &&\n        endTokens[0].nestingId.length < currNestingId.length)) &&\n    startTokenIdx >= 0 &&\n    firstBefore\n  ) {\n    endTokens.unshift(firstBefore);\n  }\n  const endToken = endTokens.find(isInsideCurrentNestingId);\n  const allowedCommands = [\"select\", \"update\", \"delete\", \"insert\"];\n  if (\n    startToken?.type === \"keyword.sql\" &&\n    endToken &&\n    allowedCommands.includes(startToken.textLC)\n  ) {\n    return {\n      limits: [startToken.offset, endToken.end],\n      isEmpty: false,\n    };\n  } else if (startTokens[0]?.text === \"(\" && endTokens[0]?.text === \")\") {\n    return {\n      limits: [startTokens[0].end + 1, endTokens[0].offset - 1],\n      isEmpty: true,\n    };\n  }\n};\n\n/**\n * Keywords used in determining suggestions\n */\nexport const MAIN_KEYWORDS = [\n  \"JOIN\",\n  \"SELECT\",\n  \"ON\",\n  \"CREATE\",\n  \"TABLE\",\n  \"SET\",\n  \"UPDATE\",\n  \"INTO\",\n  \"FROM\",\n  \"WHERE\",\n  \";\",\n  \"\\n\",\n  \"ALTER\",\n  \"COPY\",\n  \"REFRESH\",\n  \"REINDEX\",\n  \"GRANT\",\n  \"REVOKE\",\n] as const;\n\nexport const playButtonglyphMarginClassName = \"active-code-block-play\";\nexport const highlightCurrentCodeBlock = async (\n  editor: editor.IStandaloneCodeEditor,\n  currCodeBlock?: CodeBlock,\n) => {\n  const codeBlockId =\n    currCodeBlock ?\n      [\"L\", currCodeBlock.startLine, currCodeBlock.endLine].join(\"-\")\n    : \"\";\n  const monaco = await getMonaco();\n  const model = editor.getModel();\n  const selection = editor.getSelection();\n  const modelContainsSelection = Boolean(\n    selection && model?.getValueInRange(selection),\n  );\n  const noDecor = !currCodeBlock || modelContainsSelection;\n  return editor.createDecorationsCollection(\n    noDecor ?\n      []\n    : [\n        {\n          range: new monaco.Range(\n            currCodeBlock.startLine,\n            1,\n            currCodeBlock.endLine,\n            1,\n          ),\n          options: {\n            isWholeLine: true,\n            linesDecorationsClassName: [\n              \"active-code-block-decoration\",\n              codeBlockId,\n            ].join(\" \"),\n            glyphMarginClassName:\n              currCodeBlock.textLC ? playButtonglyphMarginClassName : undefined,\n            // glyphMarginHoverMessage: {\n            //   value: \"**Run this statement**\\n\\nOnly this section of the script will be executed unless text is selected. This behaviour can be changed in options\\n\\nExecute hot keys: ctrl+e, alt+e\",\n            // }\n          },\n        },\n      ],\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/completionUtils/getQueryReturnType.ts",
    "content": "import type { AnyObject, SQLHandler } from \"prostgles-types\";\nimport { asName, includes, tryCatchV2 } from \"prostgles-types\";\nimport type { ColType } from \"@common/utils\";\n\nconst isQueryValid = async (rawQuery: string, sql: SQLHandler) => {\n  const queryWithSemicolon = getSQLQuerySemicolon(rawQuery, true);\n  const res = await sql(\n    `\n      EXPLAIN\n      ${queryWithSemicolon}\n      `,\n    {},\n    { returnType: \"default-with-rollback\" },\n  ).catch((_e) => false);\n  return { isValid: Boolean(res), queryWithSemicolon };\n};\n\n/**\n * Get statement return type ensuring any dangerous commands are not commited\n */\nconst getQueryReturnType = async (\n  rawQuery: string,\n  sql: SQLHandler,\n  includeTableOid = false,\n): Promise<ColType[]> => {\n  /** Check if it's a data returning statement to avoid useless error logs */\n  const { queryWithSemicolon, isValid } = await isQueryValid(rawQuery, sql);\n\n  if (!isValid) {\n    return [];\n  }\n  if (includeTableOid) {\n    const colTypes = await getTableExpressionReturnTypeWithTableOIDs(\n      rawQuery,\n      sql,\n    );\n    return colTypes;\n  }\n\n  const viewName = \"prostgles_temp_view_getQueryReturnType\" + Date.now();\n  const result = await sql(\n    `\n      CREATE OR REPLACE TEMP VIEW \"${viewName}\" AS \n      ${queryWithSemicolon}\n\n      SELECT\n        column_name,\n        format('%I', column_name) as escaped_column_name,\n        data_type, \n        udt_name, \n        current_schema() as schema\n      FROM information_schema.columns c \n      WHERE c.table_name = '${viewName}'\n      \n    `,\n    {},\n    { returnType: \"default-with-rollback\" },\n  );\n\n  return result.rows as ColType[];\n};\n\n/**\n * Does not fail on duplicate columns\n */\nconst getTableExpressionReturnTypeWithTableOIDs = async (\n  query: string,\n  sql: SQLHandler,\n): Promise<ColType[]> => {\n  const queryWithoutSemicolon = getSQLQuerySemicolon(query, false);\n  const result = await sql(\n    `\n      SELECT * \n      FROM (\n        ${queryWithoutSemicolon}\n      ) prostgles_temp_table_getQueryReturnType\n      LIMIT 0;\n    `,\n    {},\n    { returnType: \"default-with-rollback\" },\n  );\n\n  const cols: ColType[] = result.fields.map((f) => {\n    return {\n      table_oid: f.tableID,\n      column_name: f.name,\n      escaped_column_name:\n        /^[a-z_][a-z0-9_$]*$/.test(f.name) ? f.name : asName(f.name),\n      data_type: f.dataType,\n      udt_name: f.dataType,\n      schema: \"public\",\n    };\n  });\n  return cols;\n};\n\ntype ExpressionResult =\n  | { colTypes: ColType[]; error?: undefined }\n  | { colTypes?: undefined; error: unknown };\nconst cached = new Map<string, ExpressionResult>();\n\nexport const getTableExpressionReturnType = async (\n  expression: string,\n  sql: SQLHandler,\n  includeTableOid = false,\n): Promise<ExpressionResult> => {\n  const cachedValue = cached.get(expression);\n  if (cachedValue) {\n    return cachedValue;\n  }\n\n  try {\n    const result = await tryCatchV2(async () => {\n      const colTypes = await getQueryReturnType(\n        expression,\n        sql,\n        includeTableOid,\n      );\n      return { colTypes };\n    });\n    let colTypes = result.data?.colTypes;\n    const { error } = result;\n    if (!colTypes) {\n      if (\n        includes([\"42701\", \"42P16\"], (error as AnyObject | undefined)?.code)\n      ) {\n        colTypes = await getTableExpressionReturnTypeWithTableOIDs(\n          expression,\n          sql,\n        );\n      }\n    }\n    if (!colTypes) {\n      console.warn(error);\n      throw error ?? new Error(\"No columns found\");\n    }\n    cached.set(expression, { colTypes });\n    return { colTypes };\n  } catch (error) {\n    console.warn(error);\n    cached.set(expression, { error });\n    return {\n      error,\n    };\n  }\n};\n\nexport const getSQLQuerySemicolon = (\n  rawQuery: string,\n  shouldEndWithSemicolon: boolean,\n) => {\n  const queryWithoutComments = removePostgresComments(rawQuery).trim();\n  const endsWithSemicolon = queryWithoutComments.endsWith(\";\");\n\n  if (shouldEndWithSemicolon) {\n    return endsWithSemicolon ? rawQuery : rawQuery + \"\\n;\";\n  }\n  return endsWithSemicolon ? queryWithoutComments.slice(0, -1) : rawQuery;\n};\n\n/**\n * Removes PostgreSQL comments from SQL code while preserving string literals\n * Handles:\n * - Single-line comments (-- comment)\n * - Multi-line block comments (\\/* comment *\\/)\n * - Nested block comments\n * - Comments inside string literals (preserved)\n */\nconst removePostgresComments = (sql: string) => {\n  let result = \"\";\n  let i = 0;\n\n  while (i < sql.length) {\n    const char = sql[i];\n    const nextChar = sql[i + 1];\n\n    // Handle string literals (single quotes)\n    if (char === \"'\") {\n      result += char;\n      i++;\n      // Continue until closing quote, handling escaped quotes\n      while (i < sql.length) {\n        result += sql[i];\n        if (sql[i] === \"'\" && sql[i - 1] !== \"\\\\\") {\n          i++;\n          break;\n        }\n        i++;\n      }\n      continue;\n    }\n\n    // Handle string literals (double quotes - identifiers in PostgreSQL)\n    if (char === '\"') {\n      result += char;\n      i++;\n      while (i < sql.length) {\n        result += sql[i];\n        if (sql[i] === '\"' && sql[i - 1] !== \"\\\\\") {\n          i++;\n          break;\n        }\n        i++;\n      }\n      continue;\n    }\n\n    // Handle single-line comments (--)\n    if (char === \"-\" && nextChar === \"-\") {\n      i += 2;\n      // Skip until end of line\n      while (i < sql.length && sql[i] !== \"\\n\") {\n        i++;\n      }\n      // Keep the newline\n      if (i < sql.length && sql[i] === \"\\n\") {\n        result += \"\\n\";\n        i++;\n      }\n      continue;\n    }\n\n    // Handle multi-line block comments (/* ... */)\n    if (char === \"/\" && nextChar === \"*\") {\n      i += 2;\n      let depth = 1;\n\n      // Handle nested comments (PostgreSQL supports nested /* */ comments)\n      while (i < sql.length && depth > 0) {\n        if (sql[i] === \"/\" && sql[i + 1] === \"*\") {\n          depth++;\n          i += 2;\n        } else if (sql[i] === \"*\" && sql[i + 1] === \"/\") {\n          depth--;\n          i += 2;\n        } else {\n          i++;\n        }\n      }\n      continue;\n    }\n\n    // Regular character\n    result += char;\n    i++;\n  }\n\n  return result;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/completionUtils/getTableExpressionReturnTypes.ts",
    "content": "import type { ColType } from \"@common/utils\";\nimport { getColumnSuggestionLabel } from \"../../SQLEditorSuggestions\";\nimport { asSQL } from \"../KEYWORDS\";\nimport {\n  getKind,\n  type ParsedSQLSuggestion,\n  type SQLMatchContext,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport { getTableExpressionReturnType } from \"./getQueryReturnType\";\nimport {\n  getTabularExpressions,\n  type TabularExpression,\n} from \"./getTabularExpressions\";\n\nexport type GetTableExpressionSuggestionsArgs = Pick<\n  SQLMatchContext,\n  \"ss\" | \"sql\"\n> & {\n  cb: SQLMatchContext[\"cb\"];\n  parentCb?: SQLMatchContext[\"cb\"];\n};\n\nexport const getTableExpressionSuggestions = async (\n  args: GetTableExpressionSuggestionsArgs,\n  require: \"table\" | \"columns\",\n  onlyCurrentBlock = false,\n): Promise<{\n  columns: ParsedSQLSuggestion[];\n  tables: ParsedSQLSuggestion[];\n  tablesWithAliasInfo: (TabularExpression & {\n    s: ParsedSQLSuggestion;\n  })[];\n  columnsWithAliasInfo: (TabularExpression & {\n    s: ParsedSQLSuggestion;\n  })[];\n}> => {\n  const isWith = args.cb.ftoken?.textLC === \"with\";\n  const cannotSuggestTableSubqueries =\n    isWith && require === \"table\" && args.cb.ltoken?.textLC === \"join\";\n  let expressions = getTabularExpressions(\n    args,\n    require,\n    onlyCurrentBlock,\n  ).filter((e) => {\n    /** Cannot join to aliased subqueries */\n    if (cannotSuggestTableSubqueries) {\n      return e.kwd === \"as\" || e.type === \"tableOrView\";\n    }\n    return true;\n  });\n\n  /**\n   * Exclude columns from CTEs. Only select statement columns allowed */\n  if (\n    isWith &&\n    require === \"columns\" &&\n    args.cb.tokens.some((t) => !t.nestingId && t.textLC === \"from\")\n  ) {\n    expressions = expressions.filter((e) => e.kwd !== \"as\");\n  }\n\n  const columnsWithAliasInfo: (TabularExpression & {\n    s: ParsedSQLSuggestion;\n  })[] = [];\n  const tablesWithAliasInfo: (TabularExpression & {\n    s: ParsedSQLSuggestion;\n  })[] = [];\n\n  const getColumnSuggestion = (\n    cs:\n      | { type: \"s\"; s: ParsedSQLSuggestion }\n      | { type: \"col\"; colType: ColType },\n    tableAlias?: string,\n    isCteAlias = false,\n  ): Pick<\n    ParsedSQLSuggestion,\n    | \"label\"\n    | \"sortText\"\n    | \"insertText\"\n    | \"name\"\n    | \"filterText\"\n    | \"escapedIdentifier\"\n  > => {\n    const c = cs.type === \"s\" ? cs.s.colInfo! : cs.colType;\n    const colName =\n      cs.type === \"s\" ?\n        cs.s.escapedIdentifier!\n      : cs.colType.escaped_column_name;\n\n    const prevText = args.cb.prevText.trim();\n    const hasNoAlias = cs.type === \"s\" && cs.s.escapedParentName === tableAlias;\n    const endsWithThisAlias = tableAlias && prevText.endsWith(`${tableAlias}.`);\n    const label =\n      tableAlias && !hasNoAlias ? `${tableAlias}.${colName}` : colName;\n    const insertText =\n      isCteAlias || endsWithThisAlias || hasNoAlias ? colName : label;\n    return {\n      name: label,\n      label: getColumnSuggestionLabel(\n        { name: colName, ...c },\n        tableAlias ?? (cs.type === \"s\" ? cs.s.tablesInfo!.name : \"\"),\n      ),\n      insertText,\n      filterText: colName,\n      escapedIdentifier: colName,\n      sortText: \"-1\",\n    };\n  };\n\n  for (const e of expressions) {\n    if (e.type === \"tableOrView\") {\n      tablesWithAliasInfo.push({ ...e, s: e.table });\n      columnsWithAliasInfo.push(\n        ...e.columns.map((s) => ({\n          ...e,\n          s: { ...s, ...getColumnSuggestion({ type: \"s\", s }, e.alias) },\n          alias: e.alias ?? \"\",\n        })),\n      );\n      continue;\n    }\n\n    if (!args.sql) continue;\n    const { colTypes } = await getTableExpressionReturnType(\n      e.getQuery(),\n      args.sql,\n    );\n    if (!colTypes) continue;\n    const colTypesWithDefs = colTypes.map((c) => {\n      return {\n        ...c,\n        column_name: c.escaped_column_name,\n        definition: `${c.escaped_column_name} ${c.data_type}`,\n      };\n    });\n    if (e.alias) {\n      const s: ParsedSQLSuggestion = {\n        kind: getKind(\"table\"),\n        documentation: {\n          value: asSQL(\n            `${e.alias} (\\n` +\n              colTypesWithDefs.map((c) => \"  \" + c.definition).join(\", \\n\") +\n              \"\\n)\",\n          ),\n        },\n        detail: `(expression) ${e.alias}`,\n        insertText: e.alias,\n        label: e.alias,\n        name: e.alias,\n        range: undefined as any,\n        escapedIdentifier: e.alias,\n        type: \"table\",\n        sortText: \"-1\",\n        schema: colTypes[0]?.schema,\n        tablesInfo: {\n          escaped_identifiers: [e.alias],\n          // identifiers: [expression.alias],\n          schema: \"public\",\n          cols: colTypes.map((c) => ({\n            ...c,\n            cConstraint: undefined,\n            character_maximum_length: 0,\n            data_type: c.data_type,\n            udt_name: c.udt_name,\n            name: c.escaped_column_name,\n            comment: \"\",\n            definition: \"\",\n            escaped_identifier: c.escaped_column_name,\n            has_default: false,\n            nullable: false,\n            numeric_precision: 0,\n            numeric_scale: 0,\n            ordinal_position: 0,\n            column_default: \"\",\n          })),\n          comment: \"\",\n          escaped_identifier: e.alias,\n          escaped_name: e.alias,\n          is_view: true,\n          name: e.alias,\n          oid: 0,\n          relkind: \"v\",\n          view_definition: undefined,\n        },\n      };\n      tablesWithAliasInfo.push({ ...e, s });\n    }\n\n    const tAlias = e.alias ?? e.cteAlias;\n    const tableAliasHeader = !tAlias ? \"\" : `**${tAlias}**\\n\\n`;\n    columnsWithAliasInfo.push(\n      ...colTypesWithDefs.map((c) => {\n        const s: ParsedSQLSuggestion = {\n          kind: getKind(\"column\"),\n          documentation: { value: tableAliasHeader + asSQL(c.definition) },\n          ...getColumnSuggestion(\n            { type: \"col\", colType: c },\n            e.alias,\n            e.type === \"subquery\" && e.kwd === \"as\",\n          ),\n          range: undefined as any,\n          schema: c.schema,\n          colInfo: {\n            cConstraint: undefined,\n            data_type: c.data_type,\n            udt_name: c.udt_name,\n            name: c.escaped_column_name,\n            comment: \"\",\n            character_maximum_length: 0,\n            definition: \"\",\n            escaped_identifier: c.escaped_column_name,\n            has_default: false,\n            nullable: false,\n            numeric_precision: 0,\n            numeric_scale: 0,\n            ordinal_position: 0,\n            column_default: \"\",\n          },\n          parentName: e.alias ?? \"\",\n          escapedParentName: e.alias ?? \"\",\n          type: \"column\",\n          sortText: \"-1\",\n        };\n\n        return {\n          s,\n          ...e,\n          alias: e.alias ?? \"\",\n        };\n      }),\n    );\n  }\n\n  const columns = columnsWithAliasInfo.map(({ s }) => s);\n\n  const tablesSuggestions = tablesWithAliasInfo.map((t) => t.s);\n  return {\n    columns,\n    tables: tablesSuggestions,\n    tablesWithAliasInfo,\n    columnsWithAliasInfo: columnsWithAliasInfo,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/completionUtils/getTabularExpressions.ts",
    "content": "import { getParentFunction, preSubQueryKwds } from \"../MatchSelect\";\nimport type {\n  ParsedSQLSuggestion,\n  SQLMatchContext,\n} from \"../monacoSQLSetup/registerSuggestions\";\nimport type { CodeBlock } from \"./getCodeBlock\";\nimport type { GetTableExpressionSuggestionsArgs } from \"./getTableExpressionReturnTypes\";\nimport type { TokenInfo } from \"./getTokens\";\n\nconst getAliasToken = (\n  tokens: SQLMatchContext[\"cb\"][\"tokens\"],\n  expressionLastTokenIdx: number,\n) => {\n  if (expressionLastTokenIdx === -1) return;\n  const aliasToken = tokens[expressionLastTokenIdx + 1];\n  const aliasToken2 = tokens[expressionLastTokenIdx + 2];\n  if (aliasToken?.textLC === \"as\") {\n    return aliasToken2;\n  }\n  if (aliasToken?.type !== \"identifier.sql\") {\n    return undefined;\n  }\n  return aliasToken;\n};\n\nconst tablePrecedingKeywords = [\n  \"from\",\n  \"join\",\n  \"lateral\",\n  \"update\",\n  /**\n   * comma is may be used as an alias for CROSS JOIN\n   * Only valid if we already have a tabular expression\n   */\n  \",\",\n] as const;\nconst withTablePrecedingKeywords = [\"as\"] as const;\nconst policyTablePrecedingKeywords = [\"on\"] as const;\nconst alterTablePrecedingKeywords = [\"table\"] as const;\nconst allTablePrecedingKeywords = [\n  ...tablePrecedingKeywords,\n  ...withTablePrecedingKeywords,\n  ...policyTablePrecedingKeywords,\n  ...alterTablePrecedingKeywords,\n] as const;\n\nexport type TabularExpression = {\n  kwd: (typeof allTablePrecedingKeywords)[number];\n  alias: string | undefined;\n  getQuery: (selectedColumns?: string) => string;\n  endOffset: number;\n  cteAlias?: string;\n} & (\n  | {\n      type: \"function\";\n      func: ParsedSQLSuggestion;\n    }\n  | {\n      type: \"subquery\";\n    }\n  | {\n      type: \"tableOrView\";\n      table: ParsedSQLSuggestion;\n      columns: ParsedSQLSuggestion[];\n    }\n);\n\nexport const getTabularExpressions = (\n  {\n    cb: _cb,\n    ss,\n    parentCb,\n  }: Pick<GetTableExpressionSuggestionsArgs, \"ss\" | \"cb\" | \"parentCb\">,\n  require: \"columns\" | \"table\",\n  onlyCurrentBlock = false,\n) => {\n  let { tokens } = { ..._cb };\n  let parentTokensForLateral = tokens.slice(0, 0);\n  /** If is in a subquery then ignore parent UNLESS this is a lateral expression */\n  if (_cb.currNestingId) {\n    const func = getParentFunction(_cb);\n    if (func && preSubQueryKwds.includes(func.func.textLC)) {\n      if (func.func.textLC === \"lateral\") {\n        parentTokensForLateral = [\n          ...tokens.filter((t) => t.offset < func.func.offset),\n        ];\n      }\n      const startTokenIdx = _cb.tokens.findLastIndex((t, i) => {\n        const prevToken = _cb.tokens[i - 1];\n        return (\n          t.offset <= _cb.currOffset &&\n          t.nestingId === _cb.currNestingId &&\n          prevToken &&\n          prevToken.nestingId !== _cb.currNestingId\n        );\n      });\n      const startToken = _cb.tokens[startTokenIdx];\n      if (startToken) {\n        const lastTokenIdx = _cb.tokens.findIndex((t, i) => {\n          const nextToken = _cb.tokens[i - 1];\n          const r =\n            t.offset > startToken.offset &&\n            t.nestingId === _cb.currNestingId &&\n            nextToken?.nestingId !== _cb.currNestingId;\n          return r;\n        });\n        tokens = tokens.slice(startTokenIdx, lastTokenIdx).map((t) => ({\n          ...t,\n          nestingId: t.nestingId.slice(0, -_cb.currNestingId.length),\n        }));\n      }\n    }\n  }\n\n  let expressions: TabularExpression[] = [];\n\n  /** If inside a CTE then add previous WITH statement CTE defs (WITH ... update)*/\n  if (\n    parentCb?.ftoken?.textLC === \"with\" &&\n    parentCb.currNestingId &&\n    !onlyCurrentBlock\n  ) {\n    const pFunc = getParentFunction(parentCb);\n    if (pFunc?.func.textLC === \"as\" && !pFunc.func.nestingId) {\n      const prevExpressions = getTabularExpressions(\n        { cb: parentCb, ss },\n        require,\n      ).filter((e) => e.endOffset < parentCb.currOffset);\n      expressions = [...prevExpressions];\n    }\n  }\n\n  expressions = [...expressions, ...getExpressions(tokens, _cb, ss, parentCb)];\n\n  if (parentTokensForLateral.length && require === \"columns\") {\n    expressions = [\n      ...expressions,\n      ...getExpressions(parentTokensForLateral, _cb, ss, parentCb),\n    ];\n  }\n\n  return expressions.filter((s) => s.kwd === \"from\" || s.alias);\n};\n\nconst getExpressions = (\n  tokens: TokenInfo[],\n  cb: CodeBlock,\n  ss: ParsedSQLSuggestion[],\n  parentCb: CodeBlock | undefined,\n) => {\n  const getQuerySection = (\n    firstToken: TokenInfo,\n    lastToken: TokenInfo,\n    includeEnd = false,\n  ) => {\n    const textOffset = cb.blockStartOffset;\n    return (parentCb ?? cb).text.slice(\n      firstToken.offset - textOffset,\n      (includeEnd ? lastToken.end : lastToken.offset) - textOffset,\n    );\n  };\n  const getClosingParenthesis = (fromIdx: number) => {\n    const closingParenthesesIdx = tokens.findIndex(\n      (nt, idx) => idx >= fromIdx && nt.text === \")\" && !nt.nestingId,\n    );\n    const firstToken = tokens[fromIdx];\n    const lastToken = tokens[closingParenthesesIdx];\n    if (closingParenthesesIdx === -1 || !firstToken || !lastToken) return;\n    const aliasToken = getAliasToken(tokens, closingParenthesesIdx);\n    return {\n      closingParenthesesIdx,\n      closingParentheses: lastToken,\n      nestedTokens: tokens.slice(fromIdx, closingParenthesesIdx),\n      alias: aliasToken?.text,\n      aliasToken,\n      query: getQuerySection(firstToken, lastToken),\n    };\n  };\n\n  const withParentCbExpressions =\n    parentCb && parentCb.tokens[0]?.textLC === \"with\" ?\n      getTabularExpressions({ cb: parentCb, ss }, \"table\")\n    : [];\n\n  const expressions: TabularExpression[] = [];\n  const isWith = tokens[0]?.textLC === \"with\";\n  const isPolicy = tokens[1]?.textLC === \"policy\";\n  const isIndex = tokens[1]?.textLC === \"index\";\n  const isAlterTable =\n    tokens[0]?.textLC === \"alter\" && tokens[1]?.textLC === \"table\";\n  const indexOrPolicy = isIndex || isPolicy;\n  let isWithAsSectionFinished = false;\n  tokens.forEach((t, i) => {\n    const prevToken = tokens[i - 1];\n    const nextToken = tokens[i + 1];\n    const tableKeywods = [\n      ...tablePrecedingKeywords,\n      ...(isAlterTable ? alterTablePrecedingKeywords\n      : indexOrPolicy ? policyTablePrecedingKeywords\n      : isWith && !isWithAsSectionFinished ? withTablePrecedingKeywords\n      : []),\n    ].filter((tk) => expressions.length || tk !== \",\");\n\n    if (!isWithAsSectionFinished && t.textLC === \"select\" && !t.nestingId) {\n      isWithAsSectionFinished = true;\n    }\n    const kwd = tableKeywods.find((v) => v === prevToken?.textLC);\n\n    if (prevToken && !prevToken.nestingId && kwd) {\n      /** CTE section finished */\n      if (isWith && kwd !== \"as\" && kwd !== \",\") {\n        isWithAsSectionFinished = true;\n      }\n\n      /** CTE */\n      if (isWith && kwd === \"as\") {\n        const prevPrevToken = tokens[i - 2];\n        if (!prevPrevToken) return;\n        const closing = getClosingParenthesis(i + 1);\n        if (!closing) return;\n\n        const getQuery = (selectedColumns = \"*\") =>\n          [\n            getQuerySection(tokens[0]!, closing.closingParentheses, true),\n            `SELECT ${selectedColumns} FROM ${prevPrevToken.text}`,\n          ].join(\"\\n\");\n\n        expressions.push({\n          type: \"subquery\",\n          kwd,\n          ...closing,\n          getQuery,\n          endOffset: closing.closingParentheses.end,\n          alias: prevPrevToken.text,\n        });\n\n        /** Function */\n      } else if (\n        !indexOrPolicy &&\n        nextToken?.textLC === \"(\" &&\n        !t.nestingId &&\n        !nextToken.nestingId\n      ) {\n        const [func, ...otherFuncs] = ss.filter(\n          (s) => s.type === \"function\" && s.escapedIdentifier === t.text,\n        );\n        if (func && !otherFuncs.length) {\n          const closing = getClosingParenthesis(i + 2);\n          if (!closing) return;\n          const getQuery = (selectedColumns = \"*\") =>\n            `SELECT ${selectedColumns} FROM ${t.text}(${closing.query})`;\n          expressions.push({\n            type: \"function\",\n            kwd,\n            func,\n            ...closing,\n            endOffset: closing.closingParentheses.end,\n            getQuery,\n          });\n        }\n\n        /** Subquery */\n      } else if (t.text === \"(\" && !t.nestingId) {\n        const closing = getClosingParenthesis(i + 1);\n        if (!closing) return;\n        const { aliasToken } = closing;\n        let aliasColumnDefinition = \"\";\n        const tokensAfterAlias =\n          aliasToken && cb.tokens.filter((t) => t.offset >= aliasToken.end);\n        if (tokensAfterAlias?.[0]?.textLC === \"(\") {\n          const closingAliasIndex = tokensAfterAlias.findIndex(\n            (t) => t.text === \")\" && !t.nestingId,\n          );\n          if (closingAliasIndex !== -1) {\n            aliasColumnDefinition = tokensAfterAlias\n              .map((t) => t.text)\n              .slice(0, closingAliasIndex + 1)\n              .join(\"\");\n          }\n        }\n        let getQuery = (selectedColumns = \"*\") =>\n          `SELECT ${selectedColumns} FROM (${closing.query}) t${aliasColumnDefinition}`;\n        /** Lateral allows using previous table columns within the subquery. Must include all previous tables */\n        if (kwd === \"lateral\") {\n          const fromKwd = tokens.find(\n            (t) => t.textLC === \"from\" && !t.nestingId,\n          );\n          if (!fromKwd || fromKwd.offset > t.offset) return;\n          if (!aliasToken) return;\n          let isWithStart = \"\";\n          if (isWith && isWithAsSectionFinished) {\n            const selectKwd = tokens.find(\n              (t) => t.textLC === \"select\" && !t.nestingId,\n            );\n            const withEndToken = tokens.findLast(\n              (t) =>\n                selectKwd &&\n                t.textLC === \")\" &&\n                !t.nestingId &&\n                t.offset < selectKwd.offset,\n            );\n            isWithStart =\n              !withEndToken ? \"\" : (\n                getQuerySection(tokens[0]!, withEndToken, true) + \"\\n\"\n              );\n          }\n          getQuery = (selectedColumns = `${closing.alias}.*`) =>\n            [\n              isWithStart,\n              `SELECT ${selectedColumns}`,\n              getQuerySection(fromKwd, closing.aliasToken!, true),\n              \"ON TRUE\",\n            ].join(\"\\n\");\n        }\n\n        expressions.push({\n          type: \"subquery\",\n          kwd,\n          ...closing,\n          endOffset: closing.closingParentheses.end,\n          getQuery,\n        });\n\n        /** Table or view or CTE alias */\n      } else if (t.type === \"identifier.sql\" && !t.nestingId) {\n        const [matchingTable, ...otherTables] = ss.filter((s) => {\n          return (\n            ([\"table\", \"view\", \"mview\"] as const).some((t) => s.type === t) &&\n            matchTableFromSuggestions(s as any, t)\n          );\n        });\n        const alias =\n          indexOrPolicy || isAlterTable ? undefined : (\n            getAliasToken(tokens, i)?.text\n          );\n\n        /** Table or view */\n        if (matchingTable && !otherTables.length) {\n          const columns = ss.filter(\n            (s) =>\n              s.type === \"column\" &&\n              s.escapedParentName === matchingTable.escapedIdentifier,\n          );\n          if (columns.length) {\n            expressions.push({\n              type: \"tableOrView\",\n              kwd,\n              table: matchingTable,\n              columns,\n              endOffset: t.end,\n              alias: alias || matchingTable.escapedIdentifier,\n              getQuery: (selectedColumns = \"*\") =>\n                `SELECT ${selectedColumns} FROM ${matchingTable.escapedIdentifier} ${alias ? alias : \"\"}`,\n            });\n          }\n          /** CTE alias */\n        } else if (\n          !matchingTable &&\n          ((isWith && isWithAsSectionFinished) ||\n            withParentCbExpressions.length)\n        ) {\n          const withExpressions =\n            isWith && isWithAsSectionFinished ? expressions : (\n              withParentCbExpressions\n            );\n          const matchingWith = withExpressions.find(\n            (e) => e.alias === t.text && e.kwd === \"as\",\n          );\n          if (matchingWith) {\n            expressions.push({\n              ...matchingWith,\n              kwd,\n              alias,\n              cteAlias: matchingWith.alias,\n            });\n          }\n        }\n      }\n    }\n  });\n\n  return expressions;\n};\n\nexport const matchTableFromSuggestions = (\n  s:\n    | { type: \"column\"; escapedParentName: string; schema: string }\n    | {\n        type: \"table\" | \"view\" | \"mview\";\n        escapedIdentifier: string;\n        name: string;\n        schema: string;\n      },\n  t: TokenInfo,\n) => {\n  const matches =\n    s.type === \"column\" ?\n      s.escapedParentName === t.text\n    : s.escapedIdentifier === t.text || s.name === t.text;\n  if (matches) return matches;\n  const s_name = s.type === \"column\" ? s.escapedParentName : s.name;\n\n  if (t.textParts) {\n    if ([s.schema, s_name].join(\".\") === t.textParts.join(\".\")) {\n      return true;\n    }\n    const [schema, name] = t.textParts.map((t) =>\n      t.startsWith('\"') ? t : t.toLowerCase(),\n    );\n    return [s.schema, s_name].join(\".\") === [schema, name].join(\".\");\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/completionUtils/getTokens.ts",
    "content": "import type { Monaco, Token } from \"../../../W_SQL/monacoEditorTypes\";\nimport { LANG } from \"../../W_SQLEditor\";\nimport { getTokenNesting } from \"../getPrevTokensNoParantheses\";\n\nexport type TokenInfo = Pick<Token, \"offset\"> & {\n  type:\n    | \"string.sql\"\n    | \"keyword.sql\"\n    | \"white.sql\"\n    | \"identifier.sql\"\n    | \"identifier.quote.sql\"\n    | \"operator.sql\"\n    | \"delimiter.parenthesis.sql\"\n    | \"delimiter.sql\" // \",\"\n    | \"predefined.sql\" // Usually funcs like MAX / UPDATE\n    | \"keyword.choice.sql\" // WHEN\n    | \"keyword.block.sql\" // CASE\n    | \"number.sql\"\n    | \"comment.sql\"\n    | \"comment.quote.sql\";\n  end: number;\n  text: string;\n  textLC: string;\n  /**\n   * For schema.identifier tokens\n   * */\n  textParts?: [string, string];\n  lineNumber: number;\n  columnNumber: number;\n  nestingId: string;\n  funcNestingId: string;\n  nestingFuncToken: TokenInfo | undefined;\n};\n\ntype GetTokensArgs = {\n  eol: string;\n  lines: string[];\n  startOffset?: number;\n  startLine?: number;\n  currOffset?: number;\n  includeComments?: boolean;\n  editor: Monaco[\"editor\"];\n};\n\ntype GetTokensResult = {\n  tokens: TokenInfo[];\n  isCommenting: boolean;\n  text: string;\n};\n\nexport const getTokens = ({\n  eol,\n  lines,\n  startOffset = 0,\n  startLine = 1,\n  currOffset = 0,\n  editor,\n  includeComments = false,\n}: GetTokensArgs): GetTokensResult => {\n  if (startLine < 1) {\n    throw new Error(\"startLine must be greater than 0\");\n  }\n\n  let result: TokenInfo[] = [];\n  let isCommenting = false;\n  const text = lines.join(eol) + \" \";\n  const allTokens = editor.tokenize(text, LANG);\n\n  let lineStartOffset = startOffset;\n  allTokens.forEach((lineTokens, lineIdx) => {\n    if (lineIdx) {\n      const offSetForEachLineBreak = eol.length;\n      lineStartOffset +=\n        offSetForEachLineBreak + (lines[lineIdx - 1]?.length ?? 0);\n    }\n\n    for (\n      let lineTokenIdx = 0;\n      lineTokenIdx < Math.max(1, lineTokens.length - 1);\n      lineTokenIdx++\n    ) {\n      const line = lines[lineIdx];\n      const t0 = lineTokens[lineTokenIdx];\n\n      const t1 = lineTokens[lineTokenIdx + 1];\n\n      if (line === undefined) continue;\n\n      const end = t1?.offset ?? line.length;\n      const start = t0?.offset ?? 0;\n      const text = line.slice(start, end);\n      const offset = start + lineStartOffset;\n\n      const getType = (tkn: Token | undefined) =>\n        (tkn?.type as any) ??\n        ([\"::\", \"?\"].includes(text) ? \"operator.sql\" : \"unknown\");\n\n      const t: TokenInfo = {\n        offset,\n        end: offset + text.length,\n        text,\n        lineNumber: lineIdx + startLine,\n        columnNumber: start,\n        textLC: text.toLowerCase(),\n        type: getType(t0),\n        nestingId: \"\",\n        nestingFuncToken: undefined,\n        funcNestingId: \"\",\n      };\n\n      const isCurrentToken = t.offset < currOffset && t.end >= currOffset;\n      isCommenting =\n        isCommenting || (isCurrentToken && t.type.includes(\"comment\"));\n\n      result.push(t);\n\n      const isEndOfLine = lineTokenIdx === lineTokens.length - 2;\n      if (isEndOfLine) {\n        if (t1 && t1.type !== \"white.sql\") {\n          const text = line.slice(t1.offset);\n          const offset = t1.offset + lineStartOffset;\n\n          const lastT: TokenInfo = {\n            offset,\n            end: offset + text.length,\n            text: text,\n            lineNumber: t.lineNumber,\n            columnNumber: t1.offset,\n            textLC: text.toLowerCase(),\n            type: getType(t1),\n            nestingId: \"\",\n            nestingFuncToken: undefined,\n            funcNestingId: \"\",\n          };\n          result.push(lastT);\n        } else {\n          // console.log(t1, line)\n        }\n      }\n    }\n  });\n\n  result = result\n    .map((t, i) => {\n      /** Ensure escaped identifiers include end quotes  */\n      if (i) {\n        const prevT = result[i - 1];\n        const nextT = result[i + 1];\n        if (\n          t.type === \"identifier.sql\" &&\n          prevT?.text === `\"` &&\n          prevT.type === \"identifier.quote.sql\" &&\n          nextT?.text === `\"` &&\n          nextT.type === \"identifier.quote.sql\"\n        ) {\n          return {\n            ...t,\n            offset: t.offset - 1,\n            end: t.end + 1,\n            text: `\"${t.text}\"`,\n            textLC: `\"${t.textLC}\"`,\n          };\n        }\n      }\n      return t;\n    })\n    .filter((t) => t.type !== \"white.sql\" && t.type !== \"identifier.quote.sql\");\n\n  /** Groups:\n   *  - schema.identifier tokens into one\n   *  - #> into one\n   * */\n  let result1: typeof result = [];\n  let idx = 0;\n  while (result.length && idx < result.length) {\n    const t = result[idx];\n    const t1 = result[idx + 1];\n    const t2 = result[idx + 2];\n    if (\n      t &&\n      t1 &&\n      t2 &&\n      t.type === \"identifier.sql\" &&\n      t1.text === \".\" &&\n      // information_schema.columns has 'columns' being a keyword\n      (t2.type === \"identifier.sql\" || t2.type === \"keyword.sql\") &&\n      t.end === t1.offset &&\n      t1.end === t2.offset\n    ) {\n      result1.push({\n        ...t,\n        textParts: [t.text, t2.text],\n        text: [t.text, t1.text, t2.text].join(\"\"),\n        textLC: [t.textLC, t1.textLC, t2.textLC].join(\"\"),\n        end: t2.end,\n      });\n      idx = idx + 3;\n    } else if (\n      t &&\n      t1 &&\n      t.text === \"#\" &&\n      t1.text === \">\" &&\n      t.end === t1.offset\n    ) {\n      result1.push({\n        ...t,\n        text: [t.text, t1.text].join(\"\"),\n        textLC: [t.textLC, t1.textLC].join(\"\"),\n        end: t1.end,\n      });\n      idx = idx + 2;\n    } else if (t) {\n      result1.push(t);\n      idx++;\n    }\n  }\n\n  result1 = result1.map((t) => {\n    return {\n      ...t,\n      type:\n        t.textLC === \"ilike\" || t.textLC === \"like\" ? \"operator.sql\" : t.type,\n    };\n  });\n\n  if (!includeComments) {\n    result1 = result1.filter(\n      (t) => t.type !== \"comment.sql\" && t.type !== \"comment.quote.sql\",\n    );\n  }\n\n  const tokens = getTokenNesting(result1);\n\n  return {\n    text,\n    tokens,\n    isCommenting,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/getExpected.ts",
    "content": "import { isDefined } from \"prostgles-types\";\nimport type { SQLSuggestion } from \"../W_SQLEditor\";\nimport { SUGGESTION_TYPES } from \"../W_SQLEditor\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport type { CodeBlock } from \"./completionUtils/getCodeBlock\";\nimport { getJoinSuggestions } from \"./getJoinSuggestions\";\nimport type { ParsedSQLSuggestion } from \"./monacoSQLSetup/registerSuggestions\";\nimport { getKind } from \"./monacoSQLSetup/registerSuggestions\";\nimport { matchTableFromSuggestions } from \"./completionUtils/getTabularExpressions\";\n\nexport type RawExpect = string | string[] | readonly string[];\n\n/**\n * Provide tableOrView to search for view, mview, table\n */\nexport const getExpected = (\n  rawExpect: RawExpect,\n  cb: CodeBlock,\n  ss: ParsedSQLSuggestion[],\n): { suggestions: ParsedSQLSuggestion[] } => {\n  if (!rawExpect) return { suggestions: [] };\n\n  const expect =\n    rawExpect === \"trigger\" && cb.l1token?.textLC === \"event\" ?\n      \"eventTrigger\"\n    : rawExpect;\n  const exp =\n    Array.isArray(expect) ?\n      { expect, inParens: false }\n    : cleanExpectFull(expect as string);\n\n  const types = exp?.expect;\n\n  const columnExtraSnippets = suggestSnippets([\n    { label: \"*\", docs: \"All columns\", kind: getKind(\"keyword\") },\n  ]).suggestions;\n\n  const extra =\n    (\n      types?.join() === \"column\" &&\n      !exp?.inParens &&\n      cb.ltoken?.textLC === \"returning\"\n    ) ?\n      [...columnExtraSnippets]\n    : types?.join() === \"role\" ?\n      suggestSnippets([\n        { label: \"CURRENT_USER\", docs: \"Name of current execution context\" },\n        { label: \"SESSION_USER\", docs: \"Session user name\" },\n      ]).suggestions\n    : ([] as ParsedSQLSuggestion[]);\n\n  const maybeSchemaName =\n    cb.currToken?.text === \".\" ? cb.ltoken?.text\n    : cb.currToken?.text.includes(\".\") ? cb.currToken.text.split(\".\")[0]\n    : undefined;\n  const isSearchingSchemaTable =\n    maybeSchemaName ?\n      ss.find((s) => s.type === \"schema\" && s.name === maybeSchemaName)?.name\n    : undefined;\n  const suggestions = ss\n    .filter(\n      (s) =>\n        types?.includes(s.type) &&\n        (!isSearchingSchemaTable || s.schema === isSearchingSchemaTable),\n    )\n    .map((s) => {\n      const schemaSort = s.schema === \"public\" ? \"a\" : \"b\";\n      const sortText =\n        s.userInfo ? s.userInfo.priority\n        : s.dataTypeInfo ? s.dataTypeInfo.priority\n        : s.funcInfo ? `${schemaSort}${s.funcInfo.extension ? \"b\" : \"a\"}`\n        : `${(s.type === \"column\" || s.type === \"index\") && cb.tableIdentifiers.some((tableIdentifier) => matchTableFromSuggestions(s as any, tableIdentifier)) ? \"0a\" : \"b\"}${schemaSort}`;\n\n      /** Do not add schema name again if it exists */\n      let insertText = s.insertText;\n      if (\n        isSearchingSchemaTable &&\n        s.insertText.includes(`${isSearchingSchemaTable}.`)\n      ) {\n        insertText = s.escapedName ?? s.escapedIdentifier ?? s.name;\n      }\n      return {\n        ...s,\n        sortText,\n        insertText:\n          exp?.inParens ?\n            `(${s.insertText || s.escapedIdentifier})`\n          : insertText,\n      };\n    })\n    .concat(extra.map((s) => ({ ...s, sortText: \"a\" })));\n\n  const fixedTableQueries = [\"create index\", \"create policy\", \"create trigger\"];\n  const [table, ...otherTables] = cb.tableIdentifiers;\n  if (\n    (rawExpect == \"(column)\" || rawExpect == \"column\") &&\n    fixedTableQueries.some((q) => cb.textLC.trim().startsWith(q)) &&\n    table &&\n    !otherTables.length\n  ) {\n    const tableColumns = suggestions.filter(\n      (s) => s.type === \"column\" && matchTableFromSuggestions(s as any, table),\n    );\n    if (tableColumns.length) {\n      return { suggestions: tableColumns };\n    }\n  }\n\n  const joinSuggestions = getJoinSuggestions({\n    ss,\n    rawExpect,\n    cb,\n  });\n\n  if (\n    !suggestions.length &&\n    SUGGESTION_TYPES.some((ot) => types?.includes(ot))\n  ) {\n    return suggestSnippets([{ label: `No ${types} found` }]);\n  }\n\n  return { suggestions: [...suggestions, ...joinSuggestions] };\n};\n\n/**\n * Provide tableOrView to search for view, mview, table\n */\nexport const cleanExpectFull = (\n  expectRaw?: string,\n  afterExpect?: string  ,\n):\n  | { expect: SQLSuggestion[\"type\"][]; inParens: boolean; kwd?: string }\n  | undefined => {\n  let inParens = false;\n  let expect = expectRaw;\n  if (expectRaw?.startsWith(\"(\") && expectRaw.endsWith(\")\")) {\n    inParens = true;\n    expect = expectRaw.slice(1, -1);\n  }\n  if (!expect) return undefined;\n  if (expect === \"procedure\") return { expect: [\"function\"], inParens };\n  if (\n    expectRaw?.toLowerCase() === \"event\" &&\n    afterExpect?.toLowerCase() === \"trigger\"\n  ) {\n    return { expect: [\"eventTrigger\"], inParens, kwd: \"EVENT TRIGGER\" };\n  }\n  if (expect.toUpperCase() === \"MATERIALIZED\")\n    return { expect: [\"mview\"], inParens };\n  if ([\"user\", \"group\", \"owner\", \"authorization\"].includes(expect)) {\n    return { expect: [\"role\"], inParens };\n  }\n  if (expect === \"datatype\") return { expect: [\"dataType\"], inParens };\n  if (expect === \"tableOrView\") {\n    return { expect: [\"view\", \"table\", \"mview\"], inParens };\n  }\n  const cleanExp = [\n    SUGGESTION_TYPES.find(\n      (t) => t.toLowerCase() === expect.toLowerCase().trim(),\n    ),\n  ].filter(isDefined);\n  return {\n    expect: cleanExp,\n    inParens,\n  };\n};\n\nexport const cleanExpect = (\n  expectRaw?: string,\n  afterExpect?: string  ,\n) => cleanExpectFull(expectRaw, afterExpect)?.expect;\n\nexport const parseExpect = (cb: CodeBlock) => {\n  const expect = cb.tokens[1]?.textLC;\n  return cleanExpect(expect);\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/getJoinSuggestions.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport type { CodeBlock } from \"./completionUtils/getCodeBlock\";\nimport type { RawExpect } from \"./getExpected\";\nimport type { ParsedSQLSuggestion } from \"./monacoSQLSetup/registerSuggestions\";\nimport { getKind } from \"./monacoSQLSetup/registerSuggestions\";\n\ntype Args = {\n  ss: ParsedSQLSuggestion[];\n  rawExpect: RawExpect;\n  cb: CodeBlock;\n};\n\nexport const getJoinSuggestions = ({ ss, rawExpect, cb }: Args) => {\n  const { prevIdentifiers, ltoken, prevTokens } = cb;\n  if (!prevIdentifiers.length) {\n    return [];\n  }\n  if (ltoken?.textLC !== \"join\" || rawExpect !== \"tableOrView\") {\n    return [];\n  }\n  const prevTableIndexes = prevTokens\n    .map((_, i) => {\n      const prevToken = prevTokens[i - 1];\n      const isTable = prevToken && [\"from\", \"join\"].includes(prevToken.textLC);\n      return isTable ? i : undefined;\n    })\n    .filter(isDefined);\n\n  return getTableJoins(prevTableIndexes, {\n    ss,\n    rawExpect,\n    cb,\n  });\n};\n\nexport const removeQuotes = (str: string) => {\n  if (str.startsWith('\"')) {\n    str = str.slice(1);\n  }\n  if (str.endsWith('\"')) {\n    str = str.slice(0, -1);\n  }\n  return str;\n};\n\nexport const getStartingLetters = (rawString) => {\n  const inputString = removeQuotes(rawString);\n  if (typeof inputString !== \"string\" || inputString.length === 0) {\n    return \"\";\n  }\n\n  const words = inputString.split(/[_-]|(?=[A-Z])/);\n  const startingLetters = words.map((word) => word.charAt(0));\n\n  return startingLetters.join(\"\");\n};\n\nconst getTableJoins = (prevTableTokenIndexes: number[], { ss, cb }: Args) => {\n  const tableTokenIdx = prevTableTokenIndexes[0]!;\n  const fromTable = cb.prevTokens[tableTokenIdx]?.text;\n  if (!fromTable) return [];\n\n  const prevTablesIncludingFromTable = prevTableTokenIndexes\n    .map((i) => {\n      const tableName = cb.prevTokens[i]?.text;\n      const tableAliasToken =\n        cb.prevTokens[i + 1]?.textLC !== \"as\" ?\n          cb.prevTokens[i + 1]\n        : cb.prevTokens[i + 2];\n      if (!tableName) {\n        return;\n      }\n      const { tablesInfo } =\n        ss.find(\n          (t) =>\n            t.type === \"table\" &&\n            t.tablesInfo?.escaped_identifiers.includes(tableName),\n        ) ?? {};\n      const alias =\n        tableAliasToken?.type === \"identifier.sql\" ?\n          tableAliasToken.text\n        : tableName;\n      return tablesInfo && { tableName, tablesInfo, alias };\n    })\n    .filter(isDefined);\n\n  const isInPrevTables = (oid: number) =>\n    prevTablesIncludingFromTable.some((pt) => pt.tablesInfo.oid === oid);\n\n  const joinConstraints = ss\n    .map(({ type, constraintInfo }) => {\n      if (type !== \"constraint\") return;\n      if (!constraintInfo) return;\n      const {\n        contype,\n        confkey,\n        conkey,\n        table_name,\n        table_oid,\n        ftable_name,\n        ftable_oid,\n      } = constraintInfo;\n      if (!isDefined(contype) || contype !== \"f\") return;\n      if (\n        conkey === null ||\n        confkey === null ||\n        ftable_oid === null ||\n        ftable_name === null\n      )\n        return;\n\n      if (!isInPrevTables(ftable_oid) && isInPrevTables(table_oid)) {\n        return {\n          table_oid,\n          ftable_name,\n          ftable_oid,\n          conkey,\n          confkey,\n        };\n      }\n      if (!isInPrevTables(table_oid) && isInPrevTables(ftable_oid)) {\n        return {\n          table_oid: ftable_oid,\n          ftable_name: table_name,\n          ftable_oid: table_oid,\n          conkey: confkey,\n          confkey: conkey,\n        };\n      }\n    })\n    .filter(isDefined);\n\n  const currentIndentSize =\n    cb.thisLineLC.trim() ?\n      cb.thisLine.split(\" \").findIndex((char) => char !== \" \")\n    : 0;\n  const indentSize = currentIndentSize + 2;\n  const joinOptions = prevTablesIncludingFromTable\n    .map(({ tablesInfo, alias }) => {\n      return joinConstraints\n        .filter(({ table_oid }) => table_oid === tablesInfo.oid)\n        .map(({ ftable_name, ftable_oid, conkey, confkey }) => {\n          const ftable = ss.find(\n            (t) => t.type === \"table\" && t.tablesInfo?.oid === ftable_oid,\n          );\n          if (!ftable) return;\n          const joinToTableAlias = getStartingLetters(ftable_name);\n          const condition = conkey\n            .map((ordPos, cidx) => {\n              const toColumnPosition = confkey[cidx];\n              const fromTableCol = tablesInfo.cols.find(\n                (c) => c.ordinal_position === ordPos,\n              )?.escaped_identifier;\n              if (!fromTableCol) return;\n              const toTable = ss.find(\n                (s) =>\n                  s.type === \"table\" &&\n                  s.escapedIdentifier?.includes(ftable_name),\n              );\n              const toColumn = toTable?.cols?.find(\n                (c) => c.ordinal_position === toColumnPosition,\n              )?.escaped_identifier;\n              if (!toColumn) return;\n              return [\n                `${joinToTableAlias}.${toColumn}`,\n                `${alias}.${fromTableCol}`,\n              ].join(\" = \");\n            })\n            .map(\n              (condition, i) =>\n                `${\" \".repeat(indentSize)}${!i ? \"ON\" : \"AND\"} ${condition}`,\n            )\n            .join(\"\\n\");\n          const joinQuery = `${ftable_name} ${joinToTableAlias}\\n${condition}\\n`;\n          return { joinQuery, ftable };\n        })\n        .filter(isDefined);\n    })\n    .flat();\n\n  const { suggestions } = suggestSnippets(\n    joinOptions.map(({ joinQuery, ftable }) => ({\n      label: joinQuery.replaceAll(\"\\n\", \"\"),\n      insertText: joinQuery,\n      sortText: \"a\",\n      kind: getKind(\"table\"),\n      docs: ftable.documentation,\n    })),\n  );\n\n  return suggestions;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/getMatch.ts",
    "content": "import { MatchAlter } from \"./MatchAlter/MatchAlter\";\nimport { MatchCopy } from \"./MatchCopy\";\nimport { MatchCreate } from \"./MacthCreate/MatchCreate\";\nimport { MatchLast } from \"./MatchLast\";\nimport { MatchFirst } from \"./MatchFirst\";\nimport { MatchSelect } from \"./MatchSelect\";\nimport { MatchDrop } from \"./MatchDrop\";\nimport { MatchUpdate } from \"./MatchUpdate\";\nimport { MatchInsert } from \"./MatchInsert\";\nimport { MatchComment } from \"./MatchComment\";\nimport { MatchDelete } from \"./MathDelete\";\nimport { MatchReassign } from \"./MatchReassign\";\nimport { MatchWith } from \"./MatchWith\";\nimport { MatchGrant } from \"./MatchGrant\";\nimport type { SQLMatchContext } from \"./monacoSQLSetup/registerSuggestions\";\nimport { MatchVacuumOrAnalyze } from \"./MatchVacuum\";\nimport { MatchReindex } from \"./MatchReindex\";\nimport { MatchPublication } from \"./MatchPublication\";\nimport { MatchSubscription } from \"./MatchSubscription\";\nimport { MatchSet } from \"./MatchSet\";\n\nexport const SQLMatchers = {\n  MatchSet,\n  MatchSubscription,\n  MatchPublication,\n  MatchGrant,\n  MatchAlter,\n  MatchCreate,\n  MatchUpdate,\n  MatchSelect,\n  MatchWith,\n  MatchCopy,\n  MatchInsert,\n  MatchDrop,\n  MatchDelete,\n  MatchComment,\n  MatchReassign,\n  MatchLast,\n  MatchVacuum: MatchVacuumOrAnalyze,\n  MatchReindex,\n} as const;\ntype MatchFilter = (keyof typeof SQLMatchers)[];\n\nexport const getMatch = async ({\n  cb,\n  setS,\n  sql,\n  ss,\n  filter,\n}: SQLMatchContext & { filter?: MatchFilter }) => {\n  const firstTry = await MatchFirst({ cb, ss, setS, sql });\n  if (firstTry) {\n    return { firstTry, match: undefined };\n  }\n\n  const match = Object.entries(SQLMatchers)\n    .filter(([name]) => !filter?.length || filter.includes(name as any))\n    .map((m) => m[1])\n    .find((m) => m.match(cb));\n\n  return { match, firstTry: undefined };\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/getPGObjects.ts",
    "content": "import type { SQLHandler, ValidatedColumnInfo } from \"prostgles-types\";\nimport { tryCatch } from \"prostgles-types\";\nimport type { TopKeyword } from \"./KEYWORDS\";\nimport { TOP_KEYWORDS, asSQL } from \"./KEYWORDS\";\nimport { missingKeywordDocumentation } from \"../SQLEditorSuggestions\";\nimport { EXCLUDE_FROM_SCHEMA_WATCH } from \"@common/utils\";\nimport { fixIndent } from \"../../../demo/scripts/sqlVideoDemo\";\n\nexport type PGDatabase = {\n  Name: string;\n  Owner: string;\n  Encoding: string;\n  Collate: string;\n  Ctype: string;\n  \"Access privileges\": string | null;\n  Size: string;\n  Tablespace: string;\n  Description: string | null;\n  IsCurrent: boolean;\n  escaped_identifier: string;\n};\n\nexport type CASCADE =\n  | \"CASCADE\"\n  | \"RESTRICT\"\n  | \"SET NULL\"\n  | \"SET DEFAULT\"\n  | \"NO ACTION\";\nexport type PGConstraint = {\n  conname: string;\n  definition: string;\n  table_name: string;\n  ftable_name: string | null;\n  escaped_ftable_name: string | null;\n  escaped_table_name: string;\n  conkey: number[] | null;\n  confkey: number[] | null;\n  schema: string;\n  escaped_identifier: string;\n  contype: \"c\" | \"f\" | \"p\" | \"u\" | \"e\";\n  on_update_action: CASCADE | null;\n  on_delete_action: CASCADE | null;\n  table_oid: number;\n  ftable_oid: number | null;\n};\n\nexport type PG_Role = {\n  is_connected: boolean;\n  usename: string;\n  usesuper: boolean;\n  usecreatedb: boolean;\n  usebypassrls: boolean;\n  userepl: boolean;\n  escaped_identifier: string;\n  rolconfig: string[];\n  rolcanlogin: boolean;\n  priority: string;\n  is_current_user: boolean;\n  table_grants: string | null;\n};\n\nexport type PG_Index = {\n  schemaname: string;\n  indexname: string;\n  indexdef: string;\n  escaped_identifier: string;\n  escaped_tablename: string;\n  type: string;\n  owner: string;\n  tablename: string;\n  persistence: string;\n  access_method: string;\n  table_size: string;\n  description: string | null;\n  idx_scan: string;\n  idx_tup_read: string;\n  idx_tup_fetch: string;\n  index_size: string;\n};\n\nexport type PG_Trigger = {\n  disabled: boolean;\n  trigger_catalog: string;\n  trigger_schema: string;\n  trigger_name: string;\n  event_manipulation: string;\n  event_object_schema: string;\n  event_object_table: string;\n  action_statement: string;\n  action_orientation: string;\n  action_timing: string;\n  action_condition: string | null;\n  escaped_identifier: string;\n  definition: string;\n  function_definition?: string;\n};\n\nexport type PG_Rule = {\n  escaped_identifier: string;\n  tablename_escaped: string;\n  schemaname: string;\n  tablename: string;\n  rulename: string;\n  definition: string;\n};\n\nexport type PG_Policy = {\n  escaped_identifier: string;\n  policyname: string;\n  tablename: string;\n  tablename_escaped: string;\n  schemaname: string;\n  type: \"PERMISSIVE\" | \"RESTRICTIVE\";\n  roles: string[] | null;\n  definition: string;\n  cmd: null | \"SELECT\" | \"INSERT\" | \"UPDATE\" | \"DELETE\" | \"ALL\";\n  using: string;\n  with_check: string;\n};\n\nexport type PG_EventTrigger = {\n  Name: string;\n  escaped_identifier: string;\n  Event: string;\n  Owner: string;\n  Enabled: string;\n  Function: any;\n  Tags: string;\n  Description: string | null;\n  function_definition?: string;\n};\n\nexport type PG_Extension = {\n  name: string;\n  escaped_identifier: string;\n  default_version: string;\n  comment: string;\n  installed: boolean;\n};\n\nexport type PG_Keyword = {\n  label: string;\n  topKwd?: TopKeyword;\n  documentation: string;\n  insertText: string;\n  barelabel?: boolean;\n  catdesc?: string;\n  /**\n   * - C = unreserved (cannot be function or type name)\n   * - R = reserved\n   * - U = unreserved\n   * - T = reserved (can be function or type name)\n   */\n  catcode?: \"T\" | \"R\" | \"U\" | \"C\";\n};\n\ntype PG_Publication = {\n  oid: number;\n  pubname: string;\n  escaped_identifier: string;\n  pubowner: number;\n  puballtables: boolean;\n  pubinsert: boolean;\n  pubupdate: boolean;\n  pubdelete: boolean;\n  pubtruncate: boolean;\n  tables: string[];\n};\n\ntype PG_Subscription = {\n  oid: number;\n  subdbid: number;\n  subname: string;\n  subowner: number;\n  subenabled: boolean;\n  /**\n   * Removed because it causes privilege error for non admins\n   *  */\n  // subconninfo: string;\n  subslotname: string;\n  subsynccommit: string;\n  subpublications: string[];\n  escaped_identifier: string;\n};\n\ntype PG_Schema = {\n  name: string;\n  owner: string;\n  access_privileges: string | null;\n  comment: string;\n  escaped_identifier: string;\n  is_in_search_path: boolean;\n};\n\nexport type PG_Function = {\n  name: string;\n  schema: string;\n  args: {\n    label: string;\n    data_type: string;\n  }[];\n  arg_udt_names: string[] | null;\n  restype: string | null;\n  restype_udt_name: string | null;\n  arg_list_str: string;\n  description: string | null;\n  args_length: string;\n  is_aggregate: string;\n  /**\n   * a = aggregate, f = function, p = procedure, w = window function\n   */\n  prokind: \"a\" | \"f\" | \"p\" | \"w\";\n  /**\n   * provolatile tells whether the function's result depends only on its input arguments, or is affected by outside factors.\n   * It is i for “immutable” functions, which always deliver the same result for the same inputs.\n   * It is s for “stable” functions, whose results (for fixed inputs) do not change within a scan.\n   * It is v for “volatile” functions, whose results might change at any time.\n   * (Use v also for functions with side-effects, so that calls to them cannot get optimized away.)\n   */\n  provolatile: \"i\" | \"s\" | \"v\";\n  /**\n   * Function returns a set (i.e., multiple values of the specified data type)\n   * */\n  proretset: boolean;\n  definition: string | null;\n  func_signature: string;\n  escaped_identifier: string;\n  escaped_name: string;\n  extension?: string;\n};\n\nexport async function getFuncs(args: {\n  db: DB;\n  name?: string;\n  searchTerm?: string;\n  minArgs?: number;\n  limit?: number;\n  distinct?: boolean;\n}): Promise<PG_Function[]> {\n  const { db, minArgs = 0, limit = 10, distinct = false, searchTerm } = args;\n  let { name } = args;\n  if (searchTerm) {\n  } else if (name === undefined) {\n    name = \"\";\n  } else if (name === \"\") {\n    return [];\n  }\n\n  const argQ = \" AND pronargs >= ${minArgs} \",\n    lQ = \" LIMIT ${limit}\",\n    rootQ = `\n      SELECT * \n      FROM (\n        SELECT *\n          , upper(name) || '(' || concat_ws(',', arg_list_str) || ')' || ' => ' || restype || E'\\n' || description as func_signature\n          , CASE WHEN is_aggregate OR prolang IN (12, 13) THEN '' ELSE pg_get_functiondef(oid) END as definition\n          , CASE \n              WHEN schema IS NULL OR schema IN (${searchSchemas})\n                THEN format('%I', name) \n              ELSE format('%I.%I', schema, name) \n          END as escaped_identifier,\n        format('%I', name) as escaped_name\n        FROM (\n          SELECT p.proname AS name\n                , pg_catalog.pg_get_function_identity_arguments(p.oid) AS arg_list_str\n                , pg_catalog.pg_get_function_result(p.oid) as restype\n                , t.typname as restype_udt_name\n                , d.description\n                , n.nspname as schema\n                , pronargs as args_length\n                , prokind = 'a' as is_aggregate\n                , prokind\n                , proretset\n                , p.oid\n                , p.prolang -- 12, 13 are internal,c languages\n                , ext.extension\n                , provolatile\n                , arg_udt_names\n          FROM pg_catalog.pg_proc p\n          LEFT JOIN pg_type t \n            ON p.prorettype = t.oid\n          LEFT JOIN (\n            SELECT p.oid, array_agg(t.typname ORDER BY argtypoid_idx)::_TEXT as arg_udt_names\n            FROM pg_catalog.pg_proc p, \n              unnest(proargtypes) WITH ORDINALITY a(argtypoid, argtypoid_idx)\n            LEFT JOIN pg_catalog.pg_type t\n              ON argtypoid = t.oid\n            GROUP BY p.oid\n          ) at\n            ON at.oid = p.oid\n          LEFT JOIN pg_catalog.pg_description d ON d.objoid = p.oid\n          LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\n          LEFT JOIN (\n            SELECT p.oid , (array_agg(e.extname))[1] as extension\n            FROM pg_catalog.pg_extension AS e\n                INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)\n                INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)\n                INNER JOIN pg_catalog.pg_namespace AS ne ON (ne.oid = e.extnamespace)\n                INNER JOIN pg_catalog.pg_namespace AS np ON (np.oid = p.pronamespace)\n            WHERE d.deptype = 'e'\n            GROUP BY  p.oid\n          ) ext\n          ON ext.oid = p.oid\n          WHERE TRUE\n          ${argQ}\n        ) tt\n      ) tttt\n      WHERE (name ilike \\${name} OR escaped_identifier = \\${name})\n      `,\n    distQ = `\n    SELECT DISTINCT ON (length(name::text), name) *\n    FROM (\n      SELECT * \n      FROM (\n        ${rootQ}  \n      ) t\n      WHERE TRUE\n      ORDER BY arg_list_str ILIKE '%cstring%' OR arg_list_str = 'cstring'\n    ) t     \n    ORDER BY length(name::text), name, arg_list_str, description \n    ${lQ}\n  `,\n    q = rootQ + \"\\n\" + lQ;\n\n  const finalQuery = distinct ? distQ : q;\n  const funcs = await db\n    .sql(finalQuery, { name: name || \"%\", limit, minArgs })\n    .then((d) =>\n      d.rows.map((r: PG_Function) => {\n        const args = (r.arg_list_str ? r.arg_list_str.split(\",\") : []).map(\n          (a, i) => {\n            const data_type = a.trim().split(\" \").at(-1) ?? a;\n            return { label: `arg${i}: ${data_type}`, data_type };\n          },\n        );\n        r.arg_list_str = args.map((a) => a.label).join(\", \");\n\n        /** Some builtin functions (left, right) can be placed without double quotes.  */\n        if (\n          r.schema === \"pg_catalog\" &&\n          r.escaped_name.includes('\"') &&\n          !r.escaped_name.endsWith(`_user\"`) &&\n          /^[a-z_]+$/.test(r.name)\n        ) {\n          r.escaped_identifier = r.name;\n        }\n        if (r.name === \"format\" && args.length > 1) {\n          r.description += [\n            `\\n arg1 format: %[position][flags][width]type`,\n            `type:`,\n            `-  s formats the argument value as a simple string. A null value is treated as an empty string.`,\n            `-  I treats the argument value as an SQL identifier, double-quoting it if necessary. It is an error for the value to be null (equivalent to quote_ident).`,\n            `-  L quotes the argument value as an SQL literal. A null value is displayed as the string NULL, without quotes (equivalent to quote_nullable).`,\n            `\\nExamples: `,\n            `SELECT format('INSERT INTO %I VALUES(%L)', 'locations', 'C:\\\\Program Files');`,\n            `Result: INSERT INTO locations VALUES('C:\\\\Program Files')`,\n          ].join(\"\\n\");\n        }\n        if (r.name === \"dblink\") {\n          r.description = [\n            r.description || \"\",\n            `Executes a query in a remote database\\n\\n`,\n            asSQL(\n              fixIndent(`SELECT * \n          FROM dblink(\n            'dbname=mydb ',\n            'select proname, prosrc from pg_proc'\n          ) AS t1(proname name, prosrc text)\n          WHERE proname LIKE 'bytea%';`),\n            ),\n          ].join(\"\\n\");\n        }\n        return { ...r, args };\n      }),\n    );\n  if (funcs.length === limit) {\n    console.warn(\n      \"Function 8k limit reached. Some function suggestions might be missing...\",\n    );\n  }\n  return funcs;\n}\n\nexport type PG_Table = {\n  oid: number;\n  schema: string;\n  relkind: \"r\" | \"v\" | \"m\";\n  name: string;\n  escaped_identifier: string;\n  escaped_identifiers: string[];\n  /** like escaped_identifier but does not include schema */\n  escaped_name: string;\n  comment: string;\n  view_definition?: string;\n  is_view: boolean;\n  cols: {\n    name: string;\n    data_type: string;\n    udt_name: string;\n    escaped_identifier: string;\n    nullable: boolean;\n    has_default: boolean;\n    column_default?: string | null;\n    comment: string;\n    ordinal_position: number;\n    cConstraint: PGConstraint | undefined;\n    definition: string;\n\n    /**\n     * NUMERIC(numeric_precision, numeric_scale)\n     */\n    numeric_precision: number | null;\n    numeric_scale: number | null;\n    /**\n     * VARCHAR(character_maximum_length)\n     */\n    character_maximum_length: number | null;\n  }[];\n  tableStats?: TableStats;\n  constraints?: PGConstraint[];\n};\n\ntype TableStats = {\n  relid: number;\n  table_name: string;\n  seq_scans: string;\n  idx_scans: string | null;\n  n_live_tup: string;\n  n_dead_tup: string;\n  last_vacuum: string | null;\n  last_autovacuum: string | null;\n  table_size: string;\n  might_need_index: boolean;\n};\n\nconst searchSchemas = `\nSELECT btrim(\n  unnest(\n    string_to_array(\n      concat_ws(\n        ',', \n        current_setting('search_path'),\n        'pg_catalog'\n      ), \n      ','\n    )\n  )\n)`;\n\nconst searchSchemaQuery = `\n  WITH cte1 AS (\n    ${searchSchemas} as searchpath\n  )\n` as const;\nconst getSearchSchemas = async (db: DB) => {\n  const query = `\n    ${searchSchemaQuery}\n    SELECT quote_ident(schema_name) \n    FROM information_schema.schemata\n    WHERE schema_name::TEXT IN ( \n      SELECT searchpath\n      FROM cte1\n    ) \n  `;\n  const searchSchemas: string[] = await db.sql(\n    query,\n    {},\n    { returnType: \"values\" },\n  );\n  return { searchSchemas };\n};\n\nexport async function getTablesViewsAndCols(\n  db: DB,\n  tableName?: string,\n): Promise<PG_Table[]> {\n  /** Used to prevent permission erorrs */\n  const allowedSchemasQuery = `(SELECT schema_name FROM information_schema.schemata)`;\n  const { searchSchemas } = await getSearchSchemas(db);\n  const tablesAndViews = (await db.sql(\n    `\n    SELECT\n    c.oid,\n    relkind,\n    nspname as schema, \n    relname as name,\n    format('%I', relname) as escaped_name,\n    relkind IN ('v', 'm') AS is_view,\n    CASE WHEN relkind IN ('v', 'm') THEN pg_get_viewdef(format('%I.%I', nspname, relname), true) END AS view_definition ,\n    obj_description(format('%I.%I', nspname, relname)::REGCLASS) as comment,\n    CASE WHEN current_schema() = nspname THEN format('%I', relname) ELSE format('%I.%I', nspname, relname) END as escaped_identifier,\n    json_agg((\n      SELECT x FROM (\n        SELECT column_name as name,\n        format('%I', COALESCE(column_name, '')) as escaped_identifier,\n        data_type, \n        udt_name, \n        is_nullable <> 'NO' as nullable,\n        column_default IS NOT NULL as has_default,\n        column_default,\n        ordinal_position,\n        numeric_precision ,\n        numeric_scale ,\n        character_maximum_length,\n        col_description(format('%I.%I', nspname, relname)::REGCLASS, ordinal_position) as comment\n      ) as x\n    ) ORDER BY ordinal_position ) AS cols\n    -- cols\n    FROM pg_catalog.pg_class AS c\n    JOIN pg_catalog.pg_namespace AS ns\n      ON c.relnamespace = ns.oid\n    LEFT JOIN information_schema.columns cols /* FOR SOME REASON MAT VIEW COLS ARE NOT HERE (relkind=m)*/\n    ON (cols.table_schema, cols.table_name) IN ((nspname, relname))\n    WHERE relkind IN ('r', 'v', 'm' ) \n    AND nspname IN ${allowedSchemasQuery}\n    ${tableName ? \" AND relname = ${tableName} \" : \"\"}\n    AND relname NOT ILIKE 'prostgles_shell_%'\n    GROUP BY c.oid, relkind, nspname, relname;\n    `,\n    { tableName },\n    { returnType: \"rows\" },\n  )) as PG_Table[];\n\n  const { tableAndViewsStats = [] } = await tryCatch(async () => {\n    const tableAndViewsStats = (await db.sql(\n      `\n      SELECT relid,\n        relname AS table_name,\n        to_char(seq_scan, '999,999,999,999') AS seq_scans,\n        to_char(idx_scan, '999,999,999,999') AS idx_scans,\n        to_char(n_live_tup, '999,999,999,999') AS n_live_tup,\n        to_char(n_dead_tup, '999,999,999,999') AS n_dead_tup, \n        TO_CHAR(now() - last_vacuum, 'YY\"yrs\" mm\"mts\" DD\"days\" MI\"mins\" ago') AS last_vacuum,\n        TO_CHAR(now() - last_autovacuum, 'YY\"yrs\" mm\"mts\" DD\"days\" MI\"mins\" ago') AS last_autovacuum,\n        pg_size_pretty(pg_relation_size(relid::regclass)) AS table_size,\n          (50 * seq_scan > idx_scan -- more than 2%\n          AND n_live_tup > 10000\n          AND pg_relation_size(relid::regclass) > 5000000) as might_need_index\n      FROM pg_stat_all_tables\n      WHERE schemaname <> 'information_schema'\n      AND schemaname NOT ILIKE 'pg_%';\n      `,\n      {},\n      { returnType: \"rows\" },\n    )) as TableStats[];\n    return { tableAndViewsStats };\n  });\n\n  return tablesAndViews.map((t) => {\n    t.tableStats = tableAndViewsStats.find((s) => s.relid === t.oid);\n    t.escaped_identifiers = searchSchemas\n      .map((schema) => `${schema}.${t.escaped_name}`)\n      .concat([t.escaped_identifier]);\n    if (\n      [...searchSchemas, \"pg_catalog\"].some((s) =>\n        t.escaped_identifier.startsWith(`${s}.`),\n      )\n    ) {\n      t.escaped_identifiers.push(t.escaped_identifier.split(\".\")[1]!);\n    }\n\n    t.cols = t.cols\n      .filter((c) => c.udt_name)\n      .map((c) => {\n        // const cConstraint = tConstraints.find(con => [\"p\", \"f\", \"c\"].includes(con.contype) && con.conkey?.includes(c.ordinal_position)); // con.columns?.join() === c.name);\n\n        // const dataType = [\"USER-DEFINED\"].includes(c.data_type.toUpperCase())? c.udt_name.toUpperCase() : c.data_type.toUpperCase();\n\n        // c.definition = [\n        //   c.escaped_identifier,\n        //   dataType +\n        //     ((c.udt_name.toLowerCase() === \"numeric\" && c.numeric_precision !== null)?\n        //       `(${[c.numeric_precision, c.numeric_scale].join(\", \")})` :\n        //         c.character_maximum_length !== null ? `(${c.character_maximum_length})` :\n        //         \"\"\n        //     ),\n        //   c.nullable? \"\" : \"NOT NULL\",\n        //   c.column_default !== null? `DEFAULT ${c.column_default}` : \"\",\n        //   cConstraint? `, \\n ${cConstraint.definition}` : \"\"\n        // ].filter(v => v.trim()).join(\" \");\n\n        return c;\n      });\n    return t;\n  });\n}\n\nconst TOP_DATA_TYPES = [\n  \"numeric\",\n  \"integer\",\n  \"real\",\n  \"bigint\",\n  \"serial\",\n  \"boolean\",\n  \"geography\",\n  \"geometry\",\n  \"uuid\",\n  \"text\",\n  \"varchar\",\n  \"json\",\n  \"jsonb\",\n  \"text\",\n  \"tsvector\",\n  \"timestamp\",\n  \"timestamptz\",\n];\n\nexport type PG_DataType = {\n  name: string;\n  udt_name: ValidatedColumnInfo[\"udt_name\"];\n  schema: string;\n  desc: string;\n  priority: string;\n};\n\nexport async function getDataTypes(db: DB): Promise<PG_DataType[]> {\n  const q = `\n  SELECT n.nspname as schema,\n    pg_catalog.format_type(t.oid, NULL) AS name,\n    t.typname as udt_name,\n    pg_catalog.obj_description(t.oid, 'pg_type') as desc\n  FROM pg_catalog.pg_type t\n    LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\n  WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))\n    AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)\n    AND pg_catalog.pg_type_is_visible(t.oid)\n    AND t.typname <> 'internal'\n  ORDER BY 1, 2; \n\n  `;\n\n  let types = (await db.sql(q, {}, { returnType: \"rows\" })) as PG_DataType[];\n  const int = types.find((t) => t.udt_name === \"int4\");\n  const bigint = types.find((t) => t.udt_name === \"int8\");\n\n  if (int && bigint) {\n    types = [\n      ...types,\n      {\n        ...int,\n        desc: \"autoincrementing four-byte integer\",\n        name: \"serial\",\n      },\n      {\n        ...bigint,\n        desc: \"autoincrementing eight-byte integer\",\n        name: \"bigserial\",\n      },\n    ];\n  }\n  return types.map((t) => {\n    const priority = TOP_DATA_TYPES.indexOf(t.name.toLowerCase());\n    return {\n      ...t,\n      name: t.name.startsWith('\"') ? t.name : t.name.toUpperCase(),\n      priority: priority > -1 ? (priority + \"\").padStart(2, \"0\") : \"z\",\n    };\n  });\n}\n\nexport type PG_Setting = {\n  name: string;\n  unit: string | null;\n  setting: string | null;\n  setting_pretty: {\n    value: string;\n    min_val: string;\n    max_val: string;\n  } | null;\n  description: string;\n  min_val?: string;\n  max_val?: string;\n  reset_val?: string;\n  escaped_identifier: string;\n  enumvals?: null | any[];\n  category?: null | string;\n  vartype?: null | string;\n  pending_restart?: null | boolean;\n};\n\nconst getSettings = (db: DB): Promise<PG_Setting[]> => {\n  return db.sql(\n    `\n    SELECT \n      name, \n      CASE \n        WHEN unit ilike '%kb' AND setting IS NOT NULL THEN \n          jsonb_build_object(\n            'value', format('%L', pg_size_pretty((1024 * (CASE WHEN LEFT(unit, 1) = '8' THEN 8 ELSE 1 END) * setting::NUMERIC)::BIGINT)),\n            'min_val', format('%L', pg_size_pretty((1024 * (CASE WHEN LEFT(unit, 1) = '8' THEN 8 ELSE 1 END) * min_val::NUMERIC)::BIGINT)),\n            'max_val', format('%L', pg_size_pretty((1024 * (CASE WHEN LEFT(unit, 1) = '8' THEN 8 ELSE 1 END) * max_val::NUMERIC)::BIGINT))\n          )\n      END as setting_pretty,\n      setting,\n      unit, \n      short_desc as description, \n      min_val, \n      max_val, \n      reset_val\n    ,format('%I', name) as escaped_identifier, enumvals, category, vartype \n    FROM pg_catalog.pg_settings \n    order by name \n\n  `,\n    {},\n    { returnType: \"rows\" },\n  ) as any;\n};\n\nexport type PGOperator = {\n  schema: string;\n  name: string;\n  left_arg_types: string[] | null;\n  right_arg_types: string[] | null;\n  result_type: string;\n  description: string;\n};\n\nexport const PRIORITISED_OPERATORS = [\"=\", \">\", \"LIKE\", \"ILIKE\", \"IN\"];\nexport const getOperators = async (db: DB): Promise<PGOperator[]> => {\n  const operators: PGOperator[] = (await db.sql(\n    `\n    SELECT \n      schema, \n      name, \n      array_remove(array_agg(DISTINCT left_arg_type), NULL) as left_arg_types,\n      array_remove(array_agg(DISTINCT right_arg_type), NULL) as right_arg_types,\n      result_type, \n      description\n    FROM (\n      SELECT n.nspname as schema,\n        o.oprname AS name,\n        CASE WHEN o.oprkind='l' THEN NULL ELSE pg_catalog.format_type(o.oprleft, NULL) END AS \"left_arg_type\",\n        CASE WHEN o.oprkind='r' THEN NULL ELSE pg_catalog.format_type(o.oprright, NULL) END AS \"right_arg_type\",\n        pg_catalog.format_type(o.oprresult, NULL) AS \"result_type\",\n        coalesce(pg_catalog.obj_description(o.oid, 'pg_operator'),\n                pg_catalog.obj_description(o.oprcode, 'pg_proc')) AS \"description\"\n      FROM pg_catalog.pg_operator o\n          LEFT JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace\n      WHERE TRUE --n.nspname = 'pg_catalog'\n            AND n.nspname <> 'information_schema'\n        AND pg_catalog.pg_operator_is_visible(o.oid)\n      ORDER BY 1, 2, 3, 4\n    ) t\n    WHERE result_type = 'boolean'\n    GROUP BY  schema, \n      name, \n      result_type, \n      description\n  `,\n    {},\n    { returnType: \"rows\" },\n  )) as any;\n\n  const like = operators.find(\n    (o) => o.name === \"~~\" && o.description.toLowerCase().includes(\" like \"),\n  );\n  const nlike = operators.find(\n    (o) => o.name === \"!~~\" && o.description.toLowerCase().includes(\" like \"),\n  );\n  if (like && nlike) {\n    operators.push({\n      ...like,\n      name: \"LIKE\",\n      description: like.description + `.\\n\\n Same as ${like.name} operator`,\n    });\n    operators.push({\n      ...nlike,\n      name: \"NOT LIKE\",\n      description: nlike.description + `.\\n\\n Same as ${nlike.name} operator`,\n    });\n  }\n  const ilike = operators.find(\n    (o) => o.name === \"~~*\" && o.description.toLowerCase().includes(\" like \"),\n  );\n  const nilike = operators.find(\n    (o) => o.name === \"!~~*\" && o.description.toLowerCase().includes(\" like \"),\n  );\n  if (ilike && nilike) {\n    operators.push({\n      ...ilike,\n      name: \"ILIKE\",\n      description: ilike.description + `.\\n\\n Same as ${ilike.name} operator`,\n    });\n    operators.push({\n      ...nilike,\n      name: \"NOT ILIKE\",\n      description: nilike.description + `.\\n\\nSame as ${nilike.name} operator`,\n    });\n  }\n  operators.push({\n    left_arg_types: [\"any\"],\n    result_type: \"boolean\",\n    right_arg_types: [\"any\"],\n    schema: \"pg_catalog\",\n    name: \"IN\",\n    description: `IN operator. Returns true if the left argument is equal to any element in the right argument or subquery.`,\n  });\n  operators.push({\n    left_arg_types: [\"any\"],\n    result_type: \"boolean\",\n    right_arg_types: [\"any\"],\n    schema: \"pg_catalog\",\n    name: \"BETWEEN\",\n    description: `BETWEEN operator. Returns true if the left argument is between/inclusive of the range endpoints.`,\n  });\n  operators.push({\n    left_arg_types: [\"any\"],\n    result_type: \"boolean\",\n    right_arg_types: [\"any\"],\n    schema: \"pg_catalog\",\n    name: \"DISTINCT FROM\",\n    description: `IS DISTINCT FROM operator. Returns true if the left argument is Not equal to the right expression, treating null as a comparable value.`,\n  });\n  return operators;\n};\n\nexport const PG_OBJECT_QUERIES = {\n  operators: {\n    sql: undefined,\n    type: {} as PGOperator,\n    getData: getOperators,\n  },\n  settings: {\n    sql: undefined,\n    type: {} as PG_Setting,\n    getData: getSettings,\n  },\n\n  dataTypes: {\n    type: {} as PG_DataType,\n    sql: undefined,\n    getData: getDataTypes,\n  },\n  tables: {\n    sql: undefined,\n    type: {} as PG_Table,\n    getData: (db: DB) => getTablesViewsAndCols(db),\n  },\n  functions: {\n    sql: undefined,\n    getData: (db: DB) =>\n      getFuncs({\n        db,\n        minArgs: 0,\n        limit: 8000,\n        distinct: false,\n      }),\n    type: {} as PG_Function,\n  },\n\n  subscriptions: {\n    sql: `SELECT \n        oid\n      , subdbid\n      , subname\n      , subowner\n      , subenabled\n      -- , subconninfo\n      , subslotname\n      , subsynccommit\n      , subpublications \n      , format('%I', subname) as escaped_identifier \n      FROM pg_catalog.pg_subscription `,\n    type: {} as PG_Subscription,\n  },\n  schemas: {\n    sql: `\n      ${searchSchemaQuery}\n      SELECT n.nspname AS \"name\",                                          \n        pg_catalog.pg_get_userbyid(n.nspowner) AS \"owner\",                 \n        pg_catalog.array_to_string(n.nspacl, E'\\n') AS \"access_privileges\",\n        pg_catalog.obj_description(n.oid, 'pg_namespace') AS \"comment\",\n        format('%I', n.nspname) as escaped_identifier,\n        CASE WHEN n.nspname IN (select searchpath from cte1) THEN true ELSE false END as is_in_search_path\n      FROM pg_catalog.pg_namespace n                                       \n      --WHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'      \n      ORDER BY 1;\n    `,\n    type: {} as PG_Schema,\n  },\n  publications: {\n    sql: `\n      SELECT p.*, t.tables,\n        format('%I', p.pubname) as escaped_identifier \n      FROM pg_catalog.pg_publication p\n      LEFT JOIN (\n        SELECT pubname, string_agg(tablename::TEXT, ', ') as tables\n        FROM pg_catalog.pg_publication_tables \n        GROUP BY pubname\n      ) t\n      ON p.pubname = t.pubname \n    `,\n    type: {} as PG_Publication,\n  },\n  databases: {\n    sql: `\n      /* psql \\\\l+ --list databases */\n      SELECT d.datname as \"Name\",\n            format('%I', datname) as escaped_identifier ,\n            d.datname = current_database() as \"IsCurrent\",\n            pg_catalog.pg_get_userbyid(d.datdba) as \"Owner\",\n            pg_catalog.pg_encoding_to_char(d.encoding) as \"Encoding\",\n            d.datcollate as \"Collate\",\n            d.datctype as \"Ctype\",\n            pg_catalog.array_to_string(d.datacl, E'\\n') AS \"Access privileges\",\n            CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n                  THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n                  ELSE 'No Access'\n            END as \"Size\",\n            t.spcname as \"Tablespace\",\n            pg_catalog.shobj_description(d.oid, 'pg_database') as \"Description\"\n      FROM pg_catalog.pg_database d\n        JOIN pg_catalog.pg_tablespace t on d.dattablespace = t.oid\n      ORDER BY 1;\n    `,\n    type: {} as PGDatabase,\n  },\n\n  constraints: {\n    sql: `\n      SELECT conname,\n      conkey ,  confkey ,\n      pg_get_constraintdef(c.oid) as definition, contype, \n      rel.relname as table_name ,\n      format('%I', conname) as escaped_identifier,\n      format('%I', rel.relname) as escaped_table_name,\n      frel.relname as ftable_name,\n      CASE WHEN frel.relname IS NOT NULL THEN format('%I', frel.relname) END as escaped_ftable_name,\n      nspname as schema, \n      CASE c.confdeltype\n          WHEN 'a' THEN 'NO ACTION'\n          WHEN 'r' THEN 'RESTRICT'\n          WHEN 'c' THEN 'CASCADE'\n          WHEN 'n' THEN 'SET NULL'\n          WHEN 'd' THEN 'SET DEFAULT'\n          ELSE NULL -- Should only be relevant for FKs\n      END as on_delete_action,\n      CASE c.confupdtype\n          WHEN 'a' THEN 'NO ACTION'\n          WHEN 'r' THEN 'RESTRICT'\n          WHEN 'c' THEN 'CASCADE'\n          WHEN 'n' THEN 'SET NULL'\n          WHEN 'd' THEN 'SET DEFAULT'\n          ELSE NULL -- Should only be relevant for FKs\n      END as on_update_action ,\n      c.conrelid as table_oid,\n      c.confrelid as ftable_oid\n      FROM pg_catalog.pg_constraint c\n      INNER JOIN pg_catalog.pg_class rel\n        ON rel.oid = c.conrelid\n      LEFT JOIN pg_catalog.pg_class frel\n        ON frel.oid = c.confrelid\n      INNER JOIN pg_catalog.pg_namespace nsp\n        ON nsp.oid = connamespace\n    `,\n    type: {} as PGConstraint,\n  },\n\n  roles: {\n    sql: `\n      WITH grants AS (\n        SELECT grantee,\n        string_agg(schema_table_privilege,  E'\\n') as table_grants\n        FROM (\n          SELECT grantee, table_schema,\n            string_agg(format('%I.%I', table_schema, table_name) || ': ' || table_privilege, E'\\n') as schema_table_privilege\n          FROM \n          (\n            SELECT grantee, table_schema, table_name,\n            CASE WHEN COUNT(table_privilege) = 7 THEN 'ALL' ELSE \n            E'\\n' || string_agg(repeat(' ', 6) || table_privilege, E',\\n') END as table_privilege\n            FROM \n            (\n              SELECT cp.grantee, cp.table_schema, cp.table_name,\n              CASE WHEN count(cp.column_name) < MAX(total_columns) AND MAX(cp.column_name) <> '' THEN\n              format(\n                '%s ( %s )', \n                cp.privilege_type, \n                string_agg(cp.column_name, ', ' ORDER BY cp.column_name) \n              ) ELSE cp.privilege_type END as table_privilege\n              FROM (\n                SELECT table_schema, table_name, grantee, privilege_type, column_name \n                FROM information_schema.column_privileges cp\n                UNION  \n                SELECT table_schema, table_name, grantee, privilege_type, '' as column_name\n                FROM information_schema.table_privileges \n              ) cp\n              LEFT JOIN (\n                SELECT table_name, table_schema, MAX(ordinal_position) as total_columns\n                FROM information_schema.columns \n                GROUP BY table_name, table_schema\n              ) c\n              ON c.table_name = cp.table_name\n              AND c.table_schema = cp.table_schema\n              WHERE cp.table_schema NOT IN ( 'information_schema', 'pg_catalog' )\n              GROUP BY cp.grantee, cp.table_schema, cp.table_name, cp.privilege_type\n            ) t\n            GROUP BY grantee, table_schema, table_name\n          ) tt\n          GROUP BY grantee, table_schema\n        ) ttt\n        GROUP BY grantee\n      )\n\n      SELECT \n        format('%I', rolname ) as escaped_identifier,\n        rolname as usename,  \n        rolsuper  as usesuper, rolcreatedb as usecreatedb, rolbypassrls as usebypassrls, \n        rolreplication  as userepl, rolconfig, rolcanlogin,\n        lpad(ROW_NUMBER () OVER (ORDER BY sort_text)::text, 2, '0') as priority,\n        rolname = CURRENT_USER AS is_current_user,\n        table_grants,\n        EXISTS (\n          SELECT 1\n          FROM pg_stat_activity\n          WHERE usename = rolname\n        ) as is_connected\n      FROM (\n        SELECT *, replace(format('%I', rolname ), 'pg_', 'Ω') as sort_text\n        FROM pg_catalog.pg_roles \n      ) t\n      LEFT JOIN grants g\n      ON t.rolname = g.grantee\n      ORDER BY sort_text\n    `,\n    type: {} as PG_Role,\n  },\n\n  indexes: {\n    sql: `\n      SELECT n.nspname as schemaname,\n        c.relname as indexname,\n        pg_get_indexdef(c.oid) as indexdef,\n        format('%I', c.relname) as escaped_identifier,\n        CASE WHEN current_schema() = n.nspname THEN format('%I', c2.relname) ELSE format('%I.%I', n.nspname, c2.relname) END as escaped_tablename,\n        CASE c.relkind WHEN 'r' \n          THEN 'table' WHEN 'v' \n          THEN 'view' WHEN 'm' \n          THEN 'materialized view' \n          WHEN 'i' THEN 'index' \n          WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' \n          WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' \n          WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \"type\",\n        pg_catalog.pg_get_userbyid(c.relowner) as \"owner\",\n        c2.relname as tablename,\n        CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' \n        WHEN 'u' THEN 'unlogged' END as \"persistence\",\n        am.amname as \"access_method\",\n        pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \"table_size\",\n        pg_catalog.obj_description(c.oid, 'pg_class') as \"description\"\n        , to_char(idx_scan, '999,999,999,999') as idx_scan\n        , to_char(idx_tup_read, '999,999,999,999') as idx_tup_read\n        , to_char(idx_tup_fetch, '999,999,999,999') as idx_tup_fetch\n        , pg_size_pretty(pg_relation_size(stat.indexrelid::regclass)) as index_size\n      FROM pg_catalog.pg_class c\n          LEFT JOIN pg_catalog.pg_stat_all_indexes stat\n            ON c.oid = stat.indexrelid\n          LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n          LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\n          LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\n          LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\n      WHERE c.relkind IN ('i','I','')\n            AND n.nspname <> 'pg_catalog'\n            AND n.nspname !~ '^pg_toast'\n            AND n.nspname <> 'information_schema'\n        AND pg_catalog.pg_table_is_visible(c.oid)\n      ORDER BY 1,2;\n    `,\n    type: {} as PG_Index,\n  },\n\n  triggers: {\n    sql: `\n      SELECT \n        tgenabled = 'D' as disabled,\n        trigger_catalog, trigger_schema, trigger_name, event_manipulation, \n        event_object_schema, \n        format('%I.%I', trigger_schema, event_object_table ) as event_object_table,\n        action_statement, action_orientation,\n        action_timing, action_condition,\n        format('%I', trigger_name ) as escaped_identifier\n        , pg_get_triggerdef(oid) as definition\n        , pg_get_functiondef(tgfoid) as function_definition\n      FROM information_schema.triggers t\n      LEFT JOIN pg_catalog.pg_trigger tr \n      ON t.trigger_name = tr.tgname\n  `,\n    type: {} as PG_Trigger,\n  },\n\n  policies: {\n    sql: (tableName?: string) => {\n      /** Used to prevent error \"permission denied for table pg_authid\" */\n      let roles_query = \"\";\n      // try {\n      //   await db.sql(\"SELECT 1 FROM pg_authid\", {});\n      // roles_query = `ELSE ARRAY(\n      //   SELECT pg_authid.rolname\n      //   FROM pg_authid\n      //   WHERE pg_authid.oid = ANY (pol.polroles)\n      //   ORDER BY pg_authid.rolname\n      // )::text[]`\n      // } catch(err){\n\n      // }\n      roles_query = `ELSE ARRAY( \n        SELECT pg_roles.rolname\n        FROM pg_roles\n        WHERE pg_roles.oid = ANY (pol.polroles)\n        ORDER BY pg_roles.rolname\n      )::text[]`;\n\n      return `\n      /*  ${EXCLUDE_FROM_SCHEMA_WATCH} */\n      ${searchSchemaQuery}\n      SELECT *, \n        concat_ws( \n          E'\\\\n', \n          'CREATE POLICY ' || escaped_identifier,\n          'ON ' || tablename,\n          'AS ' || \"type\",\n          'FOR ' || cmd,\n          'TO ' || array_to_string(roles, ', '),\n          CASE WHEN \"using\" IS NOT NULL THEN 'USING (' || trim(\"using\", '()') || ')' END,\n          CASE WHEN with_check IS NOT NULL THEN 'WITH CHECK (' || trim(with_check, '()') || ')' END\n        ) as definition\n        FROM (\n          SELECT\n            CASE WHEN nspname IN (SELECT searchpath FROM cte1) THEN format('%I', polname) ELSE format('%I.%I', nspname, polname) END as escaped_identifier,\n            CASE WHEN nspname IN (SELECT searchpath FROM cte1) THEN format('%I', c.relname) ELSE format('%I.%I', nspname, c.relname) END as tablename_escaped,\n            n.nspname AS schemaname,\n            c.relname AS tablename,\n            pol.polname AS policyname,\n            CASE\n              WHEN pol.polpermissive THEN 'PERMISSIVE'::text\n              ELSE 'RESTRICTIVE'::text\n            END AS type,\n            CASE\n                WHEN pol.polroles = '{0}'::oid[] THEN NULL\n                ${roles_query}\n              END AS roles,\n            CASE pol.polcmd\n                WHEN 'r'::\"char\" THEN 'SELECT'::text\n                WHEN 'a'::\"char\" THEN 'INSERT'::text\n                WHEN 'w'::\"char\" THEN 'UPDATE'::text\n                WHEN 'd'::\"char\" THEN 'DELETE'::text\n                WHEN '*'::\"char\" THEN 'ALL'::text\n                ELSE NULL::text\n            END AS cmd,\n            pg_get_expr(pol.polqual, pol.polrelid) AS \"using\",\n            pg_get_expr(pol.polwithcheck, pol.polrelid) AS with_check\n          FROM pg_policy pol\n            JOIN pg_class c ON c.oid = pol.polrelid\n          LEFT JOIN pg_namespace n ON n.oid = c.relnamespace\n          ${!tableName ? \"\" : `WHERE quote_ident(c.relname) = \\${tableName}`}\n        ) t `;\n    },\n    type: {} as PG_Policy,\n  },\n\n  eventTriggers: {\n    sql: `\n      SELECT evtname as \"Name\", evtevent as \"Event\", pg_catalog.pg_get_userbyid(e.evtowner) as \"Owner\",\n        format('%I', evtname ) as escaped_identifier,\n        case evtenabled when 'O' then 'enabled'  when 'R' then 'replica'  when 'A' then 'always'  when 'D' then 'disabled' end as \"Enabled\",\n        e.evtfoid::pg_catalog.regproc as \"Function\", \n        pg_catalog.array_to_string(array(select x from pg_catalog.unnest(evttags) as t(x)), E',\\n') as \"Tags\",\n        pg_catalog.obj_description(e.oid, 'pg_event_trigger') as \"Description\"\n        , pg_get_functiondef(evtfoid::REGCLASS) as function_definition\n      FROM pg_catalog.pg_event_trigger e \n      ORDER BY 1\n    `,\n    type: {} as PG_EventTrigger,\n  },\n\n  rules: {\n    sql: `\n      SELECT *\n        , format('%I', rulename) as escaped_identifier \n        , format('%I.%I', schemaname, tablename) as tablename_escaped\n      FROM pg_catalog.pg_rules\n    `,\n    type: {} as PG_Rule,\n  },\n\n  keywords: {\n    sql: undefined,\n    type: {} as PG_Keyword,\n    getData: async (db: DB) => {\n      const allKeywords = (\n        await db.sql(\n          \"select upper(word) as word, barelabel, catcode, catdesc from pg_get_keywords();\",\n          {},\n          { returnType: \"rows\" },\n        )\n      ).concat([\n        { word: \"RAISE\" },\n        { word: \"NOTICE\" },\n        { word: \"IF NOT EXISTS\" },\n        { word: \"INSERT INTO\" },\n        { word: \"DELETE FROM\" },\n      ]) as {\n        word: string;\n        barelabel?: boolean;\n      }[];\n      return allKeywords.map(({ word: label, ...rest }) => {\n        const topKwd = TOP_KEYWORDS.find((k) => k.label === label);\n        let documentation = missingKeywordDocumentation[label] ?? label;\n        const insertText =\n          label === \"IN\" ? `IN ( $0 )` : (\n            (topKwd?.insertText ??\n            ([\"BEGIN\", \"COMMIT\"].includes(label) ? label + \";\\n\" : label))\n          );\n        if (topKwd?.info) {\n          documentation = topKwd.info;\n        }\n        return {\n          label,\n          topKwd,\n          documentation,\n          insertText,\n          ...rest,\n        } satisfies PG_Keyword;\n      });\n    },\n  },\n\n  extensions: {\n    sql: `SELECT *, format('%I', COALESCE(name, '')) as escaped_identifier, installed_version IS NOT NULL as installed  FROM pg_available_extensions`,\n    type: {} as PG_Extension,\n  },\n} as const satisfies Record<\n  string,\n  | {\n      type: any;\n      sql: string | ((...args: any) => string);\n      getData?: undefined;\n    }\n  | {\n      type: any;\n      sql?: undefined;\n      getData: (...args: any) => Promise<any>;\n    }\n>;\n\ntype PG_OBJECT_DATA = {\n  [key in keyof typeof PG_OBJECT_QUERIES]: (typeof PG_OBJECT_QUERIES)[key][\"type\"][];\n};\n\ntype DB = { sql: SQLHandler };\nexport const getPGObjects = async (db: DB) => {\n  const data: PG_OBJECT_DATA = Object.fromEntries(\n    await Promise.all(\n      Object.entries(PG_OBJECT_QUERIES).map(async ([type, qparams]) => {\n        const { sql: sqlOrFunc } = qparams;\n        let result: any[] = [];\n        try {\n          if (sqlOrFunc === undefined) {\n            result = await qparams.getData(db);\n          } else {\n            let sql = \"\";\n            if (typeof sqlOrFunc === \"function\") {\n              sql = sqlOrFunc();\n            } else {\n              sql = sqlOrFunc;\n            }\n            result = await db.sql(sql, {}, { returnType: \"rows\" });\n          }\n        } catch (e) {\n          console.error(`Could not load ${type}`, e);\n        }\n\n        return [type, result];\n      }),\n    ),\n  );\n\n  return data;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/getPrevTokensNoParantheses.ts",
    "content": "import type { TokenInfo } from \"./completionUtils/getTokens\";\n\nexport const getPrevTokensNoParantheses = (\n  prevTokens: TokenInfo[],\n  excludeParantheses = false,\n) => {\n  const pt = structuredClone(prevTokens);\n  const prevTokensReversed: typeof prevTokens = [];\n  let hasParanthese = 0;\n  pt.slice(0)\n    .reverse()\n    .forEach((t: TokenInfo) => {\n      const whiteToken: TokenInfo = {\n        ...t,\n        type: \"white.sql\",\n        text: \" \",\n        textLC: \" \",\n      };\n\n      if (excludeParantheses && t.text === \"()\") {\n        prevTokensReversed.push(whiteToken);\n        return;\n      }\n\n      /** Monaco includes double parantheses: t.text === \"))\"  */\n      const uniqueText = Array.from(new Set(t.text.split(\"\"))).join(\"\");\n      const pushCurrentParantheses = () => {\n        t.text.split(\"\").map((text, i) => {\n          prevTokensReversed.push({\n            ...(excludeParantheses ? whiteToken : { ...t, text }),\n            offset: t.offset - i,\n            end: t.end - i,\n          });\n        });\n      };\n      if (uniqueText === \"(\" && hasParanthese) {\n        hasParanthese--;\n        if (!hasParanthese) {\n          pushCurrentParantheses();\n        }\n        return;\n      }\n\n      if (uniqueText === \")\") {\n        hasParanthese++;\n        if (hasParanthese === 1) {\n          pushCurrentParantheses();\n        }\n        return;\n      }\n\n      const willPush = !hasParanthese ? t : whiteToken;\n      prevTokensReversed.push(willPush);\n    });\n  return prevTokensReversed.slice(0).reverse();\n};\n\nexport const getTokenNesting = (\n  rawTokens: TokenInfo[],\n): (TokenInfo & {\n  nestingId: string;\n  nestingFuncToken: TokenInfo | undefined;\n})[] => {\n  const startNest = \"(\";\n  const endNest = \")\";\n\n  /** Separate grouped tokens */\n  const tokens = rawTokens.flatMap((t) => {\n    const textChars = t.text.split(\"\");\n\n    const uniqueText = Array.from(new Set(textChars));\n    const allTextIsParens = uniqueText.every((tokenChar) =>\n      [startNest, endNest].some((v) => v === tokenChar),\n    );\n    if (allTextIsParens) {\n      return textChars.map((text, i) => {\n        const offset = t.offset + i;\n        return {\n          ...t,\n          text,\n          textLC: text,\n          offset,\n          end: offset + 1,\n        };\n      });\n    }\n    return t;\n  });\n\n  let nestingGroupId = 0;\n  /** 111 */\n  let nestingId = \"\";\n  const nestingFuncTokens: Record<string, TokenInfo | undefined> = {};\n  const normalTokens = tokens.map((t, i) => {\n    const maybeFuncToken = tokens[i - 2];\n    const prevToken = tokens[i - 1];\n    if (prevToken?.text === startNest) {\n      nestingId += \"1\";\n      nestingFuncTokens[nestingId] = maybeFuncToken;\n    }\n\n    if (t.text === endNest) {\n      nestingId = nestingId.slice(0, -1);\n      if (!nestingId) {\n        nestingGroupId++;\n      }\n    }\n\n    return {\n      ...t,\n      nestingId,\n      nestingFuncToken: nestingFuncTokens[nestingId],\n    };\n  });\n\n  const funcNests = getFuncNesting(normalTokens);\n\n  return funcNests;\n};\n\nconst getFuncNesting = (tokensWithNestingId: TokenInfo[]) => {\n  const withFuncsTokens = getDollarFunctions(tokensWithNestingId);\n\n  let funcNestingId = \"\";\n  type NestEndMatcher = (t: TokenInfo, i: number) => boolean;\n  const matchers = {\n    end: (t: TokenInfo, i: number) => {\n      return t.textLC === \"end\";\n    },\n    loop: (t: TokenInfo, i: number) => {\n      return (\n        t.textLC === \"end\" && tokensWithNestingId[i + 1]?.textLC === \"loop\"\n      );\n    },\n  } satisfies Record<string, NestEndMatcher>;\n  const nestMatcher = (t: TokenInfo, i: number): NestEndMatcher | undefined => {\n    const prevPrevToken = tokensWithNestingId[i - 2];\n    const prevToken = tokensWithNestingId[i - 1];\n    if (prevToken?.textLC === \"begin\") {\n      return matchers.end;\n    } else if (\n      prevToken?.textLC === \"loop\" &&\n      prevPrevToken?.textLC !== \"end\"\n    ) {\n      return matchers.loop;\n    }\n\n    return undefined;\n  };\n\n  let nests: NestEndMatcher[] = [];\n  return tokensWithNestingId.map((t, i) => {\n    const isInsidefunc = withFuncsTokens.find(\n      ({ startIdx, endIdx }) => i >= startIdx + 2 && i <= endIdx - 1,\n    );\n\n    if (isInsidefunc) {\n      const newNest = nestMatcher(t, i);\n      if (newNest) {\n        nests.push(newNest);\n      } else {\n        const lastNest = nests.at(-1);\n        const ended = lastNest?.(t, i);\n        if (ended) {\n          nests = nests.slice(0, -1);\n        }\n      }\n\n      funcNestingId = \"1\".repeat(nests.length);\n\n      if (plpgKeywords.includes(t.textLC.toUpperCase())) {\n        t.type = \"keyword.sql\";\n      }\n    } else {\n      funcNestingId = \"\";\n    }\n\n    return {\n      ...t,\n      funcNestingId: isInsidefunc ? \"1\" + funcNestingId : \"\",\n    };\n  });\n};\n\nexport const getDollarFunctions = (\n  tokens: TokenInfo[],\n): { startIdx: number; endIdx: number }[] => {\n  const plpgPrecedingKeywords = [\"function\", \"do\", \"procedure\"];\n  const result: { startIdx: number; endIdx: number }[] = [];\n  tokens.forEach((t, i) => {\n    const prevToken = tokens[i - 1];\n    if (\n      !t.nestingId &&\n      t.text.startsWith(\"$\") &&\n      prevToken &&\n      plpgPrecedingKeywords.includes(prevToken.textLC)\n    ) {\n      const endIdx = tokens.findIndex(\n        (et) => et.text === t.text && et.offset > t.offset,\n      );\n      if (endIdx > -1) {\n        result.push({ startIdx: i, endIdx });\n      }\n    }\n  });\n  return result;\n};\n\nconst plpgKeywords = [\n  \"ALL\",\n  \"BEGIN\",\n  \"BY\",\n  \"CASE\",\n  \"DECLARE\",\n  \"ELSE\",\n  \"END\",\n  \"EXECUTE\",\n  \"FOR\",\n  \"FOREACH\",\n  \"FROM\",\n  \"IF\",\n  \"IN\",\n  \"INTO\",\n  \"LOOP\",\n  \"NOT\",\n  \"NULL\",\n  \"OR\",\n  \"STRICT\",\n  \"THEN\",\n  \"TO\",\n  \"USING\",\n  \"WHEN\",\n  \"WHILE\",\n];\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/jsonbPathSuggest.ts",
    "content": "import { isObject } from \"prostgles-types\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { asSQL } from \"./KEYWORDS\";\nimport { getTableExpressionSuggestions } from \"./completionUtils/getTableExpressionReturnTypes\";\nimport {\n  type SQLMatcherResultArgs,\n  getKind,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport { isDefined } from \"../../../utils/utils\";\nexport const jsonbPathSuggest = async ({\n  cb,\n  ss,\n  parentCb,\n  sql,\n}: SQLMatcherResultArgs) => {\n  const { currToken, prevText, ltoken, offset } = cb;\n\n  /** field->'path' */\n  let lastTextContin = prevText.trim().split(\" \").at(-1);\n  const JSON_SELS = [\"->\", \"->>\", \"#>\"];\n  const isJsonPath =\n    [\"operator.sql\", \"string.sql\", \"number.sql\"].includes(\n      cb.ltoken?.type ?? \"\",\n    ) &&\n    lastTextContin &&\n    JSON_SELS.some((sel) => lastTextContin?.includes(sel));\n  const edgeToken =\n    isJsonPath ? undefined\n    : currToken?.type === \"identifier.sql\" && offset === currToken.end ?\n      currToken\n    : ltoken?.type === \"identifier.sql\" && offset - 1 === ltoken.end ? ltoken\n    : undefined;\n\n  if (isJsonPath || edgeToken) {\n    /**\n     * json_col_name\n     */\n    const maybeJSON =\n      edgeToken ??\n      cb.prevTokens\n        .slice(0)\n        .reverse()\n        .find((t) => t.type.includes(\"identifier\"));\n    const { columnsWithAliasInfo } = await getTableExpressionSuggestions(\n      { parentCb, cb, ss, sql },\n      \"columns\",\n    );\n    const maybeJsonCol = columnsWithAliasInfo.find(\n      (c) =>\n        (c.s.insertText === maybeJSON?.text ||\n          c.s.escapedIdentifier === maybeJSON?.text) &&\n        c.s.colInfo?.udt_name.startsWith(\"json\"),\n    );\n    if (maybeJSON && maybeJsonCol) {\n      if (isJsonPath) {\n        const ending = JSON_SELS.find((sel) => lastTextContin!.endsWith(sel));\n        if (ending) {\n          lastTextContin = lastTextContin!.slice(0, -ending.length);\n        }\n      }\n\n      /**\n       * maybeJSON->path->path\n       */\n      const selectorTokens = [...cb.prevTokens, cb.currToken]\n        .filter((t) => t && t.offset >= maybeJSON.offset)\n        .filter(isDefined)\n        .map((t, i, arr) => {\n          /* Exclude last json operator */\n          return i === arr.length - 1 && JSON_SELS.includes(t.text) ?\n              \"\"\n            : t.text;\n        })\n        .join(\"\");\n      const selector = edgeToken?.text ?? selectorTokens;\n      try {\n        const query =\n          maybeJsonCol.getQuery(`DISTINCT ${selector} as v`) +\n          `\\nWHERE ${selector} IS NOT NULL \n          LIMIT 5`;\n        const { rows } =\n          !query ?\n            { rows: [] }\n          : ((await sql?.(\n              query,\n              {},\n              { returnType: \"default-with-rollback\" },\n            )) ?? { rows: [] });\n\n        if (rows.length) {\n          let suggestions: { label: string; type: string; vals: string[] }[] =\n            [];\n          const firstVal = rows[0]?.v;\n          const getType = (val: any) =>\n            Array.isArray(val) ? \"Array\" : typeof val;\n          const getVal = (val: any): string => val; // (getType(val) !== \"Array\" && !isObject(val))? val.toString() : JSON.stringify(val, null, 2)\n          if (Array.isArray(firstVal)) {\n            suggestions = [\n              {\n                label: \"->0\",\n                type: getType(firstVal),\n                vals: firstVal.map((v) => getVal(v)),\n              },\n            ];\n          } else if (isObject(firstVal)) {\n            rows.forEach(({ v }) =>\n              Object.keys(v).map((key) => {\n                const val = v[key];\n                const type = getType(val);\n                const s = {\n                  label: `->${type !== \"object\" ? \">\" : \"\"}'${key}'`,\n                  type,\n                  vals: [getVal(val)],\n                };\n                const matchIdx = suggestions.findIndex(\n                  (_s) => _s.label === s.label,\n                );\n                if (matchIdx < 0) {\n                  suggestions.push(s);\n                } else {\n                  suggestions[matchIdx]!.vals = Array.from(\n                    new Set([...suggestions[matchIdx]!.vals, ...s.vals]),\n                  );\n                }\n              }),\n            );\n          }\n\n          if (suggestions.length) {\n            const sugs = suggestSnippets(\n              suggestions.map(({ label: rawLabel, type, vals }) => {\n                const label =\n                  (\n                    cb.currToken?.type === \"operator.sql\" &&\n                    rawLabel.startsWith(cb.currToken.text)\n                  ) ?\n                    rawLabel.slice(cb.currToken.text.length)\n                  : cb.currToken?.text.includes(\">\") ?\n                    rawLabel.slice(1 + rawLabel.indexOf(\">\"))\n                  : rawLabel;\n\n                return {\n                  label: { label, description: type },\n                  docs: asSQL(\n                    `Values:\\n\\n${vals\n                      .map((value) => {\n                        const v =\n                          Array.isArray(value) ? value.slice(0, 2) : value;\n                        const res = JSON.stringify(v, null, 2);\n                        if (res.length > 500) {\n                          return JSON.stringify(getJSONStructure(v), null, 2);\n                        }\n                        return res;\n                      })\n                      .join(\",\\n\")}`,\n                    \"javascript\",\n                  ),\n                  insertText: label,\n                  insertTextRules: 1,\n                  kind: getKind(\"column\"),\n                };\n              }),\n            );\n\n            return sugs;\n          }\n        }\n      } catch (err) {\n        // console.warn(err);\n      }\n    }\n  }\n};\n\nconst getJSONStructure = (json: any) => {\n  if (Array.isArray(json)) {\n    if (json.length) {\n      return getJSONStructure(json[0]);\n    }\n    return [\"any[]\"];\n  } else if (isObject(json)) {\n    return Object.fromEntries(\n      Object.keys(json).map((key) => [key, getJSONStructure(json[key])]),\n    );\n  } else {\n    return typeof json;\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/monacoSQLSetup/registerHover.ts",
    "content": "import type {\n  editor,\n  languages,\n  Monaco,\n  Position,\n} from \"../../../W_SQL/monacoEditorTypes\";\nimport { LANG, type SQLSuggestion } from \"../../W_SQLEditor\";\nimport { isDefined, isObject } from \"prostgles-types\";\nimport type {\n  MonacoSuggestion,\n  ParsedSQLSuggestion,\n} from \"./registerSuggestions\";\n\nexport const registerHover = (\n  monaco: Monaco,\n  sqlSuggestions: SQLSuggestion[],\n  provideCompletionItems: (\n    model: editor.ITextModel,\n    position: Position,\n    context: languages.CompletionContext,\n  ) => Promise<{\n    suggestions: (\n      | MonacoSuggestion\n      | languages.CompletionItem\n      | ParsedSQLSuggestion\n    )[];\n  }>,\n) => {\n  return monaco.languages.registerHoverProvider(LANG, {\n    provideHover: async function (model, position, token, context) {\n      const curWord = model.getWordAtPosition(position);\n\n      if (!curWord || !sqlSuggestions.length) {\n        return;\n      }\n\n      const startOfWordPosition = new monaco.Position(\n        position.lineNumber,\n        curWord.startColumn,\n      );\n      const justAfterStartOfWordPosition = new monaco.Position(\n        position.lineNumber,\n        curWord.startColumn + 1,\n      );\n      const offset = model.getOffsetAt(startOfWordPosition);\n      const modelValue = model.getValue();\n      /* set current word to empty string to get all suggestions */\n      const val =\n        modelValue.slice(0, offset) +\n        \" \" +\n        modelValue.slice(offset + curWord.word.length);\n      const newModel = monaco.editor.createModel(val, LANG);\n      const { suggestions } = await provideCompletionItems(\n        newModel,\n        justAfterStartOfWordPosition,\n        {\n          triggerKind: monaco.languages.CompletionTriggerKind.Invoke,\n          triggerCharacter: \" \",\n        },\n      );\n      newModel.dispose();\n      let matches = suggestions.filter(\n        (s) =>\n          s.insertText === curWord.word ||\n          s.insertText.toLowerCase() === curWord.word.toLowerCase() ||\n          (s as ParsedSQLSuggestion).escapedIdentifier === curWord.word,\n      );\n      if (!matches.length) {\n        matches = suggestions.filter(\n          (s) =>\n            (s as ParsedSQLSuggestion).escapedIdentifier?.startsWith(`\"`) &&\n            (s as ParsedSQLSuggestion).escapedIdentifier?.includes(\n              curWord.word,\n            ),\n        );\n      }\n      if (!matches.length) {\n        matches = suggestions.filter(\n          (s) => (s as ParsedSQLSuggestion).escapedName === curWord.word,\n        );\n      }\n\n      const [_matchingSuggestion, ...otherMatches] = matches;\n      // TODO ensure escapeIdentifier works (\"table name\" ends up as two words (in curWord) and doesn't always match)\n      // console.log(curWord.word, _matchingSuggestion, other);\n      let matchingSuggestion = _matchingSuggestion;\n      if (otherMatches.length && matchingSuggestion) {\n        /** Matched many similar functions. Pick first*/\n        if (\n          otherMatches.every(\n            (s) =>\n              matchingSuggestion &&\n              \"type\" in s &&\n              \"type\" in matchingSuggestion &&\n              \"name\" in s &&\n              \"name\" in matchingSuggestion &&\n              s.type === matchingSuggestion.type &&\n              s.type === \"function\" &&\n              s.name === matchingSuggestion.name,\n          )\n        ) {\n        } else {\n          matchingSuggestion = undefined;\n        }\n      }\n      const sm =\n        matchingSuggestion ??\n        sqlSuggestions.find(\n          (s) => s.type === \"keyword\" && s.name === curWord.word.toUpperCase(),\n        );\n\n      if (sm) {\n        const detail = \"detail\" in sm ? sm.detail : \"\";\n        const documentationText =\n          isObject(sm.documentation) ? sm.documentation.value\n          : typeof sm.documentation === \"string\" ? sm.documentation\n          : \"\";\n        return {\n          range: new monaco.Range(\n            position.lineNumber,\n            curWord.startColumn,\n            position.lineNumber,\n            curWord.endColumn,\n          ),\n          contents: [\n            !detail ? undefined : { value: `**${detail}**` },\n            { value: documentationText },\n            // { value: '![my image](https://fdvsdfffdgdgdfg.com/favicon.ico)' }\n          ].filter(isDefined),\n        };\n      }\n\n      return {\n        contents: [],\n      };\n    },\n  });\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/monacoSQLSetup/registerSuggestions.ts",
    "content": "import type { SQLHandler } from \"prostgles-types\";\nimport { format } from \"sql-formatter\";\nimport { debounce } from \"../../../Map/DeckGLWrapped\";\nimport type {\n  editor,\n  IDisposable,\n  IRange,\n  languages,\n  Monaco,\n  Position,\n} from \"../../../W_SQL/monacoEditorTypes\";\nimport type { SQLSuggestion } from \"../../W_SQLEditor\";\nimport { LANG } from \"../../W_SQLEditor\";\nimport { getKeywordDocumentation } from \"../../SQLEditorSuggestions\";\nimport type { CodeBlock } from \"../completionUtils/getCodeBlock\";\nimport { getCurrentCodeBlock } from \"../completionUtils/getCodeBlock\";\nimport { getStartingLetters, removeQuotes } from \"../getJoinSuggestions\";\nimport { getMatch } from \"../getMatch\";\nimport { registerHover } from \"./registerHover\";\n\nexport const triggerCharacters = [\n  \".\",\n  \"/\",\n  \"?\",\n  \"\\\\\",\n  \"=\",\n  /**\n   * Mobile devices can't press Ctrl + Space. Use space instead\n   */\n  \" \",\n] as const;\n\nexport type MonacoSuggestion = PRGLMetaInfo &\n  languages.CompletionItem &\n  Pick<SQLSuggestion, \"dataTypeInfo\">;\nexport type ParsedSQLSuggestion = MonacoSuggestion &\n  Omit<SQLSuggestion, \"documentation\">;\n\nexport type SQLMatchContext = {\n  cb: CodeBlock;\n  ss: ParsedSQLSuggestion[];\n  setS: ParsedSQLSuggestion[];\n  sql: SQLHandler | undefined;\n};\nexport type GetKind = (type: SQLSuggestion[\"type\"]) => number;\nexport type SuggestionItem =\n  | languages.CompletionItem\n  | MonacoSuggestion\n  | ParsedSQLSuggestion;\nexport type SQLMatcherResultType = {\n  suggestions: SuggestionItem[];\n};\nexport type SQLMatcherResultArgs = SQLMatchContext & {\n  /** Used for lateral join subquery which can use columns from previous tables */\n  parentCb?: CodeBlock;\n  options?: {\n    MatchSelect?: {\n      excludeInto?: boolean;\n    };\n  };\n};\nexport type SQLMatcher = {\n  match: (cb: CodeBlock) => boolean;\n  result: (\n    args: SQLMatcherResultArgs,\n  ) => Promise<SQLMatcherResultType> | SQLMatcherResultType;\n};\n\ntype PRGLMetaInfo = {\n  escapedParentName?: string;\n  schema?: string;\n};\n\ntype Kind = {\n  Class: number;\n  Color: number;\n  Constant: number;\n  Constructor: number;\n  Customcolor: number;\n  Enum: number;\n  EnumMember: number;\n  Event: number;\n  Field: number;\n  File: number;\n  Folder: number;\n  Function: number;\n  Interface: number;\n  Issue: number;\n  Keyword: number;\n  Method: number;\n  Module: number;\n  Operator: number;\n  Property: number;\n  Reference: number;\n  Snippet: number;\n  Struct: number;\n  Text: number;\n  TypeParameter: number;\n  Unit: number;\n  User: number;\n  Value: number;\n  Variable: number;\n};\n\nlet sqlHoverProvider: IDisposable | undefined;\n\n/**\n * Used to ensure connecting to other databases will show the correct suggestions\n */\nlet sqlCompletionProvider: IDisposable | undefined;\nlet sqlInlineCompletionProvider: IDisposable | undefined;\nlet sqlFormattingProvider: IDisposable | undefined;\ntype Args = {\n  suggestions: SQLSuggestion[];\n  settingSuggestions: SQLSuggestion[];\n  sql?: SQLHandler;\n  editor: editor.IStandaloneCodeEditor;\n  monaco: Monaco;\n};\n\nconst getRespectedSortText = (\n  cb: CodeBlock,\n  monaco: Monaco,\n  {\n    suggestions,\n  }: {\n    suggestions: (\n      | MonacoSuggestion\n      | languages.CompletionItem\n      | ParsedSQLSuggestion\n    )[];\n  },\n) => {\n  const { currToken } = cb;\n  const currTextRaw = currToken?.text;\n  const range = new monaco.Range(\n    cb.position.lineNumber,\n    cb.position.column,\n    cb.position.lineNumber,\n    cb.position.column,\n  );\n  if (!currTextRaw) {\n    return {\n      suggestions: suggestions.map((s) => ({\n        ...s,\n        range,\n      })),\n    };\n  }\n  const currTextLC = removeQuotes(currTextRaw).toLowerCase();\n  const sortText = Array.from(\n    new Set(suggestions.map((s) => s.sortText)),\n  ).sort();\n  const isSingleSortText = sortText.length === 1;\n  // if (isSingleSortText) return { suggestions };\n\n  const getRangeAndFilter = (\n    rawFilterText: string | undefined,\n  ): Pick<\n    languages.CompletionItem,\n    \"range\" | \"filterText\" | \"insertTextRules\"\n  > => {\n    let range: languages.CompletionItem[\"range\"] | undefined;\n    if (currTextRaw.startsWith(`\"`)) {\n      range = new monaco.Range(\n        currToken.lineNumber,\n        currToken.columnNumber,\n        currToken.lineNumber,\n        currToken.columnNumber + currTextRaw.length,\n      );\n      return {\n        range,\n        filterText: `\"` + rawFilterText,\n      };\n    }\n    if (isSingleSortText) {\n      return {\n        range: range as IRange,\n      };\n    }\n    return {\n      range: range as IRange,\n      // insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n      filterText: rawFilterText,\n    };\n  };\n\n  /**\n   *  SELECT name\n   *  FROM pg_class\n   *\n   *  yields unwanted list of \"name\" functions at the top, not respecting sortText\n   *  if user is searching for something that matches expression columns then\n   *  add 5 other matching functions at most to not obfuscate the columns\n   */\n  const fixedSuggestions = suggestions.map((s) => {\n    const sortIndex = sortText.indexOf(s.sortText);\n    const itemTextRaw =\n      \"escapedIdentifier\" in s && s.escapedIdentifier ? s.escapedIdentifier\n      : \"escapedName\" in s && s.escapedName ? s.escapedName\n      : \"name\" in s ? s.name\n      : typeof s.label === \"string\" ? s.label\n      : s.label.label;\n    const itemText = removeQuotes(itemTextRaw).toLowerCase();\n    if (sortIndex > 0) {\n      return {\n        ...s,\n        ...getRangeAndFilter(s.filterText),\n      };\n    }\n\n    let filterText: string | undefined;\n    const idxOfItemText = itemText.indexOf(currTextLC);\n    if (idxOfItemText > -1) {\n      filterText = itemText.slice(idxOfItemText);\n    } else {\n      const startingLetters = getStartingLetters(itemText).toLowerCase();\n      const idxOfStartingLetters = startingLetters.indexOf(currTextLC);\n      if (idxOfStartingLetters > -1) {\n        filterText =\n          startingLetters.slice(idxOfStartingLetters) + \" \" + (itemText || \"\");\n      }\n    }\n    return {\n      ...s,\n      insertTextRules:\n        monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n      ...getRangeAndFilter(filterText),\n    };\n  });\n\n  return {\n    suggestions: fixedSuggestions,\n  };\n};\nexport let KNDS = {} as Kind;\n\nexport function registerSuggestions(args: Args) {\n  const { suggestions, settingSuggestions, sql, monaco, editor } = args;\n  const sqlSuggestions = suggestions;\n  KNDS = monaco.languages.CompletionItemKind;\n\n  const provideCompletionItems = async (\n    model: editor.ITextModel,\n    position: Position,\n    context: languages.CompletionContext,\n  ): Promise<{\n    suggestions: (\n      | MonacoSuggestion\n      | languages.CompletionItem\n      | ParsedSQLSuggestion\n    )[];\n  }> => {\n    /* TODO Add type checking within for func arg types && table cols && func return types*/\n    function parseSuggestions(sugests: SQLSuggestion[]): ParsedSQLSuggestion[] {\n      return sugests.map((s, i) => {\n        const res: ParsedSQLSuggestion = {\n          ...s,\n          range: undefined as any,\n          kind: getKind(s.type),\n          insertText: s.insertText || s.name,\n          detail: s.detail || `(${s.type})`,\n          type: s.type,\n          escapedParentName: s.escapedParentName,\n          documentation: {\n            value: s.documentation || `${s.type} ${s.detail || \"\"}`,\n          },\n          insertTextRules:\n            monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n        };\n\n        const info = getKeywordDocumentation(s.name);\n        if (info) {\n          res.documentation = {\n            value: info,\n          };\n        }\n\n        return res;\n      });\n    }\n    const ss = parseSuggestions(sqlSuggestions);\n\n    const setS = parseSuggestions(settingSuggestions);\n\n    const cBlock = await getCurrentCodeBlock(model, position);\n    const myEditor = monaco.editor\n      .getEditors()\n      .find((e) => e.getModel()?.id === model.id);\n    const editorNode = myEditor?.getDomNode();\n    if (editorNode) {\n      (editorNode as any)._cBlock = cBlock;\n    }\n    const isFormattingCode =\n      context.triggerCharacter === \"\\n\" && cBlock.thisLineLC.length > 0;\n    const isCommenting = cBlock.currToken?.type === \"comment.sql\";\n    if (isFormattingCode || isCommenting) {\n      return { suggestions: [] };\n    }\n\n    const { firstTry, match } = await getMatch({ cb: cBlock, ss, setS, sql });\n    if (firstTry) {\n      return getRespectedSortText(cBlock, monaco, firstTry);\n    } else if (match) {\n      const res = await match.result({ cb: cBlock, ss, setS, sql });\n      return getRespectedSortText(cBlock, monaco, res);\n    }\n\n    const suggestions = ss\n      .filter((s) => s.topKwd?.start_kwd)\n      .map((s) => ({\n        ...s,\n        sortText: `a${s.topKwd!.priority ?? 99}`,\n      }));\n\n    return {\n      suggestions,\n    };\n  };\n\n  sqlFormattingProvider?.dispose();\n  sqlFormattingProvider =\n    monaco.languages.registerDocumentFormattingEditProvider(LANG, {\n      displayName: LANG.toUpperCase(),\n      provideDocumentFormattingEdits: (model) => {\n        // const newText = await getFormattedSql(model);\n\n        const tabWidth = model.getOptions().indentSize || 2;\n        const newText = format(model.getValue(), {\n          language: \"postgresql\",\n          expressionWidth: 80,\n          indentStyle: \"standard\",\n          tabWidth,\n          linesBetweenQueries: 2,\n          logicalOperatorNewline: \"before\",\n        });\n        return [\n          {\n            range: model.getFullModelRange(),\n            text: newText,\n          },\n        ];\n      },\n    });\n  sqlHoverProvider?.dispose();\n  sqlHoverProvider = registerHover(\n    monaco,\n    sqlSuggestions,\n    provideCompletionItems,\n  );\n\n  sqlCompletionProvider?.dispose();\n  sqlCompletionProvider = monaco.languages.registerCompletionItemProvider(\n    LANG,\n    {\n      triggerCharacters: triggerCharacters.slice(0),\n      provideCompletionItems: async (\n        model,\n        position,\n        context,\n      ): Promise<{\n        suggestions: (MonacoSuggestion | languages.CompletionItem)[];\n      }> => {\n        const res = await provideCompletionItems(model, position, context);\n        return res;\n      },\n      resolveCompletionItem: (item, token) => {\n        hackyFixMonacoSortFilter(args.editor);\n        return item;\n      },\n    },\n  );\n\n  // TODO: Add inline completion using LLMs\n  // setInterval(() => {\n  //   editor.trigger(\"demo\", \"editor.action.inlineSuggest.trigger\", {});\n  // }, 2e3)\n  // sqlInlineCompletionProvider?.dispose();\n  // sqlInlineCompletionProvider = monaco.languages.registerInlineCompletionsProvider(LANG, {\n  //   freeInlineCompletions: (completions) => {\n\n  //   },\n  //   provideInlineCompletions: async (model, position) => {\n  //     // const suggestionText = \"hello there\\n  dwadwa \\n dwadwa\";\n  //     // const maxColumn = model.getLineMaxColumn(position.lineNumber);\n  //     // const maxLine = model.getLineCount();\n  //     // const line = model.getLineContent(position.lineNumber);\n  //     const contentAfterCusor = model.getLineContent(position.lineNumber).slice(position.column - 1);\n\n  //     const insertText = \"hello there\\n  dwadwa \\n dwadwa\"; // \"hello there dwadwa\";\n  //     return {\n  //       items: [\n  //         {\n  //           // range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, contentAfterCusor? model.getLineMaxColumn(position.lineNumber) : position.column),\n  //           // range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.lineNumber ),\n  //           insertText,\n  //           // insertText: \"hello there\\n  dwadwa \\n dwadwa\",\n  //           // additionalTextEdits: [\n  //           //   {\n  //           //     range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),\n  //           //     text: \"\"\n  //           //   }\n  //           // ],\n  //         }\n  //       ],\n  //     }\n  //   }\n  // });\n}\nexport const getKind = (\n  type: SQLSuggestion[\"type\"],\n  KindMap = KNDS,\n): number => {\n  // return KNDS.Text\n  if (type === \"function\") {\n    return KindMap.Function;\n  } else if (type === \"column\") {\n    return KindMap.Field;\n  } else if (type === \"table\") {\n    return KindMap.EnumMember;\n  } else if (type === \"view\" || type === \"mview\") {\n    return KindMap.Value; // return KNDS.Constant\n  } else if (type === \"dataType\") {\n    return KindMap.Variable;\n  } else if (type === \"operator\") {\n    return KindMap.Operator;\n  } else if (type === \"keyword\") {\n    return KindMap.Keyword;\n  } else if (type === \"extension\") {\n    return KindMap.Module;\n  } else if (type === \"schema\") {\n    return KindMap.Folder;\n  } else if (type === \"setting\") {\n    return KindMap.Property;\n  } else if (type === \"folder\") {\n    return KindMap.Folder;\n  } else if (type === \"file\") {\n    return KindMap.File;\n  } else if (type === \"snippet\") {\n    return KindMap.Snippet;\n  } else if (type === \"database\") {\n    return KindMap.Struct;\n  } else if (type === \"role\" || type === \"policy\") {\n    return KindMap.User;\n  } else if (type === \"trigger\" || type === \"eventTrigger\") {\n    return KindMap.Event;\n  }\n  return KindMap.Keyword;\n};\n\nexport function isUpperCase(str) {\n  return str == str.toUpperCase() && str != str.toLowerCase();\n}\n\n/**\n * Extra fix below does not work for all cases\n * monaco filters items without caring about sortText too much (relname \"-1\" is not shown. name \"b\" is shown instead).\n * Need to insure that sortText first group items are shown if they contain the word\n *\n * SELECT *\n * FROM pg_catalog.pg_class\n * ORDER BY name -> relname\n */\nconst hackyFixMonacoSortFilter = debounce(\n  (editor: editor.IStandaloneCodeEditor) => {\n    const suggestWidget = (editor as any).getContribution(\n      \"editor.contrib.suggestController\",\n    ).widget;\n    const allCompletionItems: ParsedSQLSuggestion[] | undefined =\n      suggestWidget._value._completionModel?._items.map((d) => d.completion);\n    if (!allCompletionItems) return;\n    const shownCompletionItems = suggestWidget._value._list.view.items.map(\n      (e) => e.element.completion,\n    ) as ParsedSQLSuggestion[];\n    const firstItem: ParsedSQLSuggestion | undefined =\n      suggestWidget._value._list.view.items[0]?.element.completion;\n    const focusedItem: {\n      filterTextLow: string;\n      word: string;\n      completion: ParsedSQLSuggestion;\n    } = suggestWidget._value._focusedItem ?? {};\n    const { filterTextLow, completion } = focusedItem;\n    const didScroll =\n      suggestWidget._value?._list?.view.scrollable._state.scrollTop;\n    const cb = (editor.getDomNode() as any)?._cBlock as CodeBlock | undefined;\n    const word = focusedItem.word || cb?.currToken?.text;\n    // console.log(cb, suggestWidget, focusedItem, shownCompletionItems)\n    if (\n      !didScroll &&\n      cb?.currToken?.type !== \"string.sql\" &&\n      allCompletionItems.length &&\n      word &&\n      filterTextLow &&\n      firstItem\n    ) {\n      const itemThatShouldBeHigher = allCompletionItems.find(\n        (s) =>\n          (s.name as any)?.includes(word) &&\n          s.sortText &&\n          s.sortText < (firstItem.sortText ?? \"zzz\"),\n      );\n      if (\n        itemThatShouldBeHigher &&\n        !shownCompletionItems\n          .slice(0, 10)\n          .find((s) => s.name === itemThatShouldBeHigher.name)\n      ) {\n        // && !filterTextLow.includes(word)\n        editor.trigger(\"demo\", \"hideSuggestWidget\", {});\n        editor.trigger(\"demo\", \"editor.action.triggerSuggest\", {});\n      }\n    }\n  },\n  500,\n);\n\n// function searchObjectForValue(obj, searchValue) {\n//   const results = [];\n//   const visited = new WeakSet();\n\n//   function traverse(currentObj, path = '') {\n//     if (typeof currentObj !== 'object' || currentObj === null) {\n//       return;\n//     }\n\n//     // Check for circular reference\n//     if (visited.has(currentObj)) {\n//       return;\n//     }\n\n//     visited.add(currentObj);\n\n//     for (let key in currentObj) {\n//       if (currentObj.hasOwnProperty?.(key) && !Number.isFinite(+key)) {\n//         const newPath = path ? `${path}.${key}` : key;\n//         const value = currentObj[key];\n\n//         if (typeof value === 'string' && value === (searchValue)) {\n//           results.push({ path: newPath, value: value });\n//         } else if (typeof value === 'object' && value !== null) {\n//           traverse(value, newPath);\n//         }\n//       }\n//     }\n//   }\n\n//   traverse(obj);\n//   return results;\n// }\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/suggestColumnLike.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unnecessary-condition */\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { getTableExpressionSuggestions } from \"./completionUtils/getTableExpressionReturnTypes\";\nimport {\n  getKind,\n  type ParsedSQLSuggestion,\n  type SQLMatcherResultArgs,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport { suggestFuncArgs } from \"./suggestFuncArgs\";\n\ntype Args = Pick<\n  SQLMatcherResultArgs,\n  \"cb\" | \"ss\" | \"parentCb\" | \"setS\" | \"sql\"\n>;\nexport const suggestColumnLike = async (\n  { cb, parentCb, ss, setS, sql }: Args,\n  withFuncArgs = true,\n): Promise<{\n  suggestions: ParsedSQLSuggestion[];\n}> => {\n  const { prevIdentifiers, currToken, ltoken, nextTokens } = cb;\n  const addTable =\n    ltoken?.textLC === \"select\" &&\n    (!nextTokens.some((t) => t.textLC === \"from\" || t.text === \"FROM \") ||\n      nextTokens[0]?.text === \")\");\n  const addTableInline = nextTokens[0]?.text === \")\";\n\n  if (withFuncArgs) {\n    const funcArgs = await suggestFuncArgs({ cb, ss, parentCb, setS, sql });\n    if (funcArgs) return funcArgs;\n  }\n\n  /**\n   * If cannot addTable AND is a cte expression then only suggest columns that are already in the expression\n   */\n  const onlyColumnsFromExpression =\n    !addTable &&\n    cb.currNestingFunc?.textLC === \"as\" &&\n    cb.currNestingFunc.nestingId.length === 0 &&\n    parentCb?.ftoken?.textLC === \"with\";\n  const expression = await getTableExpressionSuggestions(\n    { parentCb, cb, ss, sql },\n    \"columns\",\n    onlyColumnsFromExpression,\n  );\n  const expressionTables = expression?.tablesWithAliasInfo; //.filter(c => c.endOffset < cb.currOffset)\n  const expressionColumns = expression?.columnsWithAliasInfo\n    // .filter(c => c.endOffset < cb.currOffset)\n    .flatMap((c) => c.s);\n\n  // const expressionColumns = columnsWithAliasInfo.filter(c => c.endOffset < cb.currOffset).flatMap(c => c.s);\n  // const expressionTables = tablesWithAliasInfo.filter(c => c.endOffset < cb.currOffset).flatMap(c => c.s);\n  const dotPrefix =\n    !currToken ? undefined\n    : currToken.text === \".\" ? ltoken?.text\n    : currToken.text.includes(\".\") ? currToken.text.split(\".\")[0]\n    : undefined;\n  const activeAliasTable =\n    !dotPrefix ? undefined : (\n      expressionTables.find((t) => t.alias === dotPrefix)\n    );\n  if (activeAliasTable) {\n    const tableAliasCols = expressionColumns\n      .filter(\n        (c) => c.escapedParentName === activeAliasTable.s.escapedIdentifier,\n      )\n      .map((c) => {\n        if (!cb.currToken) return c;\n        /**\n         * Must ensure we don't add alias if it's already there\n         */\n        return {\n          ...c,\n          insertText: c.escapedIdentifier ?? c.name,\n        };\n      });\n    return { suggestions: tableAliasCols };\n  }\n\n  const maybeWrittingSchema =\n    !activeAliasTable && cb.currToken?.text === \".\" && dotPrefix ?\n      dotPrefix\n    : undefined;\n  const activeSchema =\n    !maybeWrittingSchema ? undefined : (\n      ss.find(\n        (s) =>\n          s.type === \"schema\" && s.escapedIdentifier === maybeWrittingSchema,\n      )\n    );\n\n  /** Allow all other columns only if can add tablename to end */\n  const otherColumns =\n    !addTable ?\n      []\n    : ss.filter((s) => {\n        if (s.type !== \"column\") return false;\n        return !expressionColumns.some(\n          (t) => t.escapedIdentifier === s.escapedParentName,\n        );\n      });\n\n  const funcs = ss.filter((s) => {\n    return (\n      s.type === \"function\" &&\n      (!activeSchema || s.schema === activeSchema.escapedIdentifier)\n    );\n  });\n\n  const colAndFuncSuggestions = [\n    ...expressionColumns.map((s) => ({ isPrioritised: true, s })),\n    ...otherColumns.map((s) => ({ isPrioritised: false, s })),\n    /**\n     * This is now handled by fixMonacoSortFilter\n     * Slice WAS used to ensure columns are prioritised for cases when the middle of word matches:\n     *\n     * SELECT *\n     * FROM pg_catalog.pg_class\n     * ORDER BY name -> relname\n     */\n    ...funcs.map((s) => ({ isPrioritised: false, s })), // .slice(...(cb.currToken? [0] : [0, 0])) ,\n  ].map(({ s, isPrioritised }) => {\n    const prioritiseColumn =\n      s.type === \"column\" &&\n      expressionColumns.some(\n        (t) => t.escapedIdentifier === s.escapedParentName,\n      ) &&\n      !prevIdentifiers.some((pi) => pi.text === s.name);\n    const sortText =\n      isPrioritised ?\n        s.sortText\n      : `${\n          s.type === \"function\" ?\n            (\n              cb.textLC.startsWith(\"create index\") &&\n              s.funcInfo?.provolatile === \"i\"\n            ) ?\n              \"c\"\n            : cb.currToken?.text === \".\" && s.schema === cb.ltoken?.text ? \"a\"\n            : \"d\"\n          : prioritiseColumn ? \"a\"\n          : \"b\"\n        }${s.schema === \"public\" ? \"a\" : \"b\"}`;\n\n    if (s.type === \"column\") {\n      const delimiter = addTableInline ? \" \" : \"\\n\";\n      return {\n        ...s,\n        /**\n         * Prevent adding another alias\n         * Some dead table (no activeAliasTable) aliases are ignored and a repeating alias is added\n         */\n        insertText:\n          dotPrefix ? (s.escapedIdentifier ?? s.name) : s.insertText.trim(),\n        sortText,\n        ...(addTable && {\n          insertTextRules: 4,\n          label: {\n            ...(typeof s.label === \"object\" ? s.label : {}),\n            label: s.name + \" ...\",\n          },\n          insertText:\n            s.insertText +\n            `$0${delimiter}FROM ${s.escapedParentName}${delimiter}LIMIT 200`,\n        }),\n      };\n    }\n\n    return {\n      ...s,\n      ...(s.type === \"function\" &&\n        s.insertText &&\n        activeSchema && {\n          insertText: s.insertText\n            ?.split(`${activeSchema.insertText}.`)\n            .join(\"\"),\n        }),\n      sortText,\n    };\n  });\n\n  let suggestions = [...colAndFuncSuggestions];\n  const selectKwds = ss\n    .filter(\n      (s) =>\n        s.type === \"keyword\" &&\n        [\n          ltoken?.textLC === \"select\" ? \"DISTINCT\" : \"\",\n          \"COALESCE\",\n          \"CASE\",\n          \"NULLIF\",\n          \"LEAST\",\n          \"GREATEST\",\n        ].includes(s.name),\n    )\n    .map((s) => ({\n      ...s,\n    }));\n\n  const prevKwd = cb\n    .getPrevTokensNoParantheses()\n    .slice()\n    .reverse()\n    .find((t) => t.type === \"keyword.sql\");\n  if (prevKwd?.textLC === \"select\") {\n    if (!(cb.currToken && !activeAliasTable)) {\n      selectKwds.unshift({\n        insertText: \"*\",\n        kind: getKind(\"keyword\"),\n        type: \"keyword\",\n        label: \"*\",\n        name: \"*\",\n        detail: \"(keyword)\",\n        documentation: { value: \"All columns\" },\n        sortText: \"a\",\n      } as any);\n    }\n\n    if (cb.prevText.endsWith(\") \") && ltoken?.textLC === \")\") {\n      const prevFunc = cb.prevTokens.find((t, i) => {\n        return (\n          ltoken?.nestingId === t.nestingId &&\n          cb.prevTokens[i + 1]?.textLC === \"(\"\n        );\n      });\n      if (prevFunc) {\n        const func = ss.find(\n          (s) => s.type === \"function\" && s.escapedIdentifier === prevFunc.text,\n        );\n        if (func?.funcInfo?.is_aggregate) {\n          return suggestSnippets([\n            {\n              insertText: \"FILTER (WHERE $0)\",\n              kind: getKind(\"keyword\"),\n              label: \"FILTER (WHERE ...)\",\n              docs: \"FILTER is much like WHERE, except that it removes rows only from the input of the particular aggregate function that it is attached to\",\n              sortText: \"0\",\n            },\n          ]);\n        }\n      }\n    }\n  }\n\n  if (selectKwds.length) {\n    suggestions = [...suggestions, ...(selectKwds as any)];\n  }\n  return {\n    suggestions,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/suggestCondition.ts",
    "content": "import { suggestSnippets } from \"./CommonMatchImports\";\nimport { getParentFunction } from \"./MatchSelect\";\nimport { matchNested } from \"./MatchWith\";\nimport { getTableExpressionSuggestions } from \"./completionUtils/getTableExpressionReturnTypes\";\nimport type { TokenInfo } from \"./completionUtils/getTokens\";\nimport { jsonbPathSuggest } from \"./jsonbPathSuggest\";\nimport {\n  getKind,\n  type SQLMatchContext,\n  type SQLMatcherResultArgs,\n  type SQLMatcherResultType,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport { suggestColumnLike } from \"./suggestColumnLike\";\nimport { suggestFuncArgs } from \"./suggestFuncArgs\";\nimport { allowedOperands, suggestValue } from \"./suggestValue\";\n\nexport const suggestCondition = async (\n  args: SQLMatchContext & Pick<SQLMatcherResultArgs, \"parentCb\">,\n  isCondition = false,\n): Promise<SQLMatcherResultType | undefined> => {\n  const { cb, ss, sql, parentCb, setS } = args;\n  const {\n    ltoken,\n    l1token,\n    l2token,\n    prevText,\n    thisLineLC,\n    getPrevTokensNoParantheses,\n  } = cb;\n  const prevTokens = getPrevTokensNoParantheses(true);\n  const keywordsThatActAsIdentifiersWhenInSelect = [\"type\"];\n  const prevKwdTokens = [...prevTokens].reverse().filter((t, i, arr) => {\n    const nextToken = arr[i + 1];\n    const isKwd =\n      ([\"keyword.choice.sql\", \"keyword.sql\"].includes(t.type) &&\n        nextToken?.textLC !== \"::\") ||\n      t.textLC === \"join\";\n    if (cb.ftoken?.textLC === \"select\") {\n      return (\n        isKwd && !keywordsThatActAsIdentifiersWhenInSelect.includes(t.textLC)\n      );\n    }\n    return isKwd;\n  });\n  const prevKwdToken = prevKwdTokens[0];\n\n  const func = getParentFunction(cb);\n  if (func?.func.textLC === \"exists\") {\n    if (cb.l1token?.textLC === cb.currNestingFunc?.textLC) {\n      return suggestSnippets([{ label: \"SELECT\" }]);\n    }\n    const res = await matchNested(args, [\"MatchSelect\"], undefined);\n    if (res) return res;\n  }\n\n  const conditionPrecedingKeywords = [\n    \"where\",\n    \"on\",\n    \"having\",\n    \"when\",\n    \"using\",\n    \"check\",\n  ];\n  if (conditionPrecedingKeywords.includes(prevKwdToken?.textLC ?? \"\") && func) {\n    const funcSuggestions = await suggestFuncArgs({\n      cb,\n      parentCb,\n      ss,\n      setS,\n      sql,\n    });\n    if (funcSuggestions) {\n      return funcSuggestions;\n    }\n  }\n\n  const parensConditionKwd =\n    (\n      prevTokens.some((t) =>\n        [\"policy\", \"publication\", \"subscription\"].includes(t.textLC),\n      )\n    ) ?\n      [\"using\", \"check\", \"where\"].find((kwd) => kwd === prevKwdToken?.textLC)\n    : undefined;\n  const expectsCondition =\n    isCondition ||\n    !!parensConditionKwd ||\n    prevKwdToken?.textLC === \"where\" ||\n    prevKwdToken?.textLC === \"when\" ||\n    (prevKwdToken?.textLC === \"on\" &&\n      prevTokens.some((t) => t.textLC === \"join\") &&\n      cb.thisLinePrevTokens.length);\n\n  const getPrevCol = async (colName: string | undefined) => {\n    if (!colName) return undefined;\n    const expr = await getTableExpressionSuggestions(\n      { parentCb, cb, ss, sql },\n      \"columns\",\n    );\n    const col = expr.columnsWithAliasInfo.find(({ alias, s }) =>\n      [s.escapedIdentifier, `${alias}.${s.escapedIdentifier}`].includes(\n        colName,\n      ),\n    );\n    return col?.s;\n  };\n  const suggestColumnByType = async (prevColToken?: TokenInfo) => {\n    const colLike = await suggestColumnLike({ cb, ss, parentCb, setS, sql });\n    if (\n      (ltoken?.type === \"operator.sql\" && l1token?.type === \"identifier.sql\") ||\n      prevColToken\n    ) {\n      const prevCol = await getPrevCol(prevColToken?.text ?? l1token!.text);\n      if (prevCol) {\n        return {\n          suggestions: colLike.suggestions.map((s) => ({\n            ...s,\n            sortText:\n              s.colInfo ?\n                s.colInfo.data_type === prevCol.colInfo?.data_type ?\n                  \"a\"\n                : \"b\"\n              : s.funcInfo?.restype === prevCol.colInfo?.data_type ? \"c\"\n              : s.sortText,\n          })),\n        };\n      }\n    }\n    const needsParens =\n      !cb.currToken &&\n      parensConditionKwd &&\n      ltoken?.textLC === parensConditionKwd;\n    if (needsParens) {\n      return {\n        suggestions: colLike.suggestions.map((s) => ({\n          ...s,\n          insertText: `(${s.insertText} $0 )`,\n        })),\n      };\n    }\n    return colLike;\n  };\n\n  /**\n   * Skip this case so it ends up in MatchWith and the nested logic is parsed\n   */\n  const isCOmingFromMatchFirstAndIsWithCte =\n    cb.currNestingFunc?.textLC === \"as\" && cb.ftoken?.textLC === \"with\";\n  if (isCOmingFromMatchFirstAndIsWithCte) {\n    return undefined;\n  }\n\n  /* If expecting condition statement and writing dot then expect alias.col */\n  if (\n    cb.ltoken?.type === \"identifier.sql\" &&\n    cb.currToken?.text === \".\" &&\n    (isCondition || allowedOperands.includes(cb.l1token?.textLC ?? \"\"))\n  ) {\n    return await suggestColumnByType(cb.l2token);\n  }\n\n  /** Must not match on alias select */\n  if (cb.currToken?.text === \".\" || !cb.thisLinePrevTokens.length) {\n    return undefined;\n  }\n  const jsonbPath = await jsonbPathSuggest(args);\n  if (jsonbPath) {\n    return jsonbPath;\n  }\n  const getPreviousIdentifier = () => {\n    const reversedTokens = cb.prevTokens.slice(0).reverse();\n    const idx = reversedTokens.findIndex(\n      (t) => t.type === \"identifier.sql\" || t.type === \"identifier.quote.sql\",\n    );\n    const identifier = reversedTokens[idx];\n    const colTokens = reversedTokens.slice(1, idx + 1);\n    if (!identifier) return undefined;\n    return {\n      identifierText: identifier.text,\n      colTokens: colTokens.slice(0).reverse(),\n    };\n  };\n\n  /** Convenient autocomplete (column = 'value') */\n  const suggestedValues = await suggestValue(\n    args,\n    prevKwdToken,\n    getPreviousIdentifier,\n  );\n  if (suggestedValues) {\n    return suggestedValues;\n  }\n\n  /** Is inside exists */\n  if (\n    prevKwdToken &&\n    expectsCondition &&\n    prevTokens.some(\n      (t) =>\n        t.textLC === \"exists\" &&\n        t.type === \"operator.sql\" &&\n        t.offset > prevKwdToken.offset,\n    )\n  ) {\n    return undefined;\n  }\n\n  if (\n    expectsCondition &&\n    (prevKwdToken?.offset === ltoken?.offset ||\n      [\"and\", \"or\"].includes(ltoken?.textLC as any) ||\n      ltoken?.type === \"operator.sql\")\n  ) {\n    return await suggestColumnByType();\n  }\n\n  const getOperators = () => {\n    const AndOr = ss.filter(\n      (s) =>\n        s.type === \"keyword\" &&\n        ltoken?.type !== \"keyword.sql\" &&\n        [\"AND\", \"OR\"].includes(s.name),\n    );\n    const ops = ss\n      .filter((s) => s.type === \"operator\")\n      .concat(\n        [\"IS NULL\", \"IS NOT NULL\"].map((name) => ({\n          name,\n          type: \"operator\",\n          label: { label: name },\n          insertText: name,\n          kind: getKind(\"operator\"),\n          range: undefined as any,\n        })),\n      );\n    return { ops, AndOr };\n  };\n\n  if (thisLineLC && expectsCondition && ltoken?.text === \")\") {\n    const { AndOr, ops } = getOperators();\n    return {\n      suggestions: ops.concat(AndOr),\n    };\n  }\n\n  /** First thing after condition keyword must ne column names  */\n  if (cb.prevTokens.some((t) => t.textLC === \"policy\")) {\n    if (\n      (cb.currNestingFunc?.textLC === \"using\" ||\n        cb.currNestingFunc?.textLC === \"check\") &&\n      cb.prevText.trim().endsWith(\"(\")\n    ) {\n      return await suggestColumnLike({ cb, ss, parentCb, setS, sql });\n    }\n  }\n\n  if (\n    thisLineLC &&\n    expectsCondition &&\n    ((getPreviousIdentifier() && cb.ltoken?.type !== \"operator.sql\") ||\n      l1token?.type === \"operator.sql\")\n  ) {\n    const { identifierText: maybeColumn } = getPreviousIdentifier() ?? {};\n    let prevCol = await getPrevCol(maybeColumn);\n\n    const { AndOr, ops } = getOperators();\n    if (cb.currToken && ops.some((s) => s.name === cb.currToken?.text)) {\n      return {\n        suggestions: [],\n      };\n    }\n    const isNotJSONBSelector = !l1token?.text.includes(\">\");\n    if (\n      isNotJSONBSelector &&\n      l1token?.type === \"operator.sql\" &&\n      ![\"and\", \"or\"].includes(l1token.textLC) &&\n      prevText.endsWith(\" \")\n    ) {\n      return {\n        suggestions: AndOr,\n      };\n    }\n\n    if (!isNotJSONBSelector && !prevCol) {\n      prevCol = await getPrevCol(l2token?.text);\n    }\n    if (prevCol?.colInfo) {\n      const { colInfo } = prevCol;\n      const leftColOperators = ops\n        .filter((o) => {\n          const leftTypes = o.operatorInfo?.left_arg_types;\n          return (\n            !o.operatorInfo || // No info means matches all\n            leftTypes?.some(\n              (t) =>\n                colInfo.data_type.toLowerCase().startsWith(t.toLowerCase()) ||\n                colInfo.udt_name.endsWith(t),\n            )\n          );\n        })\n        .concat(ltoken?.type === \"identifier.sql\" ? [] : AndOr)\n        .concat(ss.filter((s) => [\"IN\", \"NOT\"].includes(s.name.toUpperCase())))\n        .map((o) => ({ ...o }));\n      const usedOperators: Record<string, 1> = {};\n      return {\n        suggestions: leftColOperators.filter((o) => {\n          if (!usedOperators[o.name]) {\n            usedOperators[o.name] = 1;\n            return true;\n          }\n\n          return false;\n        }),\n      };\n    }\n    return {\n      suggestions: ops.concat(AndOr),\n    };\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/suggestFuncArgs.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\nimport { nameMatches } from \"./CommonMatchImports\";\nimport { getParentFunction } from \"./MatchSelect\";\nimport type {\n  ParsedSQLSuggestion,\n  SQLMatcherResultArgs,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport { suggestColumnLike } from \"./suggestColumnLike\";\n\nexport const suggestFuncArgs = async ({\n  cb,\n  parentCb,\n  ss,\n  setS,\n  sql,\n}: Pick<\n  SQLMatcherResultArgs,\n  \"cb\" | \"parentCb\" | \"ss\" | \"setS\" | \"sql\"\n>): Promise<\n  | undefined\n  | {\n      suggestions: ParsedSQLSuggestion[];\n    }\n> => {\n  if (cb.currNestingId) {\n    const insideFunc = getParentFunction(cb);\n    if (insideFunc) {\n      const funcDefs = ss.filter(\n        (s) => s.type === \"function\" && nameMatches(s, insideFunc.func),\n      );\n      if (insideFunc.func.textLC === \"current_setting\") {\n        return {\n          suggestions: setS.map((s) => ({\n            ...s,\n            insertText: `'${s.insertText || s.name}'`,\n          })),\n        };\n      }\n      if (funcDefs.some((f) => f.funcInfo?.arg_udt_names?.length)) {\n        const { suggestions } = (await suggestColumnLike(\n          { cb, parentCb, ss, setS, sql },\n          false,\n        )) as { suggestions: ParsedSQLSuggestion[] };\n        const activeArgIndex = insideFunc.prevArgs.length;\n        const activeArgs = funcDefs\n          .map((f) => f.funcInfo?.arg_udt_names?.[activeArgIndex])\n          .filter(isDefined);\n        const matchingTypeSuggestions = suggestions\n          .map((s) => {\n            if (![\"column\", \"function\"].includes(s.type)) {\n              return undefined;\n            }\n            const matchingDataTypes = [\n              [\"json\", \"jsonb\"],\n              [\n                \"numeric\",\n                \"decimal\",\n                \"float\",\n                \"real\",\n                \"integer\",\n                \"int4\",\n                \"int8\",\n                \"int2\",\n                \"bigint\",\n                \"smallint\",\n              ],\n            ];\n            const dataTypeMatches = activeArgs.some((activeArgUdtName) => {\n              const udt_name =\n                s.colInfo?.udt_name ?? s.funcInfo?.restype_udt_name;\n              if (!udt_name) return false;\n              return (\n                udt_name === activeArgUdtName ||\n                matchingDataTypes.some(\n                  (types) =>\n                    types.includes(activeArgUdtName) &&\n                    types.includes(udt_name),\n                ) ||\n                activeArgUdtName === \"any\" ||\n                (activeArgUdtName === \"anyarray\" && udt_name.startsWith(\"_\")) ||\n                (activeArgUdtName === \"bytea\" && udt_name === \"text\")\n              );\n            });\n            return {\n              ...s,\n              sortText:\n                s.type === \"column\" ?\n                  dataTypeMatches ? \"a\"\n                  : \"b\"\n                : dataTypeMatches ? \"c\"\n                : \"d\",\n            };\n          })\n          .filter(isDefined);\n        return {\n          suggestions:\n            matchingTypeSuggestions.length ?\n              matchingTypeSuggestions\n            : suggestions,\n        };\n      }\n    }\n  }\n  return undefined;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/suggestTableLike.ts",
    "content": "import type { SQLSuggestion } from \"../W_SQLEditor\";\nimport type { CodeBlock } from \"./completionUtils/getCodeBlock\";\nimport { getTableExpressionSuggestions } from \"./completionUtils/getTableExpressionReturnTypes\";\nimport { getJoinSuggestions } from \"./getJoinSuggestions\";\nimport type {\n  ParsedSQLSuggestion,\n  SQLMatchContext,\n} from \"./monacoSQLSetup/registerSuggestions\";\n\nexport const suggestTableLike = async (\n  args: Pick<SQLMatchContext, \"cb\" | \"ss\" | \"sql\"> & {\n    parentCb: CodeBlock | undefined;\n  },\n) => {\n  const { cb, ss, sql, parentCb } = args;\n  const isTableLike = (t: SQLSuggestion[\"type\"]) =>\n    [\"table\", \"view\", \"mview\"].includes(t);\n  const schemas = ss.filter((s) => s.type === \"schema\");\n  const { ltoken } = cb;\n  const maybeSchemaName =\n    cb.currToken?.text === \".\" ? cb.ltoken?.text\n    : cb.currToken?.text.includes(\".\") ? cb.currToken.text.split(\".\")[0]\n    : undefined;\n  const isSearchingSchemaTable =\n    maybeSchemaName && schemas.some((s) => s.name === maybeSchemaName);\n  let schemaMatchingTables: ParsedSQLSuggestion[] = [];\n  if (isSearchingSchemaTable && ltoken) {\n    const matchingTables = ss.filter(\n      (s) => isTableLike(s.type) && s.schema === maybeSchemaName,\n    );\n    schemaMatchingTables = matchingTables.map((t) => ({\n      ...t,\n      sortText: \"a\",\n      insertText: t.escapedName ?? t.escapedIdentifier ?? t.name,\n    }));\n  }\n\n  let aliasedTables: ParsedSQLSuggestion[] = [];\n  const withCb =\n    cb.ftoken?.textLC === \"with\" ? cb\n    : parentCb?.ftoken?.textLC === \"with\" ? parentCb\n    : undefined;\n  if (withCb) {\n    const tableExpressions = await getTableExpressionSuggestions(\n      { cb: withCb, ss, sql },\n      \"table\",\n    );\n    // const isCurrentlyInsideACte = cb.currNestingFunc?.textLC === \"as\" && cb.currNestingFunc.nestingId.length === 0;\n    aliasedTables = tableExpressions.tablesWithAliasInfo\n      /** Ensure only preceding tables are included */\n      .filter((t) => t.endOffset < cb.currOffset)\n      .map((t) => ({ ...t.s, sortText: \"a\" }));\n  }\n\n  const schemaTables =\n    isSearchingSchemaTable ? schemaMatchingTables : (\n      ss\n        .filter(\n          (s) =>\n            isTableLike(s.type) &&\n            !(\n              s.tablesInfo?.oid &&\n              aliasedTables.some(\n                (at) => at.tablesInfo?.oid === s.tablesInfo?.oid,\n              )\n            ),\n        )\n        .map((s) => ({ ...s, sortText: s.schema === \"public\" ? \"b\" : \"c\" }))\n    );\n  const tables = [...schemaTables, ...aliasedTables];\n\n  const setofFuncs = ss\n    .filter((s) => {\n      return (\n        (!isSearchingSchemaTable || s.funcInfo?.schema === maybeSchemaName) &&\n        s.funcInfo?.restype?.toLowerCase().includes(\"setof\")\n      );\n    })\n    .map((s) => ({ ...s, sortText: \"c\" }));\n\n  const joinSuggestions = getJoinSuggestions({\n    ss,\n    rawExpect: \"tableOrView\",\n    cb,\n  });\n\n  return {\n    suggestions: [\n      ...joinSuggestions.map((s) => ({ ...s, sortText: \"1\" })),\n      ...tables,\n      ...setofFuncs,\n    ],\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/suggestValue.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\nimport type { ColInfo } from \"../../W_Table/TableMenu/getChartCols\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { getTableExpressionSuggestions } from \"./completionUtils/getTableExpressionReturnTypes\";\nimport type { TokenInfo } from \"./completionUtils/getTokens\";\nimport {\n  KNDS as KindMap,\n  type SQLMatchContext,\n  type SQLMatcherResultArgs,\n  type SQLMatcherResultType,\n} from \"./monacoSQLSetup/registerSuggestions\";\n\n/**\n * Convenient autocomplete (column = 'value')\n * */\nexport const suggestValue = async (\n  args: SQLMatchContext & Pick<SQLMatcherResultArgs, \"parentCb\">,\n  prevKwdToken: TokenInfo | undefined,\n  getPreviousIdentifier: () =>\n    | {\n        identifierText: string;\n        colTokens: TokenInfo[];\n      }\n    | undefined,\n): Promise<SQLMatcherResultType | undefined> => {\n  const { cb, ss, sql, parentCb } = args;\n\n  /** Convenient autocomplete (column = 'value') */\n  if (\n    prevKwdToken?.textLC === \"where\" &&\n    (allowedOperands.includes(cb.ltoken?.textLC ?? \"\") ||\n      cb.currNestingFunc?.textLC === \"in\") &&\n    getPreviousIdentifier() &&\n    [\"select\", \"with\", \"delete\", \"update\"].includes(cb.ftoken?.textLC ?? \"\") &&\n    cb.currToken?.type === \"string.sql\"\n  ) {\n    const { identifierText: columnName, colTokens } = getPreviousIdentifier()!;\n    const currentText = `${cb.currToken.text.slice(1, -1)}`;\n    const currentFilter = `%${cb.currToken.text.slice(1, -1)}%`;\n    const [matchingTable, ...other] = ss.filter(\n      (s) =>\n        (s.type === \"table\" || s.type === \"view\") &&\n        cb.tokens.some(\n          (t) =>\n            s.escapedIdentifier === t.text ||\n            (s.schema === \"pg_catalog\" && s.escapedName === t.text),\n        ) &&\n        s.cols?.some((c) => c.escaped_identifier === columnName),\n    );\n    const getQueries = (udt_name: ColInfo[\"udt_name\"]) => {\n      let expression = columnName;\n      if (\n        [\"jsonb\", \"json\"].includes(udt_name) &&\n        colTokens.some((t) => t.text.includes(\"->\")) &&\n        colTokens.at(-1)?.type === \"string.sql\"\n      ) {\n        expression = `(${colTokens.map((t) => t.text).join(\"\")})`;\n      }\n      const querySelect = `DISTINCT LEFT(${expression}::TEXT, 500) as str, position(lower(\\${currentText}) in lower(${expression}::TEXT)) as pos`;\n      const queryFilter = ` WHERE LEFT(${expression}::TEXT, 500) ilike \\${currentFilter} ORDER BY 2,1 LIMIT 20`;\n      return {\n        querySelect,\n        queryFilter,\n      };\n    };\n    let query = \"\";\n    if (!matchingTable) {\n      const { columnsWithAliasInfo } = await getTableExpressionSuggestions(\n        { parentCb, cb, ss, sql },\n        \"columns\",\n      );\n      const col =\n        columnsWithAliasInfo.find(({ s }) => s.insertText === columnName) ||\n        /** ensure \"name\" works */\n        columnsWithAliasInfo.find(\n          ({ s }) => JSON.stringify(s.insertText) === columnName,\n        );\n      if (col) {\n        const { queryFilter, querySelect } = getQueries(\n          col.s.colInfo?.udt_name as any,\n        );\n        query = col.getQuery(querySelect) + queryFilter;\n      }\n    } else if (!other.length) {\n      const { queryFilter, querySelect } = getQueries(\n        matchingTable.cols!.find((c) => c.escaped_identifier === columnName)!\n          .udt_name as any,\n      );\n      query = `SELECT ${querySelect} FROM ${matchingTable.escapedIdentifier} ${queryFilter}`;\n    }\n    if (query) {\n      const result = await args.sql?.(\n        query,\n        { currentFilter, currentText },\n        { returnType: \"default-with-rollback\" },\n      );\n      const values = result?.rows\n        .map((r: any) => r.str?.toString())\n        .filter(isDefined);\n      if (values?.length) {\n        return suggestSnippets(\n          values.map((v) => ({\n            label: v,\n            type: \"value\",\n            kind: KindMap.Constant,\n          })),\n        );\n      }\n    }\n  }\n};\n\nexport const allowedOperands = [\n  \"=\",\n  \">\",\n  \"<\",\n  \">=\",\n  \"<=\",\n  \"<>\",\n  \"!=\",\n  \"like\",\n  \"ilike\",\n  \"~~\",\n  \"!~~\",\n];\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLCompletion/withKWDs.ts",
    "content": "import { isDefined, isObject } from \"prostgles-types\";\nimport type { CodeBlock } from \"./completionUtils/getCodeBlock\";\nimport { getCurrentNestingOffsetLimits } from \"./completionUtils/getCodeBlock\";\nimport type { SQLSuggestion } from \"../W_SQLEditor\";\nimport type { MinimalSnippet } from \"./CommonMatchImports\";\nimport { suggestSnippets } from \"./CommonMatchImports\";\nimport { getExpected } from \"./getExpected\";\nimport {\n  getKind,\n  type GetKind,\n  type ParsedSQLSuggestion,\n  type SQLMatchContext,\n} from \"./monacoSQLSetup/registerSuggestions\";\nimport { suggestFuncArgs } from \"./suggestFuncArgs\";\nimport { suggestColumnLike } from \"./suggestColumnLike\";\n\ntype ExpectString = SQLSuggestion[\"type\"] | \"condition\" | \"number\" | \"string\";\nexport type KWD = {\n  kwd: string;\n  expects?:\n    | ExpectString\n    | `(${ExpectString})`\n    | readonly ExpectString[]\n    | \"(options)\"\n    | \"=option\";\n  options?:\n    | readonly MinimalSnippet[]\n    | readonly string[]\n    | ((ss: ParsedSQLSuggestion[], cb: CodeBlock) => ParsedSQLSuggestion[]);\n\n  justAfter?: readonly string[];\n  /**\n   * Will only show exactly after the specified token text\n   */\n  exactlyAfter?: readonly string[];\n\n  excludeIf?: readonly string[] | ((cb: CodeBlock) => boolean);\n  /**\n   * Will only show if prevText contains this text with spaces on each side\n   */\n  dependsOn?: string;\n\n  /**\n   * Will only show if nextText contains this text with spaces on each side\n   */\n  dependsOnAfter?: string;\n  docs?: string;\n  canRepeat?: boolean;\n\n  include?: (cb: CodeBlock) => boolean;\n\n  /**\n   * If true will show a label text with \"optional\"\n   */\n  optional?: boolean;\n};\n\ntype Opts = {\n  notOrdered?: boolean;\n  /** Keyword where prevText starts from */\n  topResetKwd?: string;\n};\n\ntype WithKwdArgs = SQLMatchContext & {\n  opts?: Opts;\n  parentCb?: CodeBlock;\n};\n\nexport const withKWDs = <KWDD extends KWD>(\n  kwds: readonly KWDD[],\n  { cb, ss, setS, opts, sql, parentCb }: WithKwdArgs,\n): {\n  suggestKWD: (\n    vals: string[],\n    sortText?: string,\n  ) => {\n    suggestions: ParsedSQLSuggestion[];\n  };\n  prevKWD: KWDD | undefined;\n  prevIdentifiers: string[];\n  remainingKWDS: (KWDD & { docs?: string; sortText: string })[];\n  getSuggestion: (\n    delimiter?: string,\n    excludeIf?: string[],\n  ) => Promise<{\n    suggestions: ParsedSQLSuggestion[];\n  }>;\n} => {\n  const { notOrdered = false, topResetKwd } = opts ?? {};\n\n  const currNestingId = cb.currNestingId;\n  const currNestLimits = getCurrentNestingOffsetLimits(cb);\n  let currTokens = cb.tokens.slice(0);\n  if (currNestingId) {\n    const currNestingStartIdx = cb.tokens\n      .slice(0)\n      .map((t, i) => ({ t, i }))\n      .reverse()\n      .find(({ t }, i, arr) => {\n        const next = arr[i + 1]?.t;\n        return (\n          next?.text === \"(\" &&\n          next.offset <= cb.currOffset &&\n          t.nestingId === currNestingId\n        );\n      })?.i;\n    const currNestingEndIdx = cb.tokens.findIndex((t, i, arr) => {\n      const next = arr[i + 1];\n      return (\n        next?.text === \")\" &&\n        next.offset >= cb.currOffset &&\n        t.nestingId === currNestingId\n      );\n    });\n    currTokens = cb.tokens.slice(currNestingStartIdx, currNestingEndIdx + 1);\n  }\n  const currNestingTokens = currTokens.filter((t) =>\n    currNestLimits?.isEmpty === false ?\n      t.offset >= currNestLimits.limits[0] && t.end <= currNestLimits.limits[1]\n    : currNestingId === t.nestingId ||\n      kwds.some(\n        (k) =>\n          k.expects === \"(options)\" &&\n          cb.currNestingFunc?.textLC === k.kwd.toLowerCase(),\n      ),\n  );\n  const startIndex =\n    !topResetKwd ? 0 : (\n      currNestingTokens\n        .slice(0)\n        .map((t, idx) => ({ ...t, idx }))\n        .findLast(\n          (t) =>\n            t.offset <= cb.offset && t.textLC === topResetKwd.toLowerCase(),\n        )?.idx\n    );\n\n  const usedKeywordsWithoutInput = kwds\n    .flatMap((kwd, kwdIdx) => {\n      const kwdWords = kwd.kwd.split(\" \");\n      const matchedStartingIndexes: number[] = [];\n      currNestingTokens.forEach((t, i) => {\n        if (\n          kwdWords.every((kWord, kWordIdx) => {\n            return (\n              currNestingTokens[i + kWordIdx]?.text.toUpperCase() ===\n              kWord.toUpperCase()\n            );\n          })\n        ) {\n          matchedStartingIndexes.push(i);\n        }\n      });\n\n      return matchedStartingIndexes.map((startingTokenIdx) => {\n        const startingToken = currNestingTokens[startingTokenIdx];\n        if (!startingToken) return undefined;\n\n        const start = startingToken.offset;\n        const end = startingToken.offset + kwd.kwd.length;\n        return {\n          kwd,\n          kwdIdx,\n          startingToken,\n          start,\n          end,\n          length: kwd.kwd.length,\n        };\n      });\n    })\n    .filter(isDefined);\n\n  let usedKeywords = usedKeywordsWithoutInput.map((uk, i, arr) => {\n    const nextUK = arr[i + 1];\n    const inputTokens = cb.tokens.filter(\n      (t) => t.offset > uk.end && (!nextUK || t.end <= nextUK.start),\n    );\n    return {\n      ...uk,\n      inputTokens,\n    };\n  });\n\n  /** Remove smaller overlapping kwds (JOIN vs LEFT JOIN) */\n  usedKeywords = usedKeywords\n    .filter((k) => {\n      return !usedKeywords.some(\n        (longerOverlapping) =>\n          k.start < longerOverlapping.end &&\n          longerOverlapping.start < k.end &&\n          longerOverlapping.length > k.length,\n      );\n    })\n    .sort((a, b) => a.start - b.start);\n  let prevKWDTokens = usedKeywords.filter(\n    (k) => k.startingToken.end <= cb.offset,\n  );\n  if (cb.currNestingFunc) {\n    const optionsKwd = prevKWDTokens.find(\n      (k) =>\n        k.kwd.expects === \"(options)\" &&\n        cb.currNestingFunc!.textLC === k.kwd.kwd.toLowerCase(),\n    );\n    if (optionsKwd) {\n      prevKWDTokens = [optionsKwd];\n    }\n  }\n\n  const prevTokens = cb.tokens\n    .slice(startIndex)\n    .filter((t) => t.offset < cb.offset && currNestingId === t.nestingId);\n  const nextTokens = cb.tokens.filter(\n    (t) => t.offset >= cb.offset && currNestingId === t.nestingId,\n  );\n\n  const prevKWDFull = prevKWDTokens.at(-1);\n  const prevKWD = prevKWDFull?.kwd;\n  const prevKWDToken = prevKWDTokens.at(-1)?.startingToken;\n\n  const prevKWDTokenIdx = prevTokens.findIndex(\n    (t) => t.offset === prevKWDToken?.offset,\n  );\n  const prevIdentifiers = prevTokens\n    .slice(prevKWDTokenIdx)\n    .filter((t) => t.type === \"identifier.sql\")\n    .map((t) => t.text);\n\n  const prevText = prevTokens.map((t) => t.text).join(\" \");\n\n  const prevUsedKwd = usedKeywords.findLast(\n    (uk) => uk.startingToken.offset <= cb.offset && !uk.kwd.canRepeat,\n  );\n  const nextUsedKwd = usedKeywords.find(\n    (uk) => uk.startingToken.offset > cb.offset && !uk.kwd.canRepeat,\n  );\n  let _remainingKWDS =\n    notOrdered ?\n      kwds.slice(0)\n    : kwds.slice(\n        (prevUsedKwd?.kwdIdx ?? -1) + 1,\n        nextUsedKwd?.kwd ? nextUsedKwd.kwdIdx : kwds.length,\n      );\n\n  _remainingKWDS = _remainingKWDS.filter((k) => {\n    const show =\n      k.canRepeat || !usedKeywords.some(({ kwd }) => k.kwd === kwd.kwd);\n    return show;\n  });\n  _remainingKWDS = _remainingKWDS.filter((k) => {\n    let show = true as boolean;\n    if (k.justAfter) {\n      show =\n        show &&\n        prevTokens.some((t) =>\n          k.justAfter?.includes(t.text.toUpperCase() as never),\n        );\n    }\n    const prevTextIncludes = (v: string) =>\n      ` ${prevText.toLowerCase()} `.includes(` ${v.toLowerCase()} `) ||\n      prevText.toLowerCase().startsWith(`${v.toLowerCase()} `);\n    if (k.dependsOn) {\n      show = show && prevTextIncludes(k.dependsOn);\n    }\n    if (k.dependsOnAfter) {\n      show = show && prevTextIncludes(k.dependsOnAfter);\n    }\n\n    if (k.excludeIf) {\n      const { excludeIf } = k;\n      show =\n        show &&\n        !(typeof excludeIf === \"function\" ?\n          excludeIf(cb)\n        : prevTokens.some((t) => excludeIf.includes(t.text)));\n    }\n\n    if (k.exactlyAfter?.length) {\n      show = cb.ltoken?.textLC === k.kwd.toLowerCase();\n    }\n\n    if (k.include) {\n      show = k.include(cb);\n    }\n\n    return show;\n  });\n  if (\n    _remainingKWDS.some(\n      (k) => prevKWD?.kwd && k.justAfter?.includes(prevKWD.kwd as never),\n    )\n  ) {\n    _remainingKWDS = _remainingKWDS.filter(\n      (k) =>\n        k.justAfter &&\n        /** Remove used matching justAfter */\n        !prevKWDTokens.some((pkwd) =>\n          pkwd.kwd.justAfter?.some((pja) => k.justAfter?.includes(pja)),\n        ),\n    );\n  }\n\n  const remainingKWDS = _remainingKWDS.map((rk) => ({\n    ...rk,\n    sortText: kwds\n      .findIndex((_k) => _k.kwd === rk.kwd)\n      .toString()\n      .padStart(2, \"0\"),\n    docs:\n      rk.docs ||\n      ss.find((s) => s.name === rk.kwd && s.documentation)?.documentation,\n  }));\n\n  return {\n    suggestKWD: (vals, sortText) => suggestKWD(getKind, vals, sortText),\n    prevKWD,\n    prevIdentifiers,\n    remainingKWDS,\n    getSuggestion: async (\n      delimiter?: string,\n      excludeIf?: string[],\n    ): Promise<{\n      suggestions: ParsedSQLSuggestion[];\n    }> => {\n      /** TODO Add parentCb */\n      const funcArgs = await suggestFuncArgs({ cb, ss, setS, sql });\n      if (funcArgs) return funcArgs;\n\n      if (cb.currToken?.text === \";\") {\n        return { suggestions: [] };\n      }\n\n      const { ltoken, currToken } = cb;\n      if (\n        !currToken &&\n        ltoken &&\n        (!prevUsedKwd || prevUsedKwd.end < ltoken.end)\n      ) {\n        const stillWritingKwd = kwds.filter((t) =>\n          t.kwd.toUpperCase().startsWith(ltoken.text.toUpperCase() + \" \"),\n        );\n        if (stillWritingKwd.length) {\n          return suggestSnippets(\n            stillWritingKwd.map((k) => {\n              const remainingText = k.kwd.slice(ltoken.text.length + 1);\n              return {\n                label: {\n                  label: remainingText,\n                  detail: k.optional ? \" (optional)\" : undefined,\n                },\n                docs:\n                  k.docs ||\n                  ss.find((s) => s.name === k.kwd && s.documentation)\n                    ?.documentation,\n                insertText: remainingText,\n                kind: getKind(\"keyword\"),\n              };\n            }),\n          );\n        }\n      }\n\n      const [firstInputToken] = (prevKWDFull?.inputTokens ?? []).filter(\n        (t) => t.type !== \"delimiter.parenthesis.sql\" && t.end <= cb.currOffset,\n      );\n      const gapsInInputTokens = (prevKWDFull?.inputTokens ?? []).filter(\n        (t, i) => {\n          const prevT = prevKWDFull?.inputTokens[i - 1];\n          return prevT && prevT.end < t.offset;\n        },\n      );\n      const stillWritingInput =\n        prevKWD?.expects === \"(options)\" ||\n        (firstInputToken && firstInputToken.offset === cb.currToken?.offset) ||\n        Boolean(\n          prevKWD &&\n            firstInputToken &&\n            !gapsInInputTokens.length &&\n            cb.currToken,\n        );\n      const prevKWDMissingInput =\n        stillWritingInput ||\n        (!!prevKWD &&\n          (!firstInputToken ||\n            kwds.some((k) => k.kwd.toLowerCase() === firstInputToken.textLC))); //  || cb.currToken);\n      if ((prevKWD?.expects || prevKWD?.options) && prevKWDMissingInput) {\n        let firstSuggestions: ParsedSQLSuggestion[] = [];\n        if (prevKWD.options) {\n          const { options } = prevKWD;\n          if (Array.isArray(options)) {\n            const options1: (\n              | (MinimalSnippet & { type?: undefined })\n              | ParsedSQLSuggestion\n            )[] = options.map((o) =>\n              typeof o === \"string\" ? { label: o } : o,\n            );\n            const minimalSnippets = options1.filter((o) => !o.type);\n            const parsedSuggestions = options1.filter(\n              (o) => o.type,\n            ) as ParsedSQLSuggestion[];\n            firstSuggestions = [\n              ...suggestSnippets(minimalSnippets).suggestions,\n              ...parsedSuggestions,\n            ];\n            if (\n              prevKWD.expects === \"=option\" &&\n              !cb.prevText.trim().endsWith(\"=\")\n            ) {\n              firstSuggestions = firstSuggestions.map((s) => ({\n                ...s,\n                insertText: `=${(isObject(s.label) ? s.label.label : s.label) || s.escapedIdentifier || s.escapedName}`,\n              }));\n            }\n          } else if (typeof options === \"function\") {\n            firstSuggestions = options(ss, cb);\n          }\n        }\n\n        let expectedSuggestions: ParsedSQLSuggestion[] = [];\n        if (!prevKWD.expects || prevKWD.expects === \"(options)\") {\n          expectedSuggestions = [];\n        } else if (prevKWD.expects === \"column\") {\n          expectedSuggestions = (\n            await suggestColumnLike({ cb, ss, setS, parentCb, sql })\n          ).suggestions.filter((s) => s.type === \"column\");\n        } else {\n          expectedSuggestions = getExpected(\n            prevKWD.expects,\n            cb,\n            ss,\n          ).suggestions;\n        }\n\n        const result = {\n          suggestions: [\n            ...firstSuggestions.map((s) => ({\n              ...s,\n              sortText: \"0\" + (s.sortText ?? \"\"),\n            })),\n            ...expectedSuggestions.map((s) => ({\n              ...s,\n              sortText:\n                (cb.prevTokens.some((prevT) => s.insertText === prevT.text) ?\n                  \"b\"\n                : \"a\") + (s.sortText ?? \"\"),\n            })),\n          ],\n        };\n\n        if (\n          prevKWD.expects === \"(options)\" &&\n          cb.currNestingFunc?.textLC !== prevKWD.kwd.toLowerCase()\n        ) {\n          return {\n            suggestions: result.suggestions.map((s) => ({\n              ...s,\n              insertText: `(${s.insertText}$0)`,\n            })),\n          };\n        }\n\n        return result;\n      }\n\n      return suggestSnippets(\n        remainingKWDS.map((k) => {\n          const addDelimiter =\n            delimiter &&\n            cb.currToken?.textLC !== \"(\" &&\n            ![...(excludeIf ?? []), delimiter, \"(\"].some((t) =>\n              cb.ltoken?.text.trim().endsWith(t),\n            );\n\n          const insertText = (addDelimiter ? delimiter : \"\") + k.kwd;\n\n          return {\n            label: {\n              label: k.kwd,\n              detail: k.optional ? \" (optional)\" : undefined,\n            },\n            docs:\n              k.docs ||\n              ss.find((s) => s.name === k.kwd && s.documentation)\n                ?.documentation,\n            insertText,\n            kind: getKind(\"keyword\"),\n            sortText: k.sortText,\n          };\n        }),\n      );\n    },\n  };\n};\n\nexport const suggestKWD = (\n  getKind: GetKind,\n  vals: string[],\n  sortText?: string,\n) =>\n  suggestSnippets(\n    vals.map((label, idx) => ({\n      label,\n      sortText: sortText ?? idx + \"\",\n      insertText: label,\n      kind: getKind(\"keyword\"),\n      // docs: ss?.find(s => s.name === label && s.documentation)?.documentation\n    })),\n  );\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLEditorSuggestions.ts",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject, SQLHandler } from \"prostgles-types\";\nimport {\n  getKeys,\n  isDefined,\n  omitKeys,\n  pickKeys,\n  tryCatchV2,\n} from \"prostgles-types\";\nimport { TOP_KEYWORDS, asSQL } from \"./SQLCompletion/KEYWORDS\";\nimport {\n  type PG_Policy,\n  PRIORITISED_OPERATORS,\n  getPGObjects,\n} from \"./SQLCompletion/getPGObjects\";\nimport type { ParsedSQLSuggestion } from \"./SQLCompletion/monacoSQLSetup/registerSuggestions\";\nimport { SQL_SNIPPETS } from \"./SQL_SNIPPETS\";\nimport type { SQLSuggestion } from \"./W_SQLEditor\";\n\ntype DB = { sql: SQLHandler };\n\nconst asList = (arr: { label: string; value?: string }[], boldStyle = false) =>\n  arr\n    .filter(({ value }) => value !== undefined)\n    .map(({ label, value }) =>\n      boldStyle ?\n        `${label}: ${`**${value}**`.replace(\"****\", \"\")}  `\n      : `${label}:  \\`${value}\\`  `,\n    )\n    .join(\"\\n\");\n\nexport const asListObject = (\n  obj: AnyObject,\n  excludeNulls = true,\n  boldStyle?: boolean,\n) =>\n  asList(\n    Object.entries(obj)\n      .filter(([k, v]) => !excludeNulls || v !== null)\n      .map(([label, value]) => ({ label, value })),\n    boldStyle,\n  );\n\nexport const getSqlSuggestions = async (\n  db: DB,\n): Promise<{\n  suggestions: SQLSuggestion[];\n  settingSuggestions: SQLSuggestion[];\n}> => {\n  let suggestions: SQLSuggestion[] = [];\n\n  try {\n    const makeDocumentation = (obj: AnyObject) => {\n      return getKeys(obj)\n        .filter((k) => k !== \"escaped_identifier\")\n        .map((k) => `  ${k}: **${obj[k]}**   `)\n        .join(\"\\n\");\n    };\n    const {\n      constraints,\n      roles,\n      databases,\n      indexes,\n      triggers,\n      policies,\n      eventTriggers,\n      extensions,\n      keywords,\n      publications,\n      subscriptions,\n      schemas,\n      functions,\n      tables,\n      dataTypes,\n      operators,\n      settings,\n      rules,\n    } = await getPGObjects(db);\n\n    suggestions = suggestions.concat(\n      rules.map((r) => ({\n        label: { label: r.rulename, description: r.tablename },\n        name: r.rulename,\n        type: \"rule\",\n        detail: \"(rule)\",\n        schema: r.schemaname,\n        parentName: r.tablename,\n        escapedIdentifier: r.escaped_identifier,\n        insertText: r.escaped_identifier,\n        ruleInfo: r,\n        filterText: `${r.rulename} ${r.tablename}`,\n        documentation: `Table: **${r.tablename}**  \\nDefinition:\\n ${asSQL(r.definition)}`,\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      constraints.map((c) => ({\n        label: { label: c.conname, description: c.table_name },\n        name: c.conname,\n        type: \"constraint\",\n        detail: \"(constraint)\",\n        schema: c.schema,\n        parentName: c.table_name,\n        escapedIdentifier: c.escaped_identifier,\n        insertText: c.escaped_identifier,\n        constraintInfo: c,\n        filterText: `${c.conname} ${c.table_name}`,\n        documentation: `Table: **${c.table_name}**  \\nDefinition:\\n ${asSQL(`ALTER TABLE ${c.table_name} ADD CONSTRAINT \\n` + c.definition)}`,\n      })),\n    );\n\n    const extractIndexCols = (def: string) => {\n      return def.split(\"USING\")[1]?.split(\"(\")[1]?.split(\")\")[0];\n    };\n    suggestions = suggestions.concat(\n      indexes.map((p) => ({\n        detail: \"(index)\",\n        label: {\n          label: p.indexname,\n          detail: `  ${p.index_size}`,\n          description: extractIndexCols(p.indexdef),\n        },\n        name: p.indexname,\n        type: \"index\",\n        insertText: p.escaped_identifier,\n        escapedParentName: p.escaped_tablename,\n        indexInfo: p,\n        documentation:\n          asListObject(\n            omitKeys(p, [\n              \"escaped_identifier\",\n              \"indexdef\",\n              \"indexname\",\n              \"escaped_tablename\",\n              \"schemaname\",\n              \"tablename\",\n            ] as const),\n          ) +\n          `\\n\\n**Definition**: \\n${asSQL(p.indexdef)\n            .split(\" ON \")\n            .join(\" \\nON \")\n            .split(\" USING \")\n            .join(\" \\nUSING \")\n            .split(\" WHERE \")\n            .join(\" \\nWHERE \")}`,\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      policies.map((p) => ({\n        detail: \"(policy)\",\n        label: { label: p.policyname, description: p.tablename },\n        name: p.policyname,\n        type: \"policy\",\n        insertText: p.escaped_identifier,\n        policyInfo: p,\n        // documentation: `To (roles): ${p.roles}  \\nFor (commands): ${p.cmd}   \\nType: ${p.type}   \\nUsing${p.using}   \\nWith check: ${p.with_check}`\n        documentation: `**Definition**: \\n${asSQL(p.definition)}\n      `,\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      triggers.map((t) => ({\n        detail: \"(trigger)\",\n        label: t.trigger_name,\n        name: t.trigger_name,\n        type: \"trigger\",\n        insertText: t.escaped_identifier,\n        triggerInfo: t,\n        documentation:\n          asListObject(\n            pickKeys(t, [\n              \"disabled\",\n              \"trigger_schema\",\n              \"event_object_schema\",\n              \"event_object_table\",\n            ]),\n          ) +\n          \"\\n\\n\" +\n          asSQL(`${t.definition};\\n\\n${t.function_definition ?? \"\"}`)\n            .replace(\" BEFORE \", \"\\nBEFORE \")\n            .replace(\" AFTER \", \"\\nAFTER \")\n            .replace(\" ON \", \"\\nON \")\n            .replace(\" REFERENCING \", \"\\nREFERENCING \")\n            .replace(\" FOR \", \"\\nFOR \")\n            .replace(\" EXECUTE \", \"\\nEXECUTE \"),\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      eventTriggers.map((t) => ({\n        detail: \"(eventTrigger)\",\n        label: t.Name,\n        name: t.Name,\n        type: \"eventTrigger\",\n        insertText: t.escaped_identifier,\n        eventTriggerInfo: t,\n        documentation:\n          asListObject(omitKeys(t, [\"escaped_identifier\"])) +\n          `\\n\\n${asSQL(t.function_definition ?? \"\")}`,\n      })),\n    );\n\n    const { data: columnStats } = await tryCatchV2(async () => {\n      const vals = (await db.sql(\n        `\n          SELECT \n              schemaname\n            , tablename\n            , attname\n            , (most_common_vals::text::_text)[:2] as most_common_vals\n          FROM pg_catalog.pg_stats\n          WHERE schemaname = current_schema()\n      `,\n        {},\n        { returnType: \"rows\" },\n      )) as {\n        schemaname: string;\n        tablename: string;\n        attname: string;\n        most_common_vals: string[] | null;\n      }[];\n\n      return vals;\n    });\n\n    tables.forEach((t) => {\n      const type =\n        t.relkind === \"r\" ? \"table\"\n        : t.relkind === \"v\" ? \"view\"\n        : \"mview\";\n      const tConstraints = constraints.filter((c) => c.table_oid === t.oid);\n      const tIndexes = indexes.filter(\n        (index) => index.tablename === t.name && index.schemaname === t.schema,\n      );\n      const tableTriggers = triggers.filter(\n        (trg) =>\n          trg.event_object_table === t.name &&\n          trg.event_object_schema === t.schema,\n      );\n\n      const cols = t.cols.map((c) => {\n        const colConstraint = tConstraints.find(\n          (con) =>\n            [\"p\", \"f\", \"c\"].includes(con.contype) &&\n            con.conkey?.includes(c.ordinal_position),\n        );\n        const singleColConstraints = tConstraints.filter(\n          (con) =>\n            con.conkey?.length === 1 &&\n            [\"p\", \"f\", \"c\"].includes(con.contype) &&\n            con.conkey.includes(c.ordinal_position),\n        );\n\n        const dataType =\n          [\"USER-DEFINED\"].includes(c.data_type.toUpperCase()) ?\n            c.udt_name.toUpperCase()\n          : c.data_type.toUpperCase();\n\n        return {\n          ...c,\n          data_type: dataType,\n          cConstraint: colConstraint,\n          definition: [\n            c.escaped_identifier,\n            dataType +\n              ((\n                c.udt_name.toLowerCase() === \"numeric\" &&\n                c.numeric_precision !== null\n              ) ?\n                `(${[c.numeric_precision, c.numeric_scale].join(\", \")})`\n              : c.character_maximum_length !== null ?\n                `(${c.character_maximum_length})`\n              : \"\"),\n            singleColConstraints.some((c) => c.contype === \"p\") ? \"PRIMARY KEY\"\n            : c.nullable ? \"\"\n            : \"NOT NULL\",\n            c.column_default !== null ? `DEFAULT ${c.column_default}` : \"\",\n            colConstraint && colConstraint.contype !== \"p\" ?\n              `, \\n ${colConstraint.definition}`\n            : \"\",\n          ]\n            .filter((v) => v.trim())\n            .join(\" \"),\n        };\n      });\n\n      const tPolicies = policies.filter(\n        (p) => p.tablename === t.name && p.schemaname === t.schema,\n      );\n      const parseIntervalStr = (str: string | null) => {\n        return (\n          str\n            ?.split(\" \")\n            .filter((v, i, arr) => i === arr.length - 1 || !v.startsWith(\"00\"))\n            .map((v) => (v.startsWith(\"0\") ? v.slice(1) : v))\n            .join(\" \") ?? \"Never\"\n        );\n      };\n      const documentation =\n        t.is_view ?\n          `**Definition:**  \\n\\n${asSQL(t.view_definition || \"\")}`\n        : [\n            `${t.comment ? `**Comment:** \\n\\n ${t.comment}` : \"\"}`,\n            `**Columns (${cols.length}):**  \\n${asSQL(cols.map((c) => c.definition).join(\",  \\n\"))} `,\n            `**Constraints (${tConstraints.length}):** \\n ${asSQL(tConstraints.map((c) => c.definition + \";\").join(\"\\n\"))} `,\n            `**Indexes (${tIndexes.length}):** \\n ${asSQL(tIndexes.map((d) => d.indexdef + \";\").join(\"\\n\"))}  `,\n            `**Triggers (${tableTriggers.length}):** \\n ${asSQL(tableTriggers.map((d) => d.trigger_name + \";\").join(\"\\n\"))}  `,\n            `**Policies (${tPolicies.length}):** \\n ${asSQL(tPolicies.map((p) => p.definition + \";\").join(\"\\n\\n\"))} `,\n          ].join(\"\\n\") +\n          (!t.tableStats ? \"\" : (\n            `\\n ${asListObject({\n              oid: t.tableStats.relid,\n              Size: t.tableStats.table_size,\n              \"Live Tuples\": t.tableStats.n_live_tup,\n              \"Dead Tuples\": t.tableStats.n_dead_tup,\n              \"Last Vacuum\": parseIntervalStr(t.tableStats.last_vacuum),\n              \"Last AutoVacuum\": parseIntervalStr(t.tableStats.last_autovacuum),\n              \"Seq Scans\": t.tableStats.seq_scans,\n              \"Idx Scans\": t.tableStats.idx_scans,\n            })}\\n`\n          ));\n      suggestions.push({\n        OID: t.oid,\n        type,\n        label: { label: t.name, description: t.schema },\n        // name: t.escaped_identifier,\n        name: t.name,\n        subLabel: `(${t.cols.map((c) => `${c.escaped_identifier} ${c.udt_name.toUpperCase()}`).join(\", \")})`,\n        escapedIdentifier: t.escaped_identifier,\n        escapedName: t.escaped_name,\n        schema: t.schema,\n        insertText: t.escaped_identifier,\n        detail:\n          t.relkind === \"m\" ? `(materialized view) ${t.name}`\n          : t.is_view ? `(view) ${t.name}`\n          : `(table) ${t.name}`,\n        view: t.is_view ? { definition: t.view_definition! } : undefined,\n        relkind: t.relkind,\n        documentation,\n        tablesInfo: { ...t, constraints: tConstraints },\n        cols,\n      });\n      suggestions = suggestions.concat(\n        cols.map((c) => {\n          const cIndexes = tIndexes.filter((d) =>\n            d.indexdef\n              .slice(d.indexdef.indexOf(\"(\"), d.indexdef.lastIndexOf(\")\"))\n              .includes(c.escaped_identifier),\n          );\n          const indexInfo =\n            t.is_view ? \"\" : (\n              `\\n\\n\\n\\n**Indexes**(${cIndexes.length}): \\n${asSQL(cIndexes.map((i) => i.indexdef.split(\" USING \")[1] || i.indexdef).join(\", \\n\"))}`\n            );\n          const topValues =\n            columnStats\n              ?.find(\n                (cs) =>\n                  cs.schemaname === t.schema &&\n                  cs.tablename === t.name &&\n                  cs.attname === c.name,\n              )\n              ?.most_common_vals?.map((v) => v.slice(0, 20))\n              .join(\", \") ?? \"\";\n          return {\n            label: getColumnSuggestionLabel(c, t.name),\n            name: c.name,\n            detail: \"(column) \",\n            escapedParentName: t.escaped_identifier,\n            schema: t.schema,\n            parentName: t.name,\n            parentOID: t.oid,\n            escapedIdentifier: c.escaped_identifier,\n            insertText: c.escaped_identifier,\n            documentation:\n              `${asListObject({\n                Table: t.escaped_identifier,\n                Schema: t.schema,\n                Comment: c.comment,\n                ...(topValues ?\n                  {\n                    \"Common values\": topValues,\n                  }\n                : {}),\n              })}  \\n` +\n              asSQL(c.definition) +\n              indexInfo,\n            filterText: `${c.name} ${c.udt_name}`,\n            type: \"column\",\n            colInfo: c,\n          };\n        }),\n      );\n    });\n\n    suggestions = suggestions.concat(\n      databases.map((r) => ({\n        label: {\n          label: r.Name,\n          description: r.IsCurrent ? \"CURRENT_DATABASE\" : undefined,\n        },\n        name: r.Name,\n        escapedIdentifier: r.escaped_identifier,\n        insertText: r.escaped_identifier,\n        detail: `(database) `,\n        documentation: asListObject(r),\n        type: \"database\",\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      roles.map((r) => {\n        const userPolicies = policies.filter(\n          (p) => !p.roles || p.roles.includes(r.usename),\n        );\n        type TP = Record<\n          string,\n          Partial<\n            Record<\n              Exclude<PG_Policy[\"cmd\"], null>,\n              {\n                using: string;\n                check: string;\n                type: PG_Policy[\"type\"];\n              }[]\n            >\n          >\n        >;\n        const tablePolicies = userPolicies.reduce((acc, p) => {\n          const schemaQualifiedTableName = `${p.schemaname}.${p.tablename}`;\n          const tablePolicy: TP[string] = acc[schemaQualifiedTableName] ?? {\n            [p.cmd ?? \"ALL\"]: [],\n          };\n          tablePolicy[p.cmd ?? \"ALL\"] ??= [];\n          tablePolicy[p.cmd ?? \"ALL\"]?.push({\n            using: p.using,\n            check: p.with_check,\n            type: p.type,\n          });\n          return {\n            ...acc,\n            [schemaQualifiedTableName]: tablePolicy,\n          };\n        }, {} as TP);\n        const tablePoliciesSqlDefs = Object.entries(tablePolicies).map(\n          ([tablename_escaped, cmds]) => {\n            return `${tablename_escaped}  \\n${Object.entries(cmds)\n              .map(([cmd, policies]) => {\n                return [\n                  `  ${cmd}`,\n                  ...policies.map((p) => {\n                    const typeStr =\n                      p.type === \"PERMISSIVE\" ? \" \" : \" (RESTRICTIVE) \";\n                    return [\n                      `    USING${typeStr}${p.using}`,\n                      `    WITH CHECK${typeStr}${p.check}`,\n                    ].join(\"\\n\");\n                  }),\n                ].join(\"\\n\");\n              })\n              .join(\"\\n\")}`;\n          },\n        );\n        return {\n          label: {\n            label: r.usename,\n            description: [\n              r.is_current_user ? \"CURRENT_USER\"\n              : r.is_connected ? \"CONNECTED\"\n              : \"\",\n              r.usesuper ? \"usesuper\" : \"\",\n            ].join(\" \"),\n          },\n          name: r.usename,\n          userInfo: r,\n          escapedIdentifier: r.escaped_identifier,\n          insertText: r.escaped_identifier,\n          detail: `(role)`,\n          documentation:\n            asListObject(\n              omitKeys(r, [\n                \"is_connected\",\n                \"escaped_identifier\",\n                \"usename\",\n                \"is_current_user\",\n                \"priority\",\n                \"table_grants\",\n              ]),\n            ) +\n            (!r.table_grants ? \"\" : (\n              `\\n\\n**Table grants**:\\n\\n\\n${asSQL(r.table_grants)}`\n            )) +\n            `\\n\\n**Policies (${userPolicies.length}):**\\n\\n${asSQL(tablePoliciesSqlDefs.sort().join(\"\\n\\n\"))}`,\n          type: \"role\",\n        };\n      }),\n    );\n\n    suggestions = suggestions.concat(\n      functions.map((f) => {\n        const overEnding = f.prokind === \"w\" ? \" over()\" : \"\";\n        return {\n          type: \"function\",\n          name: f.escaped_identifier,\n          label: {\n            label: `${f.escaped_identifier}(${f.args.map((a) => a.data_type).join(\",\")})`,\n            description: f.extension || f.schema,\n          },\n          subLabel: f.func_signature,\n          schema: f.schema,\n          args: f.args,\n          funcInfo: f,\n          escapedIdentifier: f.escaped_identifier,\n          escapedName: f.escaped_name,\n          /** If func has arguments we exclude the parenthesis so that the user will write them and trigger func arg suggestions */\n          insertText:\n            f.escaped_identifier +\n            (f.arg_list_str && !overEnding ? \"\"\n            : f.name === \"count\" ? \"(*)\"\n            : \"()\") +\n            overEnding,\n          detail: `(${f.is_aggregate ? \"agg \" : \"\"}function) \\n${f.name}(${f.arg_list_str}) => ${f.restype}`,\n          documentation: `Schema: \\`${f.schema}\\`  \\n\\n**${f.description?.trim() ?? \"\"}**   \\n\\n${asSQL(f.definition ?? \"\")}`,\n          definition: f.definition ?? \"\",\n          funcCallDefinition: `${f.name}(${f.arg_list_str}) => ${f.restype}`,\n          filterText: `${f.escaped_identifier} ${f.args.map((a) => a.data_type).join(\", \")} ${f.extension}`,\n        };\n      }),\n    );\n\n    suggestions = suggestions.concat(\n      dataTypes.map((t) => ({\n        label: { label: t.name, description: t.desc },\n        name: t.name,\n        detail: `(data type)`,\n        documentation:\n          t.desc +\n          `  \\n\\nSchema:  \\`${t.schema}\\`  \\n Type: \\`${t.udt_name}\\`  \\n\\n https://www.postgresql.org/docs/current/datatype.html`,\n        schema: t.schema,\n        dataTypeInfo: t,\n        insertText: t.name.includes(\" \") ? t.udt_name.toUpperCase() : t.name, // use shorter notation where possible\n        type: \"dataType\",\n      })),\n    );\n    const nonArrayTypes = [`\"any\"`, \"void\"];\n    suggestions = suggestions.concat(\n      dataTypes\n        .map((t) =>\n          (\n            nonArrayTypes.includes(t.name.toLowerCase()) ||\n            t.name.toLowerCase().startsWith(\"any\")\n          ) ?\n            undefined\n          : ({\n              label: {\n                label: `${t.name}[]`,\n                description: `ARRAY of ${t.desc}`,\n              },\n              name: `_${t.name}`,\n              detail: `(data type)`,\n              documentation: `ARRAY of ${t.desc}  \\n\\nSchema:  \\`${t.schema}\\`  \\n Type: \\`${t.udt_name}[]\\`  \\n\\n https://www.postgresql.org/docs/current/datatype.html`,\n              schema: t.schema,\n              dataTypeInfo: t,\n              insertText: `${t.udt_name.toUpperCase()}[]`,\n              filterText: `${t.name} _${t.udt_name} ${t.udt_name}[]`,\n              type: \"dataType\",\n            } satisfies SQLSuggestion),\n        )\n        .filter(isDefined),\n    );\n\n    suggestions = suggestions.concat(\n      keywords.map((kwd) => ({\n        detail: `(keyword)`,\n        type: \"keyword\",\n        ...kwd,\n        keywordInfo: kwd,\n        name: kwd.label,\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      extensions.map((ex) => ({\n        label: { label: ex.name, description: ex.installed ? \"Installed\" : \"\" },\n        name: ex.name,\n        detail: `(extension) \\nv${ex.default_version}  ${ex.installed ? \"Installed\" : \"Not installed\"} `,\n        documentation: ex.comment,\n        type: \"extension\",\n        insertText: ex.escaped_identifier,\n        extensionInfo: {\n          installed: ex.installed,\n        },\n      })),\n    );\n    const schemasS = schemas.map(\n      ({\n        name: label,\n        access_privileges,\n        owner,\n        comment,\n        is_in_search_path,\n        escaped_identifier,\n      }) => ({\n        label,\n        name: label,\n        detail: \"(schema)\",\n        documentation: makeDocumentation({\n          access_privileges,\n          owner,\n          comment,\n          is_in_search_path,\n        }),\n        type: \"schema\" as const,\n        escaped_identifier,\n        insertText: escaped_identifier,\n      }),\n    );\n    suggestions = suggestions.concat(schemasS);\n\n    suggestions = suggestions.concat(\n      operators.map((o) => ({\n        type: \"operator\" as const,\n        detail: \"(operator)\",\n        name: o.name,\n        label: {\n          label: o.name,\n          // detail: `   ${o.left_arg_type}`,\n          // description: o.description ,\n          detail: `   ${o.description || \"\"}`,\n          description: o.left_arg_types?.join(\", \") || \"\",\n        },\n        operatorInfo: o,\n        sortText: PRIORITISED_OPERATORS.includes(o.name) ? \"a\" : \"b\",\n        documentation: o.description,\n        filterText: `${o.name} ${o.left_arg_types?.join(\", \")} ${o.description}`,\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      publications.map((p) => ({\n        detail: \"(publication)\",\n        label: p.pubname,\n        name: p.pubname,\n        type: \"publication\",\n        insertText: p.escaped_identifier,\n        documentation: makeDocumentation(p),\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      subscriptions.map((p) => ({\n        detail: \"(subscription)\",\n        label: p.subname,\n        name: p.subname,\n        type: \"subscription\",\n        insertText: p.escaped_identifier,\n        documentation: makeDocumentation(p),\n      })),\n    );\n\n    suggestions = suggestions.concat(\n      SQL_SNIPPETS.map((s) => ({\n        detail: \"(snippet)\",\n        type: \"snippet\",\n        label: s.label,\n        documentation: `${s.info}\\n\\n${asSQL(s.query)}`,\n        insertText: `\\n\\n${s.query}`,\n        name: s.label,\n      })),\n    );\n\n    // suggestions = suggestions.map(s => {\n    //   /* Escape non standard identifiers */\n    //   if(\n    //     !s.insertText &&\n    //     [\"extension\", \"function\", \"table\", \"column\"].includes(s.type) &&\n    //     (\n    //       s.label[0].match(/[^a-z_]/g) ||\n    //       s.label.slice(1).match(/[^a-z_0-9]/g)\n    //     )\n    //   ){\n    //     s.insertText = JSON.stringify(s.label)\n    //   }\n    //   return s;\n    // });\n\n    const settingSuggestions: SQLSuggestion[] = settings.map((s) => ({\n      label: {\n        label: s.name,\n        description: s.setting_pretty?.value ?? s.setting ?? \"\",\n      },\n      name: s.name,\n      detail: \"(setting)\",\n      type: \"setting\",\n      insertText: s.name,\n      settingInfo: s,\n      documentation: `**${s.description}**  \\n\\n${asListObject(\n        pickKeys(s, [\n          \"category\",\n          \"unit\",\n          \"setting\",\n          \"min_val\",\n          \"max_val\",\n          \"enumvals\",\n          \"reset_val\",\n          \"vartype\",\n          \"pending_restart\",\n        ]),\n      )} `,\n    }));\n\n    const res = {\n      suggestions,\n      settingSuggestions,\n    };\n    return res;\n  } catch (error) {\n    console.error(\"Failed getting sql suggestions: \", error);\n    return {\n      settingSuggestions: [],\n      suggestions: [],\n    };\n  }\n};\n\nexport const getColumnSuggestionLabel = (\n  c: { udt_name: string; data_type: string; name: string },\n  tableName: string,\n): ParsedSQLSuggestion[\"label\"] => {\n  const dataType =\n    c.udt_name.startsWith(\"_\") ? `${c.udt_name.slice(1)}[]`\n    : c.data_type.toLowerCase().includes(\"timestamp\") ? c.udt_name\n    : c.data_type;\n  return {\n    label: c.name,\n    detail: \"  \" + dataType.toUpperCase(),\n    description: tableName,\n  };\n};\n\nconst KEYWORD_TYPED = [\n  { key: \"SELECT\", type: \"column\" },\n  { key: \"RETURNING\", type: \"column\" },\n  { key: \"FROM\", type: \"table\" },\n  { key: \"INTO\", type: \"table\" },\n  { key: \"JOIN\", type: \"table\" },\n  { key: \"ON\", type: \"column\" },\n  { key: \"WHERE\", type: \"column\" },\n  { key: \"ORDER BY\", type: \"column\" },\n  { key: \"UPDATE\", type: \"table\" },\n  { key: \"DELETE\", type: \"table\" },\n  { key: \"TABLE\", type: \"table\" },\n];\n\nlet kwd = [\n  \"A\",\n  \"ABORT\",\n  \"ABS\",\n  \"ABSENT\",\n  \"ABSOLUTE\",\n  \"ACCESS\",\n  \"ACCORDING\",\n  \"ACTION\",\n  \"ADA\",\n  \"ADD\",\n  \"ADMIN\",\n  \"AFTER\",\n  \"AGGREGATE\",\n  \"ALL\",\n  \"ALLOCATE\",\n  \"ALSO\",\n  \"ALTER\",\n  \"ALWAYS\",\n  \"ANALYSE\",\n  \"ANALYZE\",\n  \"AND\",\n  \"ANY\",\n  \"ARE\",\n  \"ARRAY\",\n  \"ARRAY_AGG\",\n  \"ARRAY_MAX_CARDINALITY\",\n  \"AS\",\n  \"ASC\",\n  \"ASENSITIVE\",\n  \"ASSERTION\",\n  \"ASSIGNMENT\",\n  \"ASYMMETRIC\",\n  \"AT\",\n  \"ATOMIC\",\n  \"ATTRIBUTE\",\n  \"ATTRIBUTES\",\n  \"AUTHORIZATION\",\n  \"AVG\",\n  \"BACKWARD\",\n  \"BASE64\",\n  \"BEFORE\",\n  \"BEGIN\",\n  \"BEGIN_FRAME\",\n  \"BEGIN_PARTITION\",\n  \"BERNOULLI\",\n  \"BETWEEN\",\n  \"BIGINT\",\n  \"BINARY\",\n  \"BIT\",\n  \"BIT_LENGTH\",\n  \"BLOB\",\n  \"BLOCKED\",\n  \"BOM\",\n  \"BOOLEAN\",\n  \"BOTH\",\n  \"BREADTH\",\n  \"BY\",\n  \"C\",\n  \"CACHE\",\n  \"CALL\",\n  \"CALLED\",\n  \"CARDINALITY\",\n  \"CASCADE\",\n  \"CASCADED\",\n  \"CASE\",\n  \"CAST\",\n  \"CATALOG\",\n  \"CATALOG_NAME\",\n  \"CEIL\",\n  \"CEILING\",\n  \"CHAIN\",\n  \"CHAR\",\n  \"CHARACTER\",\n  \"CHARACTERISTICS\",\n  \"CHARACTERS\",\n  \"CHARACTER_LENGTH\",\n  \"CHARACTER_SET_CATALOG\",\n  \"CHARACTER_SET_NAME\",\n  \"CHARACTER_SET_SCHEMA\",\n  \"CHAR_LENGTH\",\n  \"CHECK\",\n  \"CHECKPOINT\",\n  \"CLASS\",\n  \"CLASS_ORIGIN\",\n  \"CLOB\",\n  \"CLOSE\",\n  \"CLUSTER\",\n  \"COALESCE\",\n  \"COBOL\",\n  \"COLLATE\",\n  \"COLLATION\",\n  \"COLLATION_CATALOG\",\n  \"COLLATION_NAME\",\n  \"COLLATION_SCHEMA\",\n  \"COLLECT\",\n  \"COLUMN\",\n  \"COLUMNS\",\n  \"COLUMN_NAME\",\n  \"COMMAND_FUNCTION\",\n  \"COMMAND_FUNCTION_CODE\",\n  \"COMMENT\",\n  \"COMMENTS\",\n  \"COMMIT\",\n  \"COMMITTED\",\n  \"CONCURRENTLY\",\n  \"CONDITION\",\n  \"CONDITION_NUMBER\",\n  \"CONFIGURATION\",\n  \"CONFLICT\",\n  \"CONNECT\",\n  \"CONNECTION\",\n  \"CONNECTION_NAME\",\n  \"CONSTRAINT\",\n  \"CONSTRAINTS\",\n  \"CONSTRAINT_CATALOG\",\n  \"CONSTRAINT_NAME\",\n  \"CONSTRAINT_SCHEMA\",\n  \"CONSTRUCTOR\",\n  \"CONTAINS\",\n  \"CONTENT\",\n  \"CONTINUE\",\n  \"CONTROL\",\n  \"CONVERSION\",\n  \"CONVERT\",\n  \"COPY\",\n  \"CORR\",\n  \"CORRESPONDING\",\n  \"COST\",\n  \"COUNT\",\n  \"COVAR_POP\",\n  \"COVAR_SAMP\",\n  \"CREATE\",\n  \"CROSS\",\n  \"CSV\",\n  \"CUBE\",\n  \"CUME_DIST\",\n  \"CURRENT\",\n  \"CURRENT_CATALOG\",\n  \"CURRENT_DATE\",\n  \"CURRENT_DEFAULT_TRANSFORM_GROUP\",\n  \"CURRENT_PATH\",\n  \"CURRENT_ROLE\",\n  \"CURRENT_ROW\",\n  \"CURRENT_SCHEMA\",\n  \"CURRENT_TIME\",\n  \"CURRENT_TIMESTAMP\",\n  \"CURRENT_TRANSFORM_GROUP_FOR_TYPE\",\n  \"CURRENT_USER\",\n  \"CURSOR\",\n  \"CURSOR_NAME\",\n  \"CYCLE\",\n  \"DATA\",\n  \"DATABASE\",\n  \"DATALINK\",\n  \"DATE\",\n  \"DATETIME_INTERVAL_CODE\",\n  \"DATETIME_INTERVAL_PRECISION\",\n  \"DAY\",\n  \"DB\",\n  \"DEALLOCATE\",\n  \"DEC\",\n  \"DECIMAL\",\n  \"DECLARE\",\n  \"DEFAULT\",\n  \"DEFAULTS\",\n  \"DEFERRABLE\",\n  \"DEFERRED\",\n  \"DEFINED\",\n  \"DEFINER\",\n  \"DEGREE\",\n  \"DELETE\",\n  \"DELIMITER\",\n  \"DELIMITERS\",\n  \"DENSE_RANK\",\n  \"DEPTH\",\n  \"DEREF\",\n  \"DERIVED\",\n  \"DESC\",\n  \"DESCRIBE\",\n  \"DESCRIPTOR\",\n  \"DETERMINISTIC\",\n  \"DIAGNOSTICS\",\n  \"DICTIONARY\",\n  \"DISABLE\",\n  \"DISCARD\",\n  \"DISCONNECT\",\n  \"DISPATCH\",\n  \"DISTINCT\",\n  \"DLNEWCOPY\",\n  \"DLPREVIOUSCOPY\",\n  \"DLURLCOMPLETE\",\n  \"DLURLCOMPLETEONLY\",\n  \"DLURLCOMPLETEWRITE\",\n  \"DLURLPATH\",\n  \"DLURLPATHONLY\",\n  \"DLURLPATHWRITE\",\n  \"DLURLSCHEME\",\n  \"DLURLSERVER\",\n  \"DLVALUE\",\n  \"DO\",\n  \"DOCUMENT\",\n  \"DOMAIN\",\n  \"DOUBLE\",\n  \"DROP\",\n  \"DYNAMIC\",\n  \"DYNAMIC_FUNCTION\",\n  \"DYNAMIC_FUNCTION_CODE\",\n  \"EACH\",\n  \"ELEMENT\",\n  \"ELSE\",\n  \"EMPTY\",\n  \"ENABLE\",\n  \"ENCODING\",\n  \"ENCRYPTED\",\n  \"END\",\n  \"END-EXEC\",\n  \"END_FRAME\",\n  \"END_PARTITION\",\n  \"ENFORCED\",\n  \"ENUM\",\n  \"EQUALS\",\n  \"ESCAPE\",\n  \"EVENT\",\n  \"EVERY\",\n  \"EXCEPT\",\n  \"EXCEPTION\",\n  \"EXCLUDE\",\n  \"EXCLUDING\",\n  \"EXCLUSIVE\",\n  \"EXEC\",\n  \"EXECUTE\",\n  \"EXISTS\",\n  \"EXP\",\n  \"EXPLAIN\",\n  \"EXPRESSION\",\n  \"EXTENSION\",\n  \"EXTERNAL\",\n  \"EXTRACT\",\n  \"FALSE\",\n  \"FAMILY\",\n  \"FETCH\",\n  \"FILE\",\n  \"FILTER\",\n  \"FINAL\",\n  \"FIRST\",\n  \"FIRST_VALUE\",\n  \"FLAG\",\n  \"FLOAT\",\n  \"FLOOR\",\n  \"FOLLOWING\",\n  \"FOR\",\n  \"FORCE\",\n  \"FOREIGN\",\n  \"FORTRAN\",\n  \"FORWARD\",\n  \"FOUND\",\n  \"FRAME_ROW\",\n  \"FREE\",\n  \"FREEZE\",\n  \"FROM\",\n  \"FS\",\n  \"FULL\",\n  \"FUNCTION\",\n  \"FUNCTIONS\",\n  \"FUSION\",\n  \"G\",\n  \"GENERAL\",\n  \"GENERATED\",\n  \"GET\",\n  \"GLOBAL\",\n  \"GO\",\n  \"GOTO\",\n  \"GRANT\",\n  \"GRANTED\",\n  \"GREATEST\",\n  \"GROUP\",\n  \"GROUPING\",\n  \"GROUPS\",\n  \"HANDLER\",\n  \"HAVING\",\n  \"HEADER\",\n  \"HEX\",\n  \"HIERARCHY\",\n  \"HOLD\",\n  \"HOUR\",\n  \"ID\",\n  \"IDENTITY\",\n  \"IF\",\n  \"IGNORE\",\n  \"ILIKE\",\n  \"IMMEDIATE\",\n  \"IMMEDIATELY\",\n  \"IMMUTABLE\",\n  \"IMPLEMENTATION\",\n  \"IMPLICIT\",\n  \"IMPORT\",\n  \"IN\",\n  \"INCLUDING\",\n  \"INCREMENT\",\n  \"INDENT\",\n  \"INDEX\",\n  \"INDEXES\",\n  \"INDICATOR\",\n  \"INHERIT\",\n  \"INHERITS\",\n  \"INITIALLY\",\n  \"INLINE\",\n  \"INNER\",\n  \"INOUT\",\n  \"INPUT\",\n  \"INSENSITIVE\",\n  \"INSERT\",\n  \"INSTANCE\",\n  \"INSTANTIABLE\",\n  \"INSTEAD\",\n  \"INT\",\n  \"INTEGER\",\n  \"INTEGRITY\",\n  \"INTERSECT\",\n  \"INTERSECTION\",\n  \"INTERVAL\",\n  \"INTO\",\n  \"INVOKER\",\n  \"IS\",\n  \"ISNULL\",\n  \"ISOLATION\",\n  \"JOIN\",\n  \"K\",\n  \"KEY\",\n  \"KEY_MEMBER\",\n  \"KEY_TYPE\",\n  \"LABEL\",\n  \"LAG\",\n  \"LANGUAGE\",\n  \"LARGE\",\n  \"LAST\",\n  \"LAST_VALUE\",\n  \"LATERAL\",\n  \"LEAD\",\n  \"LEADING\",\n  \"LEAKPROOF\",\n  \"LEAST\",\n  \"LEFT\",\n  \"LENGTH\",\n  \"LEVEL\",\n  \"LIBRARY\",\n  \"LIKE\",\n  \"LIKE_REGEX\",\n  \"LIMIT\",\n  \"LINK\",\n  \"LISTEN\",\n  \"LN\",\n  \"LOAD\",\n  \"LOCAL\",\n  \"LOCALTIME\",\n  \"LOCALTIMESTAMP\",\n  \"LOCATION\",\n  \"LOCATOR\",\n  \"LOCK\",\n  \"LOCKED\",\n  \"LOGGED\",\n  \"LOWER\",\n  \"M\",\n  \"MAP\",\n  \"MAPPING\",\n  \"MATCH\",\n  \"MATCHED\",\n  \"MATERIALIZED\",\n  \"MAX\",\n  \"MAXVALUE\",\n  \"MAX_CARDINALITY\",\n  \"MEMBER\",\n  \"MERGE\",\n  \"MESSAGE_LENGTH\",\n  \"MESSAGE_OCTET_LENGTH\",\n  \"MESSAGE_TEXT\",\n  \"METHOD\",\n  \"MIN\",\n  \"MINUTE\",\n  \"MINVALUE\",\n  \"MOD\",\n  \"MODE\",\n  \"MODIFIES\",\n  \"MODULE\",\n  \"MONTH\",\n  \"MORE\",\n  \"MOVE\",\n  \"MULTISET\",\n  \"MUMPS\",\n  \"NAME\",\n  \"NAMES\",\n  \"NAMESPACE\",\n  \"NATIONAL\",\n  \"NATURAL\",\n  \"NCHAR\",\n  \"NCLOB\",\n  \"NESTING\",\n  \"NEW\",\n  \"NEXT\",\n  \"NFC\",\n  \"NFD\",\n  \"NFKC\",\n  \"NFKD\",\n  \"NIL\",\n  \"NO\",\n  \"NONE\",\n  \"NORMALIZE\",\n  \"NORMALIZED\",\n  \"NOT\",\n  \"NOTHING\",\n  \"NOTIFY\",\n  \"NOTNULL\",\n  \"NOWAIT\",\n  \"NTH_VALUE\",\n  \"NTILE\",\n  \"NULL\",\n  \"NULLABLE\",\n  \"NULLIF\",\n  \"NULLS\",\n  \"NUMBER\",\n  \"NUMERIC\",\n  \"OBJECT\",\n  \"OCCURRENCES_REGEX\",\n  \"OCTETS\",\n  \"OCTET_LENGTH\",\n  \"OF\",\n  \"OFF\",\n  \"OFFSET\",\n  \"OIDS\",\n  \"OLD\",\n  \"ON\",\n  \"ONLY\",\n  \"OPEN\",\n  \"OPERATOR\",\n  \"OPTION\",\n  \"OPTIONS\",\n  \"OR\",\n  \"ORDER\",\n  \"ORDERING\",\n  \"ORDINALITY\",\n  \"OTHERS\",\n  \"OUT\",\n  \"OUTER\",\n  \"OUTPUT\",\n  \"OVER\",\n  \"OVERLAPS\",\n  \"OVERLAY\",\n  \"OVERRIDING\",\n  \"OWNED\",\n  \"OWNER\",\n  \"P\",\n  \"PAD\",\n  \"PARAMETER\",\n  \"PARAMETER_MODE\",\n  \"PARAMETER_NAME\",\n  \"PARAMETER_ORDINAL_POSITION\",\n  \"PARAMETER_SPECIFIC_CATALOG\",\n  \"PARAMETER_SPECIFIC_NAME\",\n  \"PARAMETER_SPECIFIC_SCHEMA\",\n  \"PARSER\",\n  \"PARTIAL\",\n  \"PARTITION\",\n  \"PASCAL\",\n  \"PASSING\",\n  \"PASSTHROUGH\",\n  \"PASSWORD\",\n  \"PATH\",\n  \"PERCENT\",\n  \"PERCENTILE_CONT\",\n  \"PERCENTILE_DISC\",\n  \"PERCENT_RANK\",\n  \"PERIOD\",\n  \"PERMISSION\",\n  \"PLACING\",\n  \"PLANS\",\n  \"PLI\",\n  \"POLICY\",\n  \"PORTION\",\n  \"POSITION\",\n  \"POSITION_REGEX\",\n  \"POWER\",\n  \"PRECEDES\",\n  \"PRECEDING\",\n  \"PRECISION\",\n  \"PREPARE\",\n  \"PREPARED\",\n  \"PRESERVE\",\n  \"PRIMARY\",\n  \"PRIOR\",\n  \"PRIVILEGES\",\n  \"PROCEDURAL\",\n  \"PROCEDURE\",\n  \"PROGRAM\",\n  \"PUBLIC\",\n  \"QUOTE\",\n  \"RANGE\",\n  \"RANK\",\n  \"READ\",\n  \"READS\",\n  \"REAL\",\n  \"REASSIGN\",\n  \"RECHECK\",\n  \"RECOVERY\",\n  \"RECURSIVE\",\n  \"REF\",\n  \"REFERENCES\",\n  \"REFERENCING\",\n  \"REFRESH\",\n  \"REGR_AVGX\",\n  \"REGR_AVGY\",\n  \"REGR_COUNT\",\n  \"REGR_INTERCEPT\",\n  \"REGR_R2\",\n  \"REGR_SLOPE\",\n  \"REGR_SXX\",\n  \"REGR_SXY\",\n  \"REGR_SYY\",\n  \"REINDEX\",\n  \"RELATIVE\",\n  \"RELEASE\",\n  \"RENAME\",\n  \"REPEATABLE\",\n  \"REPLACE\",\n  \"REPLICA\",\n  \"REQUIRING\",\n  \"RESET\",\n  \"RESPECT\",\n  \"RESTART\",\n  \"RESTORE\",\n  \"RESTRICT\",\n  \"RESULT\",\n  \"RETURN\",\n  \"RETURNED_CARDINALITY\",\n  \"RETURNED_LENGTH\",\n  \"RETURNED_OCTET_LENGTH\",\n  \"RETURNED_SQLSTATE\",\n  \"RETURNING\",\n  \"RETURNS\",\n  \"REVOKE\",\n  \"RIGHT\",\n  \"ROLE\",\n  \"ROLLBACK\",\n  \"ROLLUP\",\n  \"ROUTINE\",\n  \"ROUTINE_CATALOG\",\n  \"ROUTINE_NAME\",\n  \"ROUTINE_SCHEMA\",\n  \"ROW\",\n  \"ROWS\",\n  \"ROW_COUNT\",\n  \"ROW_NUMBER\",\n  \"RULE\",\n  \"SAVEPOINT\",\n  \"SCALE\",\n  \"SCHEMA\",\n  \"SCHEMA_NAME\",\n  \"SCOPE\",\n  \"SCOPE_CATALOG\",\n  \"SCOPE_NAME\",\n  \"SCOPE_SCHEMA\",\n  \"SCROLL\",\n  \"SEARCH\",\n  \"SECOND\",\n  \"SECTION\",\n  \"SECURITY\",\n  \"SELECT\",\n  \"SELECTIVE\",\n  \"SELF\",\n  \"SENSITIVE\",\n  \"SEQUENCE\",\n  \"SEQUENCES\",\n  \"SERIALIZABLE\",\n  \"SERVER\",\n  \"SERVER_NAME\",\n  \"SESSION\",\n  \"SESSION_USER\",\n  \"SET\",\n  \"SETOF\",\n  \"SETS\",\n  \"SHARE\",\n  \"SHOW\",\n  \"SIMILAR\",\n  \"SIMPLE\",\n  \"SIZE\",\n  \"SKIP\",\n  \"SMALLINT\",\n  \"SNAPSHOT\",\n  \"SOME\",\n  \"SOURCE\",\n  \"SPACE\",\n  \"SPECIFIC\",\n  \"SPECIFICTYPE\",\n  \"SPECIFIC_NAME\",\n  \"SQL\",\n  \"SQLCODE\",\n  \"SQLERROR\",\n  \"SQLEXCEPTION\",\n  \"SQLSTATE\",\n  \"SQLWARNING\",\n  \"SQRT\",\n  \"STABLE\",\n  \"STANDALONE\",\n  \"START\",\n  \"STATE\",\n  \"STATEMENT\",\n  \"STATIC\",\n  \"STATISTICS\",\n  \"STDDEV_POP\",\n  \"STDDEV_SAMP\",\n  \"STDIN\",\n  \"STDOUT\",\n  \"STORAGE\",\n  \"STRICT\",\n  \"STRIP\",\n  \"STRUCTURE\",\n  \"STYLE\",\n  \"SUBCLASS_ORIGIN\",\n  \"SUBMULTISET\",\n  \"SUBSTRING\",\n  \"SUBSTRING_REGEX\",\n  \"SUCCEEDS\",\n  \"SUM\",\n  \"SYMMETRIC\",\n  \"SYSID\",\n  \"SYSTEM\",\n  \"SYSTEM_TIME\",\n  \"SYSTEM_USER\",\n  \"T\",\n  \"TABLE\",\n  \"TABLES\",\n  \"TABLESAMPLE\",\n  \"TABLESPACE\",\n  \"TABLE_NAME\",\n  \"TEMP\",\n  \"TEMPLATE\",\n  \"TEMPORARY\",\n  \"TEXT\",\n  \"THEN\",\n  \"TIES\",\n  \"TIME\",\n  \"TIMESTAMP\",\n  \"TIMEZONE_HOUR\",\n  \"TIMEZONE_MINUTE\",\n  \"TO\",\n  \"TOKEN\",\n  \"TOP_LEVEL_COUNT\",\n  \"TRAILING\",\n  \"TRANSACTION\",\n  \"TRANSACTIONS_COMMITTED\",\n  \"TRANSACTIONS_ROLLED_BACK\",\n  \"TRANSACTION_ACTIVE\",\n  \"TRANSFORM\",\n  \"TRANSFORMS\",\n  \"TRANSLATE\",\n  \"TRANSLATE_REGEX\",\n  \"TRANSLATION\",\n  \"TREAT\",\n  \"TRIGGER\",\n  \"TRIGGER_CATALOG\",\n  \"TRIGGER_NAME\",\n  \"TRIGGER_SCHEMA\",\n  \"TRIM\",\n  \"TRIM_ARRAY\",\n  \"TRUE\",\n  \"TRUNCATE\",\n  \"TRUSTED\",\n  \"TYPE\",\n  \"TYPES\",\n  \"UESCAPE\",\n  \"UNBOUNDED\",\n  \"UNCOMMITTED\",\n  \"UNDER\",\n  \"UNENCRYPTED\",\n  \"UNION\",\n  \"UNIQUE\",\n  \"UNKNOWN\",\n  \"UNLINK\",\n  \"UNLISTEN\",\n  \"UNLOGGED\",\n  \"UNNAMED\",\n  \"UNNEST\",\n  \"UNTIL\",\n  \"UNTYPED\",\n  \"UPDATE\",\n  \"UPPER\",\n  \"URI\",\n  \"USAGE\",\n  \"USER\",\n  \"USER_DEFINED_TYPE_CATALOG\",\n  \"USER_DEFINED_TYPE_CODE\",\n  \"USER_DEFINED_TYPE_NAME\",\n  \"USER_DEFINED_TYPE_SCHEMA\",\n  \"USING\",\n  \"VACUUM\",\n  \"VALID\",\n  \"VALIDATE\",\n  \"VALIDATOR\",\n  \"VALUE\",\n  \"VALUES\",\n  \"VALUE_OF\",\n  \"VARBINARY\",\n  \"VARCHAR\",\n  \"VARIADIC\",\n  \"VARYING\",\n  \"VAR_POP\",\n  \"VAR_SAMP\",\n  \"VERBOSE\",\n  \"VERSION\",\n  \"VERSIONING\",\n  \"VIEW\",\n  \"VIEWS\",\n  \"VOLATILE\",\n  \"WHEN\",\n  \"WHENEVER\",\n  \"WHERE\",\n  \"WHITESPACE\",\n  \"WIDTH_BUCKET\",\n  \"WINDOW\",\n  \"WITH\",\n  \"WITHIN\",\n  \"WITHOUT\",\n  \"WORK\",\n  \"WRAPPER\",\n  \"WRITE\",\n  \"XML\",\n  \"XMLAGG\",\n  \"XMLATTRIBUTES\",\n  \"XMLBINARY\",\n  \"XMLCAST\",\n  \"XMLCOMMENT\",\n  \"XMLCONCAT\",\n  \"XMLDECLARATION\",\n  \"XMLDOCUMENT\",\n  \"XMLELEMENT\",\n  \"XMLEXISTS\",\n  \"XMLFOREST\",\n  \"XMLITERATE\",\n  \"XMLNAMESPACES\",\n  \"XMLPARSE\",\n  \"XMLPI\",\n  \"XMLQUERY\",\n  \"XMLROOT\",\n  \"XMLSCHEMA\",\n  \"XMLSERIALIZE\",\n  \"XMLTABLE\",\n  \"XMLTEXT\",\n  \"XMLVALIDATE\",\n  \"YEAR\",\n  \"YES\",\n  \"ZONE\",\n  \"IF NOT EXISTS\",\n];\nconst priority = Array.from(\n  new Set([...KEYWORD_TYPED.map((k) => k.key), ...TOP_KEYWORDS]),\n);\nkwd = kwd.sort((a, b) => +priority.includes(b) - +priority.includes(a));\n\nexport const getKeywordDocumentation = (name: string) => {\n  if (name.toUpperCase() === \"ENUM\") {\n    return `Enumerated (enum) types are data types that comprise a static, ordered set of values\n\\`\\`\\`sql\n\nCREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');\n\n\\`\\`\\`\n`;\n  }\n};\n\n/** psql -E -c '\\d+ table_name' */\nexport const getDetailedTableInfo = async (\n  tableName: string,\n  db: DBHandlerClient,\n) => {\n  const sql = db.sql;\n\n  if (!sql) return undefined;\n\n  const t = (await sql(\n    `\n    SELECT c.oid,\n      n.nspname,\n      c.relname\n    FROM pg_catalog.pg_class c\n        LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n    WHERE c.relname OPERATOR(pg_catalog.~) $1 COLLATE pg_catalog.default\n      AND pg_catalog.pg_table_is_visible(c.oid)\n    ORDER BY 2, 3;\n    \n    `,\n    [`^(${tableName})$`],\n    { returnType: \"row\" },\n  )) as {\n    oid: number;\n    nspname: string;\n    relname: string;\n  };\n\n  const cls = (await sql(\n    `\n  SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, false AS relhasoids, c.relispartition, pg_catalog.array_to_string(c.reloptions || array(select 'toast.' || x from pg_catalog.unnest(tc.reloptions) x), ', ')\n  , c.reltablespace, CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, c.relpersistence, c.relreplident, am.amname\n  FROM pg_catalog.pg_class c\n   LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n  LEFT JOIN pg_catalog.pg_am am ON (c.relam = am.oid)\n  WHERE c.oid = $1;\n  \n  `,\n    [t.oid],\n    { returnType: \"row\" },\n  )) as {\n    oid: number;\n    nspname: string;\n    relname: string;\n  };\n\n  const attrs = (await sql(\n    `\n  SELECT a.attname,\n    pg_catalog.format_type(a.atttypid, a.atttypmod),\n    (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid, true)\n     FROM pg_catalog.pg_attrdef d\n     WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef),\n    a.attnotnull,\n    (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n     WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation,\n    a.attidentity,\n    a.attgenerated,\n    a.attstorage,\n    CASE WHEN a.attstattarget=-1 THEN NULL ELSE a.attstattarget END AS attstattarget,\n    pg_catalog.col_description(a.attrelid, a.attnum)\n  FROM pg_catalog.pg_attribute a\n  WHERE a.attrelid = $1 AND a.attnum > 0 AND NOT a.attisdropped\n  ORDER BY a.attnum;\n  \n  `,\n    [t.oid],\n    { returnType: \"row\" },\n  )) as {\n    oid: number;\n    nspname: string;\n    relname: string;\n  };\n\n  `\n  ********* QUERY **********\n**************************\n\n********* QUERY **********\n**************************\n\n********* QUERY **********\n**************************\n\n********* QUERY **********\nSELECT c2.relname, i.indisprimary, i.indisunique, i.indisclustered, i.indisvalid, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true),\n  pg_catalog.pg_get_constraintdef(con.oid, true), contype, condeferrable, condeferred, i.indisreplident, c2.reltablespace\nFROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i\n  LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x'))\nWHERE c.oid = '22228' AND c.oid = i.indrelid AND i.indexrelid = c2.oid\nORDER BY i.indisprimary DESC, c2.relname;\n**************************\n\n********* QUERY **********\nSELECT r.conname, pg_catalog.pg_get_constraintdef(r.oid, true)\nFROM pg_catalog.pg_constraint r\nWHERE r.conrelid = '22228' AND r.contype = 'c'\nORDER BY 1;\n**************************\n\n********* QUERY **********\nSELECT true as sametable, conname,\n  pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n  conrelid::pg_catalog.regclass AS ontable\nFROM pg_catalog.pg_constraint r\nWHERE r.conrelid = '22228' AND r.contype = 'f'\n     AND conparentid = 0\nORDER BY conname\n**************************\n\n********* QUERY **********\nSELECT conname, conrelid::pg_catalog.regclass AS ontable,\n       pg_catalog.pg_get_constraintdef(oid, true) AS condef\n  FROM pg_catalog.pg_constraint c\n WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('22228')\n                     UNION ALL VALUES ('22228'::pg_catalog.regclass))\n       AND contype = 'f' AND conparentid = 0\nORDER BY conname;\n**************************\n\n********* QUERY **********\nSELECT pol.polname, pol.polpermissive,\n  CASE WHEN pol.polroles = '{0}' THEN NULL ELSE pg_catalog.array_to_string(array(select rolname from pg_catalog.pg_roles where oid = any (pol.polroles) order by 1),',') END,\n  pg_catalog.pg_get_expr(pol.polqual, pol.polrelid),\n  pg_catalog.pg_get_expr(pol.polwithcheck, pol.polrelid),\n  CASE pol.polcmd\n    WHEN 'r' THEN 'SELECT'\n    WHEN 'a' THEN 'INSERT'\n    WHEN 'w' THEN 'UPDATE'\n    WHEN 'd' THEN 'DELETE'\n    END AS cmd\nFROM pg_catalog.pg_policy pol\nWHERE pol.polrelid = '22228' ORDER BY 1;\n**************************\n\n********* QUERY **********\nSELECT oid, stxrelid::pg_catalog.regclass, stxnamespace::pg_catalog.regnamespace AS nsp, stxname,\n  (SELECT pg_catalog.string_agg(pg_catalog.quote_ident(attname),', ')\n   FROM pg_catalog.unnest(stxkeys) s(attnum)\n   JOIN pg_catalog.pg_attribute a ON (stxrelid = a.attrelid AND\n        a.attnum = s.attnum AND NOT attisdropped)) AS columns,\n  'd' = any(stxkind) AS ndist_enabled,\n  'f' = any(stxkind) AS deps_enabled,\n  'm' = any(stxkind) AS mcv_enabled,\n  -1 AS stxstattarget\nFROM pg_catalog.pg_statistic_ext\nWHERE stxrelid = '22228'\nORDER BY 1;\n**************************\n\n********* QUERY **********\nSELECT pubname\nFROM pg_catalog.pg_publication p\nJOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\nWHERE pr.prrelid = '22228'\nUNION ALL\nSELECT pubname\nFROM pg_catalog.pg_publication p\nWHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('22228')\nORDER BY 1;\n**************************\n\n********* QUERY **********\nSELECT t.tgname, pg_catalog.pg_get_triggerdef(t.oid, true), t.tgenabled, t.tgisinternal,\n  NULL AS parent\nFROM pg_catalog.pg_trigger t\nWHERE t.tgrelid = '22228' AND (NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D') \n    OR EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n        AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))\nORDER BY 1;\n**************************\n\n********* QUERY **********\nSELECT c.oid::pg_catalog.regclass\nFROM pg_catalog.pg_class c, pg_catalog.pg_inherits i\nWHERE c.oid = i.inhparent AND i.inhrelid = '22228'\n  AND c.relkind != 'p' AND c.relkind != 'I'\nORDER BY inhseqno;\n**************************\n\n********* QUERY **********\nSELECT c.oid::pg_catalog.regclass, c.relkind, false AS inhdetachpending, pg_catalog.pg_get_expr(c.relpartbound, c.oid)\nFROM pg_catalog.pg_class c, pg_catalog.pg_inherits i\nWHERE c.oid = i.inhrelid AND i.inhparent = '22228'\nORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text;\n**************************\n  \n  `;\n};\n\nexport const missingKeywordDocumentation = {\n  IN: `The right-hand side is a parenthesized list of scalar expressions. The result is \"true\" if the left-hand expression's result is equal to any of the right-hand expressions. This is a shorthand notation for: \n\n${asSQL(`expression = value1\nOR\nexpression = value2\nOR\n\n--Example usage:\nvalue IN (value1,value2,...)\nvalue IN (SELECT column_name FROM table_name);\n`)}`,\n  RAISE:\n    `Report messages and raise errors. Can only be used in PL/pgSQL\nhttps://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html\n\n` +\n    asSQL(`RAISE EXCEPTION 'something not right';\n\nRAISE NOTICE 'Calling cs_create_job(%)', v_job_id;\n`),\n\n  REFRESH:\n    `Replace the contents of a materialized view\nhttps://www.postgresql.org/docs/current/sql-refreshmaterializedview.html\n\n` + asSQL(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_view;`),\n\n  ORDER:\n    `Sepcify how rows will be ordered\nhttps://www.postgresql.org/docs/current/queries-order.html\n\n` +\n    asSQL(`\nORDER BY col_name, col_name DESC\n\nORDER BY col_name DESC NULLS FIRST\nORDER BY col_name ASC NULLS LAST`),\n\n  JOIN:\n    `Combine two or more result sets into one\nhttps://www.postgresql.org/docs/current/tutorial-join.html\n\n` +\n    asSQL(`\nSELECT *\nFROM weather w\nINNER JOIN cities c\nON w.city = c.name;`),\n\n  FROM:\n    `Precedes a table or expression\n` +\n    asSQL(`\nSELECT ... FROM table_name;\n\nDELETE FROM table_name;\n\nSELECT * FROM (SELECT max(a), ... FROM ...)`),\n\n  FOR:\n    `Iterate through data. Can only be executed within a function or code block\n\n` +\n    asSQL(`\nDO $func$  \nDECLARE usr RECORD;\nBEGIN\n\nFOR usr IN SELECT * FROM users\nLOOP\n\n  DELETE FROM sessions \n  WHERE user_id = usr.id;\nEND LOOP;\n\nEND $func$;\n`),\n  INTO:\n    `SELECT INTO creates a new table and fills it with data computed by a query. The new table's columns have the names and data types associated with the output columns of the SELECT. The data is not returned to the client, as it is with a normal SELECT. \n\n` +\n    asSQL(`SELECT * \nINTO films_recent \nFROM films WHERE date_prod >= '2002-01-01';`),\n\n  LIMIT:\n    `If a limit count is given, no more than that many rows will be returned (but possibly fewer, if the query itself yields fewer rows). \n\nLIMIT ALL is the same as omitting the LIMIT clause, as is LIMIT with a NULL argument.\nWhen using LIMIT, it is important to use an ORDER BY clause that constrains the result rows into a unique order. Otherwise you will get an unpredictable subset of the query's rows. You might be asking for the tenth through twentieth rows, but tenth through twentieth in what ordering? The ordering is unknown, unless you specified ORDER BY.\n\n` +\n    asSQL(`\nSELECT select_list\nFROM table_expression\nLIMIT 10`),\n\n  OFFSET:\n    `OFFSET says to skip that many rows before beginning to return rows.   \n\nOFFSET 0 is the same as omitting the OFFSET clause, as is OFFSET with a NULL argument.\nThe rows skipped by an OFFSET clause still have to be computed inside the server; therefore a large OFFSET might be inefficient.\n\n` +\n    asSQL(`\nSELECT select_list\nFROM table_expression\nOFFSET 3`),\n  HAVING:\n    `The fundamental difference between WHERE and HAVING is this: \n\n**WHERE** selects input rows **before** groups and **aggregates are computed** (thus, it controls which rows go into the aggregate computation), whereas  \n\n**HAVING** selects group rows **after** groups and **aggregates are computed.**  \n\n` +\n    asSQL(`\nSELECT \n  city, \n  max(temp_lo), \n  count(*) FILTER (WHERE temp_lo < 30)\nFROM weather\nGROUP BY city\nHAVING max(temp_lo) < 40;\n  `),\n\n  FILTER:\n    `If FILTER is specified, then only the input rows for which the filter_clause evaluates to true are fed to the aggregate function; other rows are discarded. For example:\n\n` +\n    asSQL(`\nSELECT\n  count(*) AS unfiltered,\n  count(*) FILTER (WHERE i < 5) AS filtered\nFROM generate_series(1,10) AS s(i);\n unfiltered | filtered\n------------+----------\n         10 |        4\n(1 row)\n`),\n\n  WHERE: `Where condition is any expression that evaluates to a result of type boolean. Any row that does not satisfy this condition will be eliminated from the output. A row satisfies the condition if it returns true when the actual row values are substituted for any variable references.\n`,\n\n  RETURNING: `Obtain data from modified rows while they are being manipulated.\n\nhttps://www.postgresql.org/docs/current/dml-returning.html\nThe allowed contents of a RETURNING clause are the same as a SELECT command's output list (see Section 7.3). It can contain column names of the command's target table, or value expressions using those columns.\n`,\n\n  DISTINCT: `If SELECT DISTINCT is specified, all duplicate rows are removed from the result set (one row is kept from each group of duplicates). \n\nSELECT DISTINCT ON ( expression [, ...] ) keeps only the first row of each set of rows where the given expressions evaluate to equal. The DISTINCT ON expressions are interpreted using the same rules as for ORDER BY (see above). Note that the “first row” of each set is unpredictable unless ORDER BY is used to ensure that the desired row appears first\nExample:\n${asSQL(`\n--list locations\nSELECT DISTINCT location\nFROM weather_reports\n\n--most recent weather report for each location\nSELECT DISTINCT ON (location) location, time, report\nFROM weather_reports\nORDER BY location, time DESC;\n`)}`,\n\n  COALESCE: `The COALESCE function returns the first of its arguments that is not null. Null is returned only if all arguments are null.`,\n  NULLIF: `The NULLIF function returns a null value if value1 equals value2: \\n\\n${asSQL(\"NULLIF(value1, value2)\")}`,\n  GREATEST: `The GREATEST and LEAST functions select the largest or smallest value from a list of any number of expressions. ${asSQL(\"GREATEST(value1, value2, ...)\")}`,\n  LEAST: `The GREATEST and LEAST functions select the largest or smallest value from a list of any number of expressions. ${asSQL(\"LEAST(value1, value2, ...)\")}`,\n  CASE: `The SQL CASE expression is a generic conditional expression, similar to if/else statements in other programming languages:\\n${asSQL(\"SELECT CASE WHEN 1 THEN 'one' ELSE 'none' END  \")}`,\n} as const;\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQLSmartEditor.tsx",
    "content": "import { mdiPlay } from \"@mdi/js\";\nimport type { SQLHandler } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { DashboardState } from \"../Dashboard/Dashboard\";\nimport { W_SQLEditor } from \"./W_SQLEditor\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport { useIsMounted } from \"prostgles-client\";\n\ntype SQLSmartEditorProps = {\n  title: string;\n  asPopup?: boolean;\n  contentTop?: React.ReactNode;\n  query: string;\n  sql: SQLHandler;\n  hint?: React.ReactNode;\n  onSuccess?: VoidFunction;\n  onCancel?: VoidFunction;\n  suggestions?: DashboardState[\"suggestions\"];\n};\n\nexport const SQLSmartEditor = ({\n  query: propsQuery,\n  sql,\n  hint,\n  onSuccess,\n  onCancel,\n  suggestions,\n  title,\n  contentTop,\n  asPopup = true,\n}: SQLSmartEditorProps) => {\n  const [error, setError] = useState<any>();\n  const [running, setRunning] = useState(false);\n  const [query, setQuery] = useState(propsQuery);\n  const getIsMounted = useIsMounted();\n  const onRunQuery = async (q: string = query) => {\n    if (!getIsMounted()) return;\n    try {\n      setRunning(true);\n      await sql(query);\n      onSuccess?.();\n      if (!getIsMounted()) return;\n      setError(undefined);\n    } catch (err) {\n      if (!getIsMounted()) return;\n      setError(err);\n    }\n    if (!getIsMounted()) return;\n    setRunning(false);\n  };\n\n  const cancelBtnProps: BtnProps = { onClick: onCancel };\n  const runBtnProps: BtnProps = {\n    variant: \"filled\",\n    color: \"action\",\n    className: \"ml-auto\",\n    iconPath: mdiPlay,\n    onClickPromise: () => onRunQuery(),\n    title: \"Run query\",\n    \"data-command\": \"SQLSmartEditor.Run\",\n    style: { alignSelf: \"flex-end\" },\n  };\n\n  const innerFooter =\n    !asPopup ?\n      <FlexRow>\n        <Btn {...cancelBtnProps}>Cancel</Btn>\n        <Btn {...runBtnProps}>Run</Btn>\n      </FlexRow>\n    : null;\n\n  const content = (\n    <FlexCol className=\"f-1 min-h-0 gap-p5 mt-1\">\n      {contentTop}\n      <W_SQLEditor\n        suggestions={suggestions}\n        className=\"f-1 b b-color rounded o-hidden\"\n        autoFocus={false}\n        value={query}\n        style={{\n          minHeight: \"200px\",\n          minWidth: \"600px\",\n        }}\n        sql={sql}\n        onChange={(v) => {\n          if (!getIsMounted()) return;\n          setQuery(v);\n          setError(undefined);\n        }}\n        onRun={onRunQuery}\n        sqlOptions={{\n          executeOptions: \"full\",\n          errorMessageDisplay: \"both\",\n          lineNumbers: \"off\",\n        }}\n      />\n      {error && <ErrorComponent error={error} className=\"m-1 f-0\" />}\n      {!!hint && <InfoRow color=\"info\">{hint}</InfoRow>}\n      {innerFooter}\n    </FlexCol>\n  );\n\n  if (!asPopup) return content;\n\n  return (\n    <Popup\n      onClose={onCancel}\n      positioning=\"center\"\n      showFullscreenToggle={{}}\n      title={title}\n      footerButtons={[\n        onCancel ? { ...cancelBtnProps, label: \"Cancel\" } : undefined,\n        { ...runBtnProps, label: \"Run\" },\n      ]}\n    >\n      {content}\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/SQL_SNIPPETS.ts",
    "content": "import { fixIndent } from \"../../demo/scripts/sqlVideoDemo\";\n\nexport const SQL_SNIPPETS: { label: string; info: string; query: string }[] = [\n  {\n    label: \"Cache Hit Ratio\",\n    info: `Ideal caching ratio is 0.99 or higher, which means that at least 99% of reads are performed from the cache and no more than 1% from disk`,\n    query: `SELECT relname, \n  sum(heap_blks_read) as heap_read,\n  sum(heap_blks_hit)  as heap_hit,\n  sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as ratio\nFROM \n  pg_statio_user_tables\nWHERE heap_blks_read <> 0\nGROUP BY relname`,\n  },\n  {\n    label: \"Index usage\",\n    info: \"\",\n    query: `\nSELECT relname,   \n  100 * idx_scan / (seq_scan + idx_scan) percent_of_times_index_used,   \n  n_live_tup rows_in_table \nFROM pg_stat_user_tables \nWHERE seq_scan + idx_scan > 0 \nORDER BY n_live_tup DESC;\n`,\n  },\n  {\n    label: \"Active queries\",\n    info: \"\",\n    query: fixIndent(`\n      SELECT \n        datid, datname, pid, usesysid, usename, application_name, client_addr, \n        client_hostname, client_port, backend_start, xact_start, query_start, \n        state_change, wait_event_type, wait_event, state, \n        backend_xid, backend_xmin, query, backend_type, \n        pg_blocking_pids(pid) as blocked_by,\n        COALESCE(cardinality(pg_blocking_pids(pid)), 0) blocked_by_num,\n        md5(pid || query) as id_query_hash\n      FROM pg_catalog.pg_stat_activity\n      WHERE pid <> pg_backend_pid() `),\n  },\n];\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/W_SQLEditor.css",
    "content": ".MonacoEditor .monaco-editor-overlaymessage .message {\n  background-color: var(--red-100) !important;\n  color: var(--red-900) !important;\n  border-color: var(--red-400) !important;\n  padding: 5px !important;\n}\n\n.MonacoEditor .monaco-editor-overlaymessage .anchor {\n  border-top-color: var(--red-900) !important;\n}\n\n.MonacoEditor .monaco-editor {\n  outline: none !important;\n}\n "
  },
  {
    "path": "client/src/dashboard/SQLEditor/W_SQLEditor.tsx",
    "content": "import React from \"react\";\nimport \"./W_SQLEditor.css\";\n\nimport ReactDOM from \"react-dom\";\nimport type { MonacoSuggestion } from \"./SQLCompletion/monacoSQLSetup/registerSuggestions\";\nimport { registerSuggestions } from \"./SQLCompletion/monacoSQLSetup/registerSuggestions\";\n\nexport const LANG = \"sql\";\n\nlet monacoPromise:\n  | Promise<typeof import(\"monaco-editor/esm/vs/editor/editor.api\")>\n  | undefined;\n/**\n * This option seems to start downloading monaco (870.js) from the start: webpackPrefetch: true\n */\nexport const getMonaco = async () => {\n  monacoPromise ??= import(\n    /* webpackChunkName: \"monaco_editor\" */ /*  webpackPrefetch: 99 */ \"monaco-editor/esm/vs/editor/editor.api\"\n  );\n  const monaco = await monacoPromise;\n  return monaco;\n};\n\nexport const SUGGESTION_TYPES = [\n  \"table\",\n  \"view\",\n  \"mview\",\n  \"column\",\n  \"function\",\n  \"dataType\",\n  \"extension\",\n  \"keyword\",\n  \"schema\",\n  \"setting\",\n  \"role\",\n  \"database\",\n  \"folder\",\n  \"file\",\n  \"snippet\",\n  \"policy\",\n  \"publication\",\n  \"subscription\",\n  \"index\",\n  \"operator\",\n  \"constraint\",\n  \"trigger\",\n  \"eventTrigger\",\n  \"rule\",\n  \"user\",\n] as const;\n\nexport const SUGGESTION_TYPE_DOCS: Record<\n  (typeof SUGGESTION_TYPES)[number],\n  string\n> = {\n  column:\n    \"A set of data values of a particular type. Is part of a table or a view\",\n  constraint: \"A way to limit the kind of data that can be stored in a table\",\n  table: `A table is a collection of related data held in a table format within a database`,\n  view: `A view is similar to a table but it can also show data from multiple tables`,\n  database: `A place to store all data`,\n  dataType: \"A set of possible values and a set of allowed operations on it\",\n  extension:\n    \"A script that creates new SQL objects such as functions, data types, operators and index support methods\",\n  file: \"\",\n  folder: \"\",\n  function:\n    \"A set of SQL and procedural commands such as declarations, assignments, loops, flow-of-control etc. stored on the database server and can be involved using the SQL interface. \",\n  index:\n    \"An index is a pointer to data in a table. A common way to enhance database performance.\",\n  keyword: \"SQL command\",\n  mview:\n    \"Materialized view. Similar to views with the exception that the underlying data is saved\",\n  operator: \"Used in comparing values\",\n  policy: \"A row-level security policy for a table\",\n  publication:\n    \"A publication is a set of changes generated from a table or a group of tables, and might also be described as a change set or replication set. Each publication exists in only one database. Publications are different from schemas and do not affect how the table is accessed.\",\n  role: \"PostgreSQL manages database access permissions using the concept of roles. A role can be thought of as either a database user, or a group of database users, depending on how the role is set up. Roles can own database objects (for example, tables) and can assign privileges on those objects to other roles to control who has access to which objects. Furthermore, it is possible to grant membership in a role to another role, thus allowing the member role use of privileges assigned to the role it is a member of.\",\n  user: \"A user is a role with LOGIN permission\",\n  schema:\n    \"A database contains one or more named schemas, which in turn contain tables. Schemas also contain other kinds of named objects, including data types, functions, and operators. \",\n  setting:\n    \"Server configuration parameters. Can be specified for SYSTEM, DATABASE or ROLE\",\n  snippet: \"A prostgles snippet\",\n  rule: \"An alternative action to be performed on insertions, updates, or deletions in database tables\",\n  subscription:\n    \"A subscription represents a replication connection to the publisher. Hence, in addition to adding definitions in the local catalogs, this command normally creates a replication slot on the publisher.\",\n  trigger:\n    \"A trigger is a function invoked automatically whenever an event associated with a table occurs. An event could be any of the following: INSERT, UPDATE, DELETE or TRUNCATE.\",\n  eventTrigger:\n    \"An event trigger is a function invoked automatically whenever the designated event occurs and the WHEN condition associated with the trigger, if any, is satisfied, the trigger function will be executed\",\n};\n\nexport const DB_OBJ_LABELS: Record<keyof typeof SUGGESTION_TYPES, string> =\n  SUGGESTION_TYPES.reduce(\n    (a, v) => ({\n      ...a,\n      [v]: v === \"mview\" ? \"materialized view\" : v,\n    }),\n    {} as Record<keyof typeof SUGGESTION_TYPES, string>,\n  );\n\ntype CodeBlockSignature = {\n  numberOfLineBreaks: number;\n  numberOfSemicolons: number;\n  currentLineNumber: number;\n  query: string;\n};\n\nexport type SQLSuggestion = {\n  type: (typeof SUGGESTION_TYPES)[number];\n  name: string;\n  label: MonacoSuggestion[\"label\"];\n  subLabel?: string;\n  escapedIdentifier?: string;\n  escapedName?: string;\n  OID?: number;\n  schema?: string;\n  parentOID?: number;\n  parentName?: string;\n  escapedParentName?: string;\n  insertText?: string;\n  detail?: string;\n  filterText?: string;\n  sortText?: string;\n  documentation?: string;\n  insertTextRules?: any;\n  definition?: string;\n  funcCallDefinition?: string;\n  args?: PG_Function[\"args\"];\n\n  extensionInfo?: {\n    installed: boolean;\n  };\n\n  tablesInfo?: PG_Table;\n  relkind?: string;\n  view?: {\n    definition: string;\n  };\n\n  funcInfo?: PG_Function;\n\n  operatorInfo?: PGOperator;\n\n  userInfo?: PG_Role;\n\n  policyInfo?: PG_Policy;\n\n  triggerInfo?: PG_Trigger;\n\n  settingInfo?: PG_Setting;\n\n  eventTriggerInfo?: PG_EventTrigger;\n\n  constraintInfo?: PGConstraint;\n\n  topKwd?: TopKeyword;\n  keywordInfo?: PG_Keyword;\n  colInfo?: PG_Table[\"cols\"][number];\n\n  /**\n   * Extra info for \"dataType\"\n   */\n  dataTypeInfo?: PG_DataType;\n\n  cols?: PG_Table[\"cols\"];\n\n  ruleInfo?: PG_Rule;\n};\n\nexport type MonacoError = Pick<\n  editor.IMarkerData,\n  \"code\" | \"message\" | \"severity\" | \"relatedInformation\"\n> & {\n  position?: number;\n  length?: number;\n};\n\nimport Btn from \"@components/Btn\";\nimport { getDataTransferFiles } from \"@components/FileInput/useFileDropZone\";\nimport { FlexCol } from \"@components/Flex\";\nimport {\n  MonacoEditor,\n  type MonacoEditorProps,\n} from \"@components/MonacoEditor/MonacoEditor\";\nimport { getSelectedText } from \"@components/MonacoEditor/useMonacoEditorAddActions\";\nimport { mdiPlay } from \"@mdi/js\";\nimport type { IPosition } from \"monaco-editor/esm/vs/editor/editor.api\";\nimport type { SQLHandler } from \"prostgles-types\";\nimport { isEmpty, isEqual, omitKeys } from \"prostgles-types\";\nimport { SECOND } from \"../Charts\";\nimport type { DashboardState } from \"../Dashboard/Dashboard\";\nimport type { WindowData } from \"../Dashboard/dashboardUtils\";\nimport RTComp from \"../RTComp\";\nimport type { editor } from \"../W_SQL/monacoEditorTypes\";\nimport type { TopKeyword } from \"./SQLCompletion/KEYWORDS\";\nimport type { CodeBlock } from \"./SQLCompletion/completionUtils/getCodeBlock\";\nimport {\n  getCurrentCodeBlock,\n  highlightCurrentCodeBlock,\n  playButtonglyphMarginClassName,\n} from \"./SQLCompletion/completionUtils/getCodeBlock\";\nimport type {\n  PGConstraint,\n  PGOperator,\n  PG_DataType,\n  PG_EventTrigger,\n  PG_Function,\n  PG_Keyword,\n  PG_Policy,\n  PG_Role,\n  PG_Rule,\n  PG_Setting,\n  PG_Table,\n  PG_Trigger,\n} from \"./SQLCompletion/getPGObjects\";\nimport { addSqlEditorFunctions } from \"./addSqlEditorFunctions\";\nimport type { GetFuncs } from \"./registerFunctionSuggestions\";\nimport { registerFunctionSuggestions } from \"./registerFunctionSuggestions\";\nimport { scrollToLineIfNeeded } from \"./utils/scrollToLineIfNeeded\";\nimport { setMonacEditorError } from \"./utils/setMonacEditorError\";\n\nexport type SQLEditorRef = {\n  editor: editor.IStandaloneCodeEditor;\n  getSelectedText: () => string;\n  getCurrentCodeBlock: () => Promise<CodeBlock> | undefined;\n};\n\ntype P = {\n  value: string;\n  onChange: (newValue: string, cursorPosition: any) => void;\n  debounce?: number;\n  onRun?: (code: string, isSelected: boolean) => void;\n  suggestions?: DashboardState[\"suggestions\"] & {\n    onLoaded?: VoidFunction;\n  };\n  error?: MonacoError;\n  getFuncDef?: GetFuncs;\n  /**\n   * Triggered when user presses ESC\n   */\n  onStopQuery?: (terminate: boolean) => any;\n  sql?: SQLHandler;\n  onMount?: (ref: SQLEditorRef) => void;\n  onUnmount?: (editor: any, cursorPosition: any) => void | Promise<void>;\n  cursorPosition?: IPosition;\n  onDidSetCursorPosition?: () => void;\n  style?: React.CSSProperties;\n  className?: string;\n  autoFocus?: boolean;\n  sqlOptions?: Partial<WindowData[\"sql_options\"]>;\n  activeCodeBlockButtonsNode?: React.ReactNode;\n  onDidChangeActiveCodeBlock?: (cb: CodeBlock | undefined) => void;\n};\ntype S = {\n  value: string;\n  editorMounted: boolean;\n};\n\nexport class W_SQLEditor extends RTComp<P, S> {\n  ref?: HTMLElement;\n  editor?: editor.IStandaloneCodeEditor;\n  error?: MonacoError;\n  value?: string;\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      value: props.value ?? \"\",\n      editorMounted: false,\n    };\n  }\n\n  onMount() {\n    window.addEventListener(\n      \"beforeunload\",\n      async (e) => {\n        await this.onUnmount();\n      },\n      false,\n    );\n  }\n\n  async onUnmount() {\n    await this.props.onUnmount?.(this.editor, this.editor?.getPosition());\n    if (this.rootRef) this.resizeObserver?.unobserve(this.rootRef);\n  }\n\n  tooltipHandler: any;\n  loadedSuggestions: DashboardState[\"suggestions\"];\n  loadedFuncs = false;\n  resizeObserver?: ResizeObserver;\n  onDelta = async (dp, ds) => {\n    const {\n      error,\n      getFuncDef,\n      value,\n      sql,\n      autoFocus = false,\n      sqlOptions,\n    } = { ...this.props };\n    const EOL = this.editor?.getModel()?.getEOL() || \"\\n\";\n    const monaco = await getMonaco();\n    if (value && this.curVal === undefined) {\n      this.curVal = value;\n      this.setState({ value });\n    }\n\n    if (!this.resizeObserver && this.rootRef) {\n      this.resizeObserver = new ResizeObserver((entries) => {\n        this.editor?.revealLineInCenterIfOutsideViewport(\n          this.editor.getPosition()?.lineNumber ?? value.split(EOL).length,\n        );\n      });\n      this.resizeObserver.observe(this.rootRef);\n    }\n\n    const { suggestions } = this.props;\n\n    /* LOAD SUGGESTIONS */\n    if (\n      this.editor &&\n      suggestions &&\n      this.loadedSuggestions?.dbKey !== suggestions.dbKey\n    ) {\n      this.loadedSuggestions = { ...suggestions };\n      registerSuggestions({\n        ...suggestions,\n        sql,\n        editor: this.editor,\n        monaco,\n      });\n\n      /* SET FUNC AUTOCOMPLETE */\n      if (getFuncDef && !this.loadedFuncs) {\n        registerFunctionSuggestions(\n          monaco,\n          getFuncDef,\n          suggestions.suggestions,\n        );\n        suggestions.onLoaded?.();\n        this.loadedFuncs = true;\n        if (autoFocus) {\n          this.editor.focus();\n        }\n      }\n    }\n\n    /* SET ERROR */\n    if (this.editor && !isEqual(error, this.error)) {\n      this.error = error;\n      setMonacEditorError(\n        this.editor,\n        monaco,\n        this.getCurrentCodeBlock,\n        sqlOptions?.errorMessageDisplay,\n        error,\n      );\n    }\n  };\n\n  inDebounce: any;\n  curVal?: string;\n  onChange = (val: string) => {\n    const { onChange, debounce = 300 } = this.props;\n\n    this.curVal = val;\n    if (this.inDebounce) window.clearTimeout(this.inDebounce);\n    this.inDebounce = setTimeout(() => {\n      onChange(val, this.editor?.getPosition());\n      this.inDebounce = null;\n    }, debounce);\n  };\n\n  rootRef?: HTMLDivElement;\n\n  currentDecorations: editor.IEditorDecorationsCollection | undefined;\n\n  get canExecuteBlocks() {\n    const { executeOptions } = this.props.sqlOptions ?? {};\n    return executeOptions !== \"full\";\n  }\n  getCurrentCodeBlock = () => {\n    const { executeOptions } = this.props.sqlOptions ?? {};\n    if (executeOptions !== \"full\") {\n      const model = this.editor?.getModel();\n      const position = this.editor?.getPosition();\n      if (model && !model.isDisposed() && position) {\n        return getCurrentCodeBlock(model, position, undefined, {\n          smallestBlock: executeOptions === \"smallest-block\",\n        });\n      }\n\n      return undefined;\n    }\n  };\n\n  onRun = async () => {\n    if (!this.props.onRun) return;\n    const selection = getSelectedText(this.editor);\n    const codeBlock = await this.getCurrentCodeBlock();\n    const text = selection || codeBlock?.text;\n    if (text) {\n      this.props.onRun(text, true);\n    } else {\n      this.props.onRun(this.editor?.getValue() ?? \"\", false);\n    }\n  };\n\n  codeBlockSignature?: CodeBlockSignature;\n  currentCodeBlock: CodeBlock | undefined;\n\n  get editorOptions() {\n    const { sqlOptions } = this.props;\n\n    const { canExecuteBlocks } = this;\n    return {\n      fixedOverflowWidgets: true,\n      folding: true,\n      automaticLayout: true,\n      padding: {\n        top: 12,\n      },\n      parameterHints: {\n        enabled: !window.isMobileDevice,\n      },\n      quickSuggestions: {\n        strings: true,\n      },\n      ...omitKeys(sqlOptions ?? {}, [\n        \"maxCharsPerCell\",\n        \"renderMode\",\n        \"executeOptions\",\n        \"errorMessageDisplay\",\n      ]),\n\n      ...(canExecuteBlocks && {\n        /** Needed for play button and chart buttons */\n        glyphMargin: true,\n        lineNumbersMinChars: 3,\n      }),\n    } satisfies MonacoEditorProps[\"options\"];\n  }\n\n  onMonacoEditorMount = (editor: editor.IStandaloneCodeEditor) => {\n    const { onMount, sqlOptions } = this.props;\n    addSqlEditorFunctions(\n      editor,\n      sqlOptions?.executeOptions === \"smallest-block\",\n    );\n\n    this.setState({ editorMounted: true });\n    if (onMount) {\n      onMount({\n        editor,\n        getSelectedText: () => getSelectedText(editor),\n        getCurrentCodeBlock: this.getCurrentCodeBlock,\n      });\n    }\n    this.editor = editor;\n    setActions(editor, this);\n    editor.onDidChangeModelContent((e) => {\n      this.onChange(editor.getValue());\n      setActiveCodeBlock.bind(this)(undefined);\n    });\n    editor.onDidChangeCursorPosition((e) => {\n      setActiveCodeBlock.bind(this)(e);\n    });\n\n    const { cursorPosition, onDidSetCursorPosition } = this.props;\n    if (cursorPosition && !isEmpty(cursorPosition)) {\n      this.editor.setPosition(cursorPosition);\n\n      setTimeout(() => {\n        if (!this.mounted || !this.editor) return;\n        scrollToLineIfNeeded(this.editor, cursorPosition.lineNumber || 1);\n        onDidSetCursorPosition?.();\n      }, SECOND / 2);\n    } else {\n      onDidSetCursorPosition?.();\n    }\n  };\n\n  render() {\n    const { value = \"\" } = this.state;\n    const {\n      style = {},\n      className = \"\",\n      sqlOptions,\n      activeCodeBlockButtonsNode,\n    } = this.props;\n\n    const { canExecuteBlocks, rootRef } = this;\n    const glyphPlayBtnElem = rootRef?.querySelector(\n      `.${playButtonglyphMarginClassName}`,\n    );\n    if (glyphPlayBtnElem && glyphPlayBtnElem.children.length > 1) {\n      glyphPlayBtnElem.children[1]?.remove();\n      console.warn(\"Removed extra play button\");\n    }\n    const glyphPlayBtn =\n      canExecuteBlocks && glyphPlayBtnElem ?\n        ReactDOM.createPortal(\n          <FlexCol\n            className=\"GlyphButtons gap-0 mt-auto\"\n            style={{\n              /** Ensures we can click add chart btn */\n              zIndex: 2,\n            }}\n          >\n            <Btn\n              iconPath={mdiPlay}\n              size=\"micro\"\n              onClick={this.onRun}\n              data-command=\"W_SQLEditor.executeStatement\"\n              color=\"action\"\n              title={`Run this statement**\\n\\nOnly this section of the script will be executed unless text is selected. This behaviour can be changed in options\\n\\nExecute hot keys: ctrl+e, alt+e`}\n            />\n            {activeCodeBlockButtonsNode}\n          </FlexCol>,\n          glyphPlayBtnElem,\n        )\n      : null;\n\n    return (\n      <div\n        className={\n          /** o-hidden is required to ensure monaco code is not visible in W_SQLBottomBar when results are shown in a low height (220px) table */\n          \"sqleditor f-1 min-h-0 min-w-0 flex-col relative o-hidden \" +\n          className\n        }\n        ref={(e) => {\n          if (e) this.rootRef = e;\n        }}\n        style={style}\n        onDragOver={(e) => {\n          e.preventDefault();\n          e.stopPropagation();\n        }}\n        onDrop={(e) => {\n          let text = e.dataTransfer.getData(\"text\");\n          if (text) {\n            text = ` ${text} `;\n            this.editor?.trigger(\"keyboard\", \"type\", { text });\n          } else {\n            const [file, ...otherFiles] = getDataTransferFiles(e);\n            if (otherFiles.length) {\n              alert(\"Only one file can be dropped at a time\");\n            } else if (file?.type.includes(\"text\")) {\n              const reader = new FileReader();\n              reader.onload = (event) => {\n                const text = event.target?.result;\n                if (!text || !this.editor || !this.mounted) return;\n                this.editor.setValue(text as string);\n              };\n              reader.readAsText(file);\n            }\n          }\n          e.preventDefault();\n          e.stopPropagation();\n          return false;\n        }}\n      >\n        {glyphPlayBtn}\n        <MonacoEditor\n          className=\"f-1 min-h-0\"\n          language={LANG}\n          value={value}\n          loadedSuggestions={this.props.suggestions}\n          /** This is used to show documentation (expandSuggestionDocs) */\n          expandSuggestionDocs={sqlOptions?.expandSuggestionDocs}\n          options={this.editorOptions}\n          onMount={this.onMonacoEditorMount}\n        />\n      </div>\n    );\n  }\n}\n\nconst setActiveCodeBlock = async function (\n  this: W_SQLEditor,\n  e: editor.ICursorPositionChangedEvent | undefined,\n) {\n  const editor = this.editor;\n  if (!editor) return;\n\n  /** Codeblock end line changes when going from empty line to content */\n  const model = editor.getModel();\n  const value = model?.getValue() ?? \"\";\n\n  const EOL = model?.getEOL() ?? \"\\n\";\n  const codeBlockSignature: CodeBlockSignature = {\n    numberOfLineBreaks: value.split(EOL).length,\n    numberOfSemicolons: value.split(\";\").length,\n    query: value,\n    currentLineNumber:\n      e?.position.lineNumber ??\n      editor.getPosition()?.lineNumber ??\n      this.codeBlockSignature?.currentLineNumber ??\n      1,\n  };\n\n  /** When just moving the cursor, trigger only if cursor exited currentCodeBlock\n   * Only if currentCodeBlock doesn't have gaps or semicolons\n   */\n  if (\n    e &&\n    this.currentCodeBlock &&\n    !this.currentCodeBlock.text.split(EOL).filter((v) => !v.trim()).length &&\n    !this.currentCodeBlock.text.trim().slice(0, -1).includes(\";\")\n  ) {\n    const { startLine, endLine, text } = this.currentCodeBlock;\n    const codeBlockTextDidNotChange = value\n      .slice(this.currentCodeBlock.blockStartOffset)\n      .startsWith(text);\n    if (\n      codeBlockTextDidNotChange &&\n      e.position.lineNumber >= startLine &&\n      e.position.lineNumber <= endLine\n    ) {\n      return;\n    }\n  }\n\n  let currentDecorationsNotRendered = false;\n  const currentPosition = editor.getPosition();\n  const currDecorationsIds = (this.currentDecorations as any)\n    ?._decorationIds as string[] | undefined;\n  if (currentPosition && currDecorationsIds) {\n    const renderedDecorators = editor.getLineDecorations(\n      currentPosition.lineNumber,\n    );\n    currentDecorationsNotRendered = !renderedDecorators?.some((d) =>\n      currDecorationsIds.includes(d.id),\n    );\n  }\n  const signatureDiffers =\n    !isEqual(this.codeBlockSignature, codeBlockSignature) ||\n    currentDecorationsNotRendered;\n  const codeBlockLinesChanged =\n    this.codeBlockSignature?.numberOfLineBreaks !==\n      codeBlockSignature.numberOfLineBreaks ||\n    this.codeBlockSignature.currentLineNumber !==\n      codeBlockSignature.currentLineNumber;\n  const noSelection = !document.getSelection()?.toString();\n  if (signatureDiffers && noSelection) {\n    this.codeBlockSignature = codeBlockSignature;\n    const codeBlock = await this.getCurrentCodeBlock();\n    this.currentCodeBlock = codeBlock;\n    this.props.onDidChangeActiveCodeBlock?.(this.currentCodeBlock);\n\n    // removePlayDecoration({ editor, EOL, value });\n\n    if (codeBlockLinesChanged) {\n      this.currentDecorations?.clear();\n      this.currentDecorations = await highlightCurrentCodeBlock(\n        editor,\n        codeBlock,\n      );\n    }\n  }\n};\n\ntype Args = {\n  editor: editor.IStandaloneCodeEditor;\n  value: string;\n  EOL: string;\n};\n\n/**\n * If used incorrectly it breaks code snippet tab to next param.\n */\nconst removePlayDecoration = ({ editor, value, EOL }: Args) => {\n  const existingDecorations = value.split(EOL).flatMap((_, idx) => {\n    const decorations = editor.getLineDecorations(idx);\n    return (\n      decorations\n        ?.filter((decor) => {\n          return (\n            decor.options.glyphMarginClassName === \"active-code-block-play\"\n          );\n        })\n        .map((d) => d.id) ?? []\n    );\n  });\n  console.log(existingDecorations);\n  editor.removeDecorations(existingDecorations);\n};\n\nconst setActions = async (\n  editor: editor.IStandaloneCodeEditor,\n  comp: W_SQLEditor,\n) => {\n  const monaco = await getMonaco();\n\n  /** Use actions instead of commands due to this bug:\n   * https://github.com/microsoft/monaco-editor/issues/2947\n   */\n  editor.addAction({\n    id: \"run-sql\",\n    label: \"Execute SQL\",\n    keybindings: [\n      monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,\n      monaco.KeyMod.Alt | monaco.KeyCode.KeyE,\n      monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyE,\n      monaco.KeyCode.F5,\n    ],\n    run: comp.onRun,\n  });\n  editor.addAction({\n    id: \"cancel-running-sql\",\n    label: \"Cancel running query\",\n    keybindings: [monaco.KeyCode.Escape],\n    run: () => {\n      editor.trigger(\"demo\", \"hideSuggestWidget\", {});\n      editor.trigger(\"demo\", \"closeParameterHints\", {});\n      /**\n       * Array.from(this._commands).filter(([k]) => k.toLowerCase().includes(\"hint\") && console.log(k))\n       */\n      comp.props.onStopQuery?.(false);\n    },\n  });\n\n  /** Enter newline only when not accepting a suggestion */\n  // this.editor?.addCommand(monaco.KeyCode.Enter, () => {\n  //   this.editor?.trigger('newline', 'type', { text: EOL });\n  // }, '!suggestWidgetVisible && !renameInputVisible && !inSnippetMode && !quickFixWidgetVisible');\n};\n\ndeclare module \"react\" {\n  interface HTMLAttributes<T> {\n    sqlRef?: SQLEditorRef;\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/addSqlEditorFunctions.ts",
    "content": "import type { editor } from \"../W_SQL/monacoEditorTypes\";\nimport { getCurrentCodeBlock } from \"./SQLCompletion/completionUtils/getCodeBlock\";\nimport { getMonaco } from \"./W_SQLEditor\";\nexport const addSqlEditorFunctions = async (\n  editor: editor.IStandaloneCodeEditor,\n  smallestBlock: boolean,\n) => {\n  const monaco = await getMonaco();\n  editor.addAction({\n    id: \"select1\",\n    label: \"Select word\",\n    contextMenuGroupId: \"selection\",\n    keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],\n    run: (editor) => {\n      const model = editor.getModel();\n      const position = editor.getPosition();\n\n      if (!model || !position) return;\n\n      const word = model.getWordAtPosition(position);\n      if (word) {\n        editor.setSelection({\n          startColumn: word.startColumn,\n          startLineNumber: position.lineNumber,\n          endLineNumber: position.lineNumber,\n          endColumn: word.endColumn,\n        });\n      }\n    },\n  });\n\n  editor.addAction({\n    id: \"select2CB\",\n    label: \"Select code block\",\n    contextMenuGroupId: \"selection\",\n    keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyB],\n    run: async (editor) => {\n      const model = editor.getModel();\n      const position = editor.getPosition();\n\n      if (!model || !position) return;\n      let cb = await getCurrentCodeBlock(model, position, undefined, {\n        smallestBlock,\n      });\n      if (smallestBlock) {\n        const selection = editor.getSelection();\n        if (selection) {\n          const selectedText = editor\n            .getModel()\n            ?.getValueInRange(selection)\n            .trim();\n          if (selectedText?.trim() === cb.text.trim()) {\n            cb = await getCurrentCodeBlock(model, position, undefined, {\n              smallestBlock: true,\n              expandFrom: {\n                startLine: selection.startLineNumber,\n                endLine: selection.endLineNumber,\n              },\n            });\n          }\n        }\n      }\n      const lastToken = cb.tokens.at(-1);\n      if (cb.textLC.trim() && lastToken) {\n        editor.setSelection({\n          startColumn: 1,\n          startLineNumber: cb.startLine,\n          endLineNumber: cb.endLine,\n          endColumn: lastToken.end + 2,\n        });\n      }\n    },\n  });\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/defineCustomMonacoSQLTheme.ts",
    "content": "import { isDefined } from \"../../utils/utils\";\nimport { LANG, getMonaco } from \"./W_SQLEditor\";\n\nexport const CUSTOM_MONACO_SQL_THEMES = {\n  light: \"myCustomLightTheme\",\n  dark: \"myCustomDarkTheme\",\n} as const;\nlet themeWasSet = false;\n\ntype TokenColors = {\n  string: string;\n  identifiersInclComplex: string;\n  inbuiltFuncs: string;\n};\n\nconst getRules = (\n  { identifiersInclComplex, inbuiltFuncs, string }: TokenColors,\n  isDark: boolean,\n) => {\n  const uniqueVals = new Set([identifiersInclComplex, inbuiltFuncs, string]);\n  if (uniqueVals.size !== 3) {\n    throw new Error(\n      \"Token colors must be unique to ensure tokenization within suggestion markdown is consistent for svg screenshots\",\n    );\n  }\n  return [\n    { token: `string.${LANG}`, foreground: string },\n    { token: \"operator.scope\", foreground: \"569cd6\" },\n    /* Table names */\n    { token: `identifier.${LANG}`, foreground: identifiersInclComplex },\n    {\n      token: `complexIdentifiers.${LANG}`,\n      foreground: identifiersInclComplex,\n    },\n    /** Inbuilt funcs */\n    { token: \"predefined.sql\", foreground: inbuiltFuncs },\n    /** Do to ensure tokenization splits suggestion markdown tokens consistently */\n    !isDark ? undefined : { token: \"delimiter\", foreground: \"D4D4D4\" },\n  ].filter(isDefined);\n};\n\nexport const defineCustomMonacoSQLTheme = async (): Promise<boolean> => {\n  if (themeWasSet) return false;\n  themeWasSet = true;\n  const monaco = await getMonaco();\n  monaco.editor.defineTheme(CUSTOM_MONACO_SQL_THEMES.light, {\n    base: \"vs\", // can also be vs-dark or hc-black or vs\n    inherit: true, // can also be false to completely replace the builtin rules\n    colors: {},\n    rules: getRules(\n      {\n        string: \"#930000\",\n        identifiersInclComplex: \"#6c06ab\",\n        inbuiltFuncs: \"#c700c6\",\n      },\n      false,\n    ),\n  });\n\n  monaco.editor.defineTheme(CUSTOM_MONACO_SQL_THEMES.dark, {\n    base: \"vs-dark\", // can also be vs-dark or hc-black or vs\n    inherit: true,\n    colors: {},\n    rules: getRules(\n      {\n        string: \"#db5050\",\n        identifiersInclComplex: \"#c88eecff\",\n        inbuiltFuncs: \"#de1ddeff\",\n      },\n      true,\n    ),\n  });\n  return true;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/getFormattedSql.ts",
    "content": "import type { editor } from \"monaco-editor\";\nimport { getMonaco } from \"./W_SQLEditor\";\nimport {\n  type TokenInfo,\n  getTokens,\n} from \"./SQLCompletion/completionUtils/getTokens\";\n\nconst getNestingInfo = (\n  tokens: TokenInfo[],\n  nestingId: string,\n  isStart: boolean,\n) => {\n  let nestingLength = 0;\n  let nestingContainedKeywords = false;\n\n  const tokensOrdered = isStart ? [...tokens] : [...tokens].reverse();\n\n  tokensOrdered.findIndex((_t, i) => {\n    nestingContainedKeywords =\n      nestingContainedKeywords || _t.type === \"keyword.sql\";\n    const nextToken = tokensOrdered[i + 1];\n    nestingLength += _t.text.length;\n    return (\n      nextToken?.text === (isStart ? \")\" : \"(\") && _t.nestingId === nestingId\n    );\n  });\n\n  return {\n    nestingLength,\n    nestingContainedKeywords,\n  };\n};\n\nexport const getFormattedSql = async (model: editor.ITextModel) => {\n  const lines = model.getLinesContent();\n  const editor = (await getMonaco()).editor;\n  const eol = model.getEOL();\n  const { tokens } = getTokens({ lines, eol, editor, includeComments: true });\n  let lastKwdToken: TokenInfo | undefined;\n  const newText = tokens\n    .map((t, i) => {\n      const prevToken = tokens[i - 1];\n      const nextToken = tokens[i + 1];\n      const newLineKeywords = [\n        \"do\",\n        \"declare\",\n        \"for\",\n        \"begin\",\n        \"set\",\n        \"create\",\n        \"alter\",\n        \"drop\",\n        \"truncate\",\n        \"insert\",\n        \"update\",\n        \"delete\",\n        \"with\",\n        \"grant\",\n        \"select\",\n        \"from\",\n        \"where\",\n        \"group\",\n        \"having\",\n        \"order\",\n        \"limit\",\n        \"offset\",\n        \"union\",\n        \"intersect\",\n        \"except\",\n        \"on\",\n        \"join\",\n        \"left\",\n        \"right\",\n        \"full\",\n        \"cross\",\n        \"natural\",\n        \"inner\",\n      ];\n      const newLineOperators = [\"and\", \"or\", \"exists\"];\n      const noPrevSpaceSymbols = [\"(\", \")\", \",\", \"[\", \"]\", \"{\", \"}\", \".\"];\n      const currNestingSpaces = \"  \".repeat(\n        t.nestingId.length + t.funcNestingId.length,\n      );\n      const withNewline = () => `${eol}${currNestingSpaces}${t.text}`;\n\n      if (t.text === \";\" && (!t.nestingId || t.funcNestingId)) {\n        return `${t.text}${eol.repeat(t.funcNestingId ? 1 : 2)}${currNestingSpaces}`;\n      }\n\n      if (t.text === \")\" && prevToken?.nestingId) {\n        const { nestingLength, nestingContainedKeywords } = getNestingInfo(\n          tokens.slice(0, i + 1),\n          prevToken.nestingId,\n          false,\n        );\n        if (nestingLength > 40 || nestingContainedKeywords) {\n          return withNewline();\n        }\n      }\n\n      const needsNewLine =\n        t.textLC === \"loop\" && nextToken?.textLC === \";\" ? undefined\n        : (\n          (t.type === \"keyword.sql\" || t.type === \"operator.sql\") &&\n          newLineKeywords.includes(t.textLC)\n        ) ?\n          \"kwd\"\n        : t.type === \"operator.sql\" && newLineOperators.includes(t.textLC) ?\n          \"op\"\n        : undefined;\n      if (needsNewLine) {\n        if (needsNewLine === \"kwd\") {\n          lastKwdToken = t;\n        } else {\n          if (tokens.at(i - 2)?.textLC === \"between\") {\n            return ` ${t.text}`;\n          }\n        }\n        return withNewline();\n      }\n      if (lastKwdToken?.textLC === \"select\" && t.text === \",\") {\n        return \",\" + eol + currNestingSpaces;\n      }\n\n      let addPrevSpace = !noPrevSpaceSymbols.includes(t.text);\n      if (\n        (prevToken?.type === \"keyword.sql\" ||\n          (prevToken && [\"in\", \"exists\"].includes(prevToken.textLC))) &&\n        t.text === \"(\"\n      ) {\n        addPrevSpace = true;\n      }\n      if (t.text === \"*\" && prevToken?.text === \"(\") {\n        addPrevSpace = false;\n      }\n      if (prevToken?.text === \"(\") {\n        const { nestingLength, nestingContainedKeywords } = getNestingInfo(\n          tokens.slice(i - 1),\n          t.nestingId,\n          true,\n        );\n        if (nestingLength > 50 || nestingContainedKeywords) {\n          return withNewline();\n        }\n      }\n      if (t.funcNestingId && !nextToken?.funcNestingId) {\n        addPrevSpace = false;\n      }\n      return addPrevSpace ? ` ${t.text}` : t.text;\n    })\n    .join(\"\");\n\n  return newText;\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/registerFunctionSuggestions.ts",
    "content": "import { getCurrentCodeBlock } from \"./SQLCompletion/completionUtils/getCodeBlock\";\nimport { asSQL } from \"./SQLCompletion/KEYWORDS\";\nimport { getParentFunction } from \"./SQLCompletion/MatchSelect\";\nimport type { SQLSuggestion } from \"./W_SQLEditor\";\nimport { LANG } from \"./W_SQLEditor\";\nimport type { PG_Function } from \"./SQLCompletion/getPGObjects\";\nimport type {\n  IDisposable,\n  Monaco,\n  languages,\n} from \"../W_SQL/monacoEditorTypes\";\n\nexport type GetFuncs = (\n  name: string,\n  minArgs: number,\n) => Promise<PG_Function[]>;\n\nlet sqlFunctionSuggestions: IDisposable | undefined;\nexport function registerFunctionSuggestions(\n  monaco: Monaco,\n  getFunc: GetFuncs,\n  suggestions: SQLSuggestion[],\n) {\n  sqlFunctionSuggestions?.dispose();\n  sqlFunctionSuggestions = monaco.languages.registerSignatureHelpProvider(\n    LANG,\n    {\n      signatureHelpTriggerCharacters: [\"(\", \",\"],\n      provideSignatureHelp: async function (model, position) {\n        const textUntilPosition = model.getValueInRange({\n          startLineNumber: 1,\n          startColumn: 1,\n          endLineNumber: position.lineNumber,\n          endColumn: position.column,\n        });\n\n        const cb = await getCurrentCodeBlock(model, position);\n        const previousToken = cb.getPrevTokensNoParantheses(true);\n\n        const prevLines: string[] = textUntilPosition\n          .split(model.getEOL())\n          .slice(0)\n          .reverse();\n\n        const parentFunction = getParentFunction(cb);\n        const funcName = parentFunction?.func.text;\n        if (!funcName) return null;\n        const numberOfDelimiters = parentFunction.prevDelimiters.reduce(\n          (a, v) => a + v.text.split(\",\").length - 1,\n          0,\n        );\n        const activeParameter = numberOfDelimiters;\n        // const activeParameter = Math.max(0, parentFunction.prevArgs.length - 1 + extraDelimiters);\n        const result: languages.ProviderResult<languages.SignatureHelpResult> =\n          {\n            value: {\n              activeParameter: activeParameter,\n              activeSignature: 0,\n              signatures: [],\n            },\n            dispose: () => {\n              // empty\n            },\n          };\n        const value = result.value;\n\n        const valuesIdx = previousToken.findIndex(\n          (t) => t.textLC === \"values\" && t.type === \"keyword.sql\",\n        );\n        const tokensBeforeValues = previousToken.slice(0, valuesIdx);\n        const tokensAfterValues = previousToken.slice(valuesIdx);\n        const textAfterValuesLC = tokensAfterValues\n          .map((t) => t.textLC)\n          .join(\"\");\n        const isInsertInto =\n          ((textAfterValuesLC.includes(\"(\") &&\n            !textAfterValuesLC.includes(\")\")) ||\n            cb.currToken?.textLC.includes(\"(\")) &&\n          cb.prevTokens.some((t, i, arr) => {\n            return (\n              t.textLC === \"insert\" &&\n              arr[i + 1]?.textLC === \"into\" &&\n              arr.some((at) => at.textLC === \"values\")\n            );\n          });\n\n        if (!isInsertInto) {\n          const funcs = await getFunc(funcName, activeParameter);\n          // console.log(funcs);\n          value.signatures = funcs.map((f) => ({\n            label: `${f.name}(${f.arg_list_str})`,\n            documentation: f.description ?? undefined,\n            parameters: f.args,\n          }));\n          value.activeSignature = funcs.findIndex(\n            (f) => f.args.length >= activeParameter + 1,\n          );\n\n          return result;\n        } else {\n          const intoTkn = cb.prevTokens.find((t, i, arr) => {\n            return i && arr[i - 1]?.textLC === \"insert\" && t.textLC === \"into\";\n          });\n          const tEndToken = cb.prevTokens.find((t) => t.text === \"(\");\n          if (!intoTkn || !tEndToken) return null;\n          const tableNameTokens = cb.prevTokens.filter(\n            (t, i) => t.offset > intoTkn.offset && t.offset < tEndToken.offset,\n          );\n          /** Some tables are a series of tokens (schema_name.table_name) */\n          const tableName = tableNameTokens.map((t) => t.text).join(\"\");\n\n          const insertLine = prevLines\n            .map((l) => {\n              const v = l.replace(/\\s\\s+/g, \" \").trim();\n              if (v.toUpperCase().includes(\"INSERT INTO\")) return v;\n            })\n            .find((v) => v);\n          const tableInfo = suggestions.find(\n            (s) => s.type === \"table\" && s.escapedIdentifier === tableName,\n          );\n          let columnList =\n            insertLine\n              ?.replace(\"VALUES(\", \"\")\n              .split(\"(\")[1]\n              ?.split(\")\")[0]\n              ?.split(\",\") ?? [];\n\n          if (\n            !columnList.length &&\n            !tokensBeforeValues.some((t) => t.textLC.includes(\"(\")) &&\n            tableInfo?.cols\n          ) {\n            columnList = tableInfo.cols\n              .sort((a, b) => a.ordinal_position - b.ordinal_position)\n              .map((c) => c.escaped_identifier);\n          }\n          const argList = columnList.map((v) => {\n            const col = tableInfo?.cols!.find(\n              (c) => c.escaped_identifier === v.trim(),\n            );\n            // a.label += `${(colInfo.has_default || colInfo.nullable)? \"?\" : \"\"}: ${colInfo.data_type}`;\n            return {\n              label:\n                v.trim() +\n                (!col ? \"\" : (\n                  `${col.nullable || col.has_default ? \"?\" : \"\"}: ${col.udt_name}`\n                )),\n              data_type: col?.udt_name,\n              documentation: {\n                value: asSQL(`${col?.definition ?? \"\"}`),\n              },\n            };\n          });\n\n          if (argList.length) {\n            value.signatures = [\"VALUES\"].map((f) => ({\n              label: `${f}(${argList.map((a) => a.label).join(\", \")})`,\n              documentation: {\n                value:\n                  \"**hint**: Columns that are nullable or have default values and can be filled with *NULL* or *DEFAULT*\",\n              },\n              parameters: argList,\n            }));\n            return {\n              value,\n              dispose: () => {\n                // empty\n              },\n            };\n          }\n        }\n      },\n    },\n  );\n}\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/utils/scrollToLineIfNeeded.ts",
    "content": "import type { editor } from \"monaco-editor\";\n\nexport const scrollToLineIfNeeded = (\n  editor: editor.IStandaloneCodeEditor,\n  lineNumber: number,\n) => {\n  const [vL1] = editor.getVisibleRanges();\n  if (\n    vL1 &&\n    !(\n      vL1.startLineNumber - 1 <= lineNumber &&\n      vL1.endLineNumber + 1 >= lineNumber\n    )\n  ) {\n    editor.revealLineInCenterIfOutsideViewport(lineNumber);\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/SQLEditor/utils/setMonacEditorError.ts",
    "content": "import type { editor, IRange } from \"monaco-editor\";\nimport type { MonacoError } from \"../W_SQLEditor\";\nimport type { CodeBlock } from \"../SQLCompletion/completionUtils/getCodeBlock\";\nimport { scrollToLineIfNeeded } from \"./scrollToLineIfNeeded\";\nimport type { WindowData } from \"../../Dashboard/dashboardUtils\";\nimport type { MonacoEditorImport } from \"../../CodeEditor/utils/useSetMonacoTsLibraries\";\n\nexport const setMonacEditorError = async (\n  editor: editor.IStandaloneCodeEditor,\n  monaco: MonacoEditorImport,\n  getCurrentCodeBlock: () => Promise<CodeBlock> | undefined,\n  errorMessageDisplay: WindowData[\"sql_options\"][\"errorMessageDisplay\"],\n  error: MonacoError | undefined,\n) => {\n  const model = editor.getModel();\n  if (!model) return;\n  if (!error) monaco.editor.setModelMarkers(model, \"test\", []);\n  else {\n    let offset: Partial<IRange> = {};\n    if (typeof error.position === \"number\") {\n      let pos = error.position - 1 || 0;\n      let len = error.length || 10;\n      let selectionStartIndex = 0;\n      let codeLength = model.getValue().length;\n      let selectionLength = 0;\n      const sel = editor.getSelection();\n      if (sel) {\n        const selection = editor.getModel()?.getValueInRange(sel);\n        // let selectionOffset = 0;\n        if (selection) {\n          selectionStartIndex = model.getOffsetAt({\n            column: sel.startColumn,\n            lineNumber: sel.startLineNumber,\n          });\n          selectionLength = selection.length;\n        }\n      }\n\n      if (!selectionLength) {\n        const codeBlock = await getCurrentCodeBlock();\n        if (codeBlock) {\n          selectionStartIndex = model.getOffsetAt({\n            column: 1,\n            lineNumber: codeBlock.startLine,\n          });\n          selectionLength = codeBlock.text.length;\n        }\n      }\n\n      if (selectionLength) {\n        codeLength = selectionLength;\n        pos += selectionStartIndex;\n      }\n      /** Ensure error does not extend beyond active code */\n      len = Math.max(1, Math.min(len, selectionStartIndex + codeLength - pos));\n\n      const s = model.getPositionAt(pos);\n      const e = model.getPositionAt(pos + len);\n      offset = {\n        startLineNumber: s.lineNumber,\n        startColumn: s.column,\n        endLineNumber: e.lineNumber,\n        endColumn: e.column,\n      };\n      /** Do not reposition cursor on error */\n      // this.editor.setPosition(s);\n      scrollToLineIfNeeded(editor, s.lineNumber);\n    }\n\n    if (errorMessageDisplay !== \"bottom\") {\n      const messageContribution = editor.getContribution(\n        \"editor.contrib.messageController\",\n      );\n      const pos = editor.getPosition();\n      (messageContribution as any).showMessage(error.message, {\n        lineNumber: offset.startLineNumber ?? pos?.lineNumber,\n        column: offset.endColumn ?? pos?.column,\n      });\n    }\n\n    monaco.editor.setModelMarkers(model, \"test\", [\n      {\n        startLineNumber: 0,\n        startColumn: 0,\n        endLineNumber: 0,\n        endColumn: 5,\n        code: \"error.code\",\n        ...error,\n        ...offset,\n      },\n    ]);\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/SampleSchemas.tsx",
    "content": "import React from \"react\";\nimport { Select } from \"@components/Select/Select\";\nimport type { DBSMethods } from \"./Dashboard/DBS\";\nimport { FlexCol } from \"@components/Flex\";\nimport { W_SQLEditor } from \"./SQLEditor/W_SQLEditor\";\nimport CodeExample from \"./CodeExample\";\nimport type { SampleSchema } from \"@common/utils\";\nimport { usePromise } from \"prostgles-client\";\n\ntype P = {\n  name?: string;\n  title?: string;\n  dbsMethods: Pick<DBSMethods, \"getSampleSchemas\">;\n  onChange: (schema: SampleSchema) => void;\n};\nexport const SampleSchemas = ({ dbsMethods, onChange, name, title }: P) => {\n  const sampleSchemas = usePromise(dbsMethods.getSampleSchemas!);\n  const schema = sampleSchemas?.find((s) => s.name === name);\n  return (\n    <FlexCol>\n      <Select\n        label={title ?? \"Create demo schema (optional)\"}\n        value={name}\n        data-command=\"ConnectionServer.SampleSchemas\"\n        fullOptions={sampleSchemas?.map((s) => ({ key: s.name })) ?? []}\n        onChange={(name) =>\n          onChange(sampleSchemas!.find((s) => s.name === name)!)\n        }\n      />\n      {!schema ?\n        null\n      : schema.type === \"sql\" ?\n        <W_SQLEditor\n          value={schema.file}\n          sqlOptions={{ lineNumbers: \"off\" }}\n          style={{\n            minHeight: \"200px\",\n            minWidth: \"600px\",\n          }}\n          className=\"rounded b b-color \"\n          onChange={() => {}}\n        />\n      : <CodeExample\n          language=\"typescript\"\n          value={schema.tableConfigTs + \"\\n\\n\" + schema.onMountTs}\n        />\n      }\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/ERDSchema/ERDSchema.tsx",
    "content": "import React, { useCallback, useRef } from \"react\";\nimport { FlexCol } from \"@components/Flex\";\nimport type { SchemaGraphProps } from \"../SchemaGraph\";\nimport type { useSchemaGraphControls } from \"../SchemaGraphControls\";\nimport { useCanvasPanZoom } from \"./useCanvasPanZoom\";\nimport { useDrawSchemaShapes } from \"./useDrawSchemaShapes\";\nimport { useSetPanShapes } from \"./usePanShapes\";\nimport { useSchemaShapes, type SchemaShape } from \"./useSchemaShapes\";\n\nexport type ColumnDisplayMode = \"none\" | \"all\" | \"references\";\nexport type ColumnColorMode = \"default\" | \"root\" | \"on-update\" | \"on-delete\";\nexport type ERDSchemaProps = Omit<\n  SchemaGraphProps,\n  \"theme\" | \"db_schema_filter\"\n> &\n  Pick<\n    ReturnType<typeof useSchemaGraphControls>,\n    \"displayMode\" | \"columnDisplayMode\" | \"columnColorMode\"\n  >;\nexport const ERDSchema = ({\n  tables,\n  db,\n  dbs,\n  connectionId,\n  displayMode,\n  columnDisplayMode,\n  columnColorMode,\n}: ERDSchemaProps & Pick<SchemaGraphProps, \"db_schema_filter\">) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const divRef = useRef<HTMLDivElement>(null);\n\n  const { shapesRef, dbConfId, shapesVersion, canAutoPosition, dbConf } =\n    useSchemaShapes({\n      tables,\n      db,\n      dbs,\n      connectionId,\n      canvasRef,\n      displayMode,\n      columnDisplayMode,\n      columnColorMode,\n    });\n\n  const { onRenderShapes, positionRef, scaleRef, setScaleAndPosition } =\n    useDrawSchemaShapes({\n      shapesRef,\n      canvasRef,\n      shapesVersion,\n      columnColorMode,\n      canAutoPosition,\n      dbConf,\n    });\n\n  const { handleWheel } = useCanvasPanZoom({\n    canvasRef,\n    onRenderShapes,\n    positionRef,\n    scaleRef,\n    setScaleAndPosition,\n  });\n\n  const onPanEnded = useCallback(() => {\n    const newPositions = shapesRef.current\n      .filter(\n        (s): s is Extract<SchemaShape, { type: \"rectangle\" }> =>\n          s.type === \"rectangle\",\n      )\n      .reduce(\n        (acc, { coords: [x, y], data }) => ({\n          ...acc,\n          [data.name]: {\n            x,\n            y,\n          },\n        }),\n        {} as Record<string, { x: number; y: number }>,\n      );\n    if (!dbConfId || displayMode !== \"all\") return;\n    void dbs.database_configs.update(\n      {\n        id: dbConfId,\n      },\n      {\n        table_schema_positions: newPositions,\n        table_schema_transform: {\n          scale: scaleRef.current,\n          translate: positionRef.current,\n        },\n      },\n    );\n  }, [\n    dbConfId,\n    dbs.database_configs,\n    displayMode,\n    positionRef,\n    scaleRef,\n    shapesRef,\n  ]);\n\n  useSetPanShapes({\n    setScaleAndPosition,\n    positionRef,\n    scaleRef,\n    canvas: canvasRef.current,\n    //@ts-ignore\n    shapesRef,\n    node: divRef.current,\n    onRenderShapes,\n    onPanEnded,\n  });\n  return (\n    <FlexCol\n      ref={divRef}\n      className=\"f-1 bg-color-1\"\n      style={{ overflow: \"hidden\" }}\n    >\n      <canvas onWheel={handleWheel} className=\"f-1\" ref={canvasRef} />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/ERDSchema/getInitialPlacement.ts",
    "content": "import {\n  forceCenter,\n  forceCollide,\n  forceLink,\n  forceManyBody,\n  forceSimulation,\n} from \"d3\";\nimport type { SchemaShape } from \"./useSchemaShapes\";\n\ntype Shape = SchemaShape;\n\nexport const getInitialPlacement = (shapes: Shape[]): Shape[] => {\n  // Separate rectangles and links\n  const rectangles = shapes.filter((s) => s.type === \"rectangle\");\n  const links = shapes.filter((s) => s.type === \"linkline\");\n\n  const nodes = rectangles.map((rect) => ({\n    id: rect.id,\n    x: rect.coords[0] - rect.w / 2,\n    y: rect.coords[1] - rect.h / 2,\n    width: rect.w,\n    height: rect.h,\n    fx: null, // Allow movement\n    fy: null,\n  }));\n\n  const d3Links = links.map((link) => ({\n    source: link.sourceId,\n    target: link.targetId,\n  }));\n\n  // Create collision force that prevents rectangle overlap\n  const collisionForce = forceCollide()\n    .radius((d: any) => Math.max(d.width, d.height) / 2 + 20) // Add padding\n    .strength(1)\n    .iterations(20);\n\n  // Create custom rectangle collision force for more accurate collision detection\n  const rectangleCollision = (alpha: number) => {\n    const padding = 20;\n    const nodes2 = nodes;\n\n    for (let i = 0; i < nodes2.length; i++) {\n      for (let j = i + 1; j < nodes2.length; j++) {\n        const node1 = nodes2[i];\n        const node2 = nodes2[j];\n\n        if (!node1 || !node2) continue;\n        const dx = node2.x - node1.x;\n        const dy = node2.y - node1.y;\n\n        const minDistX = (node1.width + node2.width) / 2 + padding;\n        const minDistY = (node1.height + node2.height) / 2 + padding;\n\n        if (Math.abs(dx) < minDistX && Math.abs(dy) < minDistY) {\n          // Calculate overlap\n          const overlapX = minDistX - Math.abs(dx);\n          const overlapY = minDistY - Math.abs(dy);\n\n          // Separate along the axis with less overlap\n          if (overlapX < overlapY) {\n            const moveX = (overlapX / 2) * Math.sign(dx) * alpha;\n            node1.x -= moveX;\n            node2.x += moveX;\n          } else {\n            const moveY = (overlapY / 2) * Math.sign(dy) * alpha;\n            node1.y -= moveY;\n            node2.y += moveY;\n          }\n        }\n      }\n    }\n  };\n\n  // Create the simulation\n  const simulation = forceSimulation(nodes)\n    .force(\n      \"link\",\n      forceLink(d3Links)\n        .id((d: any) => d.id)\n        .strength(0.5),\n    )\n    .force(\n      \"charge\",\n      forceManyBody()\n        .strength(-300) // Repulsion between all nodes\n        .distanceMax(500),\n    )\n    .force(\"collision\", collisionForce)\n    .force(\"rectangleCollision\", rectangleCollision)\n    .force(\"center\", forceCenter(0, 0)) // Center the graph\n    .force(\"boundingBox\", () => {\n      // Keep nodes within reasonable bounds\n      nodes.forEach((node) => {\n        node.x = Math.max(-1000, Math.min(1000, node.x));\n        node.y = Math.max(-1000, Math.min(1000, node.y));\n      });\n    });\n\n  simulation.stop();\n  for (let i = 0; i < 300; i++) {\n    simulation.tick();\n  }\n\n  // Update rectangle positions\n  const updatedRectangles = rectangles.map((rect) => {\n    const node = nodes.find((n) => n.id === rect.id)!;\n    return {\n      ...rect,\n      coords: [Math.round(node.x), Math.round(node.y)] as [number, number],\n    };\n  });\n\n  return [...updatedRectangles, ...links];\n};\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/ERDSchema/useCanvasPanZoom.ts",
    "content": "import { useCallback } from \"react\";\nimport {\n  clamp,\n  maxScale,\n  minScale,\n  type useDrawSchemaShapes,\n} from \"./useDrawSchemaShapes\";\n\nexport const useCanvasPanZoom = (\n  props: ReturnType<typeof useDrawSchemaShapes> & {\n    canvasRef: React.RefObject<HTMLCanvasElement>;\n  },\n) => {\n  const { canvasRef, positionRef, scaleRef, setScaleAndPosition } = props;\n  const handleZoom = useCallback(\n    (newScale, mouseX, mouseY) => {\n      // Clamp the scale value between min and max\n      const clampedScale = clamp(newScale, minScale, maxScale);\n\n      if (mouseX !== undefined && mouseY !== undefined) {\n        const position = positionRef.current;\n        // Zoom toward mouse position\n        const factor = clampedScale / scaleRef.current;\n        const newX = mouseX - factor * (mouseX - position.x);\n        const newY = mouseY - factor * (mouseY - position.y);\n\n        setScaleAndPosition({\n          scale: clampedScale,\n          position: { x: newX, y: newY },\n        });\n      } else {\n        // Just zoom toward center if no mouse position provided\n        setScaleAndPosition({ scale: clampedScale });\n      }\n    },\n    [positionRef, scaleRef, setScaleAndPosition],\n  );\n  const handleWheel = useCallback(\n    (e) => {\n      if (!canvasRef.current) return;\n      const rect = canvasRef.current.getBoundingClientRect();\n      const mouseX = e.clientX - rect.left;\n      const mouseY = e.clientY - rect.top;\n\n      const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;\n      const newScale = scaleRef.current * zoomFactor;\n\n      handleZoom(newScale, mouseX, mouseY);\n    },\n    [canvasRef, handleZoom, scaleRef],\n  );\n\n  return { handleWheel };\n};\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/ERDSchema/useDrawSchemaShapes.ts",
    "content": "import type { Deck, MapView, OrthographicView } from \"deck.gl\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { createHiPPICanvas } from \"src/dashboard/Charts/createHiPPICanvas\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { LinkLine, Rectangle } from \"../../Charts/CanvasChart\";\nimport { drawShapes, type ShapeV2 } from \"../../Charts/drawShapes/drawShapes\";\nimport { getCssVariableValue } from \"../../Charts/TimeChart/onRenderTimechart\";\nimport type { ColumnColorMode } from \"./ERDSchema\";\nimport { getInitialPlacement } from \"./getInitialPlacement\";\nimport type { SchemaShape, useSchemaShapes } from \"./useSchemaShapes\";\n\nexport const minScale = 0.1;\nexport const maxScale = 5;\nconst minPan = -2000;\nconst maxPan = 2000;\n\nexport const clamp = (value: number, min: number, max: number) => {\n  return Math.min(Math.max(value, min), max);\n};\n\nexport const useDrawSchemaShapes = (\n  props: Pick<\n    ReturnType<typeof useSchemaShapes>,\n    \"shapesRef\" | \"shapesVersion\" | \"canAutoPosition\" | \"dbConf\"\n  > & {\n    canvasRef: React.RefObject<HTMLCanvasElement>;\n    columnColorMode: ColumnColorMode;\n  },\n) => {\n  const {\n    shapesRef,\n    canvasRef,\n    shapesVersion,\n    columnColorMode,\n    canAutoPosition,\n    dbConf,\n  } = props;\n  const positionRef = useRef({ x: 0, y: 0 });\n  const scaleRef = useRef(1);\n\n  const animationRef = useRef<{\n    progress: number;\n    startedAt: number;\n    duration: number;\n    startPos?: [number, number];\n  }>({ progress: 0, startedAt: 0, duration: 400 });\n\n  const getShapes = useCallback(() => {\n    if (animationRef.current.progress < 1 && shapesRef.current.length) {\n      animationRef.current.startPos ??= shapesRef.current.find(\n        (s) => s.type === \"rectangle\",\n      )?.coords ?? [0, 0];\n      const res = shapesRef.current.slice(0).map((s) =>\n        s.type === \"rectangle\" ?\n          {\n            ...s,\n            coords: interpolatePosition(\n              animationRef.current.startPos!,\n              s.coords,\n              animationRef.current.progress,\n            ),\n          }\n        : s,\n      );\n      animationRef.current.startedAt =\n        animationRef.current.startedAt || Date.now();\n      animationRef.current.progress = Math.min(\n        1,\n        (Date.now() - animationRef.current.startedAt) /\n          animationRef.current.duration,\n      );\n      return res;\n    }\n    return shapesRef.current;\n  }, [shapesRef]);\n\n  const onRenderShapes = useCallback(\n    (hoveredRectangle?: Rectangle) => {\n      const render = () => {\n        if (!canvasRef.current) {\n          return;\n        }\n        const ctx = canvasRef.current.getContext(\"2d\");\n        if (!ctx) {\n          return;\n        }\n        const shapes = getShapes();\n        const { width: w, height: h } =\n          canvasRef.current.parentElement!.getBoundingClientRect();\n        ctx.canvas.width = w;\n        ctx.canvas.height = h;\n        const canvas = canvasRef.current;\n        createHiPPICanvas(canvas, w, h);\n\n        let drawnShapes = shapes.slice(0);\n        if (hoveredRectangle) {\n          const relatedShapes: SchemaShape[] = [];\n          const otherShapes: SchemaShape[] = [];\n          const hoveredRectangleLinks = shapes.filter(\n            (s): s is LinkLine =>\n              s.type === \"linkline\" &&\n              [s.sourceId, s.targetId].includes(hoveredRectangle.id),\n          );\n          shapes.forEach((s) => {\n            if (\n              /** Needed to ensure rectangles without links are matched */\n              s.id === hoveredRectangle.id ||\n              (s.type === \"rectangle\" &&\n                hoveredRectangleLinks.some((l) =>\n                  [l.sourceId, l.targetId].includes(s.id),\n                )) ||\n              /** Is a link starting or ending from the hovered rectangle */\n              (s.type === \"linkline\" &&\n                [s.sourceId, s.targetId].includes(hoveredRectangle.id))\n            ) {\n              relatedShapes.push({\n                ...s,\n                strokeStyle:\n                  columnColorMode !== \"default\" ?\n                    s.strokeStyle\n                  : getCssVariableValue(\"--active\"),\n              });\n            } else {\n              otherShapes.push({\n                ...s,\n                opacity: 0.4,\n              });\n            }\n          });\n          drawnShapes = [...otherShapes, ...relatedShapes];\n        }\n        drawShapes(drawnShapes as ShapeV2[], canvas, {\n          scale: scaleRef.current,\n          translate: positionRef.current,\n        });\n        const _drawn = {\n          shapes: drawnShapes as ShapeV2[],\n          scale: scaleRef.current,\n          translate: positionRef.current,\n        };\n        canvas._drawn = _drawn;\n        if (animationRef.current.progress < 1) {\n          requestAnimationFrame(() => onRenderShapes());\n        }\n      };\n      requestAnimationFrame(render);\n    },\n    [canvasRef, getShapes, columnColorMode],\n  );\n\n  const prevdbConf = useRef(dbConf);\n  useEffect(() => {\n    if (dbConf && !prevdbConf.current) {\n      const { table_schema_transform } = dbConf;\n      if (table_schema_transform) {\n        positionRef.current = table_schema_transform.translate;\n        scaleRef.current = table_schema_transform.scale;\n        onRenderShapes();\n      }\n    }\n  }, [dbConf, onRenderShapes]);\n\n  useEffect(() => {\n    if (canAutoPosition) {\n      shapesRef.current = getInitialPlacement(shapesRef.current.slice(0));\n      /** Move links to end */\n      shapesRef.current.sort((a, b) => a.type.localeCompare(b.type));\n    }\n    onRenderShapes();\n  }, [shapesRef, onRenderShapes, shapesVersion, canAutoPosition]);\n\n  const setScaleAndPosition = useCallback(\n    ({\n      scale,\n      position,\n    }: Partial<{\n      scale: number;\n      position: { x: number; y: number };\n    }>) => {\n      if (isDefined(scale)) {\n        scaleRef.current = clamp(scale, minScale, maxScale);\n      }\n      if (isDefined(position)) {\n        positionRef.current = {\n          x: clamp(position.x, minPan, maxPan),\n          y: clamp(position.y, minPan, maxPan),\n        };\n      }\n      onRenderShapes();\n    },\n    [onRenderShapes, positionRef, scaleRef],\n  );\n\n  return { onRenderShapes, positionRef, scaleRef, setScaleAndPosition };\n};\n\nconst interpolatePosition = (\n  [startX, startY]: [number, number],\n  [endX, endY]: [number, number],\n  progress: number,\n) =>\n  [\n    startX + (endX - startX) * progress,\n    startY + (endY - startY) * progress,\n  ] satisfies [number, number];\n\ndeclare global {\n  interface HTMLCanvasElement {\n    _drawn?: {\n      shapes: ShapeV2[];\n      scale: number;\n      translate: { x: number; y: number };\n    };\n    _deckgl?: Deck<OrthographicView[] | MapView[]>;\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/ERDSchema/useFetchSchemaForDiagram.ts",
    "content": "import { usePromise } from \"prostgles-client\";\nimport { isEmpty } from \"prostgles-types\";\nimport { fetchNamedSVG } from \"@components/SvgIcon\";\nimport { getCssVariableValue } from \"../../Charts/TimeChart/onRenderTimechart\";\nimport { PG_OBJECT_QUERIES } from \"../../SQLEditor/SQLCompletion/getPGObjects\";\nimport { COLOR_PALETTE } from \"../../W_Table/ColumnMenu/ColorPicker\";\nimport type { ERDSchemaProps } from \"./ERDSchema\";\n\nexport const useFetchSchemaForDiagram = (\n  props: ERDSchemaProps & {\n    canvasRef: React.RefObject<HTMLCanvasElement>;\n  },\n) => {\n  const {\n    db,\n    dbs,\n    connectionId,\n    tables: dbTables,\n    columnColorMode,\n    columnDisplayMode,\n    displayMode,\n  } = props;\n  const { data: dbConf } = dbs.database_configs.useFindOne(\n    {\n      $existsJoined: {\n        connections: {\n          id: connectionId,\n        },\n      },\n    },\n    {},\n    { deps: [columnDisplayMode, displayMode, columnColorMode] },\n  );\n\n  const schemaInfo = usePromise(async () => {\n    if (!dbConf) return;\n    const { sql } = db;\n    const constraints =\n      !sql ?\n        []\n      : ((await sql(\n          PG_OBJECT_QUERIES.constraints.sql,\n          {},\n          { returnType: \"rows\" },\n        )) as (typeof PG_OBJECT_QUERIES.constraints.type)[]);\n    const tables = dbTables;\n    const defaultIconColor = getCssVariableValue(\"--text-2\");\n    const columnConstraintIcons = {\n      pkey: await fetchSVGImage(\"Key\", defaultIconColor),\n      fkey: await fetchSVGImage(\"KeyLink\", defaultIconColor),\n      unqiue: await fetchSVGImage(\"AlphaU\", defaultIconColor),\n      nullable: await fetchSVGImage(\"AlphaN\", defaultIconColor),\n    };\n\n    const fkeys = constraints.filter((c) => c.contype === \"f\");\n    const getRefs = (\n      tableName: string,\n      relType: \"references\" | \"referencedBy\",\n    ) => {\n      return fkeys\n        .filter(\n          ({ table_name, ftable_name }) =>\n            (relType === \"referencedBy\" ? ftable_name : table_name) ===\n            tableName,\n        )\n        .map((c) =>\n          relType === \"referencedBy\" ? c.table_name : c.ftable_name!,\n        );\n    };\n\n    const allTableMostReferencedTop = tables\n      .map((t) => ({\n        ...t,\n        references: getRefs(t.name, \"references\"),\n        referencedBy: getRefs(t.name, \"referencedBy\"),\n      }))\n      .map((t, i, tablesWithRefs) => ({\n        ...t,\n        referenceType:\n          t.references.length ? (\"linked\" as const)\n          : t.referencedBy.length ? (\"root\" as const)\n          : (\"orphan\" as const),\n        nextReferencedBy: tablesWithRefs\n          .filter((lt) => t.referencedBy.includes(lt.name))\n          .flatMap((lt) => lt.referencedBy),\n      }))\n      .sort((a, b) => {\n        const mostReferenced = b.referencedBy.length - a.referencedBy.length;\n        return mostReferenced;\n      });\n\n    const colors = COLOR_PALETTE.slice(0);\n    const allTablesWithRootColor = allTableMostReferencedTop.map((t) => ({\n      ...t,\n      /** Root color assigned to top most referenced tables  */\n      rootColor: colors.shift(),\n    }));\n\n    const allTables = await Promise.all(\n      allTablesWithRootColor.map(async (t) => {\n        return {\n          ...t,\n          iconImage:\n            !t.icon ? undefined : (\n              await fetchSVGImage(\n                t.icon,\n                columnColorMode === \"root\" ?\n                  (t.rootColor ?? defaultIconColor)\n                : defaultIconColor,\n              )\n            ),\n        };\n      }),\n    );\n\n    const topRootTables = allTables\n      .filter((t) => t.referenceType === \"root\")\n      .sort((a, b) => {\n        const mostReferenced =\n          b.nextReferencedBy.length - a.nextReferencedBy.length;\n        return mostReferenced;\n      });\n    const topLinkedTables = allTables\n      .filter((t) => t.referenceType === \"linked\")\n      .sort((a, b) => {\n        const mostReferenced =\n          b.nextReferencedBy.length +\n          b.referencedBy.length -\n          (a.nextReferencedBy.length + a.referencedBy.length);\n        return mostReferenced;\n      });\n    const getNextTables = (prevColTableName: string) => {\n      const result = topLinkedTables.slice(0, 0);\n      const indexesToRemove: number[] = [];\n      topLinkedTables.forEach((t, i) => {\n        if (!t.references.includes(prevColTableName)) return;\n        // if (result.length >= limit) return;\n        result.push(t);\n        indexesToRemove.push(i);\n      });\n      indexesToRemove.reverse().forEach((i) => {\n        topLinkedTables.splice(i, 1);\n      });\n      return result;\n    };\n    const schemaColumns = [topRootTables];\n    while (topLinkedTables.length) {\n      const prevColTables = schemaColumns.at(-1);\n      if (!prevColTables) break;\n      const nextTables = prevColTables.flatMap((t) => getNextTables(t.name));\n      schemaColumns.push(nextTables);\n    }\n\n    schemaColumns.push(allTables.filter((t) => t.referenceType === \"orphan\"));\n\n    const INITIAL_CHART_MAX_HEIGHT = 3000;\n    let tablePositions = dbConf.table_schema_positions ?? {};\n\n    if (isEmpty(tablePositions)) {\n      let x = 0;\n      let y = 0;\n      /**\n       * Place in columns. If the column is too tall, move to next column.\n       */\n      schemaColumns.forEach((colTables) => {\n        y = 0;\n        const colTablePositions = colTables.reduce((acc, t) => {\n          y += 300;\n          if (y > INITIAL_CHART_MAX_HEIGHT) {\n            y = 0;\n            x += 300;\n          }\n          return {\n            ...acc,\n            [t.name]: { x, y },\n          };\n        }, {});\n        x += 300;\n        tablePositions = {\n          ...tablePositions,\n          ...colTablePositions,\n        };\n      });\n    }\n\n    const tablesWithPositions = allTables.map((t) => ({\n      ...t,\n      position: tablePositions[t.name],\n    }));\n\n    return {\n      tablesWithPositions,\n      fkeys,\n      columnConstraintIcons,\n    };\n  }, [columnColorMode, db, dbConf, dbTables]);\n\n  return { schemaInfo, dbConfId: dbConf?.id, dbConf };\n};\n\nconst fetchSVGImage = (iconName: string, currentcolor: string) => {\n  return fetchNamedSVG(iconName).then((rawSvgString) => {\n    if (!rawSvgString) return;\n    const svgString = rawSvgString.replaceAll(\"currentcolor\", currentcolor);\n    return new Promise<HTMLImageElement>((resolve) => {\n      // Create a data URL\n      const img = new Image();\n      img.src =\n        \"data:image/svg+xml;charset=utf-8,\" + encodeURIComponent(svgString);\n\n      img.onload = function () {\n        resolve(img);\n      };\n    });\n  });\n};\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/ERDSchema/usePanShapes.ts",
    "content": "import { useCallback, useEffect } from \"react\";\nimport { quickClone } from \"../../../utils/utils\";\nimport type { Rectangle } from \"../../Charts/CanvasChart\";\nimport type { ShapeV2 } from \"../../Charts/drawShapes/drawShapes\";\nimport { setPan, type PanEvent } from \"../../setPan\";\nimport type { useDrawSchemaShapes } from \"./useDrawSchemaShapes\";\n\nexport const useSetPanShapes = (\n  props: {\n    node: HTMLDivElement | null;\n    canvas: HTMLCanvasElement | null;\n    shapesRef: React.MutableRefObject<ShapeV2[]>;\n    onPanEnded: () => void;\n  } & ReturnType<typeof useDrawSchemaShapes>,\n) => {\n  const {\n    node,\n    canvas,\n    shapesRef,\n    onPanEnded,\n    onRenderShapes,\n    scaleRef,\n    positionRef,\n    setScaleAndPosition,\n  } = props;\n\n  const screenToWorld = useCallback(\n    ([screenX, screenY]: [number, number]) => {\n      const position = positionRef.current;\n      const scale = scaleRef.current;\n      const translatedX = (screenX - position.x) / scale;\n      const translatedY = (screenY - position.y) / scale;\n      return [translatedX, translatedY] satisfies [number, number];\n    },\n    [scaleRef, positionRef],\n  );\n\n  useEffect(() => {\n    if (!node || !canvas) return;\n    let panShapeInitialCoords: Rectangle[\"coords\"] | undefined;\n    let panShape: Rectangle | undefined;\n\n    let startPanCoords: { screenX: number; screenY: number } | undefined;\n    let startPanPosition: { x: number; y: number } | undefined;\n    const setPanShapeAndDraw = (newPanShape: Rectangle | undefined) => {\n      if (newPanShape?.id === panShape?.id) {\n        return;\n      }\n      panShape = newPanShape;\n      panShapeInitialCoords = panShape && quickClone(panShape.coords);\n      if (panShape) {\n        node.style.cursor = \"grabbing\";\n      } else {\n        node.style.cursor = \"\";\n      }\n      onRenderShapes(panShape);\n    };\n    const findAndsetPanShape = (ev: Pick<PanEvent, \"xNode\" | \"yNode\">) => {\n      const [xStart, yStart] = screenToWorld([ev.xNode, ev.yNode]);\n\n      const shapes = shapesRef.current;\n      const currPanShape = shapes.findLast((s): s is Rectangle => {\n        if (s.type !== \"rectangle\") return false;\n        const {\n          coords: [x, y],\n          w,\n          h,\n        } = s;\n        return pointInRect([xStart, yStart], { x, y, w, h });\n      });\n      setPanShapeAndDraw(currPanShape);\n    };\n\n    return setPan(node, {\n      onPointerMove: (ev) => {\n        findAndsetPanShape(ev);\n      },\n      onPanStart: (ev) => {\n        startPanCoords = { screenX: ev.xNode, screenY: ev.yNode };\n        startPanPosition = { ...positionRef.current };\n\n        const shapes = shapesRef.current;\n        findAndsetPanShape(ev);\n        if (panShape) {\n          moveToEnd(shapes, shapes.indexOf(panShape));\n        }\n      },\n      onPan: (ev) => {\n        if (!startPanCoords || !startPanPosition) {\n          setPanShapeAndDraw(undefined);\n          return;\n        }\n\n        const currentScreenX = ev.xNode;\n        const currentScreenY = ev.yNode;\n\n        // Raw screen displacement since pan start\n        const deltaScreenX = currentScreenX - startPanCoords.screenX;\n        const deltaScreenY = currentScreenY - startPanCoords.screenY;\n\n        /** Move whole canvas */\n        if (!panShape || !panShapeInitialCoords) {\n          setScaleAndPosition({\n            position: {\n              x: startPanPosition.x + deltaScreenX,\n              y: startPanPosition.y + deltaScreenY,\n            },\n          });\n          return;\n        }\n\n        /** Move shape */\n        const scale = scaleRef.current;\n        // World displacement = screen displacement / scale\n        const deltaWorldX = deltaScreenX / scale;\n        const deltaWorldY = deltaScreenY / scale;\n\n        panShape.coords = [\n          panShapeInitialCoords[0] + deltaWorldX,\n          panShapeInitialCoords[1] + deltaWorldY,\n        ];\n        onRenderShapes(panShape);\n      },\n      onPanEnd: () => {\n        setPanShapeAndDraw(undefined);\n        onPanEnded();\n      },\n      threshold: 1,\n    });\n  }, [\n    canvas,\n    node,\n    onPanEnded,\n    shapesRef,\n    onRenderShapes,\n    screenToWorld,\n    positionRef,\n    scaleRef,\n    setScaleAndPosition,\n  ]);\n};\n\nconst pointInRect = (\n  [x, y]: [number, number],\n  rect: { x: number; y: number; w: number; h: number },\n) => {\n  return (\n    x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h\n  );\n};\n\nconst moveToEnd = (array: any[], index: number) => {\n  if (index < 0 || index >= array.length) return array;\n\n  const element = array.splice(index, 1)[0];\n\n  array.push(element);\n\n  return array;\n};\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/ERDSchema/useSchemaShapes.ts",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type {\n  ChartedText,\n  Image,\n  LinkLine,\n  Rectangle,\n} from \"../../Charts/CanvasChart\";\nimport { measureText } from \"../../Charts/TimeChart/measureText\";\nimport { getCssVariableValue } from \"../../Charts/TimeChart/onRenderTimechart\";\nimport type {\n  DBSchemaTableColumn,\n  DBSchemaTableWJoins,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { ERDSchemaProps } from \"./ERDSchema\";\nimport { useFetchSchemaForDiagram } from \"./useFetchSchemaForDiagram\";\nimport { CASCADE_LEGEND } from \"../SchemaGraphControls\";\n\nexport type SchemaShape =\n  | Rectangle<DBSchemaTableWJoins, { width: number } | undefined>\n  | LinkLine;\n\nexport const useSchemaShapes = (\n  props: ERDSchemaProps & {\n    canvasRef: React.RefObject<HTMLCanvasElement>;\n  },\n) => {\n  const { canvasRef, displayMode, columnDisplayMode, columnColorMode } = props;\n  const shapesRef = useRef<SchemaShape[]>([]);\n  const [shapesVersion, setShapesVersion] = useState(0);\n  const { schemaInfo, dbConfId, dbConf } = useFetchSchemaForDiagram(props);\n\n  useEffect(() => {\n    if (!schemaInfo || !dbConf) return;\n    const { fkeys, tablesWithPositions } = schemaInfo;\n    const ctx = canvasRef.current?.getContext(\"2d\");\n    if (!ctx) {\n      return;\n    }\n    const COL_SPACING = 30;\n    const ICON_SIZE = 30;\n    const COL_ICON_SIZE = 20;\n    const PADDING = 10;\n    const nodeShapes = tablesWithPositions\n      .map((table, i) => {\n        const offset = 50 * i;\n        const x = table.position?.x ?? offset;\n        const y = table.position?.y ?? offset;\n\n        const getMeasuredChartedText = (\n          node: ChartedText,\n        ): ChartedText<{ width: number }> => ({\n          ...node,\n          data: { width: measureText(node, ctx, false).width },\n        });\n\n        const icon: Image | undefined = table.iconImage && {\n          id: `${table.name}-icon`,\n          type: \"image\",\n          coords: [PADDING / 2, PADDING / 2],\n          w: ICON_SIZE,\n          h: ICON_SIZE,\n          image: table.iconImage,\n        };\n        const headerfillStyle = getCssVariableValue(\"--text-0\");\n        const header = getMeasuredChartedText({\n          id: table.name + \"-header\",\n          type: \"text\",\n          text: table.label,\n          coords: [\n            !icon ? PADDING : icon.coords[0] + icon.w + PADDING / 2,\n            COL_SPACING,\n          ],\n          font: \"bold 22px Courier\",\n          fillStyle:\n            columnColorMode === \"root\" ?\n              (table.rootColor ?? headerfillStyle)\n            : headerfillStyle,\n        });\n\n        const columns = table.columns.filter((c) =>\n          columnDisplayMode === \"none\" ? false\n          : columnDisplayMode === \"references\" ?\n            fkeys.some(\n              (fcons) =>\n                fcons.conkey?.includes(c.ordinal_position) ||\n                fcons.confkey?.includes(c.ordinal_position),\n            )\n          : true,\n        );\n        let hasColIcon = false;\n        const colsAndDataTypes = columns.map((c, i) => {\n          const colYOffset = COL_SPACING * (i + 2);\n\n          let textFillStyle = getCssVariableValue(\"--text-0\");\n          if (columnColorMode === \"root\" && c.references?.length) {\n            textFillStyle =\n              getRootTable(c.references, tablesWithPositions)?.rootColor ??\n              textFillStyle;\n          }\n          const colName = getMeasuredChartedText({\n            id: `${table.name}-${c.name}-col`,\n            type: \"text\",\n            coords: [PADDING, colYOffset],\n            text: c.name,\n            fillStyle: textFillStyle,\n            font: \"18px sans-serif\",\n          });\n\n          const colDatatype = getMeasuredChartedText({\n            id: `${table.name}-${c.name}-col-datatype`,\n            type: \"text\",\n            coords: [PADDING * 2 + colName.data.width, colYOffset],\n            text: c.udt_name,\n            fillStyle: getCssVariableValue(\"--text-2\"),\n            font: \"16px sans-serif\",\n          });\n\n          const { fkey, nullable, pkey, unqiue } =\n            schemaInfo.columnConstraintIcons;\n          const colIconImage =\n            c.is_pkey ? pkey\n            : c.references?.length ? fkey\n            : c.is_nullable ? nullable\n            : (\n              table.info.uniqueColumnGroups?.some(\n                (ug) => ug.length === 1 && ug.includes(c.name),\n              )\n            ) ?\n              unqiue\n            : undefined;\n\n          const colIcon =\n            colIconImage &&\n            ({\n              id: `${table.name}-${c.name}-col-icon`,\n              type: \"image\",\n              coords: [PADDING * 2 + colName.data.width, colYOffset],\n              image: colIconImage,\n              h: COL_ICON_SIZE,\n              w: COL_ICON_SIZE,\n            } satisfies Image);\n          hasColIcon = hasColIcon || !!colIcon;\n          return [colName, colDatatype, colIcon] as const;\n        });\n\n        const widestColContent = colsAndDataTypes.reduce(\n          (acc, [col, colDataType]) => {\n            return Math.max(\n              acc,\n              col.data.width +\n                colDataType.data.width +\n                (hasColIcon ? COL_ICON_SIZE : 0) +\n                3 * PADDING,\n            );\n          },\n          0,\n        );\n        const rectangleContentWidth = Math.max(\n          header.data.width + header.coords[0] - PADDING,\n          widestColContent,\n        );\n\n        const cols = colsAndDataTypes.flatMap(([colName]) => colName);\n        const colsDataTypes = colsAndDataTypes.flatMap(\n          ([colName, colDatatype, colIcon]) => ({\n            ...colDatatype,\n            coords: [\n              rectangleContentWidth - colDatatype.data.width - 2 * PADDING,\n              colName.coords[1],\n            ] satisfies [number, number],\n          }),\n        );\n        const colsIcons = colsAndDataTypes\n          .flatMap(\n            ([colName, colDatatype, colIcon]) =>\n              colIcon && {\n                ...colIcon,\n                coords: [\n                  rectangleContentWidth - colIcon.w + PADDING,\n                  colName.coords[1] - colIcon.h + 4,\n                ] satisfies [number, number],\n              },\n          )\n          .filter(isDefined);\n\n        const box: Rectangle<typeof table, { width: number } | undefined> = {\n          id: table.info.oid,\n          type: \"rectangle\",\n          coords: [x - PADDING, y - COL_SPACING - PADDING],\n          w: rectangleContentWidth + 2 * PADDING,\n          h: (cols.length + 1) * COL_SPACING + 2 * PADDING,\n          fillStyle: getCssVariableValue(\"--bg-color-0\"),\n          strokeStyle: getCssVariableValue(\"--b-color-0\"),\n          lineWidth: 1,\n          borderRadius: 12,\n          children: [\n            icon,\n            header,\n            ...cols,\n            ...colsDataTypes,\n            ...colsIcons,\n          ].filter(isDefined),\n          data: { ...table, columns },\n          // elevation: 10,\n        };\n\n        return box;\n      })\n      .filter(isDefined)\n      .filter((t) => {\n        if (displayMode === \"all\") return true;\n        if (displayMode === \"relations\") {\n          return t.data.references.length || t.data.referencedBy.length;\n        }\n        return !t.data.references.length && !t.data.referencedBy.length;\n      });\n\n    const linkShapes: LinkLine[] = fkeys\n      .flatMap((fkCons) => {\n        const tbl = nodeShapes.find((n) => n.id === fkCons.table_oid);\n        const ftbl = nodeShapes.find((n) => n.id === fkCons.ftable_oid);\n        if (!tbl || !ftbl) {\n          if (displayMode !== \"leaf\") {\n            console.warn(\"link not found\", fkCons);\n          }\n          return undefined;\n        }\n        return fkCons.conkey?.map((key, i) => {\n          const colIdx = tbl.data.columns.findIndex(\n            (c) => c.ordinal_position === key,\n          );\n          const fkeyPos = fkCons.confkey?.[i];\n          if (!isDefined(fkeyPos)) {\n            return;\n          }\n          const fcolIdx = ftbl.data.columns.findIndex(\n            (c) => c.ordinal_position === fkeyPos,\n          );\n\n          return {\n            id: `${tbl.id}-link-${ftbl.id}`,\n            type: \"linkline\",\n            sourceId: ftbl.id,\n            targetId: tbl.id,\n            sourceYOffset: PADDING + COL_SPACING * (1 + fcolIdx + 0.5),\n            targetYOffset: PADDING + COL_SPACING * (1 + colIdx + 0.5),\n            lineWidth: 3,\n            strokeStyle:\n              columnColorMode === \"root\" ?\n                (ftbl.data.rootColor ?? getCssVariableValue(\"--text-2\"))\n              : columnColorMode === \"on-delete\" ?\n                CASCADE_LEGEND[fkCons.on_delete_action ?? \"NO ACTION\"].color\n              : columnColorMode === \"on-update\" ?\n                CASCADE_LEGEND[fkCons.on_update_action ?? \"NO ACTION\"].color\n              : getCssVariableValue(\"--text-2\"),\n          } satisfies LinkLine;\n        });\n      })\n      .filter(isDefined);\n\n    const shapes = [...linkShapes, ...nodeShapes];\n    shapesRef.current = shapes;\n    setShapesVersion((v) => v + 1);\n  }, [\n    canvasRef,\n    schemaInfo,\n    displayMode,\n    columnDisplayMode,\n    columnColorMode,\n    dbConf,\n  ]);\n\n  return {\n    shapesRef,\n    schemaInfo,\n    shapesVersion,\n    dbConfId,\n    dbConf,\n    canAutoPosition: dbConf && !dbConf.table_schema_positions,\n  };\n};\n\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nconst getRootTable = <T extends DBSchemaTableWJoins>(\n  references: ValidatedColumnInfo[\"references\"],\n  tables: T[],\n  prevTables: string[] = [],\n): T | undefined => {\n  if (!references) return;\n  const nextReferences: ValidatedColumnInfo[\"references\"][] = [];\n  for (const ref of references) {\n    const table = tables.find((t) => t.name === ref.ftable);\n    if (!table) continue;\n    const referencedColumns = table.columns.filter(({ name }) =>\n      ref.fcols.includes(name),\n    );\n    if (referencedColumns.some(({ is_pkey }) => is_pkey)) {\n      return table;\n    }\n    referencedColumns.forEach((c) => {\n      nextReferences.push(c.references);\n    });\n    if (\n      referencedColumns.every(\n        (c) =>\n          c.is_pkey ||\n          (!c.references &&\n            table.info.uniqueColumnGroups?.some((cg) => cg.includes(c.name))),\n      )\n    ) {\n      return table;\n    }\n  }\n  for (const ref1 of nextReferences) {\n    for (const ref2 of ref1 ?? []) {\n      const result = getRootTable([ref2], tables, [...prevTables, ref2.ftable]);\n      if (result) {\n        return result;\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/SchemaGraph.tsx",
    "content": "import { mdiRelationManyToMany } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { DBSchemaTablesWJoins } from \"../Dashboard/dashboardUtils\";\nimport type { DBS } from \"../Dashboard/DBS\";\nimport { ERDSchema } from \"./ERDSchema/ERDSchema\";\nimport {\n  SchemaGraphControls,\n  useSchemaGraphControls,\n} from \"./SchemaGraphControls\";\nimport type { Connection } from \"../../pages/NewConnection/NewConnnectionForm\";\n\nexport type SchemaGraphProps = Pick<Prgl, \"connectionId\" | \"theme\"> & {\n  db: DBHandlerClient;\n  dbs: DBS;\n  tables: DBSchemaTablesWJoins;\n  db_schema_filter: Connection[\"db_schema_filter\"];\n};\n\nexport const SchemaGraph = (props: SchemaGraphProps) => {\n  const [showSchemaDiagram, setShowSchemaDiagram] = useState(false);\n  const controlState = useSchemaGraphControls();\n  return (\n    <>\n      <Btn\n        iconPath={mdiRelationManyToMany}\n        className=\"fit \"\n        title=\"Show schema diagram\"\n        data-command={showSchemaDiagram ? undefined : \"SchemaGraph\"}\n        variant=\"outline\"\n        onClick={() => {\n          setShowSchemaDiagram(true);\n        }}\n      />\n\n      {showSchemaDiagram && (\n        <Popup\n          title={<SchemaGraphControls {...props} {...controlState} />}\n          positioning=\"fullscreen\"\n          clickCatchStyle={{ opacity: 1 }}\n          contentClassName=\"o-visible relative \"\n          onClose={() => setShowSchemaDiagram(false)}\n          data-command=\"SchemaGraph\"\n        >\n          <ERDSchema\n            key={controlState.schemaKey + props.theme}\n            {...props}\n            {...controlState}\n          />\n        </Popup>\n      )}\n    </>\n  );\n};\n// }\n\nexport const getSchemaTableColY = (i, height) => {\n  return (!i ? 8 : 16) + i * 20 - height / 2;\n};\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/SchemaGraphControls.tsx",
    "content": "import { getEntries } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { Select } from \"@components/Select/Select\";\nimport React, { useState } from \"react\";\nimport { SchemaFilter } from \"../../pages/NewConnection/SchemaFilter\";\nimport { getCssVariableValue } from \"../Charts/TimeChart/onRenderTimechart\";\nimport type { CASCADE } from \"../SQLEditor/SQLCompletion/getPGObjects\";\nimport {\n  type ColumnColorMode,\n  type ColumnDisplayMode,\n} from \"./ERDSchema/ERDSchema\";\nimport type { SchemaGraphProps } from \"./SchemaGraph\";\n\nexport const SchemaGraphControls = ({\n  columnColorMode,\n  columnDisplayMode,\n  displayMode,\n  setColumnColorMode,\n  connectionId,\n  dbs,\n  db,\n  setColumnDisplayMode,\n  setDisplayMode,\n  setSchemaKey,\n  schemaKey,\n  db_schema_filter,\n}: ReturnType<typeof useSchemaGraphControls> &\n  Pick<\n    SchemaGraphProps,\n    \"dbs\" | \"db\" | \"connectionId\" | \"db_schema_filter\"\n  >) => {\n  return (\n    <FlexRow\n      className=\"w-full\"\n      key={schemaKey}\n      data-command=\"SchemaGraph.TopControls\"\n    >\n      <div className=\"f-0\">Schema diagram</div>\n      <ScrollFade\n        className=\"flex-row gap-p5 ox-auto font-16   f-1 relative s-fit no-scroll-bar\"\n        style={{ fontWeight: \"normal\" }}\n      >\n        <SchemaFilter\n          db={db}\n          db_schema_filter={db_schema_filter}\n          asSelect={{\n            btnProps: {\n              size: \"small\",\n            },\n            asRow: true,\n            className: \"ml-auto\",\n          }}\n          onChange={(newDbSchemaFilter) => {\n            dbs.connections.update(\n              {\n                id: connectionId,\n              },\n              {\n                db_schema_filter: newDbSchemaFilter,\n              },\n            );\n          }}\n        />\n        <Select\n          data-command=\"SchemaGraph.TopControls.tableRelationsFilter\"\n          value={displayMode}\n          label=\"Tables\"\n          asRow={true}\n          size=\"small\"\n          fullOptions={DISPLAY_MODES}\n          onChange={setDisplayMode}\n        />\n        <Select\n          data-command=\"SchemaGraph.TopControls.columnRelationsFilter\"\n          value={columnDisplayMode}\n          label=\"Columns\"\n          asRow={true}\n          size=\"small\"\n          fullOptions={COLUMN_FILTER}\n          onChange={setColumnDisplayMode}\n        />\n        <Select\n          data-command=\"SchemaGraph.TopControls.linkColorMode\"\n          value={columnColorMode}\n          label=\"Color mode\"\n          asRow={true}\n          size=\"small\"\n          fullOptions={COLUMN_COLOR_MODES}\n          onChange={setColumnColorMode}\n        />\n        {[\"on-delete\", \"on-update\"].includes(columnColorMode) && (\n          <FlexRowWrap>\n            {\" \"}\n            {getEntries(CASCADE_LEGEND).map(([label, { color, title }]) => (\n              <Chip key={label} style={{ color }} title={title}>\n                {label}\n              </Chip>\n            ))}\n          </FlexRowWrap>\n        )}\n\n        <Btn\n          data-command=\"SchemaGraph.TopControls.resetLayout\"\n          clickConfirmation={{\n            buttonText: \"Reset\",\n            color: \"danger\",\n            message: \"Are you sure you want to reset the layout?\",\n          }}\n          className=\"ml-auto\"\n          size=\"small\"\n          variant=\"faded\"\n          onClickPromise={async () => {\n            await dbs.database_configs.update(\n              {\n                $existsJoined: {\n                  connections: {\n                    id: connectionId,\n                  },\n                },\n              },\n              {\n                table_schema_positions: null,\n                table_schema_transform: null,\n              },\n            );\n            setSchemaKey((k) => k + 1);\n          }}\n        >\n          Reset layout\n        </Btn>\n      </ScrollFade>\n    </FlexRow>\n  );\n};\n\nexport const useSchemaGraphControls = () => {\n  const [displayMode, setDisplayMode] = useState<SchemaGraphDisplayMode>(\"all\");\n  const [columnDisplayMode, setColumnDisplayMode] =\n    useState<ColumnDisplayMode>(\"all\");\n  const [columnColorMode, setColumnColorMode] =\n    useState<ColumnColorMode>(\"root\");\n  const [schemaKey, setSchemaKey] = useState<number>(0);\n\n  return {\n    displayMode,\n    setDisplayMode,\n    columnDisplayMode,\n    setColumnDisplayMode,\n    columnColorMode,\n    setColumnColorMode,\n    schemaKey,\n    setSchemaKey,\n  };\n};\n\nexport const getSchemaTableColY = (i, height) => {\n  return (!i ? 8 : 16) + i * 20 - height / 2;\n};\n\nconst DISPLAY_MODES = [\n  { key: \"all\", label: \"all\" },\n  { key: \"relations\", label: \"linked\" },\n  { key: \"leaf\", label: \"orphaned\" },\n] as const;\n\nconst COLUMN_COLOR_MODES = [\n  { key: \"default\", subLabel: \"Fixed color for all links\" },\n  { key: \"root\", subLabel: \"Links show root table color\" },\n  { key: \"on-delete\", subLabel: \"Links show on delete action\" },\n  { key: \"on-update\", subLabel: \"Links show on update action\" },\n] as const;\n\nconst COLUMN_FILTER = [\n  { key: \"all\" },\n  { key: \"references\" },\n  { key: \"none\" },\n] as const;\n\nexport const CASCADE_LEGEND = {\n  CASCADE: {\n    color: getCssVariableValue(\"--text-danger\"),\n    title:\n      \"Automatically deletes or updates related rows in the child table when a row in the parent table is deleted or updated\",\n  },\n  RESTRICT: {\n    color: getCssVariableValue(\"--b-warning\"),\n    title:\n      \"Prevents deletion or update of a parent row if there are dependent rows in the child table\",\n  },\n  \"NO ACTION\": {\n    color: getCssVariableValue(\"--b-warning\"),\n    title:\n      \"Similar to RESTRICT, but the check is deferred until the end of the transaction (this is the default)\",\n  },\n  \"SET NULL\": {\n    color: getCssVariableValue(\"--text-1\"),\n    title:\n      \"Sets the foreign key columns to NULL when the referenced row is deleted or updated\",\n  },\n  \"SET DEFAULT\": {\n    color: getCssVariableValue(\"--color-number\"),\n    title:\n      \"Sets the foreign key columns to their default values when the referenced row is deleted or updated\",\n  },\n} as const satisfies Record<CASCADE, { color: string; title: string }>;\n\nexport type SchemaGraphDisplayMode = (typeof DISPLAY_MODES)[number][\"key\"];\n"
  },
  {
    "path": "client/src/dashboard/SchemaGraph/types.ts",
    "content": "export type SchemaGraphNode = {\n  id: string;\n  x: number;\n  y: number;\n  fx?: number | null;\n  fy?: number | null;\n  width: number;\n  height: number;\n  hasLinks: boolean;\n  header: {\n    label: string;\n    color: string;\n  };\n  text: Array<{\n    label: string;\n    color: string;\n  }>;\n};\n\nexport type SchemaGraphLink = {\n  id: string;\n  source: SchemaGraphNode;\n  target: SchemaGraphNode;\n  color: string;\n  sourceColIndex: number;\n  targetColIndex: number;\n  sourceNode: SchemaGraphNode;\n  targetNode: SchemaGraphNode;\n};\n"
  },
  {
    "path": "client/src/dashboard/SearchAll/SearchAll.tsx",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport React from \"react\";\nimport type { Prgl } from \"../../App\";\nimport type { _Dashboard } from \"../Dashboard/Dashboard\";\nimport type { WindowData } from \"../Dashboard/dashboardUtils\";\nimport type { SQLSuggestion } from \"../SQLEditor/W_SQLEditor\";\nimport { SearchAllContent } from \"./SearchAllContent\";\nimport { SearchAllHeader } from \"./SearchAllHeader\";\nimport { useSearchAllState } from \"./hooks/useSearchAllState\";\n\nexport const SEARCH_TYPES = [\n  { key: \"views and queries\", label: \"Tables/Queries\" },\n  { key: \"rows\", label: \"Data\" },\n] as const;\n\nexport type DBObject = {\n  name: string;\n  info: string;\n  type: \"table\" | \"view\" | \"function\" | \"index\" | \"type\";\n  schema: string;\n};\n\nexport type SearchAllSuggestion = Pick<\n  SQLSuggestion,\n  \"schema\" | \"subLabel\" | \"name\" | \"escapedIdentifier\" | \"definition\"\n> & {\n  type: \"table\" | \"function\" | \"view\" | \"mview\";\n  icon?: string;\n};\n\nexport type SearchAllProps = Pick<Prgl, \"db\" | \"methods\" | \"tables\"> & {\n  suggestions: SQLSuggestion[] | undefined;\n  onClose: () => void;\n  onOpen: (arg: { table: string; filter: DetailedFilter[] }) => void;\n  onOpenDBObject: (\n    suggestion: SearchAllSuggestion | undefined,\n    method_name?: string,\n  ) => void;\n  style?: object;\n  className?: string;\n  searchType?: (typeof SEARCH_TYPES)[number][\"key\"];\n  defaultTerm?: string;\n  queries?: SyncDataItem<WindowData<\"sql\">>[];\n  loadTable: _Dashboard[\"loadTable\"];\n};\n\nexport const SearchAll = (props: SearchAllProps) => {\n  const state = useSearchAllState(props);\n  const { isLowWidthScreen } = window;\n  const margin = isLowWidthScreen ? 0 : \"2em\";\n  return (\n    <Popup\n      anchorXY={{ x: 20, y: 20 }}\n      positioning=\"inside-top-center\"\n      title={<SearchAllHeader {...state} {...props} />}\n      contentClassName=\" p-1\"\n      clickCatchStyle={{ opacity: 1 }}\n      contentStyle={{ overflow: \"unset\", paddingTop: \"1em\" }}\n      data-command=\"SearchAll.Popup\"\n      rootStyle={{\n        minHeight: \"50vh\",\n        top: margin,\n        right: margin,\n        left: margin,\n        maxHeight: `calc(100vh - 4em)`,\n      }}\n      onClose={props.onClose}\n      focusTrap={true}\n      autoFocusFirst={{\n        selector: \"input\",\n      }}\n    >\n      <SearchAllContent {...state} {...props} />\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SearchAll/SearchAllContent.tsx",
    "content": "import { FlexCol } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport React from \"react\";\nimport type { SearchAllProps } from \"./SearchAll\";\nimport type { SearchAllState } from \"./hooks/useSearchAllState\";\nimport { useSearchAllListProps } from \"./hooks/useSearchListProps\";\nimport { useSearchTables } from \"./hooks/useSearchTables\";\n\nexport const SearchAllContent = (props: SearchAllProps & SearchAllState) => {\n  const {\n    defaultTerm,\n    tablesToSearch,\n    loading,\n    matchCase,\n    setMatchCase,\n    currentSearchedTable,\n  } = props;\n  const searchRowState = useSearchTables(props);\n  const searchListProps = useSearchAllListProps({\n    ...props,\n    ...searchRowState,\n  });\n\n  return (\n    <div\n      className=\"flex-row aai-start w-full min-h-0\"\n      style={{ width: \"hh550px\", maxWidth: \"88vw\", alignSelf: \"center\" }}\n    >\n      <FlexCol className=\"min-w-0 min-h-0 f-1\">\n        {loading && (\n          <div className={\"flex-row f-0 min-h-0 ai-center h-fit \"}>\n            <Loading sizePx={18} className=\"f-0 m-p5\" show={loading} />\n            <div className=\"f-1 ta-left font-14\">{`Searching ${currentSearchedTable} (${tablesToSearch.indexOf(currentSearchedTable!) + 1}/${tablesToSearch.length})`}</div>\n          </div>\n        )}\n        <SearchList\n          key=\"search-all-db\"\n          inputProps={{\n            \"data-command\": \"SearchAll\",\n          }}\n          matchCase={{\n            value: matchCase,\n            onChange: setMatchCase,\n          }}\n          id=\"search-all-db\"\n          className={\"f-1 min-w-0 flex-col \"}\n          searchStyle={{ maxWidth: \"500px\" }}\n          defaultSearch={defaultTerm}\n          limit={1000}\n          noSearchLimit={0}\n          {...searchListProps}\n        />\n      </FlexCol>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SearchAll/SearchAllHeader.tsx",
    "content": "import ButtonGroup from \"@components/ButtonGroup\";\nimport { FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport React from \"react\";\nimport type { SearchAllState } from \"./hooks/useSearchAllState\";\n\nexport const SEARCH_TYPES = [\n  { key: \"views and queries\", label: \"Tables/Queries\" },\n  { key: \"rows\", label: \"Data\" },\n] as const;\n\nexport const SearchAllHeader = ({\n  setTablesToSearch,\n  tablesToSearch,\n  setTypesToSearch,\n  typesToSearch,\n  tablesAndViews,\n  mode,\n  setMode,\n}: SearchAllState) => {\n  return (\n    <FlexRow>\n      <div className=\"font-18 fw-600\">Quick search</div>\n      <FlexRowWrap className=\"font-16 font-normal\">\n        <ButtonGroup\n          size=\"small\"\n          className=\"o-auto\"\n          options={SEARCH_TYPES.map((s) => s.label)}\n          value={SEARCH_TYPES.find((s) => s.key === mode)?.label}\n          onChange={(sLabel) => {\n            const newMode = SEARCH_TYPES.find((s) => s.label === sLabel)?.key;\n            if (newMode) {\n              setMode(newMode);\n            }\n          }}\n        />\n\n        {mode !== \"rows\" ?\n          <Select\n            label=\"Tables/Queries/Actions\"\n            limit={1000}\n            asRow={true}\n            size=\"small\"\n            options={[\"tables\", \"queries\", \"actions\"]}\n            value={typesToSearch}\n            multiSelect={true}\n            btnProps={{ style: { maxWidth: \"250px\" } }}\n            onChange={setTypesToSearch}\n          />\n        : <Select\n            label=\"Tables\"\n            limit={1000}\n            asRow={true}\n            size=\"small\"\n            fullOptions={tablesAndViews.map((t) => ({\n              key: t.name,\n              label: [t.schema !== \"public\" ? t.schema : undefined, t.name]\n                .filter(Boolean)\n                .join(\".\"),\n              subLabel: t.subLabel,\n            }))}\n            value={tablesToSearch}\n            multiSelect={true}\n            btnProps={{ style: { maxWidth: \"250px\" } }}\n            onChange={setTablesToSearch}\n          />\n        }\n      </FlexRowWrap>\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SearchAll/SearchMatchRow.tsx",
    "content": "import { sliceText } from \"@common/utils\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport { mdiTable, mdiTableEdit } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React from \"react\";\n\nexport type SearchMatch = {\n  table: string;\n  match: (string | string[])[];\n};\ntype P = {\n  matchRow: SearchMatch[\"match\"] | null;\n};\nexport const SearchMatchRow = ({ matchRow }: P) => {\n  if (!(matchRow && Array.isArray(matchRow))) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex-row ws-pre f-1 \">\n      {matchRow.map((r, i) => {\n        if (typeof r === \"string\") {\n          /** No highlight. Show full row */\n          // mxaxWidth: \"40%\", -> to center the term\n          const noRightText = !matchRow[2];\n          const style: React.CSSProperties =\n            i ?\n              { flex: 1 }\n            : {\n                display: \"flex\",\n                flex: noRightText ? undefined : 1,\n                justifyContent: noRightText ? \"start\" : \"end\",\n              };\n          if (matchRow.length === 1) {\n            delete style.maxWidth;\n            r = sliceText(r.split(\"\\n\").join(\" \"), 25);\n          }\n          return (\n            <span\n              key={i}\n              style={{\n                ...style,\n                maxWidth: \"fit-content\",\n              }}\n              className={\n                \"fs-1 min-w-0 text-1 text-ellipsis\" + (!i ? \" \" : \" ta-left\")\n              }\n            >\n              {r}\n            </span>\n          );\n        } else if (Array.isArray(r) && typeof r[0] === \"string\") {\n          /** Highlight. Show bolded */\n          return (\n            <strong key={i} className=\"f-0\">\n              {r[0]}\n            </strong>\n          );\n        } else {\n          console.warn(\"Unexpected $term_highlight item\", r);\n\n          return null;\n        }\n      })}\n    </div>\n  );\n};\n\nexport const SearchMatchRowWithTable = ({\n  icon,\n  db,\n  match,\n}: {\n  icon: string | undefined;\n  match: SearchMatch;\n  db: DBHandlerClient;\n}) => {\n  return (\n    <div className=\"f-1 flex-row ai-start\" title=\"Open table\">\n      <div className=\"flex-col ai-start f-0 text-1\">\n        {icon ?\n          <SvgIcon icon={icon} />\n        : <Icon path={db[match.table]?.insert ? mdiTableEdit : mdiTable} />}\n      </div>\n      <div className=\"flex-col ai-start f-1\">\n        <div className=\"font-18\">{match.table}</div>\n        <div\n          style={{\n            fontSize: \"16px\",\n            opacity: 0.7,\n            textAlign: \"left\",\n            width: \"100%\",\n            marginTop: \".25em\",\n          }}\n          // className={!mode ? \"text-2\" : \"\"}\n        >\n          <SearchMatchRow matchRow={match.match} />\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SearchAll/hooks/useSearchAllState.ts",
    "content": "import { useState } from \"react\";\nimport type { SearchAllProps } from \"../SearchAll\";\nimport { useTablesAndViewsSearchItems } from \"./useTablesAndViewsSearchItems\";\n\nexport const useSearchAllState = ({\n  db,\n  suggestions,\n  tables,\n  searchType,\n}: SearchAllProps) => {\n  const [typesToSearch, setTypesToSearch] = useState<\n    (\"tables\" | \"queries\" | \"actions\")[]\n  >([\"tables\", \"queries\", \"actions\"]);\n  const [mode, setMode] = useState<\"views and queries\" | \"rows\">(\n    searchType ?? \"rows\",\n  );\n  const tablesAndViews = useTablesAndViewsSearchItems({\n    db,\n    suggestions,\n    tables,\n  });\n  const [tablesToSearch, setTablesToSearch] = useState(\n    tablesAndViews.map((t) => t.name),\n  );\n  const [matchCase, setMatchCase] = useState(false);\n  const [loading, setLoading] = useState(false);\n  const [currentSearchedTable, setCurrentSearchedTable] = useState<string>();\n\n  return {\n    tablesAndViews,\n    tablesToSearch,\n    setTablesToSearch,\n    typesToSearch,\n    setTypesToSearch,\n    mode,\n    setMode,\n    matchCase,\n    setMatchCase,\n    loading,\n    setLoading,\n    currentSearchedTable,\n    setCurrentSearchedTable,\n  };\n};\n\nexport type SearchAllState = ReturnType<typeof useSearchAllState>;\n"
  },
  {
    "path": "client/src/dashboard/SearchAll/hooks/useSearchListProps.tsx",
    "content": "import { isObject } from \"@common/publishUtils\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport type { SearchListProps } from \"@components/SearchList/SearchList\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport {\n  mdiChatQuestion,\n  mdiFunction,\n  mdiScriptTextPlay,\n  mdiTable,\n  mdiTableEdit,\n} from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport type { ChartOptions } from \"../../Dashboard/dashboardUtils\";\nimport type { SearchAllProps } from \"../SearchAll\";\nimport type { SearchAllState } from \"./useSearchAllState\";\nimport type { MethodFullDef } from \"prostgles-types\";\nimport type { useSearchTables } from \"./useSearchTables\";\nimport { SearchMatchRow } from \"../SearchMatchRow\";\nimport type { DetailedFilter } from \"@common/filterUtils\";\n\nexport const useSearchAllListProps = ({\n  mode,\n  searchRows,\n  typesToSearch,\n  methods,\n  tablesAndViews,\n  queries,\n  tables,\n  db,\n  onOpen,\n  onClose,\n  onOpenDBObject,\n  matchedRows,\n  searchTerm,\n}: SearchAllState & SearchAllProps & ReturnType<typeof useSearchTables>) => {\n  const placeholder = \"Search...\";\n\n  let items: SearchListProps[\"items\"],\n    dontHighlight = false,\n    onSearch: SearchListProps[\"onSearch\"] = undefined;\n\n  const tableHash = useMemo(\n    () => new Map(tables.map((t) => [t.name, t])),\n    [tables],\n  );\n\n  if (mode === \"views and queries\") {\n    /** Prioritise public schema */\n    items = tablesAndViews\n      .filter((s) => s.type === \"table\" && typesToSearch.includes(\"tables\"))\n      .map((suggestion) => {\n        const { name, type, subLabel, icon } = suggestion;\n        return {\n          key: name,\n          label: name,\n          subLabel,\n          contentLeft: (\n            <div className=\"f-0\">\n              {icon ?\n                <SvgIcon icon={icon} className=\"text-1p5 p-p25\" />\n              : <Icon\n                  className=\"text-1p5 p-p25\"\n                  path={\n                    type === \"table\" ? mdiTable\n                    : type === \"function\" ?\n                      mdiFunction\n                    : mdiChatQuestion\n                  }\n                />\n              }\n            </div>\n          ),\n          onPress: (e, term) => {\n            onClose();\n            onOpenDBObject(suggestion);\n          },\n        };\n      })\n      .concat(\n        (typesToSearch.includes(\"queries\") ? (queries ?? []) : []).map((q) => ({\n          key: q.id,\n          label: q.name,\n          subLabel: q.sql || \"\", // sliceText(q.sql || \"\", 200) ,\n          contentLeft: (\n            <div className=\"f-0\">\n              <Icon className=\"text-1p5 p-p25\" path={mdiScriptTextPlay} />\n            </div>\n          ),\n          onPress: (e, term) => {\n            onClose();\n            let extra = {};\n            if (\n              q.sql &&\n              term &&\n              q.sql.toLowerCase().includes(term.toLowerCase())\n            ) {\n              const lines = q.sql.split(\"\\n\").map((l) => l.toLowerCase());\n              const lineNumber = lines.findIndex((s) =>\n                s.includes(term.toLowerCase()),\n              );\n              const cursorPosition: ChartOptions<\"sql\">[\"cursorPosition\"] = {\n                column: lines[lineNumber]!.indexOf(term.toLowerCase()) + 1,\n                lineNumber: lineNumber + 1,\n              };\n\n              extra = { options: { ...(q.options || {}), cursorPosition } };\n            }\n            q.$update?.({ closed: false, ...extra }, { deepMerge: true });\n          },\n        })),\n      )\n      .concat(\n        !typesToSearch.includes(\"actions\") ?\n          []\n        : Object.entries(methods as Record<string, MethodFullDef>)\n            .filter(([k, v]) => isObject(v) && (v as any).run)\n            .map(([methodKey, method]) => ({\n              key: methodKey,\n              label: methodKey,\n              subLabel: Object.keys(method.input).join(\", \"),\n              contentLeft: (\n                <div className=\"f-0\">\n                  <Icon className=\"text-1p5 p-p25\" path={mdiFunction} />\n                </div>\n              ),\n              onPress: (e, term) => {\n                onClose();\n                onOpenDBObject(undefined, methodKey);\n              },\n            })),\n      );\n  } else {\n    onSearch = searchRows;\n    dontHighlight = true;\n    items = matchedRows?.map((m, i) => {\n      const icon = tableHash.get(m.table)?.icon;\n      return {\n        ...m,\n        key: m.$rowhash + i,\n        label: m.table,\n        content: (\n          <div className=\"f-1 flex-row ai-start\" title=\"Open table\">\n            <div className=\"flex-col ai-start f-0 text-1\">\n              {icon ?\n                <SvgIcon icon={icon} />\n              : <Icon path={db[m.table]?.insert ? mdiTableEdit : mdiTable} />}\n            </div>\n            <div className=\"flex-col ai-start f-1\">\n              <div className=\"font-18\">{m.table}</div>\n              <div\n                style={{\n                  fontSize: \"16px\",\n                  opacity: 0.7,\n                  textAlign: \"left\",\n                  width: \"100%\",\n                  marginTop: \".25em\",\n                }}\n                // className={!mode ? \"text-2\" : \"\"}\n              >\n                <SearchMatchRow key={i} matchRow={m.match} />\n              </div>\n            </div>\n          </div>\n        ),\n        onPress: () => {\n          const filter: DetailedFilter[] = [];\n          if (m.colName) {\n            filter.push({\n              fieldName: m.colName,\n              type: \"$term_highlight\",\n              value: searchTerm,\n            });\n          }\n          onOpen({\n            table: m.table,\n            filter,\n          });\n          onClose();\n        },\n      };\n    });\n  }\n\n  return { items, onSearch, dontHighlight, placeholder };\n};\n"
  },
  {
    "path": "client/src/dashboard/SearchAll/hooks/useSearchTables.tsx",
    "content": "import React, { useCallback, useState } from \"react\";\nimport type { SearchAllProps } from \"../SearchAll\";\nimport { type SearchMatch } from \"../SearchMatchRow\";\nimport type { SearchAllState } from \"./useSearchAllState\";\nimport { useIsMounted } from \"prostgles-client\";\n\nexport const useSearchTables = (props: SearchAllProps & SearchAllState) => {\n  const {\n    db,\n    mode,\n    tables,\n    setLoading,\n    tablesToSearch,\n    matchCase,\n    setCurrentSearchedTable,\n  } = props;\n\n  const [matchedRows, setMatchedRows] = useState<\n    (SearchMatch & {\n      $rowhash: any;\n      colName: string;\n    })[]\n  >();\n\n  const [searchTerm, setSearchTerm] = useState(\"\");\n\n  const searchingRef = React.useRef<{\n    timeout: ReturnType<typeof setTimeout> | null;\n    term: string;\n    mode: \"views and queries\" | \"rows\";\n    abortController: AbortController;\n  }>();\n  const getIsMounted = useIsMounted();\n  const searchRows = useCallback(\n    (searchTerm = \"\") => {\n      setSearchTerm(searchTerm);\n      if (searchingRef.current) {\n        const { timeout, abortController } = searchingRef.current;\n        timeout && clearTimeout(timeout);\n        abortController.abort();\n      }\n      const abortController = new AbortController();\n      searchingRef.current = {\n        term: searchTerm,\n        mode,\n        timeout: null,\n        abortController,\n      };\n\n      searchingRef.current.timeout = setTimeout(async () => {\n        const term = searchingRef.current?.term ?? searchTerm;\n\n        if (mode === \"rows\" && db.sql) {\n          setLoading(true);\n\n          // setItems(undefined);\n          setMatchedRows(undefined);\n\n          try {\n            for (const k of tablesToSearch) {\n              /** Exclude numeric and timestamp columns when term contains characters  */\n              let colsToSearch: \"*\" | string[] = \"*\";\n              const cols = tables.find((t) => t.name === k)?.columns;\n              if (cols) {\n                colsToSearch = cols\n                  .filter(\n                    (c) =>\n                      c.select &&\n                      c.filter &&\n                      c.udt_name !== \"geography\" &&\n                      c.udt_name !== \"geometry\" &&\n                      (!/[a-z]/i.test(term) || c.tsDataType !== \"number\"),\n                  )\n                  .map((c) => c.name);\n              }\n\n              const tableHandler = db[k];\n              if (\n                tableHandler?.find &&\n                getIsMounted() &&\n                colsToSearch.length &&\n                term === searchingRef.current?.term &&\n                !abortController.signal.aborted\n              ) {\n                setCurrentSearchedTable(k);\n                const s = {\n                    $term_highlight: [\n                      colsToSearch,\n                      term,\n                      { edgeTruncate: 30, matchCase, returnType: \"object\" },\n                    ],\n                  },\n                  filter = {\n                    $term_highlight: [\n                      colsToSearch,\n                      term,\n                      { matchCase, returnType: \"boolean\" },\n                    ],\n                  },\n                  res = await tableHandler.find(filter, {\n                    select: { $rowhash: 1, s },\n                    limit: 3,\n                  }); /** Search top 3 records per table */\n\n                // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n                if (abortController.signal.aborted) break;\n\n                const _matches = res\n                  .filter((r) => r.s)\n                  .map((r) => {\n                    const colName = Object.keys(r.s)[0]!;\n                    return {\n                      $rowhash: r.$rowhash,\n                      table: k,\n                      colName,\n                      match: r.s[colName].map((v, i) =>\n                        !i ? `${colName}: ${v}` : v,\n                      ),\n                    };\n                  });\n\n                if (\n                  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n                  searchingRef.current &&\n                  term === searchingRef.current.term &&\n                  searchingRef.current.mode === \"rows\"\n                ) {\n                  // if (items.length) {\n                  //   setItems((prev) => [...(prev || []), ...items]);\n                  // }\n                  setMatchedRows((prev) => [...(prev || []), ..._matches]);\n                } else {\n                  setLoading(false);\n                }\n              }\n            }\n          } catch (error) {\n            if (error instanceof Error && error.name !== \"AbortError\") {\n              throw error;\n            }\n          } finally {\n            setLoading(false);\n            setMatchedRows((prev) => prev);\n            // setItems((prev) => [...(prev || [])]);\n          }\n        } else console.error(\"Unexpected option\");\n      }, 600);\n    },\n    [\n      mode,\n      db,\n      setLoading,\n      tablesToSearch,\n      tables,\n      getIsMounted,\n      setCurrentSearchedTable,\n      matchCase,\n    ],\n  );\n\n  return { searchRows, matchedRows, searchTerm };\n};\n"
  },
  {
    "path": "client/src/dashboard/SearchAll/hooks/useTablesAndViewsSearchItems.ts",
    "content": "import { isDefined } from \"src/utils/utils\";\nimport type { SearchAllProps, SearchAllSuggestion } from \"../SearchAll\";\nimport { useMemo } from \"react\";\n\nexport const useTablesAndViewsSearchItems = ({\n  tables,\n  suggestions,\n  db,\n}: Pick<SearchAllProps, \"tables\" | \"suggestions\" | \"db\">) => {\n  const tableMap: Map<number, (typeof tables)[number]> = useMemo(\n    () => new Map(tables.map((t) => [t.info.oid, t])),\n    [tables],\n  );\n  const searchItems: SearchAllSuggestion[] = useMemo(\n    () =>\n      suggestions\n        ?.slice(0)\n        .map((s) => {\n          const { type, OID } = s;\n          const isUserCreatedTable =\n            (type === \"table\" || type === \"view\") &&\n            s.schema &&\n            ![\"information_schema\"].includes(s.schema) &&\n            !s.schema.startsWith(\"pg_\");\n          if (!isUserCreatedTable) return;\n          const table = !OID ? undefined : tableMap.get(OID);\n          return {\n            name: s.name,\n            schema: s.schema,\n            type,\n            icon: table?.icon,\n            escapedIdentifier: s.escapedIdentifier,\n            subLabel:\n              table ? table.columns.map((c) => c.name).join(\", \") : undefined,\n          } satisfies SearchAllSuggestion;\n        })\n        .filter(isDefined) ??\n      tables\n        .map(({ name, icon }) => {\n          const handler = db[name];\n          if (handler && \"find\" in handler && handler.find) {\n            return {\n              name,\n              type: \"table\",\n              escapedIdentifier: JSON.stringify(name),\n              icon,\n              subLabel: \"\",\n            } satisfies SearchAllSuggestion;\n          }\n        })\n        .filter(isDefined),\n    [db, suggestions, tableMap, tables],\n  );\n  return searchItems;\n};\n"
  },
  {
    "path": "client/src/dashboard/SilverGrid/SilverGrid.tsx",
    "content": "import type { ReactElement } from \"react\";\nimport React from \"react\";\nimport RTComp from \"../RTComp\";\n\nimport Btn from \"@components/Btn\";\nimport { classOverride, FlexRow } from \"@components/Flex\";\nimport { SilverGridChild } from \"./SilverGridChild\";\nimport { SilverGridResizer } from \"./SilverGridResizer\";\nimport type { TreeLayout } from \"./TreeBuilder\";\nimport { TreeBuilder } from \"./TreeBuilder\";\n\nimport type {\n  LayoutItem,\n  LayoutConfig,\n  LayoutGroup,\n} from \"@common/DashboardTypes\";\nimport { includes } from \"prostgles-types\";\n\nexport type {\n  LayoutItem,\n  LayoutConfig,\n  LayoutGroup,\n} from \"@common/DashboardTypes\";\n\nexport type CustomHeaderClassNames = {\n  close: string;\n  minimise: string;\n  fullscreen: string;\n  move: string;\n};\n\nexport type ReactSilverGridNode = ReactElement<{\n  \"data-table-name\": string | null;\n  \"data-type\": \"title\" | \"header-icons\" | \"content\";\n  \"data-title\"?: string;\n}>;\n\nexport type SilverGridProps = {\n  className?: string;\n  style?: React.CSSProperties;\n  layout: LayoutConfig | null;\n  header?: CustomHeaderClassNames;\n  onChange?: (newLayout: LayoutConfig) => void;\n  onClose?: (\n    childKey: string | number,\n    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n  ) => void | Promise<any>;\n  hideButtons?: {\n    minimize?: boolean;\n    fullScreen?: boolean;\n    close?: boolean;\n    pan?: boolean;\n    resize?: boolean;\n  };\n  children?: ReactSilverGridNode[];\n  headerIcons?: ReactSilverGridNode[];\n  _ref?: (ref: HTMLDivElement) => void;\n  layoutMode: \"fixed\" | \"editable\";\n\n  /**\n   * Something to draw on top\n   */\n  overlay?: React.ReactNode;\n\n  /**\n   * Defaults to col\n   */\n  defaultLayoutType: undefined | LayoutGroup[\"type\"];\n};\n\ntype S = {\n  layout?: LayoutConfig;\n  targetStyle: React.CSSProperties;\n  minimized?: boolean;\n};\n\nexport class SilverGridReact extends RTComp<SilverGridProps, S, any> {\n  state: S = {\n    layout: undefined,\n    targetStyle: { display: \"none\" },\n  };\n\n  treeLayout?: TreeBuilder;\n  onDelta = (dP: Partial<SilverGridProps> | undefined) => {\n    const { layout, children = [], defaultLayoutType = \"col\" } = this.props;\n\n    if (dP && (\"layout\" in dP || \"children\" in dP)) {\n      if (layout) {\n        this.treeLayout = new TreeBuilder({ ...layout }, this.onChange);\n      }\n\n      /* Check layout. Update if extra/missing items */\n      if (\n        !layout ||\n        ((layout as LayoutGroup).items.length === 0 && children.length)\n      ) {\n        setTimeout(() => {\n          this.onChange({\n            id: \"1\",\n            ...(defaultLayoutType === \"tab\" && { activeTabKey: undefined }),\n            type: defaultLayoutType as any,\n            size: 100,\n            items: children.map((c, i) => ({\n              id: c.props[\"data-key\"] || i.toString(),\n              tableName: c.props[\"data-table-name\"],\n              viewType: c.props[\"data-view-type\"],\n              title: c.props[\"data-title\"],\n              type: \"item\",\n              size: 20,\n            })),\n          });\n        }, 0);\n\n        return null;\n      }\n\n      const items = this.getItems(layout),\n        itemIds = items.map((d) => d.id),\n        orphans = children.filter(\n          (c) => !itemIds.includes(c.props[\"data-key\"]),\n        ),\n        emptyItemIds = Array.from(\n          new Set(\n            itemIds.filter(\n              (id) => !children.find((c) => c.props[\"data-key\"] == id),\n            ),\n          ),\n        );\n\n      /* Remove any empty layouts */\n      if (\"items\" in layout) {\n        const empty = this.treeLayout?.getLeafs(\n          (t) => \"items\" in t && !t.items.length,\n        );\n        if (empty?.length) {\n          empty.map((l) => {\n            let _l: TreeLayout | undefined = l;\n            while (_l) {\n              const p = _l.parent;\n              this.treeLayout?.remove(_l.id);\n              if (p && \"items\" in p && !p.items.length && !p.isRoot) {\n                _l = p;\n              } else {\n                (_l as any) = undefined;\n              }\n            }\n          });\n\n          return;\n        }\n      }\n\n      if (orphans.length) {\n        setTimeout(() => {\n          /** TODO: this logic should apply to any view that is linked to other views. */\n          const newLayoutType =\n            (\n              orphans.some((o) =>\n                includes([\"map\", \"timechart\"], o.props[\"data-view-type\"]),\n              )\n            ) ?\n              \"col\"\n            : defaultLayoutType;\n          let newLayout = { ...layout };\n          if (newLayout.type === newLayoutType) {\n            const totalSize = (newLayout.items as LayoutConfig[]).reduce(\n              (a, v) => a + v.size,\n              0,\n            );\n            newLayout.items = [\n              ...orphans.map(\n                (c, i) =>\n                  ({\n                    id: c.props[\"data-key\"] || i,\n                    title: c.props[\"data-title\"],\n                    tableName: c.props[\"data-table-name\"],\n                    viewType: c.props[\"data-view-type\"],\n                    type: \"item\",\n                    size: totalSize / orphans.length,\n                  }) satisfies LayoutItem,\n              ),\n              ...newLayout.items,\n            ];\n            /** Ensure the newly added view is shown */\n            if (newLayout.type === \"tab\") {\n              newLayout.activeTabKey = orphans[0]?.props[\"data-key\"];\n            }\n          } else {\n            newLayout.size = 50;\n            newLayout = {\n              id: \"1\",\n              ...(newLayoutType === \"tab\" && { activeTabKey: undefined }),\n              type: newLayoutType as any,\n              size: 100,\n              isRoot: true,\n              items: orphans\n                .map(\n                  (c, i) =>\n                    ({\n                      id: c.props[\"data-key\"] || i,\n                      title: c.props[\"data-title\"],\n                      tableName: c.props[\"data-table-name\"],\n                      viewType: c.props[\"data-view-type\"],\n                      type: \"item\",\n                      size: 50 / orphans.length,\n                    }) satisfies LayoutItem,\n                )\n                .concat(newLayout as any),\n            };\n          }\n          this.onChange(newLayout);\n        }, 0);\n      }\n      if (children.length && emptyItemIds.length) {\n        emptyItemIds.map((id) => {\n          this.treeLayout?.remove(id);\n        });\n      }\n    }\n  };\n\n  setTarget = (targetStyle: React.CSSProperties) => {\n    this.setState({ targetStyle });\n  };\n\n  getRoot = (): HTMLElement => {\n    if (!this.ref) throw \"Unexpected\";\n    return this.ref;\n  };\n\n  isChangingLayout?: {\n    layout: LayoutConfig;\n    timeout: any;\n  };\n  onChange = (newLayout: LayoutConfig, isTemporary = false) => {\n    const { onChange } = this.props;\n    if (onChange && !isTemporary) {\n      onChange({ ...newLayout });\n      this.setState({ layout: undefined });\n    } else {\n      this.setState({ layout: newLayout });\n    }\n  };\n\n  getItems = (\n    layout: LayoutConfig | undefined = this.state.layout,\n  ): LayoutItem[] => {\n    let res = [];\n    if (!layout) {\n    } else if (layout.type === \"item\") {\n      return [layout];\n    } else {\n      res = res.concat(...(layout.items.map(this.getItems) as any));\n    }\n\n    return res;\n  };\n\n  ref?: HTMLDivElement;\n  refTarget?: HTMLDivElement;\n  renderGrid = (\n    layout: LayoutConfig | null = this.props.layout,\n    _key?: string,\n    _onChange?: (newLayout: LayoutConfig) => void,\n  ) => {\n    const {\n      children: c,\n      header,\n      headerIcons = [],\n      onClose,\n      _ref,\n      hideButtons,\n      layoutMode,\n    } = this.props;\n    const { minimized } = this.state;\n    const children = React.Children.toArray(c) as ReactSilverGridNode[];\n    let content: React.ReactNode = null;\n\n    if (!children.length || !layout) {\n      return null;\n    }\n    const onChange = _onChange ?? this.onChange;\n    const key = _key ?? layout.id;\n\n    const getChildNode = (id: string | number) => {\n      const res = children.find((c) => c.props[\"data-key\"] == id);\n      return res;\n    };\n\n    if (layout.type === \"item\") {\n      const child = getChildNode(layout.id);\n      const headerIcon =\n        children.find(\n          (c) =>\n            c.props[\"data-key\"] == layout.id &&\n            c.props[\"data-type\"] === \"header-icons\",\n        ) || headerIcons.find((c) => c.props[\"data-key\"] == layout.id);\n\n      return (\n        <SilverGridChild\n          key={key}\n          activeTabKey={layout.id}\n          layoutMode={layoutMode}\n          title={child?.props[\"data-title\"] || \"\"}\n          hideButtons={hideButtons ?? {}}\n          layout={layout}\n          header={header}\n          headerIcon={headerIcon}\n          setTarget={this.setTarget}\n          getRoot={this.getRoot}\n          onClose={onClose}\n          moveTo={(sourceId, itemId, parentType, insertBefore) => {\n            this.treeLayout?.moveTo(sourceId, itemId, parentType, insertBefore);\n          }}\n          onChange={onChange}\n          hasSiblings={children.some((c) => c.props[\"data-key\"] != layout.id)}\n        >\n          {child}\n        </SilverGridChild>\n      );\n    } else {\n      content = [];\n\n      if (layout.type === \"tab\") {\n        const [firstItem] = layout.items;\n        if (!firstItem) {\n          return null;\n        }\n        const activeItem =\n          layout.items.find((d) => d.id === layout.activeTabKey) ?? firstItem;\n        const activeItemId = activeItem.id;\n        const child = getChildNode(activeItemId) ?? (\n          <FlexRow className=\"p-2 ai-center\">\n            Item not found\n            <Btn\n              color=\"action\"\n              onClick={() => {\n                this.treeLayout?.remove(activeItemId);\n              }}\n            >\n              Click to remove\n            </Btn>\n          </FlexRow>\n        );\n\n        const headerIcon = headerIcons.find(\n          (c) => c.props[\"data-key\"] == activeItemId,\n        );\n\n        const otherChildren: LayoutItem[] = layout.items.map((l) => ({\n          ...l,\n          title: getChildNode(l.id)?.props[\"data-title\"],\n        }));\n        content = (\n          <SilverGridChild\n            key={key}\n            activeTabKey={activeItemId}\n            title={child.props[\"data-title\"] || \"\"}\n            headerIcon={headerIcon}\n            siblingTabs={otherChildren}\n            layoutMode={layoutMode}\n            onClickSibling={(tabId) => {\n              onChange({ ...layout, activeTabKey: tabId });\n            }}\n            hasSiblings={children.some(\n              (c) => c.props[\"data-key\"] != activeItemId,\n            )}\n            layout={firstItem}\n            header={header}\n            setTarget={this.setTarget}\n            getRoot={this.getRoot}\n            onClose={onClose}\n            moveTo={(sourceId, itemId, parentType, insertBefore) => {\n              this.treeLayout?.moveTo(\n                sourceId,\n                itemId,\n                parentType,\n                insertBefore,\n              );\n            }}\n            onChange={(newLayout) => {\n              onChange(newLayout);\n            }}\n            minimize={{\n              value: !!minimized,\n              toggle: () => {\n                this.setState({ minimized: !minimized });\n              },\n            }}\n            hideButtons={hideButtons ?? {}}\n          >\n            {child}\n          </SilverGridChild>\n        );\n      } else {\n        const items = layout.items.map((l) => {\n          const nestedItemOnchange = (newLayout: LayoutConfig) => {\n            const newItems = layout.items.map((l) => {\n              if (l.id === newLayout.id) {\n                return newLayout;\n              }\n              return l;\n            });\n            onChange({\n              ...layout,\n              items: newItems,\n            });\n          };\n          return this.renderGrid(l, l.id, nestedItemOnchange);\n        });\n        items.map((c, i) => {\n          (content as React.ReactNode[]).push(c);\n          if (i < items.length - 1) {\n            (content as React.ReactNode[]).push(\n              <SilverGridResizer\n                key={\"resizer\" + i}\n                layoutMode={layoutMode}\n                type={layout.type}\n                onChange={(prevSize, nextSize) => {\n                  this.treeLayout?.update([prevSize, nextSize]);\n                }}\n              />,\n            );\n          }\n        });\n      }\n    }\n\n    const isMinimized = minimized && layout.type === \"tab\";\n\n    return (\n      <div\n        ref={(r) => {\n          if (r) {\n            this.ref = r;\n            _ref?.(r);\n          }\n        }}\n        key={key}\n        data-box-type={layout.type}\n        data-box-id={layout.id}\n        className={classOverride(\n          \"silver-grid-box flex-\" + layout.type + \" min-w-0 min-h-0\",\n        )}\n        style={{\n          flex: layout.size,\n          display: \"flex\",\n          ...(isMinimized && {\n            height: `${40 + 8}px`,\n            flex: \"none\",\n            flexShrink: 1,\n          }),\n        }}\n      >\n        {content}\n      </div>\n    );\n  };\n\n  render() {\n    const { targetStyle } = this.state;\n    const { style = {}, className = \"\", overlay, layoutMode } = this.props;\n    return (\n      <div\n        ref={(r) => {\n          if (r) this.ref = r;\n        }}\n        className={classOverride(\n          `silver-grid-component relative  ${layoutMode === \"fixed\" ? \"p-1 bg-color-3\" : \"\"}`,\n          className,\n        )}\n        style={{\n          display: \"flex\",\n          flex: 1,\n          height: \"100%\",\n          width: \"100%\",\n          ...style,\n        }}\n      >\n        {this.renderGrid()}\n        <div\n          key={\"silver-grid-view-move-target\"}\n          ref={(r) => {\n            if (r) this.refTarget = r;\n          }}\n          className={\" absolute silver-grid-view-move-target b\"}\n          style={{\n            ...targetStyle,\n            zIndex: 232,\n            transition: \".2s all ease-in-out\",\n            background: \"#dcffffb3\",\n          }}\n        />\n        {overlay}\n      </div>\n    );\n  }\n}\n\nexport function isTouchDevice() {\n  return (\n    \"ontouchstart\" in window ||\n    navigator.maxTouchPoints > 0 ||\n    (navigator as any).msMaxTouchPoints > 0\n  );\n}\n"
  },
  {
    "path": "client/src/dashboard/SilverGrid/SilverGridChild.tsx",
    "content": "import type { ReactElement } from \"react\";\nimport React from \"react\";\nimport { setPan } from \"../setPan\";\nimport { vibrateFeedback } from \"../Dashboard/dashboardUtils\";\nimport RTComp from \"../RTComp\";\nimport type {\n  CustomHeaderClassNames,\n  LayoutConfig,\n  LayoutGroup,\n  LayoutItem,\n  ReactSilverGridNode,\n  SilverGridProps,\n} from \"./SilverGrid\";\nimport { SilverGridChildHeader } from \"./SilverGridChildHeader\";\n\nexport type SilverGridChildProps = {\n  layout: LayoutItem;\n  title?: string;\n  children: ReactElement | undefined;\n  header?: CustomHeaderClassNames;\n  headerIcon?: ReactSilverGridNode;\n  siblingTabs?: Partial<LayoutItem>[];\n  hasSiblings: boolean;\n  activeTabKey: string;\n  onClickSibling?: (id: string) => void;\n  setTarget: (newStyle: React.CSSProperties) => void;\n  onChange: (newLayout: LayoutConfig) => void;\n  getRoot: () => HTMLElement;\n  onClose?: (\n    childKey: string,\n    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n  ) => void | Promise<any>;\n  moveTo: (\n    sourceId: string,\n    targetId: string,\n    parentType: \"row\" | \"col\" | \"tab\",\n    insertBefore: boolean,\n  ) => void;\n  minimize?: {\n    value: boolean;\n    toggle: VoidFunction;\n  };\n} & Required<Pick<SilverGridProps, \"hideButtons\" | \"layoutMode\">>;\n\ntype SilverGridChildState = {\n  fullscreen: boolean;\n  minimized?: boolean;\n};\n\nexport class SilverGridChild extends RTComp<\n  SilverGridChildProps,\n  SilverGridChildState,\n  any\n> {\n  state: SilverGridChildState = {\n    fullscreen: false,\n  };\n\n  source?: {\n    layout: LayoutItem;\n    parentLayout: LayoutGroup;\n  };\n  target?: {\n    node: HTMLElement;\n    position: \"tabDist\" | \"leftDist\" | \"rightDist\" | \"topDist\" | \"bottomDist\";\n  };\n\n  loaded?: {\n    headerRef: HTMLDivElement | null;\n  };\n\n  cleanupListeners?: VoidFunction;\n  onDelta(\n    deltaP: Partial<SilverGridChildProps> | undefined,\n    deltaS: Partial<SilverGridChildState> | undefined,\n    deltaD: any,\n  ): void {\n    const { header, setTarget, moveTo, hideButtons, layoutMode } = this.props;\n    const container = this.ref?.closest(\".silver-grid-component\");\n\n    if (!container || !this.ref || hideButtons.pan) {\n      return;\n    }\n    const headerRef =\n      this.refHeader ||\n      this.ref.querySelector<HTMLDivElement>(\".\" + header?.move);\n    if (!headerRef) {\n      throw \"Header missing\";\n    }\n    if (headerRef === this.loaded?.headerRef) {\n      return;\n    }\n    this.loaded = { headerRef };\n\n    if (layoutMode === \"fixed\") {\n      this.cleanupListeners?.();\n      return;\n    }\n    this.cleanupListeners = setPan(headerRef, {\n      onPress: () => {\n        if (!this.props.hasSiblings) return;\n        vibrateFeedback(15);\n      },\n      onPanStart: () => {\n        if (!this.props.hasSiblings) return;\n        this.ref!.style.display = \"none\";\n      },\n      onPanEnd: () => {\n        if (!this.props.hasSiblings) return;\n\n        try {\n          this.ref!.style.display = \"flex\";\n          setTarget({ display: \"none\" });\n          this.ref!.style.opacity = \"\";\n\n          const { position: p, node: targetNode } = this.target!;\n          let parentType: \"row\" | \"col\" | \"tab\" = \"col\";\n          let insertBefore = true;\n\n          if (p === \"tabDist\") {\n            parentType = \"tab\";\n            insertBefore = true;\n          } else if (p === \"leftDist\") {\n            parentType = \"row\";\n            insertBefore = true;\n          } else if (p === \"rightDist\") {\n            parentType = \"row\";\n            insertBefore = false;\n          } else if (p === \"topDist\") {\n            parentType = \"col\";\n            insertBefore = true;\n          } else {\n            parentType = \"col\";\n            insertBefore = false;\n          }\n\n          const targetId = targetNode.dataset[\"boxId\"]!;\n\n          moveTo(this.props.layout.id, targetId, parentType, insertBefore);\n        } catch (e) {\n          console.error(e);\n        }\n        this.target = undefined;\n      },\n      threshold: 55,\n      onPan: (p) => {\n        if (!this.props.hasSiblings) return;\n\n        const { x, y } = p;\n        const boxes = Array.from(container.querySelectorAll(\"[data-box-type]\"))\n          .map((node) => {\n            const br = node.getBoundingClientRect();\n\n            let hr,\n              tabDist = Infinity;\n\n            if (node.classList.contains(\"silver-grid-item\")) {\n              hr = node\n                .querySelector(\":scope > .silver-grid-item-header\")\n                ?.getBoundingClientRect();\n              tabDist = Math.hypot(\n                x - (hr.x + hr.width / 2),\n                y - (hr.y + hr.height / 2),\n              );\n            }\n\n            const leftDist = Math.hypot(\n                x - (br.x + 0.25 * br.width),\n                y - (br.y + 0.5 * br.height),\n              ),\n              rightDist = Math.hypot(\n                x - (br.x + 0.75 * br.width),\n                y - (br.y + 0.5 * br.height),\n              ),\n              topDist = Math.hypot(\n                x - (br.x + 0.5 * br.width),\n                y - (br.y + 0.25 * br.height),\n              ),\n              bottomDist = Math.hypot(\n                x - (br.x + 0.5 * br.width),\n                y - (br.y + 0.75 * br.height),\n              );\n\n            return {\n              node,\n              rect: br,\n              hr,\n              tabDist,\n              leftDist,\n              rightDist,\n              topDist,\n              bottomDist,\n              minDist: Math.min(\n                tabDist,\n                leftDist,\n                rightDist,\n                topDist,\n                bottomDist,\n              ),\n            };\n          })\n          .sort((a, b) => a.minDist - b.minDist);\n\n        this.ref!.style.opacity = \"0.3\";\n        const parentRect = container.getBoundingClientRect();\n        const closest = boxes[0],\n          cr = closest?.rect;\n\n        if (!cr) {\n          console.warn(\"onPan fail, closest not found\");\n          return;\n        }\n\n        let _x = cr.x - parentRect.x,\n          _y = cr.y - parentRect.y,\n          _w = cr.width / 2,\n          _h = cr.height;\n\n        this.target = {\n          node: closest.node as HTMLElement,\n          position: \"leftDist\",\n        };\n        if (closest.leftDist === closest.minDist) {\n          _x = cr.x - parentRect.x;\n          _y = cr.y - parentRect.y;\n          _w = cr.width / 2;\n          _h = cr.height;\n          this.target.position = \"leftDist\";\n        } else if (closest.rightDist === closest.minDist) {\n          _x = cr.x - parentRect.x + cr.width / 2;\n          _y = cr.y - parentRect.y;\n          _w = cr.width / 2;\n          _h = cr.height;\n          this.target.position = \"rightDist\";\n        } else if (closest.topDist === closest.minDist) {\n          _x = cr.x - parentRect.x;\n          _y = cr.y - parentRect.y;\n          _w = cr.width;\n          _h = cr.height / 2;\n          this.target.position = \"topDist\";\n        } else if (closest.bottomDist === closest.minDist) {\n          _x = cr.x - parentRect.x;\n          _y = cr.y - parentRect.y + cr.height / 2;\n          _w = cr.width;\n          _h = cr.height / 2;\n          this.target.position = \"bottomDist\";\n        } else if (closest.tabDist === closest.minDist) {\n          _x = closest.hr.x - parentRect.x;\n          _y = closest.hr.y - parentRect.y;\n          _w = closest.hr.width;\n          _h = closest.hr.height;\n          this.target.position = \"tabDist\";\n        }\n\n        const thisR = this.ref!.getBoundingClientRect();\n        if (\n          closest.node === this.ref ||\n          (_x - 2 >= thisR.x - parentRect.x &&\n            _x + _w + 2 <= thisR.x - parentRect.x + thisR.width &&\n            _y - 2 >= thisR.y - parentRect.y &&\n            _y + _h + 2 <= thisR.y - parentRect.y + thisR.height)\n        )\n          return;\n\n        setTarget({\n          display: \"block\",\n          width: `${_w}px`,\n          height: `${_h}px`,\n          transform: `translate(${_x}px, ${_y}px)`,\n          border: \"2px dashed #2d81ff\",\n        });\n\n        // console.log(thisR)\n      },\n    });\n  }\n\n  onClickClose = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {\n    const { onClose, layout } = this.props;\n\n    await onClose?.(layout.id, e);\n  };\n\n  get siblings() {\n    const { getRoot } = this.props;\n    return Array.from(\n      getRoot().querySelectorAll<HTMLDivElement>('[data-box-type=\"item\"]'),\n    );\n  }\n\n  hasSiblingFullScreened = () => {\n    const { getRoot } = this.props;\n    return Boolean(\n      this.ref &&\n        getRoot().querySelector('[data-box-type=\"item\"].fullscreen') &&\n        getRoot().querySelector('[data-box-type=\"item\"].fullscreen') !==\n          this.ref,\n    );\n  };\n\n  ref?: HTMLDivElement;\n  refHeader?: HTMLDivElement;\n  render() {\n    const { children, header, layout, minimize, layoutMode } = this.props;\n    const { fullscreen } = this.state;\n    const minimized = minimize?.value ?? this.state.minimized;\n\n    let content: any = children;\n\n    const height =\n      layoutMode === \"fixed\" ? 50\n      : window.isMobileDevice ? 32\n      : 40;\n    const isMinimized = !fullscreen && !minimize && minimized;\n\n    if (!content) return null;\n    if (!header) {\n      content = (\n        <>\n          <SilverGridChildHeader\n            {...this.props}\n            height={height}\n            onSetHeaderRef={(r) => {\n              this.refHeader = r;\n            }}\n            onClickFullscreen={() => {\n              this.setState({ fullscreen: !fullscreen });\n            }}\n            fullscreen={fullscreen}\n            onClickClose={this.onClickClose}\n            minimized={minimized}\n            onSetMinimized={(minimized) => {\n              this.setState({ minimized });\n            }}\n          />\n          {isMinimized ? null : children}\n        </>\n      );\n    }\n\n    /** Used to ensure the SQL Editor suggestions overflow is visible */\n    const setZindex = () => {\n      if (!this.hasSiblingFullScreened()) {\n        setTimeout(() => {\n          this.siblings.forEach((n) => {\n            if (n !== this.ref) {\n              n.style.zIndex = \"1\";\n            } else {\n              n.style.zIndex = this.state.fullscreen ? \"3\" : \"2\";\n            }\n          });\n        }, 100);\n      }\n    };\n\n    /** Is used to ensure that clicks on overflowing content are not disabled */\n    const isMyContent = (target: any) => this.ref?.contains(target);\n    const isFixed = this.props.layoutMode === \"fixed\";\n    return (\n      <div\n        ref={(r) => {\n          if (r) this.ref = r;\n        }}\n        className={`SilverGridChild silver-grid-box silver-grid-item bg-color-1 f-1 flex-col min-w-0 min-h-0 ${fullscreen ? \" fullscreen \" : \" \"} ${isFixed ? \"rounded shadow\" : \"\"}`}\n        data-box-id={layout.id}\n        data-box-type=\"item\"\n        data-table-name={layout.tableName}\n        data-view-type={layout.viewType}\n        /** Some content is overflowing over sibling windows. Ensure this overflow is visible */\n        onClick={({ target }) => {\n          if (isMyContent(target)) {\n            setZindex();\n          }\n        }}\n        style={{\n          flex: layout.size,\n          overflow: \"hidden\", // auto\n          ...(isMinimized && {\n            height: `${height + 8}px`,\n            flex: \"none\",\n            flexShrink: 1,\n          }),\n        }}\n      >\n        {content}\n      </div>\n    );\n  }\n}\n\ntype Box = { x: number; y: number; width: number; height: number };\nexport const getDistanceBetweenBoxes = (b1: Box, b2: Box) => {\n  const startX = b1.x + b1.width / 2;\n  const startY = b1.y + b1.height / 2;\n  const endX = b2.x + b2.width / 2;\n  const endY = b2.y + b2.height / 2;\n  return Math.hypot(endX - startX, endY - startY);\n};\n"
  },
  {
    "path": "client/src/dashboard/SilverGrid/SilverGridChildHeader.tsx",
    "content": "import {\n  mdiClose,\n  mdiFullscreen,\n  mdiFullscreenExit,\n  mdiMenu,\n  mdiUnfoldLessHorizontal,\n  mdiUnfoldMoreHorizontal,\n} from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport type { SilverGridChildProps } from \"./SilverGridChild\";\nimport { dataCommand } from \"../../Testing\";\nimport { appTheme, useReactiveState } from \"../../appUtils\";\nimport { FlexRow } from \"@components/Flex\";\nexport const GridHeaderClassname = \"silver-grid-item-header--title\" as const;\n\ntype P = SilverGridChildProps & {\n  fullscreen: boolean;\n  height: number;\n  minimized: boolean | undefined;\n  onClickClose: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;\n  onSetMinimized: (newValue: boolean) => void;\n  onClickFullscreen: VoidFunction;\n  onSetHeaderRef: (ref: HTMLDivElement) => void;\n};\n\nconst CloseButton = ({\n  tabId,\n  onClose,\n}: { tabId: string | undefined } & Pick<P, \"onClose\">) => {\n  if (!onClose || !tabId) return null;\n  return (\n    <Btn\n      className=\"SilverGridChild_CloseButton show-on-parent-hover f-0\"\n      size=\"micro\"\n      iconProps={{\n        size: 0.75,\n        path: mdiClose,\n      }}\n      iconPath={mdiClose}\n      onClick={(e) => onClose(tabId, e)}\n    />\n  );\n};\n\nconst TITLE_ID_ATTRNAME = \"data-title-item-id\" as const;\nexport const getSilverGridTitleNode = (id: string) =>\n  document.body.querySelector<HTMLDivElement>(\n    `[${TITLE_ID_ATTRNAME}=${JSON.stringify(id)}]`,\n  );\nexport const SilverGridChildHeader = (props: P) => {\n  const {\n    headerIcon,\n    minimize,\n    hideButtons: _hideButtons = {},\n    onClose,\n    layoutMode,\n    height,\n    minimized,\n    onSetHeaderRef,\n    fullscreen,\n    onSetMinimized,\n    onClickFullscreen,\n    onClickClose,\n    siblingTabs,\n    onClickSibling,\n    activeTabKey,\n  } = props;\n  const lineHeight = window.isMobileDevice ? 16 : 24;\n\n  const tabs = siblingTabs?.length ? siblingTabs : [props.layout];\n  const isFixed = layoutMode === \"fixed\";\n  const hideButtons: typeof _hideButtons =\n    isFixed ?\n      {\n        minimize: true,\n        fullScreen: false,\n        close: true,\n        pan: true,\n        resize: true,\n      }\n    : _hideButtons;\n  const { state: theme } = useReactiveState(appTheme);\n  const bgClass = theme === \"dark\" ? \"bg-color-0\" : \"bg-color-3\";\n  const bgActiveClass = theme === \"dark\" ? \"bg-color-2\" : \"bg-color-0\";\n  const btnClass = `f-0 ${isFixed && !fullscreen ? \"show-on-parent-hover\" : \"\"}`;\n\n  return (\n    <div\n      className=\"silver-grid-item-header flex-row  bg-color-0 bb b-color-0 pointer f-0 noselect relative ai-center shadow\"\n      style={\n        isFixed ?\n          {\n            paddingRight: \".25em\",\n          }\n        : {}\n      }\n    >\n      <div\n        className=\"silver-grid-item-header--icon flex-row f-0 o-hidden f-1 ai-center\"\n        style={{\n          maxWidth: \"fit-content\",\n          minWidth: \"42px\",\n        }}\n      >\n        {headerIcon}\n      </div>\n\n      <FlexRow\n        className=\"SilverGridChildHeader_tabs flex-row f-1 min-w-0 ws-nowrap ai-end text-ellipsiss ml-p25 o-auto  no-scroll-bar\"\n        style={{\n          gap: \"1px\",\n        }}\n        onWheel={(e) => {\n          e.currentTarget.scrollLeft += e.deltaY;\n        }}\n      >\n        {tabs.map((tab) => {\n          const attrs = { [TITLE_ID_ATTRNAME]: tab.id };\n          if (tab.id !== activeTabKey) {\n            const title = tab.title || tab.id;\n            const tabId = tab.id;\n            return (\n              <FlexRow\n                key={tab.id}\n                className={`gap-p25 pl-p5 pr-p25 ${bgClass}`}\n                onClick={() => {\n                  onClickSibling?.(tab.id!);\n                }}\n                style={{\n                  height: `${height - 3}px`,\n                  marginTop: \"4px\",\n                  lineHeight: \"21px\",\n                  maxWidth: \"40%\",\n                }}\n                title={title ?? \"\"}\n              >\n                <div\n                  className={\"f-1 min-w-0 ws-nowrap text-ellipsis py-p5 \"}\n                  style={{\n                    padding: \".5em 0 .5em .75em \",\n                  }}\n                  {...attrs}\n                >\n                  {title}\n                </div>\n                {isFixed ?\n                  <div style={{ width: \"1em\" }} />\n                : <CloseButton {...props} tabId={tabId} />}\n              </FlexRow>\n            );\n          }\n\n          return (\n            <FlexRow\n              key={tab.id}\n              ref={(r) => {\n                if (r) {\n                  onSetHeaderRef(r);\n                }\n              }}\n              className={`gap-p25 pl-p5 pr-p25 ${bgActiveClass}`}\n              style={{\n                height: `${height}px`,\n                lineHeight: `${lineHeight + 2}px`,\n                /** Prevent total collapse when there is not enough space */\n                minWidth: \"80px\",\n                justifyContent: \"space-between\",\n                marginTop: \"2px\",\n                /** Used to prevent unexpected scroll of tab headers */\n                overflowY: \"hidden\",\n                maxWidth: \"max(300px, 40%)\",\n              }}\n            >\n              <div\n                className={`${GridHeaderClassname} py-p5 f-1 min-w-0 max-w-fit text-ellipsis noselect `}\n                {...attrs}\n              >\n                {tab.title}\n              </div>\n              {isFixed ?\n                <div style={{ width: \"1em\" }} />\n              : <CloseButton {...props} tabId={tab.id} />}\n            </FlexRow>\n          );\n        })}\n      </FlexRow>\n\n      {!hideButtons.minimize && (\n        <Btn\n          {...dataCommand(\"dashboard.window.collapse\")}\n          className={btnClass}\n          iconPath={\n            !minimized ? mdiUnfoldLessHorizontal : mdiUnfoldMoreHorizontal\n          }\n          disabledInfo={fullscreen ? \"Must exit fullscreen\" : undefined}\n          title=\"Minimize/Maximize view\"\n          onClick={(e) => {\n            if (minimize) {\n              minimize.toggle();\n            } else {\n              onSetMinimized(!minimized);\n            }\n          }}\n        />\n      )}\n\n      {!hideButtons.fullScreen && (\n        <Btn\n          {...dataCommand(\"dashboard.window.fullscreen\")}\n          title=\"Toggle view fullscreen mode\"\n          className={btnClass}\n          iconPath={!fullscreen ? mdiFullscreen : mdiFullscreenExit}\n          onClick={onClickFullscreen}\n        />\n      )}\n      {onClose && !hideButtons.close && (\n        <Btn\n          {...dataCommand(\"dashboard.window.close\")}\n          title=\"Remove view\"\n          className={btnClass}\n          iconPath={mdiClose}\n          onClick={(e) => onClickClose(e)}\n          disabledInfo={\n            fullscreen && !hideButtons.fullScreen ?\n              \"Must exit fullscreen first\"\n            : undefined\n          }\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SilverGrid/SilverGridResizer.tsx",
    "content": "import { setPan } from \"../setPan\";\nimport React from \"react\";\n\nimport { vibrateFeedback } from \"../Dashboard/dashboardUtils\";\nimport RTComp from \"../RTComp\";\nimport type { LayoutItem } from \"./SilverGrid\";\nimport { isTouchDevice } from \"./SilverGrid\";\n\ntype LayoutSize = Pick<LayoutItem, \"id\" | \"size\">;\n\ntype SilverGridResizerProps = {\n  type: \"row\" | \"col\";\n  onChange: (prev: LayoutSize, next: LayoutSize) => void;\n  layoutMode: \"fixed\" | \"editable\";\n};\n\nexport class SilverGridResizer extends RTComp<\n  SilverGridResizerProps,\n  Record<string, never>,\n  any\n> {\n  sizes?: {\n    prev: LayoutSize;\n    next: LayoutSize;\n  };\n\n  cleanupListeners?: VoidFunction;\n  onDelta() {\n    const { onChange, layoutMode } = this.props;\n    const ref = this.ref as any;\n    if (this.cleanupListeners && layoutMode === \"fixed\") {\n      this.cleanupListeners();\n    }\n    if (!ref || ref?._hasListeners) return;\n    ref._hasListeners = true;\n    this.cleanupListeners = setPan(this.ref!, {\n      onPanEnd: () => {\n        const { prev, next } = this.sizes!;\n        onChange(prev, next);\n        this.sizes = undefined;\n      },\n      onPanStart: () => {},\n      onRelease: () => {\n        this.ref!.style.background = \"transparent\";\n      },\n      onPress: () => {\n        this.ref!.style.background = \"#00e1cc\";\n        vibrateFeedback(15);\n      },\n      onPan: (p) => {\n        if (!this.ref) throw \"ref missing\";\n        const { type } = this.props;\n        const { nextElementSibling, previousElementSibling, parentElement } =\n            this.ref,\n          nextS: HTMLElement = nextElementSibling as HTMLElement,\n          prevS: HTMLElement = previousElementSibling as HTMLElement,\n          prevRect = prevS.getBoundingClientRect(),\n          nextRect = nextS.getBoundingClientRect(),\n          leftOffset = prevRect.x,\n          rightOffset = nextRect.x + nextRect.width,\n          topOffset = prevRect.y,\n          bottomOffset = nextRect.y + nextRect.height;\n\n        let prevFlex, nextFlex;\n\n        const rWh = this.ref.getBoundingClientRect().width / 2,\n          rHh = this.ref.getBoundingClientRect().height / 2,\n          pFlex = +getComputedStyle(prevS).flexGrow,\n          nFlex = +getComputedStyle(nextS).flexGrow,\n          totalFlex = pFlex + nFlex;\n        if (type === \"row\") {\n          const pxPerFlex = (prevRect.width + nextRect.width) / totalFlex;\n\n          prevFlex = (p.x - leftOffset - rWh) / pxPerFlex;\n          nextFlex = totalFlex - prevFlex;\n        } else {\n          const pxPerFlex = (prevRect.height + nextRect.height) / totalFlex;\n\n          (prevFlex = (p.y - topOffset - rHh) / pxPerFlex),\n            (nextFlex = totalFlex - prevFlex);\n        }\n\n        prevS.style.flex = `${prevFlex}`;\n        nextS.style.flex = `${nextFlex}`;\n\n        this.sizes = {\n          prev: { id: prevS.dataset[\"boxId\"]!, size: prevFlex },\n          next: { id: nextS.dataset[\"boxId\"]!, size: nextFlex },\n        };\n      },\n    });\n  }\n\n  ref?: HTMLDivElement;\n  render() {\n    const isFixed = this.props.layoutMode === \"fixed\";\n    const size =\n      isFixed ? \"1em\"\n      : isTouchDevice() ? \"20px\"\n      : \"4px\";\n    const { type } = this.props,\n      style: React.CSSProperties = {\n        width: type === \"col\" ? \"100%\" : size,\n        height: type === \"col\" ? size : \"100%\",\n        cursor:\n          isFixed ? \"default\"\n          : type === \"col\" ? \"ns-resize\"\n          : \"ew-resize\",\n        opacity: 0.4,\n        zIndex: 2,\n        margin:\n          isTouchDevice() ?\n            type !== \"col\" ?\n              \"0 -8px\"\n            : \"-8px 0\"\n          : \"\",\n      };\n\n    return (\n      <div\n        ref={(r) => {\n          if (r) this.ref = r;\n        }}\n        style={style}\n        className={\"silver-grid-resizer noselect \"}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/SilverGrid/TreeBuilder.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unnecessary-condition */\n\nimport { isDefined } from \"../../utils/utils\";\nimport type { LayoutConfig, LayoutGroup, LayoutItem } from \"./SilverGrid\";\n\nexport type TreeLayout = LayoutConfig & { parent?: TreeLayout };\nexport class TreeBuilder {\n  layout?: LayoutConfig;\n  tree?: TreeLayout;\n  constructor(l: LayoutConfig, onChange: (newLayout: LayoutConfig) => void) {\n    this.build(l);\n    this.onChange = onChange;\n  }\n\n  private build(l: LayoutConfig) {\n    const _l = JSON.parse(JSON.stringify({ ...l }));\n    this.layout = _l;\n    this.tree = this.makeTree(_l);\n  }\n\n  private onChange = (newLayout: LayoutConfig) => {};\n\n  private makeTree = (l: LayoutConfig, lp?: LayoutConfig): TreeLayout => {\n    const res: TreeLayout = l;\n    if (lp) {\n      delete res.isRoot;\n      res.parent = lp;\n    } else {\n      res.isRoot = true;\n    }\n    if (\"items\" in l && l.items) {\n      l.items.forEach((sl) => {\n        this.makeTree(sl, res);\n      });\n    }\n\n    return res;\n  };\n\n  remove = (itemId: string, noChange = false) => {\n    const items = this._filter((d) => d.id === itemId && d.parent);\n\n    items.map((item) => {\n      (item.parent as LayoutGroup).items = (\n        item.parent as LayoutGroup\n      ).items.filter((d) => d !== item);\n    });\n\n    if (!noChange) {\n      this.onChange(this.getLayout());\n    }\n  };\n\n  getLayout = () => {\n    const removeP = (t: TreeLayout): LayoutConfig => {\n      delete t.parent;\n      if ((t as LayoutGroup).items) {\n        (t as LayoutGroup).items = (t as LayoutGroup).items.map(removeP);\n      }\n      return t;\n    };\n    const res = removeP({ ...this.tree! });\n    this.build(JSON.parse(JSON.stringify(res)) as LayoutConfig);\n    return JSON.parse(JSON.stringify(res));\n  };\n\n  _filter = (func: (item: TreeLayout) => any): TreeLayout[] => {\n    const filter = (t: TreeLayout) => {\n      let result: TreeLayout[] = [];\n      if (func(t)) {\n        result.push(t);\n      }\n      if ((t as LayoutGroup).items) {\n        (t as LayoutGroup).items.map((d) => {\n          result = result.concat(filter(d));\n        });\n      }\n      return result;\n    };\n\n    return filter(this.tree!);\n  };\n\n  refresh = (noChange = false) => {\n    this.build(this.getLayout());\n\n    /* Remove empty boxes */\n    const getEmptyItems = () =>\n      this._filter(\n        (t) => t.type !== \"item\" && t.parent && t.items && t.items.length === 0,\n      );\n    while (getEmptyItems().length) {\n      getEmptyItems().map((d) => {\n        (d.parent as LayoutGroup).items = (\n          d.parent as LayoutGroup\n        ).items.filter((dp) => dp.id != d.id);\n      });\n      const cleansedLayout = this.getLayout();\n      this.build(cleansedLayout);\n    }\n\n    const items = this._filter((t) => t.type === \"item\");\n\n    /* Unnest single items */\n    items.concat(items.map((d) => d.parent).filter(isDefined)).map((item) => {\n      let topParent = { ...item };\n      /** Climb to first parent that is root OR has more than one item */\n      while (\n        topParent.parent &&\n        topParent.parent?.type !== \"item\" &&\n        !topParent.parent.isRoot &&\n        topParent.parent.items.length <= 1\n      ) {\n        topParent = topParent.parent;\n      }\n      if (topParent.parent && topParent.parent?.type !== \"item\") {\n        topParent.parent.items = topParent.parent.items.map((d) => {\n          if (d === topParent) {\n            return {\n              ...item,\n              size: topParent.size ?? item.size,\n            };\n          }\n          return d;\n        });\n      }\n    });\n    const cleansedLayout = this.getLayout();\n    this.build(cleansedLayout);\n\n    /* Unnest redundant layout groups */\n    const getRedundant = () =>\n      this._filter((t) => Boolean(t.parent && t.parent.type === t.type));\n    while (getRedundant().length) {\n      getRedundant().map((p) => {\n        if (\n          p.parent &&\n          p.parent.type === p.type &&\n          p.type !== \"item\" &&\n          p.parent.type !== \"item\"\n        ) {\n          const idx = p.parent.items.findIndex((d) => d === p);\n          p.parent.items.splice(idx, 1, ...p.items);\n        }\n        this.build(this.getLayout());\n      });\n    }\n\n    if (!noChange) this.onChange(this.getLayout());\n  };\n\n  update = (newData: Partial<LayoutConfig>[]) => {\n    newData.map((nd) => {\n      const items = this._filter((d) => d.id == nd.id),\n        [item] = items;\n      if (item) {\n        Object.keys(nd).map((key) => {\n          item[key] = nd[key];\n        });\n      } else {\n        console.log(\"Found duplicate items. will not update\");\n      }\n    });\n    this.onChange(this.getLayout());\n  };\n\n  find = (itemId: string): TreeLayout | undefined => {\n    return this._filter((t) => t.id == itemId)[0];\n  };\n  getLeafs = (func: (t: TreeLayout) => any = () => true): TreeLayout[] => {\n    return this._filter(\n      (t) => !(\"items\" in t) || (!t.items.length && t.parent),\n    ).filter(func);\n  };\n  findFunc = (func: (t: TreeLayout) => any): TreeLayout | undefined => {\n    return this._filter(func)[0];\n  };\n\n  moveTo = (\n    sourceId: string,\n    targetId: string,\n    parentType: \"row\" | \"col\" | \"tab\",\n    insertBefore: boolean,\n  ) => {\n    const source = this.find(sourceId) as LayoutItem;\n    this.build(this.getLayout());\n    this.remove(sourceId, true);\n\n    let target = this.find(targetId);\n\n    if (source && target) {\n      if (!target.parent) {\n        /* Target is a group with required layout */\n        if (target.type === parentType && target.items) {\n          source.size =\n            target && target.items.length ? target.items[0]!.size : 50;\n          if (insertBefore) target.items.unshift(source);\n          else target.items.push(source);\n        } else {\n          source.size = 50;\n          target.size = 50;\n          target.isRoot = false;\n          target = {\n            id: Date.now().toString(),\n            size: 100,\n            isRoot: true,\n            type: parentType as any,\n            ...(parentType === \"tab\" && { activeTabKey: undefined }),\n            items: insertBefore ? [source, target] : [target, source],\n          };\n\n          this.layout = target;\n          const newTree = this.makeTree(target);\n          this.tree = newTree;\n        }\n\n        /* Target parent is a group with required layout */\n      } else {\n        const targetIdx = (target.parent as LayoutGroup).items.findIndex(\n          (d) => d.id == targetId,\n        );\n        if (target.parent.type === parentType) {\n          target.parent.items.splice(\n            insertBefore ? targetIdx : targetIdx + 1,\n            0,\n            source,\n          );\n          target.parent.items = target.parent.items.map((d) => {\n            d.size =\n              100 / Math.max(1, (target!.parent as LayoutGroup).items.length);\n            return d;\n          });\n\n          /* Target needs wrapping in the required layout */\n        } else {\n          source.size = 50;\n          target.size = 50;\n          //@ts-ignore\n          (target.parent as LayoutGroup).items[targetIdx] = {\n            id: Date.now().toString(),\n            type: parentType,\n            size:\n              Math.min(\n                ...(target.parent as LayoutGroup).items.map((d) => d.size),\n              ) || 50,\n            items:\n              insertBefore ?\n                [source, target as LayoutItem]\n              : [target as LayoutItem, source],\n          };\n          target = {\n            id: Date.now().toString(),\n            size: target.size || 50,\n            type: parentType as any,\n            ...(parentType === \"tab\" && { activeTabKey: undefined }),\n            items:\n              insertBefore ?\n                [source, target as LayoutItem]\n              : [target as LayoutItem, source],\n          };\n        }\n      }\n    }\n\n    this.refresh();\n  };\n}\n"
  },
  {
    "path": "client/src/dashboard/SmartCard/SmartCard.tsx",
    "content": "import type { DetailedFilterBase } from \"@common/filterUtils\";\nimport { classOverride } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport {\n  type AnyObject,\n  type TableInfo,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport type { SmartCardListProps } from \"../SmartCardList/SmartCardList\";\nimport type { SmartFormProps } from \"../SmartForm/SmartForm\";\nimport { RenderValue } from \"../SmartForm/SmartFormField/RenderValue\";\nimport { SmartCardActions } from \"./SmartCardActions\";\nimport { SmartCardColumn } from \"./SmartCardColumn\";\nimport { useFieldConfigParser } from \"./useFieldConfigParser\";\n\ntype NestedSmartCardProps = Pick<SmartCardProps, \"footer\" | \"excludeNulls\">;\ntype NestedSmartFormProps = Pick<\n  SmartFormProps,\n  \"hideNullBtn\" | \"enableInsert\" | \"insertBtnText\" | \"label\" | \"onSuccess\"\n>;\n\nexport type FieldConfigTable = FieldConfigBase & {\n  fieldConfigs: FieldConfigNested[];\n  tableInfo: TableInfo;\n  tableColumns: ValidatedColumnInfo[];\n  getRowFilter?: (row: AnyObject) => AnyObject;\n  select?: string | AnyObject;\n  render?: (rows: AnyObject[]) => React.ReactNode;\n  smartFormProps?: NestedSmartFormProps;\n  smartCardProps?: NestedSmartCardProps;\n};\n\nexport type FieldConfigNested = FieldConfig<any> | FieldConfigTable;\n\nexport type ParsedNestedFieldConfig = ParsedFieldConfig | FieldConfigTable;\n\nexport type FieldConfigBase<T extends AnyObject | void = void> = {\n  /* Is the column or table name */\n  name: T extends AnyObject ? keyof T : string;\n  style?: React.CSSProperties;\n  className?: string;\n\n  hide?: boolean;\n  label?: string;\n};\n\nexport type FieldConfigRender<T extends AnyObject = AnyObject> = (\n  value: any,\n  row: T,\n) => React.ReactNode;\n\nexport type ParsedFieldConfig<T extends AnyObject = AnyObject> =\n  FieldConfigBase<T> & {\n    // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents\n    select?: \"*\" | number | AnyObject | keyof T;\n    hideIf?: (value, row: T) => boolean;\n    render?: FieldConfigRender<T>;\n    /**\n     * Defaults to \"value\"\n     */\n    renderMode?: \"valueNode\" | \"value\" | \"full\";\n  };\n\nexport type FieldConfig<T extends AnyObject = AnyObject> =\n  | string\n  | ParsedFieldConfig<T>;\n\nexport type SmartCardCommonProps = {};\n\nexport type SmartCardProps<T extends AnyObject = AnyObject> = Pick<\n  Prgl,\n  \"db\" | \"tables\" | \"methods\"\n> &\n  Pick<SmartCardListProps<T>, \"tableName\" | \"tables\"> & {\n    defaultData: T;\n    rowFilter?: DetailedFilterBase[];\n\n    columns?: ValidatedColumnInfo[];\n\n    className?: string;\n    style?: React.CSSProperties;\n    contentClassname?: string;\n    contentStyle?: React.CSSProperties;\n    variant?: \"row\" | \"col\" | \"row-wrap\";\n\n    confirmUpdates?: boolean;\n    showLocalChanges?: boolean;\n\n    onChange?: (newData: AnyObject) => any;\n\n    hideColumns?: string[];\n\n    /**\n     * Used in:\n     * Changing how table columns are displayed\n     * Displaying additional custom computed columns\n     */\n    fieldConfigs?: FieldConfig<T>[] | string[];\n\n    title?: (row: T) => React.ReactNode;\n    footer?: (row: T) => React.ReactNode;\n    getActions?: (row: T) => React.ReactNode;\n\n    enableInsert?: boolean;\n\n    /**\n     * If true then will not displaye fields with null values\n     * */\n    excludeNulls?: boolean;\n\n    smartFormProps?: NestedSmartFormProps;\n\n    popupFixedStyle?: React.CSSProperties;\n\n    showViewEditBtn?: boolean;\n  };\n\nconst DEFAULT_VARIANT = \"row-wrap\";\n\nexport const SmartCard = <T extends AnyObject>(props: SmartCardProps<T>) => {\n  const {\n    className = \"\",\n    style = {},\n    fieldConfigs: _fieldConfigs,\n    footer = null,\n    title,\n    defaultData,\n    contentClassname = \"\",\n    contentStyle = {},\n    showViewEditBtn = true,\n    enableInsert = true,\n  } = props;\n\n  const [variant, setVariant] = useState(props.variant ?? DEFAULT_VARIANT);\n  const parsedFields = useFieldConfigParser(props as SmartCardProps);\n\n  if (!parsedFields) {\n    return <Loading />;\n  }\n  const { cardColumns, fieldConfigsWithColumns } = parsedFields;\n\n  const variantClass =\n    variant === \"row-wrap\" ? \"flex-row-wrap ai-start\"\n    : variant === \"row\" ? \"flex-row ai-start\"\n    : \"flex-col ai-start \";\n\n  return (\n    <div\n      className={classOverride(\n        `SmartCard card bg-color-0 relative ${variantClass} `,\n        className,\n      )}\n      style={{ padding: \".25em\", ...style }}\n    >\n      <div className=\"flex-col min-w-0 min-h-0 f-1\">\n        {title?.(defaultData)}\n        <div\n          className={classOverride(\n            `SmartCardContent o-auto f-1 min-w-0 min-h-0 gap-p5 p-p5 parent-hover ${variantClass}`,\n            contentClassname,\n          )}\n          style={{ columnGap: \"1em\", ...contentStyle }}\n        >\n          {fieldConfigsWithColumns.map(({ name, fc, col: column }, i) => {\n            const labelText = fc.label ?? column?.label ?? column?.name ?? null;\n\n            const valueNode =\n              fc.render?.(defaultData[name], defaultData) ||\n              (column && (\n                <RenderValue column={column} value={defaultData[name]} />\n              ));\n\n            if (fc.renderMode === \"full\") {\n              return <React.Fragment key={fc.name}>{valueNode}</React.Fragment>;\n            }\n            return (\n              <SmartCardColumn\n                key={`${fc.name}`}\n                className={fc.className}\n                style={fc.style}\n                labelText={labelText}\n                valueNode={valueNode}\n                renderMode={fc.renderMode ?? \"value\"}\n                labelTitle={column?.udt_name || \"\"}\n                info={column?.hint}\n              />\n            );\n          })}\n        </div>\n        {footer?.(defaultData)}\n      </div>\n      <SmartCardActions\n        {...props}\n        enableInsert={enableInsert}\n        showViewEditBtn={showViewEditBtn}\n        cardColumns={cardColumns}\n        defaultData={defaultData}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCard/SmartCardActions.tsx",
    "content": "import { mdiPencil, mdiResize } from \"@mdi/js\";\nimport { type AnyObject, type ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useMemo, useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { SmartForm } from \"../SmartForm/SmartForm\";\nimport type { SmartCardProps } from \"./SmartCard\";\n\nexport const SmartCardActions = <T extends AnyObject>(\n  props: SmartCardProps<T> & {\n    defaultData: AnyObject;\n    cardColumns: ValidatedColumnInfo[];\n  },\n) => {\n  const {\n    getActions,\n    tableName,\n    db,\n    tables,\n    onChange,\n    showViewEditBtn,\n    methods,\n    smartFormProps,\n    enableInsert,\n    defaultData,\n    rowFilter,\n    cardColumns,\n  } = props;\n  const [editMode, setEditMode] = useState(false);\n\n  const localRowFilter = useMemo(() => {\n    if (rowFilter) return rowFilter;\n    const hasPkeys = cardColumns.some((c) => c.is_pkey);\n    const pkeyCols = cardColumns.filter(\n      (c) =>\n        (hasPkeys ? c.is_pkey : c.references?.length) && defaultData[c.name],\n    );\n    if (pkeyCols.some((c) => defaultData[c.name])) {\n      return pkeyCols.map((c) => ({\n        fieldName: c.name,\n        value: defaultData[c.name],\n      }));\n    }\n    return rowFilter;\n  }, [cardColumns, defaultData, rowFilter]);\n\n  const tableHandler =\n    typeof tableName === \"string\" ? db[tableName] : undefined;\n  const allowedActions = {\n    view: showViewEditBtn && Boolean(tableHandler?.find && localRowFilter),\n    delete: showViewEditBtn && Boolean(tableHandler?.delete && localRowFilter),\n    update:\n      showViewEditBtn &&\n      (Boolean(onChange) || Boolean(tableHandler?.update && localRowFilter)),\n    insert: Boolean(tableHandler?.insert),\n  };\n\n  const showViewEditRow =\n    allowedActions.update || allowedActions.delete || allowedActions.view;\n\n  const extraActions = getActions?.(defaultData);\n  if (!showViewEditRow && !extraActions) {\n    return null;\n  }\n\n  return (\n    <FlexCol\n      className=\"show-on-parent-hover rounded\"\n      style={{\n        position: \"absolute\",\n        right: 0,\n        top: 0,\n        backdropFilter: \"blur(4px)\",\n      }}\n    >\n      {showViewEditRow && (\n        <Btn\n          className=\"f-0 \"\n          data-command=\"SmartCard.viewEditRow\"\n          iconPath={\n            allowedActions.update || allowedActions.delete ?\n              mdiPencil\n            : mdiResize\n          }\n          onClick={(e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            setEditMode(true);\n          }}\n        />\n      )}\n      {extraActions}\n      {editMode && typeof tableName === \"string\" && (\n        <SmartForm\n          db={db}\n          tables={tables}\n          methods={methods}\n          asPopup={true}\n          tableName={tableName}\n          onChange={onChange}\n          rowFilter={localRowFilter}\n          confirmUpdates={true}\n          enableInsert={enableInsert}\n          {...smartFormProps}\n          onClose={() => {\n            setEditMode(false);\n          }}\n        />\n      )}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCard/SmartCardColumn.tsx",
    "content": "import React from \"react\";\nimport { classOverride } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport type { ParsedFieldConfig } from \"./SmartCard\";\n\ntype SmartCardColumnProps = {\n  className?: string;\n  style?: React.CSSProperties;\n  labelText: string | undefined | null;\n  info: React.ReactNode | undefined;\n  labelTitle: string | undefined;\n  valueNode: React.ReactNode;\n  renderMode: Exclude<ParsedFieldConfig[\"renderMode\"], \"full\" | undefined>;\n};\nexport const SmartCardColumn = ({\n  labelText,\n  className,\n  style,\n  info,\n  labelTitle,\n  valueNode,\n  renderMode,\n}: SmartCardColumnProps) => {\n  const valueNodeWrapped = renderMode === \"value\";\n  return (\n    <div\n      className={classOverride(\n        \"SmartCardCol flex-col o-auto ai-start text-1 ta-left \",\n        className,\n      )}\n      style={{ maxHeight: \"250px\", ...style }}\n    >\n      {Boolean(labelText?.length) && (\n        <Label\n          size=\"small\"\n          variant=\"normal\"\n          title={labelTitle}\n          info={info}\n          style={{ opacity: 0.7 }}\n        >\n          {labelText}\n        </Label>\n      )}\n      {valueNodeWrapped ?\n        <div className=\"font-16 text-0 o-auto\">{valueNode}</div>\n      : valueNode}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCard/getSelectForFieldConfigs.ts",
    "content": "import type { ValidatedColumnInfo } from \"prostgles-types\";\nimport type { FieldConfig } from \"./SmartCard\";\n\nexport const getSelectForFieldConfigs = (\n  fieldConfigs?: FieldConfig<any>[],\n  columns?: ValidatedColumnInfo[],\n) => {\n  if (!fieldConfigs) return \"*\";\n  const result = fieldConfigs\n    .filter((fieldConfig) => {\n      if (columns) {\n        if (\n          columns.some((c) =>\n            typeof fieldConfig === \"string\" ?\n              c.name === fieldConfig\n            : fieldConfig.select || fieldConfig.name === c.name,\n          )\n        ) {\n          return true;\n        }\n        console.warn(\"Bad/invalid column name provided: \", fieldConfig);\n        return false;\n      }\n      return true;\n    })\n    .reduce(\n      (a, v) => ({\n        ...a,\n        [typeof v === \"string\" ? v : v.name]:\n          typeof v === \"string\" || !v.select ? 1 : v.select,\n      }),\n      {},\n    );\n\n  const pKeyCols = columns?.filter((c) => c.is_pkey);\n  if (pKeyCols?.length) {\n    return {\n      ...Object.fromEntries(pKeyCols.map(({ name }) => [name, 1])),\n      ...result,\n    };\n  }\n\n  return result;\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCard/getSmartCardColumns.ts",
    "content": "import type { AnyObject, ValidatedColumnInfo } from \"prostgles-types\";\nimport type { SmartCardListProps } from \"../SmartCardList/SmartCardList\";\n\ntype Args = Pick<SmartCardListProps<AnyObject>, \"tableName\" | \"db\">;\nexport const getSmartCardColumns = async ({\n  tableName,\n  db,\n}: Args): Promise<ValidatedColumnInfo[] | undefined> => {\n  if (typeof tableName === \"string\") {\n    const tableHandler = db[tableName];\n    return tableHandler?.getColumns?.();\n  }\n\n  const sqlRes = await db.sql?.(tableName.sqlQuery, tableName.args ?? {});\n  return sqlRes?.fields.map((f, i) => ({\n    ...f,\n    label: f.columnName ?? f.name,\n    is_generated: false,\n    comment: \"\",\n    ordinal_position: i + 1,\n    is_nullable: false,\n    is_updatable: false,\n    data_type: f.dataType,\n    element_type: \"\",\n    element_udt_name: \"\",\n    is_pkey: false,\n    has_default: false,\n    select: false,\n    orderBy: true,\n    filter: true,\n    insert: false,\n    update: false,\n    delete: false,\n  }));\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCard/parseFieldConfigs.tsx",
    "content": "import {\n  isDefined,\n  isObject,\n  type AnyObject,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport type {\n  FieldConfigNested,\n  ParsedFieldConfig,\n  ParsedNestedFieldConfig,\n} from \"./SmartCard\";\nimport React from \"react\";\nimport { RenderValue } from \"../SmartForm/SmartFormField/RenderValue\";\nimport { Checkbox } from \"@components/Checkbox\";\nimport type { DBSchemaTableWJoins } from \"../Dashboard/dashboardUtils\";\nimport { SvgIconFromURL } from \"@components/SvgIcon\";\nexport const parseFieldConfigs = (\n  configs?: FieldConfigNested[],\n  cols?: ValidatedColumnInfo[],\n  tables: DBSchemaTableWJoins[] = [],\n  // table: DBSchemaTableWJoins,\n): undefined | ParsedFieldConfig[] | ParsedNestedFieldConfig[] => {\n  if (configs) {\n    if ((configs as any) === \"*\") configs = [\"*\"];\n    if (!Array.isArray(configs)) throw \"Expecting an array of fieldConfigs\";\n    const result: ParsedFieldConfig[] | ParsedNestedFieldConfig[] = configs\n      .slice(0)\n      .flatMap((fc) => {\n        if (typeof fc === \"string\") {\n          if (cols) {\n            return getDefaultFieldConfig(cols, [fc]);\n          } else {\n            return { name: fc };\n          }\n        } else {\n          const { select, hide, render, ...restdw } = fc;\n          if (!hide && !render && isObject(select) && tables.length) {\n            const ftable = tables.find((t) => t.name === fc.name);\n            if (ftable) {\n              const { rowIconColumn } = ftable;\n              if (rowIconColumn && select[rowIconColumn] === 1) {\n                return {\n                  ...(fc as ParsedFieldConfig),\n                  render: (ftableRows: AnyObject[]) => {\n                    const logo_url = ftableRows[0]?.[rowIconColumn];\n                    if (!logo_url) return null;\n                    return (\n                      <SvgIconFromURL\n                        url={logo_url}\n                        style={{ width: \"32px\", height: \"32px\" }}\n                      />\n                    );\n                  },\n                };\n              }\n            }\n          }\n          return fc as ParsedFieldConfig;\n        }\n      })\n      .filter(isDefined);\n    const duplicate = result.find((f, i) =>\n      result.find((_f, _i) => f.name === _f.name && i !== _i),\n    );\n    if (duplicate) {\n      console.log(\"Duplicate field config name found: \" + duplicate.name);\n    }\n    return result;\n  } else {\n    /** Show all ? */\n  }\n\n  return undefined;\n};\n\nexport const getDefaultFieldConfig = (\n  cols: Pick<\n    ValidatedColumnInfo,\n    \"name\" | \"label\" | \"udt_name\" | \"tsDataType\" | \"references\"\n  >[] = [],\n  colNames?: string[],\n): ParsedNestedFieldConfig[] => {\n  let _fieldConfigs: FieldConfigNested[] | string[] | undefined = colNames;\n  /** Select utils */\n  if (colNames) {\n    _fieldConfigs = colNames.slice(0);\n\n    const allFieldsIndex = _fieldConfigs.findIndex((c) => c === \"*\");\n    if (allFieldsIndex > -1) {\n      _fieldConfigs.splice(allFieldsIndex, 0, ...cols.map((c) => c.name));\n    }\n    const allTablesIndex = _fieldConfigs.findIndex((c) => c === \"**\");\n    if (allTablesIndex > -1) {\n      _fieldConfigs.splice(\n        allTablesIndex,\n        0,\n        ...cols\n          .filter((c) => c.references?.length)\n          .flatMap((c) =>\n            c.references!.map((r) => ({\n              name: r.ftable,\n              fieldConfigs: [\"*\"],\n            })),\n          ),\n      );\n    }\n    _fieldConfigs = _fieldConfigs.filter(\n      (c) => ![\"*\", \"**\"].includes(c as string),\n    );\n  }\n\n  const getColConfig = (c: (typeof cols)[number]) => ({\n    name: c.name,\n    label: c.label,\n    render:\n      [\"postcode\", \"post_code\"].includes(c.name) ?\n        (addr: string) =>\n          !addr ?\n            <RenderValue column={c} value={addr} />\n          : <a\n              className=\"flex-col\"\n              target=\"_blank\"\n              href={`https://www.google.com/maps/search/${addr}`}\n              rel=\"noreferrer\"\n            >\n              <span>\n                <RenderValue column={c} value={addr} />\n              </span>\n            </a>\n      : c.tsDataType === \"boolean\" ?\n        (val: boolean | null) => (\n          <Checkbox\n            title={(val || \"NULL\").toString()}\n            checked={!!val}\n            className=\"no-pointer-events\"\n            readOnly={true}\n          />\n        )\n      : undefined,\n  });\n\n  const result =\n    _fieldConfigs ?\n      _fieldConfigs\n        .map((fc: any) => {\n          /** Is nested config. Return as is */\n          if (typeof fc !== \"string\") {\n            return fc;\n            /** Is colname. Find column info and prepare */\n          } else {\n            const c = cols.find((c) => c.name === fc);\n            if (!c) {\n              console.error(\n                `Could not find column ${fc}. Incorrect name or not allowed`,\n              );\n            } else {\n              return getColConfig(c);\n            }\n          }\n        })\n        .filter((c) => c)\n    : cols.map((c) => getColConfig(c));\n\n  return result;\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCard/useFieldConfigParser.ts",
    "content": "import React from \"react\";\nimport type { ParsedFieldConfig, SmartCardProps } from \"./SmartCard\";\nimport { usePromise } from \"prostgles-client\";\nimport { getSmartCardColumns } from \"./getSmartCardColumns\";\nimport { getDefaultFieldConfig, parseFieldConfigs } from \"./parseFieldConfigs\";\nimport { isDefined } from \"../../utils/utils\";\n\nexport const useFieldConfigParser = (props: SmartCardProps) => {\n  const {\n    fieldConfigs: _fieldConfigs,\n    tableName,\n    db,\n    columns: columnsFromProps,\n    hideColumns,\n    tables,\n    excludeNulls,\n    defaultData,\n  } = props;\n\n  const fetchedColumns = usePromise(async () => {\n    if (columnsFromProps) return undefined;\n    return await getSmartCardColumns({ tableName, db });\n  }, [columnsFromProps, tableName, db]);\n  const cardColumns = columnsFromProps ?? fetchedColumns;\n\n  const displayedColumns =\n    hideColumns ?\n      cardColumns?.filter((c) => hideColumns.includes(c.name))\n    : cardColumns;\n\n  const fieldConfigs =\n    parseFieldConfigs(_fieldConfigs, undefined, tables) ||\n    getDefaultFieldConfig(displayedColumns);\n\n  const fieldConfigsWithColumns = fieldConfigs\n    .filter((fc) => !fc.hide)\n    .map((fc: ParsedFieldConfig) => ({\n      name: fc.name.toString(),\n      col: displayedColumns?.find((c) => fc.name === c.name),\n      fc,\n    })) /** Do not render if has nulls and no render and excludeNulls is true  */\n    .filter(\n      ({ name, fc }) =>\n        !fc.hideIf?.(defaultData[name], defaultData) &&\n        (fc.render ||\n          !excludeNulls ||\n          (defaultData[name] !== null && isDefined(defaultData[name]))),\n    );\n\n  return (\n    cardColumns && {\n      cardColumns,\n      fieldConfigsWithColumns,\n    }\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCardList/SmartCardList.tsx",
    "content": "import React, { useMemo } from \"react\";\n\nimport type { DetailedFilter } from \"@common/filterUtils\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { classOverride } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { Pagination, usePagination } from \"@components/Table/Pagination\";\nimport type {\n  AnyObject,\n  FilterItem,\n  ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport FlipMove from \"react-flip-move\";\nimport type { Prgl } from \"../../App\";\nimport type { TestSelectors } from \"../../Testing\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { FieldConfig, SmartCardProps } from \"../SmartCard/SmartCard\";\nimport { SmartCard } from \"../SmartCard/SmartCard\";\nimport type { InsertButtonProps } from \"../SmartForm/InsertButton\";\nimport type { SmartFormProps } from \"../SmartForm/SmartForm\";\nimport type { ColumnSort } from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport { SmartCardListHeaderControls } from \"./SmartCardListHeaderControls\";\nimport { useSmartCardListState } from \"./useSmartCardListState\";\n\nexport type SmartCardListProps<T extends AnyObject = AnyObject> = Pick<\n  Prgl,\n  \"db\" | \"tables\" | \"methods\"\n> & {\n  tableName:\n    | string\n    | {\n        sqlQuery: string;\n        dataAge?: string | number;\n        args?: Record<string, any>;\n      };\n  columns?: ValidatedColumnInfo[];\n\n  className?: string;\n  style?: React.CSSProperties;\n  variant?: \"row\" | \"col\" | \"row-wrap\";\n\n  hideColumns?: string[];\n\n  /**\n   * Used in:\n   * Changing how table columns are displayed\n   * Displaying additional custom computed columns\n   */\n  fieldConfigs?: FieldConfig<T>[];\n\n  title?: React.ReactNode | ((args: { count: number }) => React.ReactNode);\n  getRowFooter?: (row: T) => React.ReactNode | React.JSX.Element;\n  footer?: React.ReactNode;\n\n  /**\n   * If true then will not displaye fields with null values\n   * */\n  excludeNulls?: boolean;\n\n  tables: CommonWindowProps[\"tables\"];\n  popupFixedStyle?: React.CSSProperties;\n  noDataComponent?: React.ReactNode;\n  /**\n   * If no data AND set to \"hide-all\" will return only noDataComponent (or nothing)\n   * */\n  noDataComponentMode?: \"hide-all\";\n\n  btnColor?: \"gray\";\n\n  showTopBar?:\n    | boolean\n    | {\n        leftContent?: React.ReactNode;\n        insert?:\n          | true\n          | Pick<\n              InsertButtonProps,\n              \"buttonProps\" | \"defaultData\" | \"fixedData\"\n            >;\n        sort?: true;\n      };\n  rowProps?: {\n    style?: React.CSSProperties;\n    className?: string;\n  };\n  onSuccess?: SmartFormProps[\"onSuccess\"];\n  enableListAnimations?: boolean;\n  getActions?: SmartCardProps<T>[\"getActions\"];\n  /**\n   * Show top N records\n   * Defaults to 20\n   */\n  limit?: number;\n  filter?:\n    | AnyObject\n    | FilterItem<T>\n    | { $and: FilterItem<T>[] }\n    | { $or: FilterItem<T>[] };\n  searchFilter?: DetailedFilter[];\n  orderBy?: ColumnSort | ColumnSort[];\n  realtime?: boolean;\n  throttle?: number;\n  orderByfields?: string[];\n  showEdit?: boolean;\n  onSetData?: (items: AnyObject[]) => void;\n} & Pick<TestSelectors, \"data-command\">;\n\nexport const SmartCardList = <T extends AnyObject>(\n  props: SmartCardListProps<T>,\n) => {\n  const {\n    tableName,\n    db,\n    methods,\n    tables,\n    className = \"\",\n    style = {},\n    popupFixedStyle,\n    fieldConfigs: _fieldConfigs,\n    getRowFooter,\n    excludeNulls,\n    footer,\n    rowProps,\n    noDataComponentMode,\n    noDataComponent,\n    onSuccess,\n    enableListAnimations = false,\n    getActions,\n    limit = 25,\n    \"data-command\": dataCommand = \"SmartCardList\",\n  } = props;\n\n  const paginationState = usePagination(limit);\n\n  const state = useSmartCardListState({\n    ...props,\n    fieldConfigs: _fieldConfigs as FieldConfig<AnyObject>[],\n    limit: paginationState.limit,\n    offset: paginationState.offset,\n  });\n  const { columns, loading, items, error, loaded, totalRows, tableControls } =\n    state;\n  const { keyCols } = useGetRowKeyCols(columns ?? [], items?.[0] ?? {});\n\n  const smartCardListStyle = useSmartCardListStyle(style);\n\n  if (error) return <ErrorComponent error={error} />;\n\n  if (!columns || !items) return <Loading />;\n\n  if (!loaded) {\n    return null;\n  }\n\n  /** Used to prevent subsequent flickers during filter change if no items */\n  const showNoDataComponent = noDataComponent && !items.length;\n  if (showNoDataComponent && noDataComponentMode === \"hide-all\") {\n    return noDataComponent;\n  }\n\n  return (\n    <ScrollFade\n      className={classOverride(\n        \"SmartCardList o-auto flex-col gap-p5 relative max-w-full\",\n        className,\n      )}\n      data-command={dataCommand}\n      style={smartCardListStyle}\n    >\n      {loading && <Loading variant=\"cover\" />}\n\n      {tableControls && (\n        <SmartCardListHeaderControls\n          {...(props as SmartCardListProps)}\n          itemsLength={items.length}\n          totalRows={totalRows}\n          columns={columns}\n          tableControls={state.tableControls}\n        />\n      )}\n      <MaybeFlipMove\n        className=\"flex-col gap-p5 relative\"\n        flipMove={enableListAnimations}\n      >\n        {showNoDataComponent ?\n          noDataComponent\n        : items.map((defaultData, i) => {\n            const key = getKeyForRowData(defaultData, keyCols);\n            return (\n              /** SmartCard wrapped in div to ensure MaybeFlipMove works */\n              <div className=\"relative\" key={key} data-key={key}>\n                <SmartCard\n                  key={key}\n                  contentClassname={rowProps?.className}\n                  contentStyle={rowProps?.style}\n                  db={db}\n                  methods={methods}\n                  tables={tables}\n                  tableName={tableName}\n                  defaultData={defaultData as T}\n                  columns={columns}\n                  excludeNulls={excludeNulls}\n                  popupFixedStyle={popupFixedStyle}\n                  fieldConfigs={_fieldConfigs}\n                  footer={getRowFooter}\n                  getActions={getActions}\n                  smartFormProps={{ onSuccess }}\n                  showViewEditBtn={\n                    \"showEdit\" in props ? props.showEdit : undefined\n                  }\n                />\n              </div>\n            );\n          })\n        }\n      </MaybeFlipMove>\n      <Pagination {...paginationState} totalRows={totalRows} />\n      {footer}\n    </ScrollFade>\n  );\n};\n\nconst MaybeFlipMove = ({\n  children,\n  flipMove,\n  className,\n}: {\n  children: React.ReactNode;\n  flipMove: boolean;\n  className?: string;\n}) => {\n  if (flipMove) {\n    return <FlipMove className={className}>{children}</FlipMove>;\n  }\n  return children;\n};\n\nconst getCols = (cols: ValidatedColumnInfo[], row: AnyObject) => {\n  const pKeyCols = cols.filter((c) => c.is_pkey);\n  if (pKeyCols.every((c) => row[c.name])) {\n    return pKeyCols;\n  }\n  return cols.filter((c) => {\n    return (\n      !c.is_nullable &&\n      (c.tsDataType === \"string\" || c.tsDataType === \"number\") &&\n      row[c.name] !== undefined\n    );\n  });\n};\n\nexport const getKeyForRowData = (\n  row: AnyObject,\n  keyCols: ValidatedColumnInfo[],\n) =>\n  !keyCols.length ?\n    JSON.stringify(row)\n  : keyCols.map((c) => row[c.name]?.toString()).join(\"-\");\n\nexport const useGetRowKeyCols = (\n  cols: ValidatedColumnInfo[],\n  row: AnyObject,\n) => {\n  return useMemo(() => {\n    const keyCols = getCols(cols, row);\n    return {\n      keyCols,\n    };\n  }, [cols, row]);\n};\n\nexport const useSmartCardListStyle = (style: React.CSSProperties) =>\n  useMemo(\n    () => ({\n      ...style,\n      /**\n       * To ensure shadow is not clipped by parent\n       */\n      padding: \"2px\",\n      margin: \"-2px\",\n      flex: \"0 1 auto\", // Allow the body to grow with content, ensuring height is always not greater than content\n    }),\n    [style],\n  );\n"
  },
  {
    "path": "client/src/dashboard/SmartCardList/SmartCardListHeaderControls.tsx",
    "content": "import { isObject, type ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { RenderFilter, type RenderFilterProps } from \"../RenderFilter\";\nimport { SortByControl } from \"../SmartFilter/SortByControl\";\nimport { SmartFilterBarSearch } from \"../SmartFilterBar/SmartFilterBarSearch\";\nimport { InsertButton } from \"../SmartForm/InsertButton\";\nimport type { SmartCardListProps } from \"./SmartCardList\";\nimport type { SmartCardListState } from \"./useSmartCardListState\";\n\nexport const SmartCardListHeaderControls = (\n  props: SmartCardListProps & {\n    totalRows: number | undefined;\n    itemsLength: number | undefined;\n    columns: ValidatedColumnInfo[];\n    tableControls: SmartCardListState[\"tableControls\"];\n  },\n) => {\n  const {\n    title,\n    totalRows,\n    db,\n    tables,\n    methods,\n    tableControls,\n    showTopBar = true,\n  } = props;\n\n  const titleNode =\n    typeof title === \"string\" ? <h4 className=\"m-0\">{title}</h4>\n    : typeof title === \"function\" ? title({ count: totalRows ?? -1 })\n    : title;\n  const showSearch = tableControls?.localFilter?.length ? true : tableControls;\n\n  const filterProps = useMemo(() => {\n    if (!tableControls || !tableControls.localFilter?.length) return;\n    return {\n      tableName: tableControls.tableName,\n      filter: { $and: tableControls.localFilter },\n      onChange: (newf) => {\n        const items = \"$and\" in newf ? newf.$and : newf.$or;\n        tableControls.setLocalFilter(items);\n      },\n    } satisfies Pick<RenderFilterProps, \"filter\" | \"onChange\" | \"tableName\">;\n  }, [tableControls]);\n\n  const showSort = isObject(showTopBar) ? showTopBar.sort : showTopBar;\n  if (\n    !showTopBar &&\n    !titleNode &&\n    !showSearch &&\n    !tableControls?.willShowInsert\n  ) {\n    return null;\n  }\n  return (\n    <FlexCol className=\"SmartCardListControls gap-p5 aid-end py-p25\">\n      {titleNode}\n\n      {showTopBar && (\n        <FlexRowWrap className=\" \">\n          {isObject(showTopBar) && showTopBar.leftContent}\n          {tableControls?.willShowInsert && (\n            <InsertButton\n              buttonProps={{\n                children: \"Add\",\n              }}\n              {...(isObject(showTopBar) && isObject(showTopBar.insert) ?\n                showTopBar.insert\n              : {})}\n              db={db}\n              tables={tables}\n              methods={methods}\n              tableName={tableControls.tableName}\n            />\n          )}\n          {showSearch && tableControls && (\n            <SmartFilterBarSearch\n              db={db}\n              tableName={tableControls.tableName}\n              tables={tables}\n              onFilterChange={tableControls.setLocalFilter}\n              filter={tableControls.localFilter ?? []}\n              extraFilters={undefined}\n              style={{\n                width: \"unset\",\n                margin: \"unset\",\n                flex: \"1\",\n              }}\n            />\n          )}\n          {tableControls?.setLocalOrderBy && showSort && (\n            <SortByControl\n              value={tableControls.localOrderBy}\n              columns={props.columns}\n              onChange={tableControls.setLocalOrderBy}\n            />\n          )}\n        </FlexRowWrap>\n      )}\n\n      {filterProps && (\n        <RenderFilter\n          db={db}\n          contextData={undefined}\n          tables={tables}\n          selectedColumns={undefined}\n          itemName={\"filter\"}\n          hideOperand={true}\n          {...filterProps}\n        />\n      )}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCardList/SmartCardListJoinedNewRecords.tsx",
    "content": "import React from \"react\";\n\nimport Btn from \"@components/Btn\";\nimport { classOverride, FlexCol } from \"@components/Flex\";\nimport { mdiDelete } from \"@mdi/js\";\nimport type { AnyObject } from \"prostgles-types\";\nimport type { Prgl } from \"../../App\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { DBSchemaTableWJoins } from \"../Dashboard/dashboardUtils\";\nimport { SmartCard } from \"../SmartCard/SmartCard\";\nimport type { SmartFormProps } from \"../SmartForm/SmartForm\";\nimport {\n  getKeyForRowData,\n  useGetRowKeyCols,\n  useSmartCardListStyle,\n  type SmartCardListProps,\n} from \"./SmartCardList\";\n\nexport type P = Pick<Prgl, \"db\" | \"tables\" | \"methods\"> &\n  Pick<SmartCardListProps, \"noDataComponent\" | \"noDataComponentMode\"> & {\n    className?: string;\n    style?: React.CSSProperties;\n    excludeNulls?: boolean;\n    tables: CommonWindowProps[\"tables\"];\n    onSuccess: SmartFormProps[\"onSuccess\"];\n    table: DBSchemaTableWJoins;\n    data: AnyObject[];\n    onChange: (newData: AnyObject[]) => void;\n  };\n\nexport const SmartCardListJoinedNewRecords = (props: P) => {\n  const {\n    db,\n    methods,\n    tables,\n    className = \"\",\n    style = {},\n    excludeNulls,\n    onSuccess,\n    onChange,\n    data,\n    table,\n    noDataComponent,\n    noDataComponentMode,\n  } = props;\n  const smartCardListStyle = useSmartCardListStyle(style);\n\n  const { keyCols } = useGetRowKeyCols(table.columns, data[0] ?? {});\n\n  /** Used to prevent subsequent flickers during filter change if no items */\n  const showNoDataComponent = noDataComponent && !data.length;\n  if (showNoDataComponent && noDataComponentMode === \"hide-all\") {\n    return noDataComponent;\n  }\n\n  return (\n    <FlexCol\n      className={classOverride(\n        \"SmartCardList o-auto gap-p5 relative \",\n        className,\n      )}\n      data-command=\"SmartCardList\"\n      style={smartCardListStyle}\n    >\n      {data.map((defaultData, i) => {\n        return (\n          <div key={getKeyForRowData(defaultData, keyCols)}>\n            <SmartCard\n              db={db}\n              methods={methods}\n              tables={tables}\n              tableName={table.name}\n              defaultData={defaultData}\n              columns={table.columns}\n              excludeNulls={excludeNulls}\n              smartFormProps={{ onSuccess }}\n            />\n            <Btn\n              iconPath={mdiDelete}\n              color=\"danger\"\n              className=\"absolute\"\n              style={{ top: \"5px\", right: \"5px\" }}\n              onClick={() => {\n                onChange(props.data.filter((_, di) => di !== i));\n              }}\n            />\n          </div>\n        );\n      })}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartCardList/useSmartCardListState.ts",
    "content": "import { useAsyncEffectQueue, usePromise } from \"prostgles-client\";\nimport { isObject, type AnyObject } from \"prostgles-types\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { getSmartGroupFilter } from \"@common/filterUtils\";\nimport { getSelectForFieldConfigs } from \"../SmartCard/getSelectForFieldConfigs\";\nimport { getSmartCardColumns } from \"../SmartCard/getSmartCardColumns\";\nimport type { SmartCardListProps } from \"./SmartCardList\";\n\nexport type SmartCardListState = ReturnType<typeof useSmartCardListState>;\nexport const useSmartCardListState = (\n  props: Pick<\n    SmartCardListProps,\n    | \"db\"\n    | \"tableName\"\n    | \"columns\"\n    | \"onSetData\"\n    | \"fieldConfigs\"\n    | \"filter\"\n    | \"throttle\"\n    | \"limit\"\n    | \"realtime\"\n    | \"orderBy\"\n    | \"showTopBar\"\n    | \"orderByfields\"\n    | \"tables\"\n    | \"searchFilter\"\n  > & {\n    offset: number;\n  },\n) => {\n  const {\n    orderByfields,\n    tableName,\n    db,\n    columns: columnsFromProps,\n    filter,\n    throttle,\n    limit,\n    offset,\n    realtime,\n    fieldConfigs,\n    orderBy,\n    onSetData,\n    showTopBar,\n    tables,\n    searchFilter,\n  } = props;\n\n  const [localOrderBy, setLocalOrderBy] = useState(\n    Array.isArray(orderBy) ? undefined : orderBy,\n  );\n  const [localFilter, setLocalFilter] = useState(searchFilter);\n\n  const fetchedColumns = usePromise(async () => {\n    if (columnsFromProps) {\n      return;\n    }\n    return await getSmartCardColumns({ tableName, db });\n  }, [columnsFromProps, db, tableName]);\n  const columns = columnsFromProps ?? fetchedColumns;\n\n  const [items, setItems] = useState<AnyObject[]>();\n  const [loading, setLoading] = useState(false);\n  const [loaded, setLoaded] = useState(false);\n\n  const [error, setError] = useState<any>(null);\n  const [totalRows, setTotalRows] = useState<number | undefined>(-1);\n\n  const tableHandler =\n    typeof tableName === \"string\" ? db[tableName] : undefined;\n\n  const smartProps = useMemo(() => {\n    if (isObject(tableName)) {\n      return {\n        type: \"sql\",\n        ...tableName,\n      } as const;\n    }\n\n    const fullFilter =\n      localFilter ?\n        getSmartGroupFilter(localFilter, filter && { filters: [filter] })\n      : filter;\n\n    const table = tables.find((t) => t.name === tableName);\n    const showInsert = isObject(showTopBar) ? showTopBar.insert : showTopBar;\n    const willShowInsert =\n      showInsert &&\n      typeof tableName === \"string\" &&\n      table?.columns.some((c) => c.insert);\n\n    const showSort = isObject(showTopBar) ? showTopBar.sort : showTopBar;\n    const willShowSort = showSort && orderByfields?.length !== 0;\n    return {\n      type: \"table\",\n      tableName,\n      filter,\n      fullFilter,\n      throttle,\n      limit,\n      realtime,\n      fieldConfigs,\n      orderBy: localOrderBy ?? orderBy,\n      localOrderBy,\n      onSetData,\n      table,\n      willShowInsert,\n      willShowSort,\n    } as const;\n  }, [\n    tableName,\n    filter,\n    throttle,\n    limit,\n    realtime,\n    fieldConfigs,\n    onSetData,\n    localFilter,\n    orderBy,\n    localOrderBy,\n    tables,\n    showTopBar,\n    orderByfields?.length,\n  ]);\n\n  useEffect(() => {\n    if (!items || smartProps.type !== \"table\") return;\n    smartProps.onSetData?.(items);\n  }, [smartProps, items]);\n\n  /** SQL data */\n  useEffect(() => {\n    if (smartProps.type === \"sql\") {\n      if (!db.sql) {\n        console.error(\"db.sql missing\");\n        setLoaded(true);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      const { sqlQuery, args } = smartProps;\n      db.sql(sqlQuery, args ?? {}, { returnType: \"rows\" })\n        .then((items) => {\n          setItems(items);\n          setLoaded(true);\n          setLoading(false);\n          setTotalRows(items.length);\n          setError(null);\n        })\n        .catch((error) => {\n          setError(error);\n          setLoading(false);\n        });\n    }\n  }, [smartProps, db]);\n\n  const tableDataHandlers = useMemo(() => {\n    if (smartProps.type === \"table\") {\n      const {\n        fieldConfigs,\n        throttle = 0,\n        fullFilter,\n        limit = 25,\n        realtime,\n        orderBy,\n      } = smartProps;\n      const select = getSelectForFieldConfigs(fieldConfigs, columns);\n\n      const setData = async () => {\n        setLoading(true);\n        try {\n          if (!tableHandler?.find) {\n            throw new Error(\"tableHandler.find missing\");\n          }\n\n          let totalRows = -1;\n          try {\n            totalRows = (await tableHandler.count?.(fullFilter)) ?? -1;\n          } catch (error) {\n            console.error(error);\n          }\n\n          const items = await tableHandler.find(fullFilter, {\n            limit,\n            orderBy,\n            select,\n            offset,\n          });\n          setItems(items);\n          setLoaded(true);\n          setTotalRows(totalRows);\n        } catch (error: any) {\n          setError(error);\n        }\n        setLoading(false);\n      };\n\n      return {\n        setData,\n        select,\n        realtime,\n        throttle,\n        limit,\n        orderBy,\n        fullFilter,\n      };\n    }\n  }, [smartProps, columns, offset, tableHandler]);\n\n  /** Table data */\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useAsyncEffectQueue(async () => {\n    if (!tableDataHandlers) return;\n    const {\n      setData,\n      realtime,\n      throttle,\n      select,\n      fullFilter = {},\n    } = tableDataHandlers;\n    if (realtime) {\n      try {\n        if (!tableHandler?.subscribe) {\n          throw new Error(\"tableHandler.subscribe missing\");\n        }\n        /** This is to not wait for the subscription to start */\n        setData();\n        const sub = await tableHandler.subscribe(\n          fullFilter,\n          { limit: 0, select, throttle },\n          () => {\n            setData();\n          },\n        );\n        return sub.unsubscribe;\n      } catch (error) {\n        setError(error);\n        setLoading(false);\n        return;\n      }\n    } else {\n      setData();\n    }\n  }, [tableDataHandlers, tableHandler]);\n\n  const tableControls = useMemo(\n    () =>\n      smartProps.type === \"table\" && smartProps.table ?\n        {\n          ...smartProps,\n          localFilter,\n          setLocalFilter,\n          localOrderBy,\n          setLocalOrderBy,\n        }\n      : undefined,\n    [localFilter, localOrderBy, smartProps],\n  );\n\n  const state = {\n    columns,\n    loading,\n    items,\n    error,\n    loaded,\n    totalRows,\n    setTotalRows,\n    tableControls,\n  };\n\n  return state;\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/AddJoinFilter.tsx",
    "content": "import React from \"react\";\nimport type {\n  JOINED_FILTER_TYPES,\n  DetailedJoinedFilter,\n} from \"@common/filterUtils\";\nimport type { DBSchemaTablesWJoins, JoinV2 } from \"../Dashboard/dashboardUtils\";\nimport {\n  JoinPathSelector,\n  getHasJoins,\n} from \"../W_Table/JoinPathSelector/JoinPathSelector\";\nimport { getFilterableCols } from \"./SmartSearch/SmartSearch\";\nimport { Label } from \"@components/Label\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport Btn from \"@components/Btn\";\nimport { mdiSetCenter, mdiSetNone } from \"@mdi/js\";\n\ntype JoinOpts = { path: JoinV2[]; type: DetailedJoinedFilter[\"type\"] };\ntype AddJoinFilterProps = {\n  tables: DBSchemaTablesWJoins;\n  tableName: string;\n  path?: JoinV2[];\n  type?: DetailedJoinedFilter[\"type\"];\n  onChange: (joinOpts: undefined | JoinOpts) => void;\n  disabledInfo?: string;\n};\nexport const JOIN_FILTER_TYPES = [\n  {\n    key: \"$existsJoined\",\n    label: \"Exists\",\n    subLabel: \"At least one matching record exists in the target table\",\n    iconPath: mdiSetCenter,\n  },\n  {\n    key: \"$notExistsJoined\",\n    label: \"Not Exists\",\n    subLabel: \"No matching records exists in the target table\",\n    iconPath: mdiSetNone,\n  },\n] satisfies {\n  key: (typeof JOINED_FILTER_TYPES)[number];\n  label: string;\n  subLabel: string;\n  iconPath: string;\n}[];\n\nexport const AddJoinFilter = ({\n  tables,\n  tableName,\n  path,\n  type = \"$existsJoined\",\n  onChange,\n  disabledInfo,\n}: AddJoinFilterProps) => {\n  const joinableTables = tables.filter(\n    (t) => getFilterableCols(t.columns).length,\n  );\n  const hasJoins = getHasJoins(tableName, tables);\n\n  if (!hasJoins) return null;\n\n  const title = path ? \"Cancel\" : \"Join to...\";\n  return (\n    <FlexCol className=\"gap-0 f-1\" data-command=\"AddJoinFilter\">\n      {path && (\n        <div className=\"flex-col f-1 o-auto br b-color\">\n          <Label className=\"px-1 py-p5 w-full bg-color-2\">\n            Current join path\n          </Label>\n          <JoinPathSelector\n            tables={joinableTables}\n            tableName={tableName}\n            onSelect={(path) => {\n              onChange({ path, type });\n            }}\n          />\n        </div>\n      )}\n      <FlexRow className={\"p-p5\"}>\n        {!path && (\n          <Btn\n            title={title}\n            iconPath={mdiSetCenter}\n            disabledInfo={disabledInfo}\n            data-command=\"SmartAddFilter.JoinTo\"\n            color={\"action\"}\n            variant={\"faded\"}\n            onClick={() => {\n              onChange({ path: [], type });\n            }}\n          >\n            {title}\n          </Btn>\n        )}\n      </FlexRow>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/MinimisedFilter.css",
    "content": ".FilterWrapper_MinimisedRoot {\n  background-color: var(--blue-50);\n}\n.FilterWrapper_Type {\n  color: rgb(66, 66, 66);\n}\n\n.dark-theme .FilterWrapper_MinimisedRoot {\n  background-color: #12477f;\n}\n.dark-theme .FilterWrapper_Type {\n  color: rgb(196, 196, 196);\n}\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/MinimisedFilter.tsx",
    "content": "import { mdiSetCenter } from \"@mdi/js\";\nimport { includes } from \"prostgles-types\";\nimport React, { type ReactNode } from \"react\";\nimport { getFinalFilterInfo, TEXT_FILTER_TYPES } from \"@common/filterUtils\";\nimport { sliceText } from \"@common/utils\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport type { FilterWrapperProps } from \"../DetailedFilterControl/FilterWrapper\";\nimport \"./MinimisedFilter.css\";\n\ntype P = FilterWrapperProps &\n  Pick<FilterWrapperProps, \"rootFilter\"> & {\n    toggle: VoidFunction;\n    toggleTitle: string;\n    disabledToggle: false | JSX.Element | undefined;\n    filterTypeLabel: string | undefined;\n  };\n\nexport const MinimisedFilter = ({\n  filter,\n  label,\n  column,\n  style,\n  toggle,\n  toggleTitle,\n  disabledToggle,\n  className,\n  filterTypeLabel,\n  rootFilter,\n}: P) => {\n  if (!filter) return null;\n\n  const maxLen = 26;\n  const getValueForDisplay = <AsText extends boolean>(\n    filterValue: unknown,\n    asFullText: AsText,\n  ): AsText extends true ? string : React.ReactNode => {\n    if (filter.contextValue) {\n      return `{{${filter.contextValue.objectName}.${filter.contextValue.objectPropertyName}}}`;\n    }\n    if (Array.isArray(filterValue)) {\n      if (filter.type === \"$between\") {\n        if (asFullText) {\n          return [\n            getValueForDisplay(filterValue[0], asFullText),\n            \"AND\",\n            getValueForDisplay(filterValue[1], asFullText),\n          ].join(\" \");\n        }\n        return (\n          <>\n            {getValueForDisplay(filterValue[0], asFullText)}\n            <span className=\"ws-pre text-1p5 font-normal font-14\"> AND </span>\n            {getValueForDisplay(filterValue[1], asFullText)}\n          </>\n        ) as AsText extends true ? string : ReactNode;\n      } else {\n        if (asFullText) {\n          return (\n            \"(\\n\" +\n            filterValue.map((v) => JSON.stringify(v)).join(\", \\n\") +\n            \"\\n)\"\n          );\n        }\n        const willEllipse = filterValue.join().length > maxLen;\n        if (willEllipse && filterValue.length < 5) {\n          return `( ${filterValue.map((v) => JSON.stringify(sliceText(v, 10))).join(\", \")} ) `;\n        }\n\n        return (\n          <>\n            ({\" \"}\n            {sliceText(JSON.stringify(filterValue).slice(1, -1), maxLen, \"...\")}{\" \"}\n            )\n            {filterValue.length > 2 && (\n              <div\n                className=\"min-w-0 min-h-0 o-hidden text-1p5 ml-p5 flex-row ai-center \"\n                style={{ fontWeight: 600 }}\n              >\n                {\" \"}\n                ({filterValue.length})\n              </div>\n            )}\n          </>\n        ) as AsText extends true ? string : ReactNode;\n      }\n    }\n\n    if (!filterValue && includes([\"$in\", \"$nin\"], filter.type)) {\n      return `( )`;\n    }\n\n    const isStringDate =\n      !TEXT_FILTER_TYPES.some(({ key }) => filter.type === key) &&\n      column.udt_name === \"date\" &&\n      typeof filterValue === \"string\" &&\n      filterValue;\n    if (\n      (filterValue &&\n        filterValue instanceof Date &&\n        (filterValue as any).toLocaleDateString) ||\n      isStringDate\n    ) {\n      const date = new Date(filterValue);\n      if (\n        column.udt_name === \"date\" ||\n        date.toISOString().endsWith(\"00:00:00.000Z\")\n      ) {\n        return date.toISOString().split(\"T\")[0] ?? \"\";\n      }\n      return date.toLocaleDateString();\n    }\n    if (filter.type === \"$ST_DWithin\") {\n      return getFinalFilterInfo(filter);\n    }\n\n    return JSON.stringify(filterValue || \"\");\n  };\n  const { disabled, value } = filter;\n\n  const filterValue = getValueForDisplay(filter.value, false);\n  const filterValueText = getValueForDisplay(filter.value, true);\n\n  let comparatorNode: React.ReactNode = null;\n  if (filter.complexFilter) {\n    comparatorNode = (\n      //@ts-ignore\n      <div className=\"p-p25\">{filter.complexFilter.comparator}</div>\n    );\n  }\n  return (\n    <div\n      className={\n        \"FilterWrapper_MinimisedRoot flex-row ai-center noselect pointer relative o-hidden \" +\n        className\n      }\n      style={{\n        opacity: filter.disabled ? \".8\" : 1,\n        ...style,\n        borderRadius: \"1em\",\n        padding: window.isMobileDevice ? \"2px 6px\" : \"6px 12px\",\n      }}\n    >\n      {disabledToggle}\n      {rootFilter && (\n        <Icon\n          title={\n            rootFilter.value.type === \"$existsJoined\" ? \"Exists\" : \"Not exists\"\n          }\n          path={mdiSetCenter}\n          size={1}\n          className=\"text-0\"\n        />\n      )}\n      <button\n        className={disabledToggle ? \"ml-p5 \" : \"\"}\n        style={{\n          background: \"transparent\",\n          opacity: value === undefined || disabled ? 0.75 : 1,\n          padding: \"0\",\n        }}\n        title={toggleTitle}\n        onClick={toggle}\n      >\n        <div\n          className={\"flex-row ai-center noselect pointer relative o-hidden \"}\n        >\n          <div\n            data-command=\"FilterWrapper_FieldName\"\n            className={\"FilterWrapper_FieldName  font-18 mr-p25 \"}\n            style={{ fontWeight: 600 }}\n          >\n            {label}\n          </div>\n          <div\n            className=\"FilterWrapper_Type mr-p25 font-14 \"\n            style={{\n              textTransform: \"uppercase\",\n              height: \"18px\",\n            }}\n          >\n            {filterTypeLabel}\n          </div>\n          {comparatorNode}\n          {filter.type !== \"not null\" && filter.type !== \"null\" && (\n            <>\n              <div\n                className=\"min-w-0 min-h-0 o-hidden font-16 flex-row ai-center \"\n                title={filterValueText}\n                style={{\n                  whiteSpace: \"nowrap\",\n                  fontWeight: 600,\n                  color:\n                    disabled ? \"var(--text-2)\"\n                    : column.tsDataType === \"string\" ? \"var(--color-text)\"\n                    : column.udt_name === \"date\" ? \"#0000ad\"\n                    : column.tsDataType === \"number\" ? \"var(--color-number)\"\n                    : \"black\",\n                }}\n              >\n                {filterValue}\n              </div>\n            </>\n          )}\n        </div>\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/SmartAddFilter.tsx",
    "content": "import type {\n  DetailedFilter,\n  DetailedJoinedFilter,\n  FilterType,\n} from \"@common/filterUtils\";\nimport type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { mdiFilterPlus } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport { _PG_date, _PG_numbers, includes } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { isDefined } from \"../../utils/utils\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { JoinV2 } from \"../Dashboard/dashboardUtils\";\nimport { getColumnDataColor } from \"../SmartForm/SmartFormField/RenderValue\";\nimport type { ColumnConfig } from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport { getJoinPathLabel } from \"../W_Table/ColumnMenu/JoinPathSelectorV2\";\nimport { getJoinPaths } from \"../W_Table/tableUtils/getJoinPaths\";\nimport { getComputedColumnSelect } from \"../W_Table/tableUtils/getTableSelect\";\nimport { AddJoinFilter } from \"./AddJoinFilter\";\nimport { getDefaultAgeFilter } from \"../DetailedFilterControl/DetailedFilterBaseTypes/AgeFilter\";\nimport { getFilterableCols } from \"./SmartSearch/SmartSearch\";\n\nexport type SmartAddFilterProps = {\n  db: DBHandlerClient;\n  tableName: string;\n  tables: CommonWindowProps[\"tables\"];\n  selectedColumns: ColumnConfig[] | undefined;\n  onChange: (\n    filter: DetailedFilter[],\n    addedFilter: DetailedFilter,\n    isAggregate: boolean,\n  ) => void;\n  detailedFilter?: DetailedFilter[];\n  className?: string;\n  style?: React.CSSProperties;\n  filterFields?: string[];\n  variant?: \"full\";\n  btnProps?: BtnProps;\n  itemName?: \"filter\" | \"condition\";\n  newFilterType?: FilterType;\n};\n\nexport const SmartAddFilter = (props: SmartAddFilterProps) => {\n  const [addFilter, setAddFilter] = useState<{\n    fieldName?: string;\n    type?: DetailedFilter[\"type\"];\n  }>();\n  const [joinOpts, setJoinOpts] = useState<{\n    path: JoinV2[];\n    type: DetailedJoinedFilter[\"type\"];\n  }>();\n  const [popupAnchor, setPopupAnchor] = useState<HTMLElement>();\n  const {\n    tableName,\n    onChange,\n    detailedFilter = [],\n    className = \"\",\n    style = {},\n    tables,\n    filterFields,\n    btnProps,\n    itemName = \"filter\",\n    variant,\n    selectedColumns = [],\n    newFilterType,\n  } = props;\n\n  const [includeLinkedColumns, setIncludeLinkedColumns] = useState(false);\n  const isCategorical = (\n    col: Pick<ValidatedColumnInfo, \"tsDataType\" | \"references\" | \"udt_name\">,\n  ) =>\n    Boolean(\n      col.references?.length ||\n      !includes([..._PG_date, ..._PG_numbers], col.udt_name),\n    );\n  const lastPathItem = joinOpts?.path.at(-1);\n  const currentTable = lastPathItem?.tableName ?? tableName;\n  const filterableTableColumns = getFilterableCols(\n    tables.find((t) => t.name === currentTable)?.columns ?? [],\n  ).filter((c) => c.filter && (!filterFields || filterFields.includes(c.name)));\n  const joinableTables = tables.filter(\n    (t) => getFilterableCols(t.columns).length,\n  );\n  if (!filterableTableColumns.length && !joinableTables.length) {\n    return null;\n  }\n  const joinColumns =\n    !includeLinkedColumns ?\n      []\n    : getJoinPaths(tableName, tables).flatMap((joinPath, i) => {\n        const { table, path } = joinPath;\n        const { label, labels } = getJoinPathLabel(joinPath, {\n          tableName,\n          tables,\n        });\n        return table.columns.flatMap((jc, idx) => {\n          return {\n            key: `${joinPath.pathStr}.${jc.name}`,\n            ranking: Number(`2.${labels.length}`),\n            label: `${table.name}.${jc.name}`,\n            subLabel: path.length > 1 ? label : undefined,\n            references: jc.references,\n            name: jc.name,\n            data_type: jc.tsDataType,\n            udt_name: jc.udt_name,\n            tsDataType: jc.tsDataType,\n            is_pkey: false,\n            computedConfig: undefined,\n            joinInfo: { ...joinPath, column: jc },\n          };\n        });\n      });\n\n  const columns = [\n    ...selectedColumns\n      .map((c) => {\n        const { computedConfig } = c;\n        if (!computedConfig) return undefined;\n        const { tsDataType, udt_name } = computedConfig;\n        return {\n          key: c.name,\n          ranking: 0,\n          name: c.name,\n          label: c.name,\n          data_type: tsDataType,\n          subLabel: undefined,\n          udt_name,\n          tsDataType,\n          is_pkey: false,\n          computedConfig: c.computedConfig,\n          joinInfo: undefined,\n          references: undefined,\n        };\n      })\n      .filter(isDefined),\n    ...filterableTableColumns.map((c, i) => ({\n      ...c,\n      key: c.name,\n      ranking: 1,\n      label: c.name,\n      subLabel: undefined,\n      computedConfig: undefined,\n      joinInfo: undefined,\n      references: c.references,\n    })),\n    ...joinColumns,\n  ];\n  let popup;\n\n  const resetState = () => {\n    setAddFilter(undefined);\n    setJoinOpts(undefined);\n  };\n\n  if (addFilter) {\n    const onAddColumnFilter = (c: (typeof columns)[number]) => {\n      const fieldName = c.name;\n      const isGeo = c.udt_name.startsWith(\"geo\");\n      const joinPath =\n        c.joinInfo?.path ??\n        joinOpts?.path.map((j) => {\n          const onObj = j.on.map((c) => Object.fromEntries(c));\n          return { table: j.tableName, on: onObj };\n        });\n      const joinType =\n        (joinOpts?.type ?? c.joinInfo) ? \"$existsJoined\" : undefined;\n\n      const type =\n        newFilterType ? newFilterType\n        : isGeo ? \"$ST_DWithin\"\n        : (\n          includes(_PG_numbers, c.udt_name) &&\n          !c.is_pkey &&\n          !c.references?.length\n        ) ?\n          \"$between\"\n        : joinPath ? \"not null\"\n        : isCategorical(c) ? \"$in\"\n        : \"$between\";\n      const innerFilter: DetailedFilter =\n        includes(_PG_date, c.udt_name) ?\n          getDefaultAgeFilter(fieldName, \"$ageNow\")\n        : {\n            fieldName,\n            type,\n            disabled: true,\n            complexFilter:\n              c.computedConfig ?\n                {\n                  type: \"$filter\",\n                  leftExpression: getComputedColumnSelect(c.computedConfig),\n                }\n              : undefined,\n          };\n\n      const newFilter: DetailedFilter =\n        joinPath && joinType ?\n          {\n            type: joinType,\n            path: joinPath,\n            filter: innerFilter,\n            disabled: true,\n          }\n        : innerFilter;\n\n      onChange(\n        [...detailedFilter, newFilter],\n        newFilter,\n        c.computedConfig?.funcDef.isAggregate ?? false,\n      );\n\n      resetState();\n    };\n\n    popup = (\n      <Popup\n        positioning=\"beneath-left\"\n        data-command=\"SmartAddFilter\"\n        clickCatchStyle={{ opacity: 0.3 }}\n        anchorEl={popupAnchor}\n        onClose={() => {\n          resetState();\n        }}\n        contentStyle={{ padding: 0 }}\n      >\n        <FlexRow>\n          {!joinOpts && (\n            <SwitchToggle\n              data-command=\"SmartAddFilter.toggleIncludeLinkedColumns\"\n              className=\"mx-p5 f-1\"\n              variant=\"row-reverse\"\n              label={{\n                label: \"Linked columns\",\n                info: \"Include columns from tables that can be joined (through existing constraints) to this table\",\n              }}\n              checked={includeLinkedColumns}\n              onChange={setIncludeLinkedColumns}\n            />\n          )}\n          <AddJoinFilter\n            tableName={tableName}\n            tables={tables}\n            disabledInfo={\n              includeLinkedColumns ? `Must disable Linked columns` : undefined\n            }\n            {...joinOpts}\n            onChange={(jo) => {\n              setIncludeLinkedColumns(false);\n              setJoinOpts(jo);\n            }}\n          />\n        </FlexRow>\n        <div\n          className={\n            \"min-s-0 p-p5 \" +\n            (window.isMobileDevice ? \" flex-col \" : \" flex-row \") +\n            \" \"\n          }\n          style={{\n            maxHeight: \"90vh\",\n          }}\n        >\n          {!!columns.length && (\n            <SearchList\n              className=\"search-list-cols f-1\"\n              style={{ maxHeight: \"unset\" }}\n              autoFocus={true}\n              items={columns.map((c) => ({\n                key: c.key,\n                label: c.label,\n                /**\n                 * contentBottom used instead of subLabel to exclude\n                 * content from search for better experience\n                 */\n                contentBottom: (\n                  <div className=\"mt-p25 text-1\">{c.subLabel}</div>\n                ),\n                ranking: c.ranking,\n                contentRight: (\n                  <div\n                    style={{\n                      color: getColumnDataColor(c),\n                      fontWeight: 300,\n                    }}\n                  >\n                    {c.udt_name.toUpperCase()}\n                  </div>\n                ),\n                onPress: () => onAddColumnFilter(c),\n              }))}\n            />\n          )}\n        </div>\n      </Popup>\n    );\n  }\n\n  const title = `Add ${itemName}`;\n  return (\n    <>\n      <Btn\n        title={title}\n        className={\"shadow bg-color-0 \" + className}\n        style={{ borderRadius: \"6px\", ...style }}\n        color=\"action\"\n        iconPath={mdiFilterPlus}\n        onClick={(e) => {\n          setPopupAnchor(e.currentTarget);\n          setAddFilter({});\n        }}\n        children={variant === \"full\" ? title : undefined}\n        data-command={\"SmartAddFilter\"}\n        disabledInfo={!columns.length ? \"No filterable columns\" : \"\"}\n        {...btnProps}\n      />\n      {popup}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/SmartFilter.tsx",
    "content": "import type { DetailedFilter, FilterType } from \"@common/filterUtils\";\nimport { isJoinedFilter } from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, classOverride } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { ContextDataSchema } from \"../AccessControl/OptionControllers/FilterControl\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport { DetailedFilterControl } from \"../DetailedFilterControl/DetailedFilterControl\";\nimport type { FilterWrapperProps } from \"../DetailedFilterControl/FilterWrapper\";\nimport type { ColumnConfig } from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport { SmartAddFilter } from \"./SmartAddFilter\";\nexport * from \"./smartFilterUtils\";\n\nexport type Operand = \"AND\" | \"OR\";\nexport type SmartFilterProps = Pick<FilterWrapperProps, \"variant\"> & {\n  db: DBHandlerClient;\n  tableName: string;\n  tables: CommonWindowProps[\"tables\"];\n  onChange: (filter: DetailedFilter[]) => void;\n  detailedFilter?: DetailedFilter[];\n  operand?: Operand;\n  onOperandChange?: (operand: Operand) => any;\n  hideOperand?: boolean;\n  className?: string;\n  style?: React.CSSProperties;\n  filterClassName?: string;\n  contextData?: ContextDataSchema;\n  hideToggle?: boolean;\n  minimised?: boolean;\n  showAddFilter?: boolean;\n  itemName: \"filter\" | \"condition\";\n  showNoFilterInfoRow?: boolean;\n  type: \"having\" | \"where\";\n  selectedColumns: ColumnConfig[] | undefined;\n  extraFilters: AnyObject[] | undefined;\n  newFilterType?: FilterType;\n};\n\nexport const SmartFilter = (props: SmartFilterProps) => {\n  const {\n    db,\n    tableName,\n    detailedFilter = [],\n    className = \"\",\n    style = {},\n    tables,\n    operand = \"AND\",\n    onOperandChange,\n    contextData,\n    hideToggle,\n    variant,\n    selectedColumns,\n    onChange,\n    filterClassName = \"\",\n    showAddFilter = false,\n    extraFilters,\n    showNoFilterInfoRow = false,\n    itemName,\n    hideOperand,\n    newFilterType,\n  } = props;\n\n  const table = useMemo(\n    () => tables.find((t) => t.name === tableName),\n    [tables, tableName],\n  );\n\n  if (!table?.columns.length) return null;\n\n  return (\n    <FlexCol\n      key={\"o\"}\n      style={{\n        ...style,\n      }}\n      className={`SmartFilter ${classOverride(\n        `min-w-0 min-h-0 gap-p5 ai-start`,\n        className,\n      )}`}\n    >\n      {detailedFilter.map((filterItem, filterItemIndex) => {\n        const filterFieldName =\n          isJoinedFilter(filterItem) ?\n            filterItem.filter.fieldName\n          : filterItem.fieldName;\n\n        const otherFilters = detailedFilter.filter(\n          (f, i) => i !== filterItemIndex,\n        );\n\n        const onChangeDetailedFilter = (\n          newFilterItem: DetailedFilter | undefined,\n        ) => {\n          let newDetailedFilter = [...detailedFilter];\n          if (newFilterItem) {\n            newDetailedFilter[filterItemIndex] = { ...newFilterItem };\n          } else {\n            newDetailedFilter = newDetailedFilter.filter(\n              (_, i) => i !== filterItemIndex,\n            );\n          }\n\n          onChange(newDetailedFilter);\n        };\n        const filterNode = (\n          <DetailedFilterControl\n            key={filterItemIndex + filterFieldName}\n            className={filterClassName}\n            filterItem={filterItem}\n            table={table}\n            tables={tables}\n            minimisedOverride={props.minimised}\n            variant={variant}\n            db={db}\n            contextData={contextData}\n            otherFilters={otherFilters}\n            onChange={onChangeDetailedFilter}\n            extraFilters={extraFilters}\n            selectedColumns={selectedColumns}\n            hideToggle={hideToggle}\n          />\n        );\n        if (\n          detailedFilter.length > 1 &&\n          filterItemIndex < detailedFilter.length - 1 &&\n          !hideOperand\n        ) {\n          return (\n            <React.Fragment key={\"filter-item\" + filterFieldName}>\n              {filterNode}\n              <Btn\n                className=\"OPERAND text-active hover\"\n                title={onOperandChange ? \"Press to toggle\" : \"Operand\"}\n                onClick={\n                  !onOperandChange ? undefined : (\n                    () => {\n                      onOperandChange(operand === \"AND\" ? \"OR\" : \"AND\");\n                    }\n                  )\n                }\n              >\n                {operand}\n              </Btn>\n            </React.Fragment>\n          );\n        }\n\n        return filterNode;\n      })}\n      {!detailedFilter.length && showNoFilterInfoRow && (\n        <InfoRow color=\"info\" variant=\"filled\">\n          No {itemName}s\n        </InfoRow>\n      )}\n      {showAddFilter && (\n        <SmartAddFilter\n          className=\"w-full mt-1 text-active\"\n          db={db}\n          tableName={tableName}\n          tables={tables}\n          itemName={itemName}\n          selectedColumns={selectedColumns}\n          newFilterType={newFilterType}\n          style={{\n            boxShadow: \"unset\",\n          }}\n          variant=\"full\"\n          onChange={(newF) => {\n            onChange([...detailedFilter, ...newF]);\n          }}\n          btnProps={{\n            variant: \"faded\",\n          }}\n        />\n      )}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/SmartSearch/SmartSearch.css",
    "content": ".SearchList_InputWrapper input {\n  /* transform: translate(0, 2px); */\n  /* line-height: 20px; */\n}\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/SmartSearch/SmartSearch.tsx",
    "content": "import React from \"react\";\nimport \"./SmartSearch.css\";\n\nimport type { DetailedFilter } from \"@common/filterUtils\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport type {\n  SearchListItem,\n  SearchListProps,\n} from \"@components/SearchList/SearchList\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject, ValidatedColumnInfo } from \"prostgles-types\";\nimport { isObject } from \"prostgles-types\";\nimport type { DashboardState } from \"../../Dashboard/Dashboard\";\nimport RTComp from \"../../RTComp\";\nimport type { ColumnConfig } from \"../../W_Table/ColumnMenu/ColumnMenu\";\nimport { onSearchItems } from \"./onSearchItems\";\n\nexport type SmartSearchOnChangeArgs = {\n  filter: DetailedFilter[];\n  /**\n   * Full column value\n   */\n  columnValue?: string | number | Date | boolean;\n\n  /**\n   * Column term value as used in $term_highlight\n   * (value converted to TEXT and if date then date part names are added)\n   */\n  columnTermValue?: string;\n  term?: string;\n  colName?: string;\n};\n\ntype P = {\n  id?: string;\n  db: DBHandlerClient;\n  tableName: string;\n  columns?: string[];\n  column?: string | ColumnConfig;\n  selectedColumns?: ColumnConfig[];\n  onChange?: (args?: SmartSearchOnChangeArgs) => void; //   source: \"row\" | \"enter\"\n  defaultValue?: string;\n  detailedFilter?: DetailedFilter[];\n  className?: string;\n  style?: React.CSSProperties;\n  label?: string;\n  placeholder?: string;\n  onType?: (term: string) => any;\n  /**\n   * If true then will show results after clicking the empty input\n   */\n  searchEmpty?: boolean;\n\n  /**\n   * Defaults to $term_highlight which is equivalent to ILIKE %term%\n   */\n  searchOptions?: {\n    type?: \"$like\" | \"$ilike\" | \"=\" | \"$term_highlight\";\n    includeColumnNames?: boolean;\n    hideMatchCase?: boolean;\n  };\n\n  onPressEnter?: (term: string, searchItems: SearchListItem[]) => void;\n\n  tables: Required<DashboardState>[\"tables\"];\n\n  searchOnFocus?: boolean;\n  variant?: \"search-no-shadow\";\n  noBorder?: boolean;\n  extraFilters?: AnyObject[];\n\n  size?: \"small\";\n\n  hideCaseControl?: boolean;\n\n  error?: any;\n\n  inputStyle?: React.CSSProperties;\n  noResultsComponent?: React.ReactNode;\n};\n\ntype S = {\n  searchKey?: number;\n  searchItems?: SearchListProps[\"items\"];\n};\n\nexport class SmartSearch extends RTComp<P, S> {\n  state: S = {\n    searchKey: Date.now(),\n  };\n\n  onDelta = (dp) => {\n    if (dp?.defaultValue) {\n      this.setState({ searchKey: Date.now() });\n    }\n  };\n\n  get filterableCols(): Pick<\n    ValidatedColumnInfo,\n    \"name\" | \"tsDataType\" | \"udt_name\" | \"is_pkey\"\n  >[] {\n    const { tableName, columns: cols, tables, column } = this.props;\n    if (column && isObject(column) && column.computedConfig) {\n      return [\n        {\n          name: column.name,\n          tsDataType: column.computedConfig.tsDataType,\n          udt_name: column.computedConfig.udt_name,\n          is_pkey: false,\n        },\n      ];\n    }\n    const tbl = tables.find((t) => t.name === tableName);\n    const columnName = this.column?.name;\n    return getFilterableCols(tbl?.columns ?? []).filter(\n      (c) =>\n        columnName === \"*\" ||\n        (columnName && c.name === columnName) ||\n        (cols?.length && cols.includes(c.name)) ||\n        (!cols && !columnName),\n    );\n  }\n\n  searchItems: SearchListItem[] = [];\n  onSearchItems = onSearchItems.bind(this);\n\n  get defaultValue() {\n    const { defaultValue } = this.props;\n    const col = this.column;\n    if (col?.udt_name === \"jsonb\" && defaultValue && isObject(defaultValue)) {\n      return JSON.stringify(defaultValue);\n    }\n    return defaultValue;\n  }\n\n  get column() {\n    const { tables, tableName, column: colNameORColConfig } = this.props;\n    const column =\n      isObject(colNameORColConfig) ?\n        colNameORColConfig.computedConfig ?\n          colNameORColConfig.computedConfig.column\n        : colNameORColConfig.name\n      : colNameORColConfig;\n    return tables\n      .find((t) => t.name === tableName)\n      ?.columns.find((c) => c.name === column);\n  }\n\n  render() {\n    const { searchItems, searchKey = 1 } = this.state;\n    const {\n      style,\n      className,\n      label,\n      placeholder = \"Search ...\",\n      onPressEnter,\n      variant,\n      searchEmpty = false,\n      detailedFilter,\n      extraFilters,\n      error,\n      inputStyle,\n      noResultsComponent,\n      noBorder,\n    } = this.props;\n\n    const { defaultValue } = this;\n\n    if (!this.filterableCols.length) {\n      return <div>No filterable columns</div>;\n    }\n\n    const rerenderProps = {\n      key: (defaultValue ?? \"\") + searchKey,\n      dataSignature: JSON.stringify([detailedFilter, extraFilters]),\n    };\n    const searchNode = (\n      <SearchList\n        {...rerenderProps}\n        label={label}\n        defaultValue={defaultValue}\n        style={style}\n        inputID={this.props.id ?? \"search-all\"}\n        inputProps={\n          this.column?.tsDataType === \"number\" ? { type: \"number\" } : undefined\n        }\n        className={`SmartSearch ${className}`}\n        items={searchItems}\n        variant={variant ?? \"search\"}\n        searchEmpty={searchEmpty}\n        onSearchItems={this.onSearchItems}\n        inputStyle={inputStyle}\n        noBorder={noBorder}\n        placeholder={placeholder}\n        onPressEnter={\n          !onPressEnter ? undefined : (\n            (term) => onPressEnter(term, this.searchItems)\n          )\n        }\n        matchCase={\n          this.props.searchOptions?.hideMatchCase ? { hide: true } : undefined\n        }\n        noResultsContent={noResultsComponent}\n      />\n    );\n\n    if (error) {\n      return (\n        <div className={\"flex-col gap-p25 \" + (className || \"\")} style={style}>\n          {searchNode}\n          <ErrorComponent error={error} />\n        </div>\n      );\n    }\n\n    return searchNode;\n  }\n}\n\nexport const getFilterableCols = (cols: ValidatedColumnInfo[]) => {\n  return cols.filter(\n    (c) =>\n      c.filter &&\n      c.select &&\n      // c.udt_name !== \"geography\" &&\n      // c.udt_name !== \"geometry\" &&\n      c.udt_name !== \"bytea\",\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/SmartSearch/getSmartSearchRows.ts",
    "content": "import { getSmartGroupFilter, type DetailedFilter } from \"@common/filterUtils\";\nimport { isObject } from \"@common/publishUtils\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport {\n  _PG_numbers,\n  type AnyObject,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport type { DashboardState } from \"../../Dashboard/Dashboard\";\nimport type { ColumnConfig } from \"../../W_Table/ColumnMenu/ColumnMenu\";\nimport {\n  getComputedColumnSelect,\n  getTableSelect,\n} from \"../../W_Table/tableUtils/getTableSelect\";\n\ntype Args = {\n  currentlySearchedColumn: string;\n  term: string;\n  matchCase: boolean;\n  db: DBHandlerClient;\n  tableName: string;\n  columns: Pick<ValidatedColumnInfo, \"name\" | \"is_pkey\" | \"udt_name\">[];\n  detailedFilter: DetailedFilter[];\n  extraFilters: AnyObject[] | undefined;\n  column?: string | ColumnConfig | undefined;\n  selectedColumns: ColumnConfig[] | undefined;\n  tables: Required<DashboardState>[\"tables\"];\n};\nexport type SmartSearchResultRows = {\n  prgl_term_highlight: string[];\n}[];\nexport const getRows = async (args: Args, limit = 3, matchStart = false) => {\n  const {\n    db,\n    currentlySearchedColumn,\n    matchCase,\n    term,\n    tableName,\n    columns,\n    detailedFilter,\n    extraFilters,\n    column,\n    selectedColumns,\n    tables,\n  } = args;\n  const getLikeFilter = (notLike = false) => ({\n    [currentlySearchedColumn]: {\n      [`$${notLike ? \"n\" : \"\"}${matchCase ? \"\" : \"i\"}like`]: `${term}%`,\n    },\n  });\n\n  /** If single column then include it to later extract exact value */\n  let computedCol: ColumnConfig | undefined;\n  let isAggregate = false;\n  const prgl_term_highlight = {\n    $term_highlight: [\n      [currentlySearchedColumn],\n      term,\n      { matchCase, edgeTruncate: 30, returnType: \"object\" },\n    ],\n  };\n  let select: { prgl_term_highlight: AnyObject } = { prgl_term_highlight };\n  if (column) {\n    if (isObject(column)) {\n      if (column.computedConfig) {\n        computedCol = column;\n        isAggregate = !!column.computedConfig.funcDef.isAggregate;\n        select[column.name] = getComputedColumnSelect(column.computedConfig);\n        if (selectedColumns) {\n          const { select: fullSelect } = await getTableSelect(\n            { table_name: tableName, columns: selectedColumns },\n            tables,\n            db,\n            {},\n            true,\n          );\n          select = {\n            ...select,\n            ...fullSelect,\n          };\n        }\n        select.prgl_term_highlight = getComputedColumnSelect(\n          column.computedConfig,\n        );\n      } else {\n        select[column.name] = 1;\n      }\n    } else {\n      select[column] = 1;\n    }\n  }\n\n  const searchFilters =\n    !term.length ? []\n    : isAggregate ? [{ [currentlySearchedColumn]: term }]\n    : matchStart ? [getLikeFilter()]\n    : [\n        getLikeFilter(true),\n        {\n          $term_highlight: [\n            [currentlySearchedColumn],\n            term,\n            { matchCase, edgeTruncate: 30, returnType: \"boolean\" },\n          ],\n        },\n      ];\n\n  const finalFilter = {\n    $and: [\n      ...(extraFilters ?? []),\n      ...(isAggregate ? [] : searchFilters),\n      ...(detailedFilter.length ? [getSmartGroupFilter(detailedFilter)] : []),\n    ],\n  };\n  const having = isAggregate ? { $and: searchFilters } : undefined;\n\n  const columnInfo = columns.find((c) => c.name === currentlySearchedColumn);\n  /** Group increases query time considerably. Must try not to use it when not crucial */\n  const groupBy = !columnInfo?.is_pkey && columnInfo?.udt_name !== \"uuid\";\n  const skipSearch = isFruitlessSearch(term, columnInfo);\n  const items =\n    skipSearch ?\n      []\n    : ((await db[tableName]?.find?.(finalFilter, {\n        select,\n        limit,\n        having,\n        groupBy,\n      })) ?? []);\n  const computedColName = computedCol?.name;\n  const result =\n    computedColName ?\n      items.map((d) => ({\n        ...d,\n        prgl_term_highlight: {\n          [computedColName]: [d[computedColName].toString()],\n        },\n      }))\n    : items;\n  return { result, isAggregate };\n};\n\n/**\n * Check if search is pointless. For example if searching for a characters in a numeric column\n */\nconst isFruitlessSearch = (\n  term: string,\n  columnInfo: Pick<ValidatedColumnInfo, \"udt_name\"> | undefined,\n) => {\n  if (!columnInfo) return false;\n  const isNumeric = _PG_numbers.some((v) => v === columnInfo.udt_name);\n  if (isNumeric) {\n    /** Is searching for negative numbers */\n    if (term === \"-\") {\n      return false;\n    }\n    const isInteger = [\"int2\", \"int4\", \"int8\"].includes(columnInfo.udt_name);\n    const termAsNumber = Number(term);\n    if (isInteger) {\n      if (!Number.isInteger(termAsNumber)) {\n        return true;\n      }\n    } else {\n      /** Is searching for fractional numbers */\n      if (term === \".\") {\n        return false;\n      }\n      if (!Number.isFinite(termAsNumber)) {\n        return true;\n      }\n    }\n  }\n\n  const isDate =\n    columnInfo.udt_name.startsWith(\"timestamp\") ||\n    columnInfo.udt_name === \"date\";\n  if (isDate) {\n    const containsOnlyNumbersOrSymbols = term\n      .split(\"\")\n      .every((c) => !isNaN(Number(c)) || c === \"-\" || c === \":\" || c === \" \");\n    if (containsOnlyNumbersOrSymbols) {\n      return false;\n    }\n    const english = new Intl.DateTimeFormat(\"en\", { weekday: \"long\" });\n    const englishFormatter = {\n      weekDay: new Intl.DateTimeFormat(\"en\", { weekday: \"long\" }),\n      month: new Intl.DateTimeFormat(\"en\", { month: \"long\" }),\n    };\n    const clientFormatter = {\n      weekDay: new Intl.DateTimeFormat(navigator.language, { weekday: \"long\" }),\n      month: new Intl.DateTimeFormat(navigator.language, { month: \"long\" }),\n    };\n    const weekdays = Array.from({ length: 7 }, (_, i) => {\n      const date = new Date(2024, 0, i + 1);\n      return [\n        englishFormatter.weekDay.format(date),\n        clientFormatter.weekDay.format(date),\n      ];\n    }).flat();\n    const months = Array.from({ length: 12 }, (_, i) => {\n      const date = new Date(2024, i, 1);\n      return [\n        englishFormatter.month.format(date),\n        clientFormatter.month.format(date),\n      ];\n    }).flat();\n    if (\n      ![...weekdays, ...months].some((name) =>\n        name.toLowerCase().includes(term.toLowerCase()),\n      )\n    ) {\n      return true;\n    }\n  }\n};\nconst testCases: {\n  term: string;\n  columnInfo: Pick<ValidatedColumnInfo, \"udt_name\">;\n  expected: boolean;\n}[] = [\n  {\n    term: \"1\",\n    columnInfo: { udt_name: \"int4\" },\n    expected: false,\n  },\n  {\n    term: \"1.1\",\n    columnInfo: { udt_name: \"int4\" },\n    expected: true,\n  },\n  {\n    term: \"1.1\",\n    columnInfo: { udt_name: \"float4\" },\n    expected: false,\n  },\n  {\n    term: \"jebruary\",\n    columnInfo: { udt_name: \"date\" },\n    expected: true,\n  },\n  {\n    term: \"february\",\n    columnInfo: { udt_name: \"date\" },\n    expected: false,\n  },\n];\ntestCases.forEach(({ term, columnInfo, expected }) => {\n  const actual = isFruitlessSearch(term, columnInfo) ?? false;\n  if (actual !== expected) {\n    console.error(\"isFruitlessSearch failed\", {\n      term,\n      columnInfo,\n      expected,\n      actual,\n    });\n    throw new Error(\"isFruitlessSearch failed\");\n  }\n});\n\nexport const getSmartSearchRows = async (\n  args: Args,\n): Promise<SmartSearchResultRows> => {\n  /* First we try to match the start. If not enough matches then match any part of text */\n  try {\n    const { result, isAggregate } = await getRows(args, undefined, true);\n    const remaining = 3 - result.length;\n    if (remaining > 0 && !isAggregate) {\n      const moreRows = await getRows(args, remaining);\n      return [...result, ...moreRows.result];\n    }\n    return result;\n  } catch (e) {\n    console.error(e);\n  }\n\n  return [];\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/SmartSearch/onSearchItems.tsx",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport type {\n  SearchListItem,\n  SearchListProps,\n} from \"@components/SearchList/SearchList\";\nimport React from \"react\";\nimport { SearchMatchRow } from \"src/dashboard/SearchAll/SearchMatchRow\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { SmartSearch } from \"./SmartSearch\";\nimport { getSmartSearchRows } from \"./getSmartSearchRows\";\n\nexport async function onSearchItems(\n  this: SmartSearch,\n  term: string,\n  opts?: { matchCase?: boolean },\n  onPartialResult?,\n): Promise<Required<SearchListProps>[\"items\"]> {\n  if (typeof term !== \"string\") {\n    return [];\n  }\n  if (!term.length && !this.props.searchEmpty) {\n    this.props.onChange?.();\n    return [];\n  }\n  const matchCase = opts?.matchCase ?? false;\n  const { db, tableName, onType, detailedFilter = [] } = this.props;\n  const column = this.column?.name;\n  const columns = this.filterableCols;\n\n  if (!columns.length) {\n    throw \"no cols \";\n  }\n  onType?.(term);\n\n  let rows: { prgl_term_highlight?: string[] }[] = [];\n  const fetchColumnResults = async (currentlySearchedColumn: string) => {\n    /* First we try to match the start. If not enough matches then match any part of text */\n    try {\n      const result = await getSmartSearchRows({\n        db,\n        tableName,\n        columns,\n        tables: this.props.tables,\n        column: this.props.column ?? currentlySearchedColumn,\n        detailedFilter,\n        extraFilters: this.props.extraFilters,\n        term,\n        selectedColumns: this.props.selectedColumns,\n        matchCase,\n        currentlySearchedColumn,\n      });\n      return result;\n    } catch (e) {\n      console.error(e);\n    }\n\n    return [];\n  };\n  const asSearchItems = (fetchedRows: { prgl_term_highlight?: string[] }[]) => {\n    const searchItems: SearchListProps[\"items\"] = fetchedRows\n      .map((r, i) => {\n        if (!r.prgl_term_highlight) return undefined;\n        const firstRowKey = Object.keys(r.prgl_term_highlight)[0]!;\n        const colName = column === \"*\" ? firstRowKey : (column ?? firstRowKey);\n        let node, columnValue, columnTermValue;\n        if (colName) {\n          /** If date then put the returned content as value */\n          columnTermValue = r.prgl_term_highlight[colName].flat().join(\"\");\n          columnValue = column ? r[column] : columnTermValue;\n\n          node = (\n            <div className=\"flex-col ws-pre f-1\">\n              {columns.length !== 1 &&\n                this.props.searchOptions?.includeColumnNames !== false && (\n                  <div\n                    className=\"f-0 color-action font-14\"\n                    style={{\n                      fontWeight: 400,\n                    }}\n                  >\n                    {colName}:\n                  </div>\n                )}\n              <div className=\"f-1 \" style={{ marginTop: \"4px\" }}>\n                <SearchMatchRow\n                  key={i}\n                  matchRow={r.prgl_term_highlight[colName]}\n                />\n              </div>\n            </div>\n          );\n        }\n\n        const stringColumnValue = columnValue?.toString() ?? \"\";\n\n        return {\n          key: i,\n          content: node,\n          label: stringColumnValue,\n          title: stringColumnValue,\n          data: columnValue,\n          onPress: () => {\n            // const newFilter: SimpleFilter = {\n            //   fieldName: colName,\n            //   type: \"$term_highlight\",\n            //   value: columnTermValue ?? term,\n            // };\n            const newFilter: DetailedFilter = {\n              fieldName: colName,\n              type: \"$in\",\n              value: [columnValue],\n              minimised: true,\n            };\n            const result: DetailedFilter[] = [\n              ...(this.props.detailedFilter ?? []),\n              newFilter,\n            ];\n            const val = {\n              filter: result,\n              columnValue,\n              columnTermValue,\n              term,\n              colName,\n            };\n            this.props.onChange?.(val);\n          },\n        } satisfies SearchListItem;\n      })\n      .filter(isDefined);\n    return searchItems;\n  };\n  const hasChars = Boolean(term && /[a-z]/i.test(term));\n  for (let i = 0; i < columns.length; i++) {\n    const col = columns[i]!;\n    let canceled: boolean = false as boolean;\n    if (canceled || (col.tsDataType === \"number\" && hasChars)) {\n      /** 100% no result due to data type mismatch */\n    } else {\n      const colRows = await fetchColumnResults(col.name);\n      rows = rows.concat(colRows);\n    }\n    const partialResult: SearchListProps[\"items\"] = asSearchItems(rows);\n    onPartialResult?.(partialResult, i === columns.length - 1, () => {\n      canceled = true;\n    });\n  }\n  return asSearchItems(rows);\n}\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/SortByControl.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiSortReverseVariant, mdiSortVariant } from \"@mdi/js\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport React from \"react\";\nimport type { ColumnSort } from \"../W_Table/ColumnMenu/ColumnMenu\";\n\ntype SortByControlProps = Pick<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"className\" | \"style\"\n> & {\n  onChange: (val: ColumnSort | undefined) => void | Promise<any>;\n  columns: ValidatedColumnInfo[];\n  value?: ColumnSort;\n  fields?: string[];\n};\nexport const SortByControl = ({\n  onChange,\n  columns,\n  value,\n  fields,\n  style = {},\n  className = \"\",\n}: SortByControlProps) => {\n  const setSort = (orderByKey: string | undefined, orderAsc = true) => {\n    if (!orderByKey) {\n      onChange(undefined);\n      return;\n    }\n    const newSort: ColumnSort = {\n      key: orderByKey,\n      asc: orderAsc,\n    };\n    onChange(newSort);\n  };\n  if (fields?.length) {\n    const bad = fields.filter((f) => columns.every((c) => c.name !== f));\n    if (bad.length) {\n      console.warn(\"Bad fields provided for SortByControl: \", bad);\n    }\n  }\n  const orderableFields = columns.filter(\n    (c) => c.filter && (!fields || fields.includes(c.name)),\n  );\n  const orderAsc = value?.asc ?? undefined;\n  const orderByKey = value?.key;\n\n  return (\n    <div\n      className={\n        \"Select flex-row gap-p25 min-h-0 f-0 relative ai-center \" + className\n      }\n      style={style}\n    >\n      <Select\n        id=\"orderbycomp\"\n        // btnProps={{ className: \"shadow bg-color-0\" }}\n        emptyLabel=\"Sort by...\"\n        asRow={true}\n        value={value?.key}\n        fullOptions={orderableFields.map((f) => ({\n          key: f.name,\n          label: f.label || f.name,\n        }))}\n        onChange={(orderByKey) => {\n          setSort(orderByKey, orderAsc);\n        }}\n        optional={true}\n      />\n      {orderByKey && (\n        <Btn\n          color=\"action\"\n          iconPath={orderAsc ? mdiSortReverseVariant : mdiSortVariant}\n          onClick={() => {\n            setSort(orderByKey, !orderAsc);\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilter/smartFilterUtils.ts",
    "content": "import type { DetailedFilter, DetailedFilterBase } from \"@common/filterUtils\";\nimport { getFinalFilter } from \"@common/filterUtils\";\nimport type {\n  DBHandlerClient,\n  TableHandlerClient,\n} from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject, ValidatedColumnInfo } from \"prostgles-types\";\nimport type { ContextDataSchema } from \"../AccessControl/OptionControllers/FilterControl\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { ColumnConfig } from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport type { FilterWrapperProps } from \"../DetailedFilterControl/FilterWrapper\";\n\nexport const testFilter = (\n  f: DetailedFilter,\n  tableHandler: TableHandlerClient,\n  cb: (err: unknown, ok?: true) => any,\n) => {\n  return tableHandler\n    .findOne(getFinalFilter(f))\n    .then(() => cb(undefined, true))\n    .catch((err: unknown) => cb(err));\n};\n\n// export const getSmartGroupFilter = (\n//   detailedFilter: SmartGroupFilter = [],\n//   extraFilters?: { detailed?: SmartGroupFilter; filters?: AnyObject[] },\n//   operand?: \"and\" | \"or\",\n// ): AnyObject => {\n//   let input = detailedFilter;\n//   if (extraFilters?.detailed) {\n//     input = [...detailedFilter, ...extraFilters.detailed];\n//   }\n//   let output = input.map((f) => getFinalFilter(f));\n//   if (extraFilters?.filters) {\n//     output = output.concat(extraFilters.filters);\n//   }\n//   const result = simplifyFilter({\n//     [`$${operand || \"and\"}`]: output.filter(isDefined),\n//   });\n\n//   return result ?? {};\n// };\n\ntype TableColumn = ValidatedColumnInfo & {\n  type: \"column\";\n};\ntype ComputedColumn = Pick<\n  ValidatedColumnInfo,\n  \"name\" | \"label\" | \"tsDataType\" | \"udt_name\"\n> & {\n  type: \"computed\";\n  /** Needed for aggregate functions */\n  columns: ColumnConfig[];\n} & Pick<Required<ColumnConfig>, \"computedConfig\">;\n\nexport type FilterColumn = TableColumn | ComputedColumn;\n\nexport type BaseFilterProps = Pick<FilterWrapperProps, \"variant\"> & {\n  db: DBHandlerClient;\n  tableName: string;\n  onChange: (filter?: DetailedFilter) => void;\n  tables: CommonWindowProps[\"tables\"];\n  column: FilterColumn;\n  /**\n   * Used to ensure that narrow down options based on other filters.\n   * No point in allowing the user to filter on values that are already filtered out.\n   */\n  otherFilters: DetailedFilter[];\n  /**\n   * Additional filters to always apply on top of otherFilters.\n   */\n  extraFilters: AnyObject[] | undefined;\n  error?: unknown;\n  filter?: DetailedFilterBase;\n  className?: string;\n  style?: React.CSSProperties;\n  contextData?: ContextDataSchema;\n};\n\nexport const DEFAULT_VALIDATED_COLUMN_INFO = {\n  comment: \"\",\n  data_type: \"text\",\n  delete: false,\n  element_type: \"\",\n  element_udt_name: \"\",\n  filter: true,\n  has_default: false,\n  insert: false,\n  udt_name: \"text\",\n  is_nullable: true,\n  is_pkey: false,\n  ordinal_position: -1,\n  select: true,\n  orderBy: true,\n  tsDataType: \"string\",\n  update: true,\n  is_updatable: true,\n  is_generated: false,\n} as const;\n"
  },
  {
    "path": "client/src/dashboard/SmartFilterBar/SmartFilterBar.tsx",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport { isJoinedFilter } from \"@common/filterUtils\";\nimport Btn, { type BtnProps } from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow, FlexRowWrap, classOverride } from \"@components/Flex\";\nimport {\n  mdiTableColumn,\n  mdiTableRow,\n  mdiUnfoldLessHorizontal,\n  mdiUnfoldMoreHorizontal,\n} from \"@mdi/js\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type { PrglCore } from \"../../App\";\nimport type { WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { SmartAddFilter } from \"../SmartFilter/SmartAddFilter\";\nimport type {\n  ColumnConfig,\n  ColumnSort,\n} from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport { SmartFilterBarFilters } from \"./SmartFilterBarFilters\";\nimport { SmartFilterBarRightActions } from \"./SmartFilterBarRightActions\";\nimport { SmartFilterBarSearch } from \"./SmartFilterBarSearch\";\nimport { useSmartFilterBarState } from \"./useSmartFilterBarState\";\n\nexport type SmartFilterBarProps = PrglCore & {\n  className?: string;\n  innerClassname?: string;\n  style?: React.CSSProperties;\n\n  filterFields?: string[];\n  leftContent?: React.ReactNode;\n  rightContent?: React.ReactNode;\n  showInsertUpdateDelete?: {\n    onSuccess?: VoidFunction;\n    showInsert?: boolean | Pick<BtnProps, \"children\">;\n    showupdate?: boolean;\n    showdelete?: boolean;\n  };\n  rowCount: number;\n  hideSort?: boolean;\n  extraFilters?: AnyObject[];\n  fixedData?: AnyObject;\n} & (\n    | {\n        w: WindowSyncItem<\"table\"> | WindowSyncItem<\"card\">;\n      }\n    | {\n        filter?: DetailedFilter[];\n        having?: DetailedFilter[];\n        table_name: string;\n        onChange: (newFilter: DetailedFilter[], isAggregate?: boolean) => any;\n        onHavingChange?: (newFilter: DetailedFilter[]) => any;\n\n        onSortChange: undefined | ((newSort: ColumnSort | undefined) => void);\n        sort?: ColumnSort;\n        columns?: ColumnConfig[];\n      }\n  );\n\nexport const SmartFilterBar = (props: SmartFilterBarProps) => {\n  const {\n    db,\n    tables,\n    leftContent,\n    filterFields,\n    style,\n    className,\n    innerClassname,\n  } = props;\n\n  const state = useSmartFilterBarState(props);\n\n  if (state.type === \"error\") {\n    return <ErrorComponent error={state.error} />;\n  }\n\n  const {\n    table,\n    filter,\n    selectedColumns,\n    onFilterChange,\n    someFiltersExpanded,\n    colFilterLayout,\n    setColFilterLayout,\n  } = state;\n\n  return (\n    <FlexCol\n      className={classOverride(\n        \"SmartFilterBar min-h-0 min-w-0 relative gap-0\",\n        className,\n      )}\n      style={style}\n      data-command=\"SmartFilterBar\"\n    >\n      {leftContent}\n\n      <FlexRowWrap\n        className={classOverride(\n          \"min-h-0 f-0 relative ai-center gap-p5\",\n          innerClassname,\n        )}\n      >\n        <SmartAddFilter\n          selectedColumns={selectedColumns ?? []}\n          tableName={table.name}\n          filterFields={filterFields}\n          db={db}\n          tables={tables}\n          detailedFilter={filter}\n          onChange={onFilterChange}\n        />\n        {Boolean(filter.length) && (\n          <>\n            <Btn\n              title=\"Expand/Collapse filters\"\n              iconPath={\n                window.isMobileDevice ?\n                  !someFiltersExpanded ?\n                    mdiUnfoldMoreHorizontal\n                  : mdiUnfoldLessHorizontal\n                : undefined\n              }\n              onClick={() => {\n                const newFilters = toggleAllFilters(filter);\n\n                onFilterChange(newFilters);\n              }}\n            >\n              {window.isMobileDevice ? null : (\n                `${!someFiltersExpanded ? \"Expand\" : \"Collapse\"} filters`\n              )}\n            </Btn>\n            <Btn\n              title=\"Filter layout\"\n              iconPath={colFilterLayout ? mdiTableColumn : mdiTableRow}\n              onClick={() => {\n                setColFilterLayout(!colFilterLayout);\n              }}\n            />\n          </>\n        )}\n        <FlexRow className={\"gap-0 f-1 mx-p5 jc-center mr-p5 ai-center\"}>\n          <SmartFilterBarSearch\n            db={db}\n            tableName={table.name}\n            tables={tables}\n            onFilterChange={onFilterChange}\n            filter={filter}\n            extraFilters={props.extraFilters}\n          />\n          <SmartFilterBarRightActions {...props} />\n        </FlexRow>\n      </FlexRowWrap>\n      <SmartFilterBarFilters\n        state={state}\n        db={db}\n        tables={tables}\n        extraFilters={props.extraFilters}\n      />\n    </FlexCol>\n  );\n};\n\nconst toggleAllFilters = (filters: DetailedFilter[], minimised?: boolean) => {\n  const someFiltersExpanded = minimised ?? filters.some((f) => !f.minimised);\n\n  return filters.map((f) => {\n    if (isJoinedFilter(f)) {\n      f.filter.minimised = someFiltersExpanded;\n    }\n    return {\n      ...f,\n      minimised: someFiltersExpanded,\n    };\n  });\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilterBar/SmartFilterBarFilters.tsx",
    "content": "import React from \"react\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport { SmartFilter } from \"../SmartFilter/SmartFilter\";\nimport type { SmartFilterBarProps } from \"./SmartFilterBar\";\nimport type { useSmartFilterBarState } from \"./useSmartFilterBarState\";\n\ntype P = Pick<SmartFilterBarProps, \"db\" | \"tables\" | \"extraFilters\"> & {\n  state: Exclude<ReturnType<typeof useSmartFilterBarState>, { type: \"error\" }>;\n};\n\nexport const SmartFilterBarFilters = ({\n  state,\n  db,\n  tables,\n  extraFilters,\n}: P) => {\n  const {\n    hasFilters,\n    hasHavingFilters,\n    filterLayoutClass,\n    selectedColumns,\n    filter,\n    table,\n    having,\n    onHavingChange,\n    onFilterChange,\n    havingOperand,\n    filterOperand,\n    onFilterOperandChange,\n    onHavingOperandChange,\n  } = state;\n\n  return (\n    <>\n      {hasHavingFilters && (\n        <FlexCol\n          key=\"having\"\n          data-key={\"having\"}\n          className={`gap-p5 min-h-0 f-0 relative ai-start my-p5 ${hasFilters ? \"bb b-color\" : \"\"} pb-p5`}\n        >\n          <Label\n            variant=\"normal\"\n            info={<>HAVING clause is used to filter aggregate expressions</>}\n          >\n            Having\n          </Label>\n          <SmartFilter\n            className={`mr-1 mt-p5 ${filterLayoutClass}`}\n            type=\"having\"\n            selectedColumns={selectedColumns ?? []}\n            itemName=\"filter\"\n            filterClassName={\"shadow b b-action\"}\n            tables={tables}\n            db={db}\n            tableName={table.name}\n            detailedFilter={having}\n            onChange={onHavingChange}\n            operand={havingOperand}\n            onOperandChange={onHavingOperandChange}\n            extraFilters={extraFilters}\n          />\n        </FlexCol>\n      )}\n      {!!filter.length && (\n        <FlexCol\n          key=\"where\"\n          data-key={\"where\"}\n          className={\"gap-p5 min-h-0 f-0 relative ai-start my-p5\"}\n        >\n          {hasHavingFilters && (\n            <Label\n              variant=\"normal\"\n              info={\n                <>\n                  WHERE clause is used to filter data before aggregate column\n                  filters are applied in the HAVING clause\n                </>\n              }\n            >\n              Where\n            </Label>\n          )}\n          <SmartFilter\n            className={`mr-1 mt-p5 ${filterLayoutClass}`}\n            type=\"where\"\n            selectedColumns={selectedColumns ?? []}\n            itemName=\"filter\"\n            filterClassName={\"shadow b b-action\"}\n            tables={tables}\n            db={db}\n            tableName={table.name}\n            detailedFilter={filter}\n            onChange={onFilterChange}\n            operand={filterOperand}\n            onOperandChange={onFilterOperandChange}\n            extraFilters={extraFilters}\n          />\n        </FlexCol>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilterBar/SmartFilterBarRightActions.tsx",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport { getFinalFilterInfo, getSmartGroupFilter } from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport { ExpandSection } from \"@components/ExpandSection\";\nimport { Footer } from \"@components/Popup/Footer\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport {\n  mdiChevronLeft,\n  mdiChevronRight,\n  mdiDelete,\n  mdiPencilOutline,\n} from \"@mdi/js\";\nimport { isObject, type AnyObject } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { pluralise } from \"../../pages/Connections/Connection\";\nimport { CodeConfirmation } from \"../BackupAndRestore/CodeConfirmation\";\nimport { InsertButton } from \"../SmartForm/InsertButton\";\nimport { SmartForm } from \"../SmartForm/SmartForm\";\nimport type { SmartFilterBarProps } from \"./SmartFilterBar\";\nimport { SmartFilterBarSort } from \"./SmartFilterBarSort\";\n\nexport const SmartFilterBarRightActions = (props: SmartFilterBarProps) => {\n  const {\n    db,\n    tables,\n    rightContent,\n    hideSort,\n    showInsertUpdateDelete = {},\n    rowCount,\n    methods: dbMethods,\n    fixedData,\n  } = props;\n\n  const { filter: _fltr = [] } = \"w\" in props ? props.w : props;\n\n  const { table_name } = \"w\" in props ? props.w : props;\n\n  const [updateAllRow, setUpdateAllRow] = useState<AnyObject | undefined>(\n    undefined,\n  );\n  const table = tables.find((t) => t.name === table_name);\n\n  const filter: DetailedFilter[] = _fltr.map((f) => ({ ...f }));\n  const finalFilter = getSmartGroupFilter(filter);\n\n  if (!table_name || !table) return null;\n\n  const commonBtnProps = {\n    variant: \"outline\",\n    className: \"shadow w-fit h-fit bg-color-0\",\n  } as const;\n\n  const tableHandler = db[table_name];\n  const filterAsString = filter.map((f) => getFinalFilterInfo(f));\n\n  const {\n    showdelete = true,\n    showInsert = true,\n    showupdate = true,\n  } = showInsertUpdateDelete;\n  const hideUpdateDelete = [showdelete, showupdate].every((v) => v === false);\n  const canUpdateOrDelete =\n    !hideUpdateDelete && (tableHandler?.delete || tableHandler?.update);\n\n  if (hideSort && rightContent && !showdelete && !showInsert && !showupdate)\n    return <></>;\n\n  return (\n    <div className=\"SmartFilterBarRightActions ml-auto pl-1 flex-row ai-center\">\n      {!hideSort && <SmartFilterBarSort {...props} table={table} />}\n      {rightContent}\n\n      <div className=\"flex-row ai-center gap-p5 ml-1\">\n        {!!canUpdateOrDelete && (\n          <ExpandSection\n            title=\"Bulk actions\"\n            label={\"\"}\n            className=\"\"\n            iconPath={(collapsed) =>\n              collapsed ? mdiChevronLeft : mdiChevronRight\n            }\n            buttonProps={{\n              \"data-command\": \"SmartFilterBar.rightOptions.show\",\n            }}\n            collapsible={true}\n          >\n            {!!tableHandler.delete && showdelete && (\n              <CodeConfirmation\n                positioning=\"beneath-right\"\n                button={\n                  <Btn\n                    iconPath={mdiDelete}\n                    title=\"Delete rows...\"\n                    {...commonBtnProps}\n                    color=\"danger\"\n                    data-command=\"SmartFilterBar.rightOptions.delete\"\n                  />\n                }\n                message={async () => {\n                  const count = await tableHandler.count?.(finalFilter);\n                  return (\n                    <div\n                      className=\"flex-col gap-1\"\n                      style={{ maxWidth: \"100%\" }}\n                    >\n                      <div className=\"font-20 bold \">\n                        Delete {count?.toLocaleString()}{\" \"}\n                        {pluralise(count ?? 0, \"record\")}{\" \"}\n                        {filterAsString.length ?\n                          \"matching the current filter\"\n                        : \"\"}\n                      </div>\n                    </div>\n                  );\n                }}\n                confirmButton={(pCLose) => (\n                  <Btn\n                    iconPath={mdiDelete}\n                    {...commonBtnProps}\n                    color=\"danger\"\n                    title=\"Delete rows\"\n                    onClickPromise={async () => {\n                      await tableHandler.delete!(finalFilter);\n                      showInsertUpdateDelete.onSuccess?.();\n                      pCLose();\n                    }}\n                  >\n                    Delete rows\n                  </Btn>\n                )}\n              />\n            )}\n\n            {!!tableHandler.update && showupdate && (\n              <PopupMenu\n                title={`Update ${rowCount} rows`}\n                positioning=\"right-panel\"\n                button={\n                  <Btn\n                    {...commonBtnProps}\n                    iconPath={mdiPencilOutline}\n                    title=\"Update rows\"\n                    color=\"action\"\n                    data-command=\"SmartFilterBar.rightOptions.update\"\n                  />\n                }\n                contentStyle={{\n                  padding: 0,\n                }}\n                render={() => (\n                  <>\n                    <SmartForm\n                      db={db}\n                      label=\"\"\n                      contentClassname=\"pt-1\"\n                      rowFilter={[]}\n                      tableName={table_name}\n                      tables={tables}\n                      methods={props.methods}\n                      onChange={setUpdateAllRow}\n                      fixedData={fixedData}\n                      showJoinedTables={false}\n                      columnFilter={(c) =>\n                        !c.is_pkey && !(!c.is_nullable && c.references?.length)\n                      }\n                    />\n                    {updateAllRow && (\n                      <Footer>\n                        <Btn onClick={() => setUpdateAllRow(undefined)}>\n                          Cancel\n                        </Btn>\n                        <Btn\n                          color=\"action\"\n                          variant=\"filled\"\n                          onClickPromise={async () => {\n                            await tableHandler.update!(\n                              finalFilter,\n                              updateAllRow,\n                            );\n                            setUpdateAllRow(undefined);\n                            showInsertUpdateDelete.onSuccess?.();\n                          }}\n                        >\n                          Update\n                        </Btn>\n                      </Footer>\n                    )}\n                  </>\n                )}\n              />\n            )}\n          </ExpandSection>\n        )}\n\n        {showInsert && (\n          <InsertButton\n            buttonProps={{\n              ...commonBtnProps,\n              ...(isObject(showInsert) && showInsert),\n            }}\n            db={db}\n            methods={dbMethods}\n            tables={tables}\n            tableName={table_name}\n            onSuccess={showInsertUpdateDelete.onSuccess}\n          />\n        )}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilterBar/SmartFilterBarSearch.tsx",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport { isJoinedFilter } from \"@common/filterUtils\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { useMemoDeep } from \"prostgles-client\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport { SmartSearch } from \"../SmartFilter/SmartSearch/SmartSearch\";\nimport { colIs } from \"../SmartForm/SmartFormField/fieldUtils\";\nimport type { SmartFilterBarProps } from \"./SmartFilterBar\";\n\ntype P = Pick<SmartFilterBarProps, \"db\" | \"tables\" | \"style\"> & {\n  tableName: string;\n  filter: DetailedFilter[];\n  extraFilters: AnyObject[] | undefined;\n  onFilterChange: (newFilter: DetailedFilter[]) => void;\n};\n\nexport const SmartFilterBarSearch = ({\n  tables,\n  db,\n  tableName,\n  filter,\n  extraFilters,\n  onFilterChange,\n  style,\n}: P) => {\n  const table = useMemoDeep(\n    () => tables.find((t) => t.name === tableName),\n    [tables, tableName],\n  );\n  if (!table) {\n    return <ErrorComponent error=\"Table not found\" />;\n  }\n  return (\n    <SmartSearch\n      tables={tables}\n      db={db}\n      style={{\n        alignSelf: \"center\",\n        width: \"500px\",\n        maxWidth: \"80vw\",\n        ...style,\n      }}\n      className=\"m-auto\"\n      tableName={tableName}\n      detailedFilter={filter}\n      extraFilters={extraFilters}\n      onPressEnter={(term) => {\n        let newGroupFilter = filter.slice(0);\n        const newF: DetailedFilter = {\n          fieldName: \"*\",\n          type: \"$term_highlight\",\n          value: term,\n          minimised: true,\n        };\n        newGroupFilter = [...filter, newF];\n        onFilterChange(newGroupFilter);\n      }}\n      onChange={(val) => {\n        const { filter: gFilter, colName, columnValue, term } = val ?? {};\n\n        if (!gFilter) {\n          onFilterChange([...filter]);\n          return;\n        }\n\n        const col = table.columns.find((c) => c.name === colName);\n\n        if (!colName) throw \"Unexpected: colName missing\";\n\n        let newGroupFilter = gFilter.slice(0);\n\n        if (\n          columnValue?.toString().length &&\n          col &&\n          (colIs(col, \"_PG_date\") || colIs(col, \"_PG_numbers\"))\n        ) {\n          const newF: DetailedFilter = {\n            fieldName: colName,\n            minimised: true,\n            ...(colIs(col, \"_PG_date\") ?\n              {\n                type: \"$term_highlight\",\n                value: term,\n              }\n            : {\n                type: \"=\",\n                value: columnValue,\n              }),\n          };\n          newGroupFilter = [...filter, newF];\n        }\n\n        onFilterChange(newGroupFilter);\n      }}\n    />\n  );\n};\n\nexport const toggleAllFilters = (\n  filters: DetailedFilter[],\n  minimised?: boolean,\n) => {\n  const someFiltersExpanded = minimised ?? filters.some((f) => !f.minimised);\n\n  return filters.map((f) => {\n    if (isJoinedFilter(f)) {\n      f.filter.minimised = someFiltersExpanded;\n    }\n    return {\n      ...f,\n      minimised: someFiltersExpanded,\n    };\n  });\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilterBar/SmartFilterBarSort.tsx",
    "content": "import { mdiSortReverseVariant, mdiSortVariant } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { Select } from \"@components/Select/Select\";\nimport type {\n  DBSchemaTableWJoins,\n  WindowSyncItem,\n} from \"../Dashboard/dashboardUtils\";\nimport type { ColumnSort } from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport type { SmartFilterBarProps } from \"./SmartFilterBar\";\n\ntype P = SmartFilterBarProps & {\n  table: DBSchemaTableWJoins;\n};\nexport const SmartFilterBarSort = ({ table, ...props }: P) => {\n  const w: WindowSyncItem<\"table\"> | WindowSyncItem<\"card\"> | undefined =\n    \"w\" in props ? props.w : undefined;\n  const { columns } = \"w\" in props ? props.w : props;\n  const setSort = (orderByKey: string | undefined, orderAsc = true) => {\n    const newSort: ColumnSort | undefined =\n      !orderByKey ? undefined : (\n        {\n          key: orderByKey,\n          asc: orderAsc,\n        }\n      );\n    if (\"w\" in props) {\n      w && w.$update({ sort: newSort && [newSort] });\n    } else if (props.onSortChange) {\n      props.onSortChange(newSort);\n    }\n  };\n  const orderableFields = columns ?? table.columns.filter((c) => c.filter);\n  let orderByKey: string | undefined;\n  let orderAsc = true;\n  if (\"w\" in props) {\n    orderByKey = w?.sort?.[0]?.key as string;\n    orderAsc = w?.sort?.[0]?.asc ?? true;\n  } else if (typeof props.sort?.key === \"string\") {\n    orderByKey = props.sort.key;\n    orderAsc = props.sort.asc ?? true;\n  }\n\n  return (\n    <div className={\"flex-row min-h-0 f-0 relative ai-center \"}>\n      <Select\n        id=\"orderbycomp\"\n        btnProps={{\n          className: \"shadow bg-color-0\",\n        }}\n        style={{\n          background: \"white\",\n        }}\n        emptyLabel=\"Sort by...\"\n        asRow={true}\n        value={orderByKey}\n        fullOptions={orderableFields.map((f) => ({\n          key: f.name,\n          label: \"label\" in f ? f.label || f.name : f.name,\n        }))}\n        onChange={(orderByKey) => {\n          setSort(orderByKey, orderAsc);\n        }}\n        optional={true}\n      />\n      {orderByKey && (\n        <Btn\n          color=\"action\"\n          iconPath={orderAsc ? mdiSortReverseVariant : mdiSortVariant}\n          onClick={() => {\n            setSort(orderByKey, !orderAsc);\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartFilterBar/useSmartFilterBarState.ts",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport { useMemo, useState } from \"react\";\nimport { IsTable, type WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport type { Operand } from \"../SmartFilter/SmartFilter\";\nimport type { SmartFilterBarProps } from \"./SmartFilterBar\";\n\nexport const useSmartFilterBarState = (props: SmartFilterBarProps) => {\n  const [colFilterLayout, setColFilterLayout] = useState(false);\n  const { table_name } = \"w\" in props ? props.w : props;\n  const { tables } = props;\n\n  const table = useMemo(() => {\n    return table_name && tables.find((t) => t.name === table_name);\n  }, [table_name, tables]);\n\n  return useMemo(() => {\n    if (!table) {\n      return {\n        type: \"error\",\n        error: `Table ${JSON.stringify(table_name)} not found`,\n      } as const;\n    }\n\n    const { filter: _fltr = [], having: _having = [] } =\n      \"w\" in props ? props.w : props;\n    const onHavingChange =\n      \"w\" in props ?\n        (having) => {\n          props.w.$update({ having }, { deepMerge: true });\n        }\n      : (havingFilter: DetailedFilter[]) => {\n          props.onHavingChange?.(havingFilter);\n        };\n    const onFilterChange = (\n      filter: DetailedFilter[],\n      addedFilter?: DetailedFilter,\n      isAggregate?: boolean,\n    ) => {\n      if (isAggregate && addedFilter) {\n        const newHaving = [..._having, addedFilter];\n        onHavingChange(newHaving);\n      } else {\n        if (\"w\" in props) {\n          props.w.$update({ filter }, { deepMerge: true });\n        } else {\n          props.onChange(filter, isAggregate);\n        }\n      }\n    };\n\n    const w: WindowSyncItem<\"table\"> | WindowSyncItem<\"card\"> | undefined =\n      \"w\" in props ? props.w : undefined;\n    const selectedColumns = \"w\" in props ? w?.columns : props.columns;\n\n    const filter: DetailedFilter[] = _fltr.map((f) => ({ ...f }));\n    const having: DetailedFilter[] = _having.map((f) => ({ ...f }));\n\n    const filterLayoutClass = colFilterLayout ? \"flex-col\" : \"flex-row-wrap\";\n    const hasFilters = !!filter.length;\n    const hasHavingFilters = !!having.length;\n\n    const havingOperand =\n      IsTable(w) && w.options.havingOperand === \"OR\" ? \"OR\" : \"AND\";\n    const filterOperand =\n      IsTable(w) && w.options.filterOperand === \"OR\" ? \"OR\" : \"AND\";\n\n    const onFilterOperandChange =\n      !w?.$update ?\n        undefined\n      : (filterOperand: Operand) => {\n          w.$update({ options: { filterOperand } }, { deepMerge: true });\n        };\n    const onHavingOperandChange =\n      !w?.$update ?\n        undefined\n      : (havingOperand: Operand) => {\n          w.$update({ options: { havingOperand } }, { deepMerge: true });\n        };\n    const someFiltersExpanded = filter.some((f) => !f.minimised);\n\n    return {\n      table,\n      filter,\n      having,\n      hasFilters,\n      hasHavingFilters,\n      filterLayoutClass,\n      selectedColumns,\n      onFilterChange,\n      onHavingChange,\n      havingOperand,\n      filterOperand,\n      colFilterLayout,\n      setColFilterLayout,\n      onFilterOperandChange,\n      onHavingOperandChange,\n      someFiltersExpanded,\n    } as const;\n  }, [props, colFilterLayout, table_name, table]);\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/ChipArrayEditor.tsx",
    "content": "import { mdiPan } from \"@mdi/js\";\nimport type {\n  PG_COLUMN_UDT_DATA_TYPE,\n  TS_COLUMN_DATA_TYPES,\n} from \"prostgles-types\";\nimport { _PG_date } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport Chip from \"@components/Chip\";\nimport { DraggableLI } from \"@components/DraggableLI\";\n\ntype P = {\n  inputType: string;\n  /**\n   * Used for styling and for adding new element default values\n   */\n  elemTsType: TS_COLUMN_DATA_TYPES;\n  elemUdtName: PG_COLUMN_UDT_DATA_TYPE;\n  values: string[]; //  TypeConversion[T][];\n  onChange: (newValues: P[\"values\"]) => void | Promise<void>;\n};\nexport const ChipArrayEditor = ({\n  inputType,\n  elemTsType,\n  elemUdtName,\n  values,\n  onChange,\n}: P) => {\n  const removeValue = (idx: number) => {\n    const newVals = [...values] as typeof values;\n    newVals.splice(idx, 1);\n    onChange(newVals);\n  };\n\n  const renderedValues = values\n    .map((value) => ({ value, isLastNew: false }))\n    .concat([{ value: \"\", isLastNew: true }]);\n  const [orderAge, setOrderAge] = useState(0);\n\n  return (\n    <div className={\"ChipArrayEditor flex-row-wrap gap-p5 ptd-p25 ai-center\"}>\n      {renderedValues.map(({ value, isLastNew }, idx) => (\n        //  Bad UX when selecting text\n\n        <DraggableLI\n          key={`${idx} ${orderAge}`}\n          idx={idx}\n          items={values}\n          onReorder={(newVals) => {\n            onChange(newVals);\n            setOrderAge(Date.now());\n          }}\n          className=\"no-decor flex-row-wrap\"\n        >\n          <InputChip\n            key={idx}\n            elemTsType={elemTsType}\n            elemUdtName={elemUdtName}\n            inputType={inputType}\n            value={value}\n            onPaste={\n              renderedValues.length === 1 ?\n                (e) => {\n                  e.stopPropagation();\n                  e.preventDefault();\n                  const clipboardData = e.clipboardData;\n                  const pastedData = clipboardData.getData(\"Text\").trim();\n\n                  const parseArray = (v: string) => {\n                    try {\n                      const parsedData = JSON.parse(v);\n                      if (Array.isArray(parsedData) && parsedData.length) {\n                        onChange(parsedData);\n                        return;\n                      }\n                    } catch (e) {}\n                  };\n\n                  /** If pasting a valid json array then fill it nicely */\n                  if (pastedData.startsWith(\"[\") && pastedData.endsWith(\"]\")) {\n                    return parseArray(pastedData);\n                  }\n                  if (\n                    pastedData.startsWith(`\"`) &&\n                    pastedData.endsWith(`\"`) &&\n                    pastedData.includes(\",\")\n                  ) {\n                    return parseArray(`[${pastedData}]`);\n                  }\n                }\n              : undefined\n            }\n            {...(isLastNew ?\n              {\n                placeholder: \"add item...\",\n                onChange: (newValue) => {\n                  onChange([...values, newValue]);\n                },\n              }\n            : {\n                onChange: (newValue) =>\n                  onChange(\n                    values.map((_v, _i) =>\n                      _i === idx ? newValue : _v,\n                    ) as typeof values,\n                  ),\n                onRemove: () => removeValue(idx),\n              })}\n          />\n        </DraggableLI>\n      ))}\n    </div>\n  );\n};\n\ntype InputChipProps = Pick<P, \"elemTsType\" | \"elemUdtName\" | \"inputType\"> & {\n  value: any;\n  onChange: (newVal) => void;\n  onPaste?: (e: React.ClipboardEvent<HTMLInputElement>) => void;\n  onRemove?: VoidFunction;\n  placeholder?: string;\n};\nconst InputChip = ({\n  onChange,\n  value,\n  onRemove,\n  placeholder,\n  inputType,\n  elemTsType,\n  elemUdtName,\n  onPaste,\n}: InputChipProps) => {\n  const numberOfChars =\n    Math.max(2, (((value)?.toString() || placeholder) ?? \"\").length) + 2;\n\n  return (\n    <Chip\n      className=\"focus-border  b b-color text-color-0 formfield-bg-color\"\n      onDelete={onRemove}\n      leftIcon={{\n        path: mdiPan,\n        className: \"show-on-parent-hover\",\n      }}\n    >\n      <input\n        className=\"bg-transparent b b-b text-0\"\n        value={value}\n        type={inputType}\n        placeholder={placeholder}\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        style={{\n          width: `${numberOfChars}ch`,\n          minWidth: _PG_date.some((v) => v === elemUdtName) ? \"250px\" : \"50px\",\n          maxWidth: \"300px\",\n\n          minHeight: elemTsType === \"boolean\" ? \"20px\" : undefined,\n        }}\n        onChange={({ target: { value } }) => {\n          onChange(value);\n        }}\n        onPaste={onPaste}\n      />\n    </Chip>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/InsertButton.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport React, { useCallback, useState } from \"react\";\nimport type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport { FileInput } from \"@components/FileInput/FileInput\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport type { SmartFormProps } from \"./SmartForm\";\nimport { SmartForm } from \"./SmartForm\";\n\nexport type InsertButtonProps = {\n  buttonProps?: BtnProps<void>;\n} & Pick<\n  SmartFormProps,\n  | \"db\"\n  | \"tables\"\n  | \"methods\"\n  | \"tableName\"\n  | \"onSuccess\"\n  | \"defaultData\"\n  | \"fixedData\"\n>;\n\nexport const InsertButton = ({\n  buttonProps,\n  tables,\n  db,\n  methods,\n  tableName,\n  onSuccess,\n  defaultData,\n  fixedData,\n}: InsertButtonProps) => {\n  const [open, setOpen] = useState(false);\n  const onClose = useCallback(() => {\n    setOpen(false);\n  }, []);\n  const [defaultFileData, setDefaultFileData] = useState(defaultData);\n  if (!db[tableName]?.insert) {\n    return null;\n  }\n\n  const table = tables.find((t) => t.name === tableName);\n  if (table?.info.isFileTable && !defaultFileData) {\n    return (\n      <FileInput\n        maxFileCount={1}\n        showDropZone={false}\n        onAdd={(files) => {\n          if (!files.length) return;\n\n          setDefaultFileData(files[0]);\n          setOpen(true);\n        }}\n      />\n    );\n  }\n\n  return (\n    <>\n      <Btn\n        iconPath={mdiPlus}\n        {...buttonProps}\n        title={t.W_Table[\"Insert row\"]}\n        data-command=\"dashboard.window.rowInsertTop\"\n        data-key={tableName}\n        color=\"action\"\n        variant=\"filled\"\n        onClick={() => setOpen(!open)}\n      />\n      {open && (\n        <SmartForm\n          asPopup={true}\n          confirmUpdates={true}\n          defaultData={defaultData}\n          fixedData={fixedData}\n          db={db}\n          tables={tables}\n          methods={methods}\n          tableName={tableName}\n          onSuccess={onSuccess}\n          onClose={onClose}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/JoinedRecords/JoinedRecords.tsx",
    "content": "import { type AnyObject } from \"prostgles-types\";\nimport React, { useEffect } from \"react\";\nimport { type DetailedFilterBase } from \"@common/filterUtils\";\nimport type { Prgl } from \"../../../App\";\nimport { FlexCol, FlexRow, classOverride } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport { Section } from \"@components/Section\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport type { SmartFormProps } from \"../SmartForm\";\nimport { ViewMoreSmartCardList } from \"../SmartFormField/ViewMoreSmartCardList\";\nimport type { NewRow, NewRowDataHandler } from \"../SmartFormNewRowDataHandler\";\nimport { JoinedRecordsAddRow } from \"./JoinedRecordsAddRow\";\nimport { JoinedRecordsSection } from \"./JoinedRecordsSection\";\nimport { useJoinedRecordsSections } from \"./useJoinedRecordsSections\";\nimport type { FieldConfig } from \"../../SmartCard/SmartCard\";\n\nexport type JoinedRecordsProps = Pick<Prgl, \"db\" | \"tables\" | \"methods\"> &\n  Pick<SmartFormProps, \"onSuccess\" | \"parentForm\"> & {\n    className?: string;\n    style?: React.CSSProperties;\n    tableName: string;\n    rowFilter?: DetailedFilterBase[];\n    newRowData: NewRow | undefined;\n    newRowDataHandler: NewRowDataHandler | undefined;\n    showRelated?: \"descendants\";\n    modeType?: \"update\" | \"insert\" | \"view\";\n    onTabChange: (tabKey: string | undefined) => void;\n    activeTabKey: string | undefined;\n    errors: AnyObject;\n    row?: AnyObject;\n    tablesToShow?: Record<\n      string,\n      | true\n      | {\n          /**\n           * @deprecated\n           */\n          fieldConfigs?: FieldConfig[];\n        }\n    >;\n  };\n\nexport const JoinedRecords = (props: JoinedRecordsProps) => {\n  const sectionData = useJoinedRecordsSections(props);\n  const {\n    sections = [],\n    isLoadingSections,\n    descendants,\n    isInsert,\n  } = sectionData;\n  const {\n    db,\n    tables,\n    methods,\n    style,\n    className = \"\",\n    modeType: action,\n    activeTabKey,\n    onTabChange,\n  } = props;\n\n  /** Open errored section */\n  useEffect(() => {\n    const erroredSection = sections.find((s) => s.error);\n    if (erroredSection && erroredSection.tableName !== activeTabKey) {\n      onTabChange(erroredSection.tableName);\n    }\n  }, [sections, onTabChange, activeTabKey]);\n\n  if (isLoadingSections) {\n    return <Loading className=\"m-1 as-center\" />;\n  } else if (!sections.length) {\n    return null;\n  }\n\n  if (action === \"insert\" && sections.every((s) => !s.canInsert)) {\n    return null;\n  }\n\n  return (\n    <FlexCol\n      data-command=\"JoinedRecords\"\n      className={classOverride(\n        \"gap-0 bt b-color min-h-0 bg-inherit f-1\",\n        className,\n      )}\n      style={style}\n    >\n      {sections.map((section) => {\n        const { label, path, count, table } = section;\n        const icon = table.icon;\n        return (\n          <Section\n            key={path.join(\".\")}\n            className=\"trigger-hover pl-1\"\n            btnProps={{\n              [\"data-command\"]: \"JoinedRecords.SectionToggle\",\n              [\"data-key\"]: path.join(\".\"),\n              style: { flex: 1 },\n            }}\n            data-command=\"JoinedRecords.Section\"\n            data-key={path.join(\".\")}\n            titleIcon={icon && <SvgIcon icon={icon} />}\n            title={\n              <FlexRow data-key={path.join(\".\")}>\n                <div>{label}</div>\n                <div className=\"text-2\" style={{ fontWeight: \"normal\" }}>\n                  {count}\n                </div>\n              </FlexRow>\n            }\n            titleRightContent={\n              props.newRowDataHandler && (\n                <FlexRow className=\"show-on-trigger-hover ml-auto gap-0\">\n                  {!isInsert && (\n                    <ViewMoreSmartCardList\n                      db={db}\n                      methods={methods}\n                      ftable={table}\n                      searchFilter={section.detailedJoinFilter}\n                      getActions={undefined}\n                      tables={tables}\n                      rootTableName={table.name}\n                    />\n                  )}\n                  <JoinedRecordsAddRow\n                    {...props}\n                    btnProps={{ size: \"small\" }}\n                    section={section}\n                    newRowDataHandler={props.newRowDataHandler}\n                  />\n                </FlexRow>\n              )\n            }\n          >\n            <JoinedRecordsSection\n              {...props}\n              section={section}\n              descendants={descendants}\n              isInsert={isInsert}\n            />\n          </Section>\n        );\n      })}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/JoinedRecords/JoinedRecordsAddRow.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport Btn, { type BtnProps } from \"@components/Btn\";\nimport { SmartForm, type SmartFormProps } from \"../SmartForm\";\nimport { useNestedInsertDefaultData } from \"../SmartFormField/useNestedInsertDefaultData\";\nimport { NewRowDataHandler } from \"../SmartFormNewRowDataHandler\";\nimport type { JoinedRecordsProps } from \"./JoinedRecords\";\nimport type { JoinedRecordSection } from \"./useJoinedRecordsSections\";\n\ntype P = Omit<JoinedRecordsProps, \"newRowDataHandler\"> & {\n  newRowDataHandler: NewRowDataHandler;\n  section: JoinedRecordSection;\n  btnProps?: Pick<BtnProps, \"size\" | \"className\">;\n};\nexport const JoinedRecordsAddRow = (props: P) => {\n  const {\n    tables,\n    db,\n    tableName,\n    methods,\n    onSuccess,\n    section,\n    newRowDataHandler,\n    rowFilter,\n    newRowData,\n    row,\n  } = props;\n\n  const [insert, setInsert] = useState<{\n    /**\n     * auto = A parent row exists. Will insert record with appropriate values in the fkey columns\n     * manual = The parent will is yet to be inserted. Data is just passed to the parent row\n     * */\n    type: \"auto\" | \"manual\";\n    smartFormProps: Pick<\n      SmartFormProps,\n      | \"parentForm\"\n      | \"columns\"\n      | \"label\"\n      | \"defaultData\"\n      | \"onInserted\"\n      | \"onSuccess\"\n    >;\n  }>();\n\n  const isInsert = !rowFilter;\n\n  const onClose = useCallback(() => {\n    setInsert(undefined);\n  }, []);\n\n  const defaultData = useNestedInsertDefaultData({\n    ftable: section.tableName,\n    tables,\n    tableName,\n    row,\n  });\n\n  const btnProps: Pick<BtnProps, \"title\" | \"onClick\" | \"disabledInfo\"> =\n    useMemo(() => {\n      if (isInsert) {\n        const fcols = tables.find((t) => t.name === section.tableName)?.columns;\n        const existingColumnData = newRowData?.[section.tableName];\n        const existingRowData =\n          existingColumnData?.type === \"nested-column\" ?\n            existingColumnData.value\n          : undefined;\n        const defaultNewRow =\n          existingRowData instanceof NewRowDataHandler ? existingRowData : (\n            undefined\n          );\n        return {\n          title: \"Add referenced record\",\n          onClick: () => {\n            setInsert({\n              type: \"manual\",\n              // onChange: (newRow) => {\n              //   const value = [\n              //     ...(newRowData?.[section.tableName]?.value ?? []),\n              //     newRow,\n              //   ];\n              //   newRowDataHandler.setColumnData(section.tableName, {\n              //     type: \"nested-table\",\n              //     value,\n              //   });\n              // },\n              smartFormProps: {\n                columns: fcols\n                  ?.filter(\n                    (c) => !c.references?.some((r) => r.ftable === tableName),\n                  )\n                  .reduce((a, v) => ({ ...a, [v.name]: 1 }), {}),\n                parentForm: {\n                  type: \"insert\",\n                  newRowDataHandler: defaultNewRow,\n                  table: tables.find((t) => t.name === tableName)!,\n                  setColumnData: (newColData) => {\n                    newRowDataHandler.setNestedTable(section.tableName, [\n                      newColData,\n                    ]);\n                    onClose();\n                  },\n                  parentForm: props.parentForm,\n                },\n              },\n            });\n          },\n        };\n      }\n      return {\n        title: \"Add new record\",\n        onClick: () => {\n          if (defaultData) {\n            setInsert({\n              type: \"auto\",\n              smartFormProps: {\n                defaultData,\n                onInserted: onClose,\n                onSuccess: onSuccess,\n              },\n            });\n          }\n        },\n        disabledInfo:\n          !section.canInsert ?\n            section.table.info.isView ? \"Cannot insert into a view\"\n            : !section.tableHandler?.insert ? \"Cannot insert into this table\"\n            : `Cannot reference more than one ${JSON.stringify(section.tableName)}`\n          : undefined,\n      };\n    }, [\n      isInsert,\n      section.canInsert,\n      section.table.info.isView,\n      section.tableName,\n      section.tableHandler,\n      newRowData,\n      newRowDataHandler,\n      tableName,\n      tables,\n      onClose,\n      onSuccess,\n      props.parentForm,\n      defaultData,\n    ]);\n\n  /** Cannot insert if nested table\n   * TODO: allow insert if path.length === 2 and first path is a mapping table\n   */\n  if (section.path.length > 1) return null;\n\n  if (isInsert && !section.tableHandler) return null;\n\n  return (\n    <>\n      {insert && (\n        <SmartForm\n          tableName={section.tableName}\n          db={db}\n          methods={methods}\n          tables={tables}\n          asPopup={true}\n          onClose={onClose}\n          {...insert.smartFormProps}\n        />\n      )}\n      <Btn\n        {...props.btnProps}\n        data-command=\"JoinedRecords.AddRow\"\n        data-key={section.tableName}\n        color=\"action\"\n        iconPath={mdiPlus}\n        {...btnProps}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/JoinedRecords/JoinedRecordsSection.tsx",
    "content": "import ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { isDefined } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { SmartCardList } from \"../../SmartCardList/SmartCardList\";\nimport { SmartCardListJoinedNewRecords } from \"../../SmartCardList/SmartCardListJoinedNewRecords\";\nimport { NewRowDataHandler } from \"../SmartFormNewRowDataHandler\";\nimport type { JoinedRecordsProps } from \"./JoinedRecords\";\nimport type { JoinedRecordSection } from \"./useJoinedRecordsSections\";\nimport { useJoinedSectionFieldConfigs } from \"./useJoinedSectionFieldConfigs\";\n\nexport const JoinedRecordsSection = ({\n  section,\n  descendants,\n  isInsert,\n  ...props\n}: JoinedRecordsProps & {\n  section: JoinedRecordSection;\n  isInsert: boolean;\n  descendants: JoinedRecordsProps[\"tables\"];\n}) => {\n  return (\n    <FlexCol className=\" p-1 \" data-command=\"JoinedRecords.Section\">\n      {section.error && (\n        <ErrorComponent\n          error={section.error}\n          variant=\"outlined\"\n          className=\" f-1\"\n        />\n      )}\n      <JoinedRecordsSectionCardList\n        {...props}\n        section={section}\n        descendants={descendants}\n        isInsert={isInsert}\n      />\n    </FlexCol>\n  );\n};\n\nconst JoinedRecordsSectionCardList = (\n  props: JoinedRecordsProps & {\n    section: JoinedRecordSection;\n    isInsert: boolean;\n    descendants: JoinedRecordsProps[\"tables\"];\n  },\n) => {\n  const {\n    db,\n    tables,\n    methods,\n    newRowData,\n    onSuccess,\n    section: s,\n    isInsert,\n    descendants,\n    newRowDataHandler,\n    tableName,\n    tablesToShow,\n  } = props;\n\n  const descendantInsertTables = useMemo(\n    () => descendants.filter((t) => db[t.name]?.insert).map((t) => t.name),\n    [db, descendants],\n  );\n\n  const nestedInsertData = useMemo(\n    () =>\n      Object.fromEntries(\n        Object.entries(newRowData ?? {})\n          .map(([k, d]) =>\n            d.type === \"nested-table\" ?\n              [\n                k,\n                d.value.map((_v) =>\n                  _v instanceof NewRowDataHandler ? _v.getRow() : _v,\n                ),\n              ]\n            : undefined,\n          )\n          .filter(isDefined),\n      ),\n    [newRowData],\n  );\n\n  const fieldConfigs = useJoinedSectionFieldConfigs({\n    sectionTable: s.table,\n    tables,\n    tableName,\n    tablesToShow,\n  });\n\n  const { count } = s;\n\n  const limit = 20;\n  if (isInsert) {\n    if (!descendantInsertTables.includes(s.tableName)) {\n      return null;\n    }\n    return (\n      <SmartCardListJoinedNewRecords\n        key={s.path.join(\".\")}\n        db={db}\n        methods={methods}\n        table={s.table}\n        tables={tables}\n        className=\"px-1\"\n        excludeNulls={true}\n        onSuccess={onSuccess}\n        data={nestedInsertData?.[s.tableName] ?? []}\n        onChange={(newData) => {\n          newRowDataHandler?.setNestedTable(s.tableName, newData);\n        }}\n        noDataComponent={\n          <InfoRow className=\" \" color=\"info\" variant=\"filled\">\n            No records\n          </InfoRow>\n        }\n        noDataComponentMode=\"hide-all\"\n      />\n    );\n  }\n\n  return (\n    <div className=\"flex-col\">\n      {count > 20 && <div>Showing top {limit} records</div>}\n      <SmartCardList\n        key={s.path.join(\".\")}\n        db={db}\n        tables={tables}\n        methods={methods}\n        tableName={s.tableName}\n        filter={s.joinFilter}\n        className=\"px-1\"\n        onSuccess={onSuccess}\n        realtime={true}\n        excludeNulls={true}\n        showTopBar={false}\n        noDataComponent={\n          <InfoRow className=\" \" color=\"info\" variant=\"filled\">\n            No records\n          </InfoRow>\n        }\n        noDataComponentMode=\"hide-all\"\n        fieldConfigs={fieldConfigs}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/JoinedRecords/getJoinFilter.ts",
    "content": "import type { DetailedFilter, DetailedFilterBase } from \"@common/filterUtils\";\nimport { isDefined } from \"../../../utils/utils\";\n\nexport const getJoinFilter = function (\n  path: string[],\n  tableName: string,\n  rowFilter: DetailedFilterBase[] = [],\n  opts?: Pick<DetailedFilter, \"minimised\" | \"disabled\">,\n): DetailedFilter[] {\n  const f: DetailedFilter[] = rowFilter.map(({ fieldName, type, value }) => {\n    const filter = {\n      fieldName,\n      value,\n      type,\n      ...opts,\n    };\n\n    /** Why hide self joins? */\n    // if(path.at(-1) === tableName){\n    //   return filter;\n    // }\n\n    return {\n      type: \"$existsJoined\",\n      path: [...path.slice(0, -1), tableName].filter(isDefined),\n      filter,\n      ...opts,\n    };\n  });\n  return f;\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/JoinedRecords/useJoinedRecordsSections.ts",
    "content": "import { getSmartGroupFilter, type DetailedFilter } from \"@common/filterUtils\";\nimport type {\n  TableHandlerClient,\n  ViewHandlerClient,\n} from \"prostgles-client/dist/prostgles\";\nimport { usePromise } from \"prostgles-client\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { useMemo, useRef, useState } from \"react\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\nimport { getJoinFilter } from \"./getJoinFilter\";\nimport type { JoinedRecordsProps } from \"./JoinedRecords\";\n\nconst getAllParentTableNames = (\n  parentForm: JoinedRecordsProps[\"parentForm\"],\n): string[] => {\n  if (!parentForm?.table) return [];\n  return [\n    parentForm.table.name,\n    ...getAllParentTableNames(parentForm.parentForm),\n  ];\n};\n\nexport type JoinedRecordSection = {\n  label: string;\n  tableName: string;\n  path: string[];\n  expanded?: boolean;\n  existingDataCount: number;\n  canInsert?: boolean;\n  error?: string;\n  joinFilter: AnyObject;\n  detailedJoinFilter: DetailedFilter[];\n  count: number;\n  table: DBSchemaTableWJoins;\n  tableHandler: Partial<TableHandlerClient> | undefined;\n};\nexport const useJoinedRecordsSections = (props: JoinedRecordsProps) => {\n  const {\n    tables,\n    db,\n    tableName,\n    modeType: action,\n    showRelated,\n    newRowData,\n    rowFilter,\n    parentForm,\n    errors,\n    tablesToShow,\n  } = props;\n  const [isLoadingSections, setIsLoadingSections] = useState(false);\n\n  const tablesMap = useMemo(() => {\n    const map = new Map<string, DBSchemaTableWJoins>();\n    tables.forEach((t) => {\n      map.set(t.name, t);\n    });\n    return map;\n  }, [tables]);\n\n  const parentFormTableNames = useMemo(\n    () => getAllParentTableNames(parentForm),\n    [parentForm],\n  );\n\n  const table = useMemo(() => tablesMap.get(tableName), [tablesMap, tableName]);\n\n  const currentSections = useRef<JoinedRecordSection[]>([]);\n  const isInsert = !rowFilter;\n\n  const { diplayedTables, descendants } = useMemo(() => {\n    const tableJoins = table?.joins.filter((j) => j.hasFkeys) ?? [];\n    const diplayedTables = tableJoins.filter(\n      (t) =>\n        (!tablesToShow || t.tableName in tablesToShow) &&\n        !parentFormTableNames.includes(t.tableName),\n    );\n\n    const descendants = tables.filter((t) =>\n      t.columns.some((c) => c.references?.some((r) => r.ftable === tableName)),\n    );\n\n    return { diplayedTables, descendants };\n  }, [tables, tableName, parentFormTableNames, table?.joins, tablesToShow]);\n\n  const nestedInsertData = useMemo(\n    () =>\n      Object.fromEntries(\n        Object.entries(newRowData ?? {})\n          .map(([k, d]) =>\n            d.type === \"nested-table\" ? [k, d.value] : undefined,\n          )\n          .filter(isDefined),\n      ) as Record<string, AnyObject[]>,\n    [newRowData],\n  );\n\n  const sections = usePromise(async () => {\n    setIsLoadingSections(true);\n    const allSections = await Promise.all(\n      diplayedTables.map(async (j) => {\n        const canInsert = db[j.tableName]?.insert && j.hasFkeys;\n        if (action === \"insert\" && !canInsert) return;\n        const path = [j.tableName];\n        const detailedJoinFilter = getJoinFilter(path, tableName, rowFilter, {\n          minimised: true,\n        });\n        const joinFilter = getSmartGroupFilter(detailedJoinFilter);\n        let countStr = \"0\";\n        let countError: string | undefined;\n        const tableHandler = db[j.tableName] as\n          | undefined\n          | Partial<TableHandlerClient | ViewHandlerClient>;\n        try {\n          if (!isInsert) {\n            countStr =\n              (await tableHandler?.count?.(joinFilter))?.toString() ?? \"0\";\n          }\n        } catch (err) {\n          countError = `Failed to db.${j.tableName}.count(${JSON.stringify(joinFilter)})`;\n          console.error(countError);\n        }\n        const existingDataCount = isInsert ? 0 : parseInt(countStr);\n\n        const count =\n          (isInsert ?\n            nestedInsertData[j.tableName]?.length\n          : existingDataCount) ?? 0;\n\n        const table = tablesMap.get(j.tableName);\n        if (!table) return;\n\n        const res: JoinedRecordSection = {\n          label: table.label,\n          tableName: j.tableName,\n          existingDataCount,\n          canInsert,\n          path,\n          error: errors[j.tableName] || countError,\n          joinFilter,\n          detailedJoinFilter,\n          count,\n          expanded: currentSections.current.find(\n            (s) => s.tableName === j.tableName,\n          )?.expanded,\n          table,\n          tableHandler,\n        };\n\n        return res;\n      }),\n    );\n\n    const sections = allSections\n      .filter(isDefined)\n      .filter(\n        (s) => !showRelated || descendants.some((t) => t.name === s.tableName),\n      );\n    setIsLoadingSections(false);\n    return sections;\n  }, [\n    diplayedTables,\n    action,\n    db,\n    rowFilter,\n    tableName,\n    isInsert,\n    nestedInsertData,\n    descendants,\n    showRelated,\n    errors,\n    tablesMap,\n  ]);\n\n  currentSections.current = sections ?? [];\n  return {\n    sections,\n    isInsert,\n    descendants,\n    isLoadingSections,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/JoinedRecords/useJoinedSectionFieldConfigs.tsx",
    "content": "import { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport { isDefined, isObject, type AnyObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\nimport type { FieldConfig } from \"../../SmartCard/SmartCard\";\nimport { SmartCardColumn } from \"../../SmartCard/SmartCardColumn\";\nimport { getBestTextColumns } from \"../SmartFormField/fetchForeignKeyOptions\";\nimport { RenderValue } from \"../SmartFormField/RenderValue\";\nimport type { JoinedRecordsProps } from \"./JoinedRecords\";\n\nexport const useJoinedSectionFieldConfigs = ({\n  sectionTable,\n  tables,\n  tableName,\n  tablesToShow,\n}: {\n  sectionTable: DBSchemaTableWJoins;\n  tableName: string | undefined;\n} & Pick<JoinedRecordsProps, \"tables\" | \"tablesToShow\">):\n  | FieldConfig[]\n  | undefined => {\n  return useMemo(() => {\n    const tableInfo = tablesToShow?.[sectionTable.name];\n    if (isObject(tableInfo) && tableInfo.fieldConfigs) {\n      return tableInfo.fieldConfigs;\n    }\n    const fileTable = tables.find((t) => t.info.isFileTable);\n    if (fileTable?.name === sectionTable.name) {\n      return [\n        {\n          name: \"url\",\n          render: (url, row) => (\n            <MediaViewer style={{ maxWidth: \"300px\" }} url={url} />\n          ),\n        },\n      ];\n    }\n\n    const rootTable =\n      !tableName ? undefined : tables.find((t) => t.name === tableName);\n    if (!rootTable) return;\n\n    const nonJoinColumnsToShow = sectionTable.columns.filter(\n      (c) =>\n        c.select && !c.references?.some((r) => r.ftable === rootTable.name),\n    );\n\n    /** We want to add one-to-one joins that have text fields */\n    const extraColumnsToShow: FieldConfig[] = sectionTable.joinsV2\n      .filter((j) => j.tableName !== rootTable.name)\n      .map((joinInfo) => {\n        const fTable = tables.find((t) => t.name === joinInfo.tableName);\n        if (!fTable) return;\n\n        const joinColumns = joinInfo.on.flatMap((conditions) =>\n          conditions.map(([col1, col2]) => col2),\n        );\n        const isOneToOneJoin = fTable.info.uniqueColumnGroups?.some(\n          (groupCols) => groupCols.every((col) => joinColumns.includes(col)),\n        );\n        if (!isOneToOneJoin) return;\n        const textCols = getBestTextColumns(fTable, joinColumns);\n        if (!textCols.length) return;\n\n        return {\n          name: fTable.name,\n          select: textCols.reduce(\n            (a, v) => ({\n              ...a,\n              [v.name]: 1,\n            }),\n            {},\n          ),\n          renderMode: \"full\",\n          render: (ftableRows) => {\n            const ftableRow = ftableRows[0] as AnyObject | undefined;\n            if (!ftableRow) return null;\n            return textCols.map((c) => {\n              const value = ftableRow[c.name];\n              return (\n                <SmartCardColumn\n                  key={fTable.name + c.name}\n                  labelText={[fTable.label, c.label || c.name].join(\".\")}\n                  info={undefined}\n                  labelTitle={c.label || c.name}\n                  renderMode={\"value\"}\n                  valueNode={\n                    <RenderValue\n                      key={fTable.name + c.name}\n                      value={value}\n                      column={c}\n                    />\n                  }\n                />\n              );\n            });\n          },\n        } satisfies FieldConfig;\n      })\n      .filter(isDefined);\n    if (extraColumnsToShow.length) {\n      return [\n        ...nonJoinColumnsToShow.map(\n          (c) =>\n            ({\n              name: c.name,\n            }) satisfies FieldConfig,\n        ),\n        ...extraColumnsToShow,\n      ] satisfies FieldConfig[];\n    }\n  }, [\n    sectionTable.columns,\n    sectionTable.joinsV2,\n    sectionTable.name,\n    tableName,\n    tables,\n    tablesToShow,\n  ]);\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartForm.tsx",
    "content": "import type { AnyObject, ValidatedColumnInfo } from \"prostgles-types\";\nimport { omitKeys } from \"prostgles-types\";\nimport React, { useCallback } from \"react\";\nimport { type DetailedFilterBase } from \"@common/filterUtils\";\nimport type { Prgl } from \"../../App\";\nimport { SuccessMessage } from \"@components/Animations\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { classOverride } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport { ifEmpty } from \"../../utils/utils\";\nimport type { DBSchemaTablesWJoins } from \"../Dashboard/dashboardUtils\";\nimport { SmartFormFieldList } from \"./SmartFormFieldList\";\nimport { SmartFormFooterButtons } from \"./SmartFormFooter/SmartFormFooterButtons\";\nimport { useSmartFormActions } from \"./SmartFormFooter/useSmartFormActions\";\nimport { type NewRowDataHandler } from \"./SmartFormNewRowDataHandler\";\nimport { SmartFormPopupWrapper } from \"./SmartFormPopup/SmartFormPopupWrapper\";\nimport { SmartFormUpperFooter } from \"./SmartFormUpperFooter/SmartFormUpperFooter\";\nimport { useSmartForm, type SmartFormState } from \"./useSmartForm\";\nimport type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport type { JoinedRecordsProps } from \"./JoinedRecords/JoinedRecords\";\nimport type { JSONBSchemaCommonProps } from \"@components/JSONBSchema/JSONBSchema\";\n\nexport type getErrorsHook = (\n  cb: (\n    newRow: AnyObject,\n  ) => Promise<SmartFormState[\"error\"] | void> | SmartFormState[\"error\"] | void,\n) => void;\n\nexport type GetRefHooks = {\n  /**\n   * Show custom errors. Will first check against column is_nullable constraint\n   */\n  getErrors: getErrorsHook;\n};\n\nexport type GetRefCB = (hooks: GetRefHooks) => void;\n\nexport type ColumnDisplayConfig = {\n  hideLabel?: boolean;\n  sectionHeader?: string;\n  onRender?: (value: any, setValue: (newValue: any) => void) => React.ReactNode;\n};\n\nexport type SmartFormProps = Pick<Prgl, \"db\" | \"tables\" | \"methods\"> & {\n  tableName: string;\n\n  label?: string;\n  /**\n   * Executed after the rowFilter data was fetched\n   */\n  onLoaded?: VoidFunction;\n  onChange?: (newRow: AnyObject) => void;\n\n  rowFilter?: DetailedFilterBase[];\n  /**\n   * If true will not \"Update\" button in bottom bar and changes will be applied immediately\n   */\n  confirmUpdates?: boolean;\n  onClose?: (dataChanged: boolean) => void;\n\n  /**\n   * Used for i18n\n   */\n  lang?: string;\n\n  /**\n   *\n   */\n  getRef?: GetRefCB;\n\n  defaultData?: AnyObject;\n\n  asPopup?: boolean;\n\n  onInserted?: (newRow: AnyObject | AnyObject[]) => void;\n  onBeforeInsert?: (newRow: AnyObject | AnyObject[]) => void;\n\n  enableInsert?: boolean;\n  insertBtnText?: string;\n  hideNullBtn?: boolean;\n\n  /**\n   * If truthy then will render jsonbSchema columns using controls instead of code editor\n   */\n  jsonbSchemaWithControls?:\n    | boolean\n    | Partial<\n        Pick<JSONBSchemaCommonProps, \"schemaStyles\" | \"tables\" | \"noLabels\">\n      >;\n\n  /**\n   * Fired after a successful update/insert\n   */\n  onSuccess?: (\n    action: \"insert\" | \"update\" | \"delete\",\n    newData?: AnyObject,\n  ) => void;\n\n  className?: string;\n\n  showJoinedTables?: boolean | JoinedRecordsProps[\"tablesToShow\"];\n\n  disabledActions?: (\"update\" | \"delete\" | \"clone\")[];\n\n  hideNonUpdateableColumns?: boolean;\n\n  /**\n   * Data used in insert or update mode that the user can't overwrite\n   */\n  fixedData?: AnyObject;\n\n  /**\n   * Used to skip through records\n   */\n  onPrevOrNext?: (increment: -1 | 1) => void;\n\n  /**\n   * Used in activating the prev/next buttons\n   */\n  prevNext?: {\n    prev: boolean;\n    next: boolean;\n  };\n\n  noRealtime?: boolean;\n  contentClassname?: string;\n\n  /**\n   * Allows inserting nested data.\n   *\n   * There are 3 cases where this is used:\n   * 1. parentForm column is a fkey column to this tableName (\"nested-column\" OR \"nested-file-column\")\n   * 2. parentForm is referenced by this tableName (\"nested-table\")\n   *\n   * There are 2 modes this is handled:\n   * a. update mode (parentForm.rowFilter is defined)\n   * b. insert mode (parentForm.rowFilter is undefined)\n   *\n   * Cases:\n   * .1.a -> insert data then update parentForm.column\n   * .1.b -> pass data to parent form\n   * .2.a -> insert data with fkey columns set to appropriate values from parentForm.row\n   * .2.b -> pass data to parent form\n   *\n   * onSuccess must be defined to close this child form\n   */\n  parentForm?: {\n    table: DBSchemaTablesWJoins[number];\n    parentForm?: SmartFormProps[\"parentForm\"];\n  } & (\n    | {\n        type: \"insert\";\n        newRowDataHandler: NewRowDataHandler | undefined;\n        setColumnData: (newData: NewRowDataHandler) => void;\n      }\n    | {\n        type: \"insert-and-update\";\n        row: AnyObject;\n        column: ValidatedColumnInfo;\n        rowFilter: SmartFormProps[\"rowFilter\"];\n      }\n  );\n} & SmartFormColumnConfig;\n\ntype SmartFormColumnConfig =\n  | {\n      columns?: Record<string, 1 | ColumnDisplayConfig>;\n      columnFilter?: never;\n    }\n  | {\n      columnFilter?: (c: ValidatedColumnInfo) => boolean;\n      columns?: never;\n    };\n\nexport const SmartForm = (props: SmartFormProps) => {\n  const { tableName } = props;\n  const stateOrError = useSmartForm(props);\n  const { mode, error, table } = stateOrError;\n  const tableInfo = table?.info;\n\n  if (!tableInfo) {\n    return <>Table {tableName} not found.</>;\n  }\n\n  if (!mode) {\n    return <> {error || \"Mode missing\"}</>;\n  }\n\n  const state: SmartFormState = {\n    ...stateOrError,\n    table,\n    mode,\n  };\n\n  return <SmartFormWithNoError props={props} state={state} />;\n};\n\nconst SmartFormWithNoError = ({\n  props,\n  state,\n}: {\n  props: SmartFormProps;\n  state: SmartFormState;\n}) => {\n  const { tableName, label, asPopup, className = \"\", onClose } = props;\n\n  const { mode, displayedColumns, errors, error, table, loading } = state;\n  const tableInfo = table.info;\n\n  const isLoading = loading || (\"loading\" in mode ? mode.loading : false);\n  const headerFromCardConfig =\n    (\n      table.card?.headerColumn &&\n      \"currentRow\" in mode &&\n      mode.currentRow &&\n      table.card.headerColumn in mode.currentRow\n    ) ?\n      (mode.currentRow[table.card.headerColumn] as string)\n    : undefined;\n  const headerText =\n    label ??\n    (headerFromCardConfig || table.label || tableInfo.comment || tableName);\n\n  const formHeader =\n    asPopup ? null\n    : !label && \"label\" in props ? null\n    : headerText ?\n      <h4\n        className=\"font-24\"\n        style={{\n          margin: 0,\n          padding: \"1em\",\n          paddingBottom: \".5em\",\n        }}\n      >\n        {headerText}\n      </h4>\n    : null;\n\n  const onCloseWrapped = useCallback(() => {\n    onClose?.(true);\n  }, [onClose]);\n\n  const actionsState = useSmartFormActions({\n    ...props,\n    ...state,\n  });\n  const { successMessage, setSuccessMessage } = actionsState;\n\n  const maxWidth = \"max-w-650\" as const;\n\n  return (\n    <SmartFormPopupWrapper\n      {...props}\n      table={table}\n      onClose={onCloseWrapped}\n      maxWidth={maxWidth}\n      displayedColumns={displayedColumns}\n      headerText={headerText}\n      rowFilterObj={\"rowFilterObj\" in mode ? mode.rowFilterObj : undefined}\n    >\n      <div\n        data-command={isLoading ? undefined : \"SmartForm\"}\n        data-key={tableName}\n        aria-disabled={isLoading}\n        style={asPopup ? { minWidth: \"350px\" } : {}}\n        className={classOverride(\n          \"SmartForm \" +\n            (asPopup ? \"\" : maxWidth) +\n            \" fade-in flex-col f-1 min-h-0 relative \" +\n            (isLoading ? \" no-pointer-events noselect \" : \" \"),\n          className,\n        )}\n      >\n        {isLoading && <Loading variant=\"cover\" />}\n\n        {formHeader}\n        <SmartFormFieldList {...props} {...state} />\n\n        <SmartFormUpperFooter {...props} {...state} />\n\n        <ErrorComponent\n          className=\"f-0 b rounded\"\n          style={{ flex: \"none\", padding: \"1em\" }}\n          withIcon={true}\n          error={\n            error ||\n            ifEmpty(\n              omitKeys(\n                errors,\n                displayedColumns.map((c) => c.name),\n              ),\n              undefined,\n            )\n          }\n        />\n        {successMessage && !error && (\n          <SuccessMessage\n            message={`${successMessage}!`}\n            duration={{\n              millis: 2e3,\n              onEnd: () => {\n                onCloseWrapped();\n                setSuccessMessage(undefined);\n              },\n            }}\n            className=\"w-full h-full bg-color-0\"\n            style={{ position: \"absolute\", zIndex: 222 }}\n          />\n        )}\n\n        <SmartFormFooterButtons {...props} {...state} {...actionsState} />\n      </div>\n    </SmartFormPopupWrapper>\n  );\n};\n\nexport const SmartFormPopup = ({\n  triggerButton,\n  ...smartFormProps\n}: SmartFormProps & {\n  triggerButton: Pick<\n    BtnProps,\n    | \"label\"\n    | \"children\"\n    | \"iconPath\"\n    | \"color\"\n    | \"variant\"\n    | \"title\"\n    | \"style\"\n    | \"className\"\n  >;\n}) => {\n  const [anchorEl, setAnchorEl] = React.useState<HTMLElement>();\n  return (\n    <>\n      <Btn\n        {...triggerButton}\n        onClick={(e) => {\n          setAnchorEl(e.currentTarget);\n        }}\n      />\n      {anchorEl && (\n        <SmartForm\n          {...smartFormProps}\n          onClose={(e) => {\n            setAnchorEl(undefined);\n            smartFormProps.onClose?.(e);\n          }}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/RenderValue.tsx",
    "content": "import { getProperty, sliceText } from \"@common/utils\";\nimport type { LocalMedia } from \"@components/FileInput/FileInput\";\nimport { ShorterText } from \"@components/ShorterText\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport { _PG_date, _PG_numbers, includes, isObject } from \"prostgles-types\";\nimport React from \"react\";\nimport { dateAsYMD_Time } from \"../../Charts\";\nimport { getPGIntervalAsText } from \"../../W_SQL/customRenderers\";\n\ntype P = {\n  column:\n    | Pick<ValidatedColumnInfo, \"udt_name\" | \"tsDataType\" | \"file\">\n    | undefined;\n  value: any;\n  /**\n   * Defaults to true\n   */\n  showTitle?: boolean;\n  maxLength?: number;\n  maximumFractionDigits?: number;\n  getValues?: () => any[];\n  style?: React.CSSProperties;\n};\nexport const renderNull = (\n  v: any,\n  style: React.CSSProperties | undefined,\n  showTitle: boolean,\n) => {\n  if ([null, undefined].includes(v)) {\n    return (\n      <i\n        style={style}\n        className=\"text-2  noselect\"\n        title={showTitle ? \"NULL\" : undefined}\n      >\n        NULL\n      </i>\n    );\n  }\n\n  return null;\n};\n\nexport const RenderValue = ({\n  column: c,\n  value,\n  showTitle = true,\n  maxLength,\n  style,\n  maximumFractionDigits = 3,\n  getValues,\n}: P): JSX.Element => {\n  const nullRender = renderNull(value, style, showTitle);\n  if (nullRender) return nullRender;\n  const getSliced = (v: string | null | undefined, _maxLength?: number) => {\n    const nullRender = renderNull(v, style, showTitle);\n    if (nullRender) return nullRender;\n    if (maxLength) return sliceText(v?.toString(), _maxLength ?? maxLength);\n\n    return v;\n  };\n\n  if (c?.file && isObject(value)) {\n    const media = value as LocalMedia;\n    return <div>{media.name}</div>;\n  }\n\n  if (c?.udt_name === \"uuid\" && value) {\n    return <ShorterText style={style} value={value} column={c} />;\n  }\n\n  const numericStyle = {\n    /* Align numbers to right for an easier read */\n    textAlign: \"right\",\n  } as const;\n\n  const { udt_name, tsDataType } = c ?? {};\n  if (tsDataType !== \"number\" && udt_name === \"int8\") {\n    return (\n      <span\n        style={{\n          color: getColumnDataColor({ udt_name: \"int4\", tsDataType: \"number\" }),\n          ...numericStyle,\n          ...style,\n        }}\n      >\n        {BigInt(value).toLocaleString()}\n      </span>\n    );\n  }\n\n  if (\n    // tsDataType === \"number\" &&\n    udt_name &&\n    includes(_PG_numbers, udt_name) &&\n    value !== undefined &&\n    value !== null\n  ) {\n    const maxDecimalsFromValues = getValues?.()\n      .map((v) => {\n        if (v === null || v === undefined) return 0;\n        const num = +v;\n        if (isNaN(num)) return 0;\n        return countDecimals(num);\n      })\n      .reduce((a, b) => Math.max(a, b), 0);\n    const getValue = () => {\n      const isFloat =\n        udt_name === \"float4\" ||\n        udt_name === \"float8\" ||\n        udt_name === \"numeric\";\n      if (!isFloat) return +value;\n      const actualDecimals = countDecimals(+value);\n      const maxDecimals =\n        +value < 1 && +value > -1 ? actualDecimals + 1 : maximumFractionDigits;\n      const slicedValue = getSliced(\n        (+value).toLocaleString(undefined, {\n          minimumFractionDigits:\n            maxDecimalsFromValues ?? Math.min(maxDecimals, actualDecimals),\n        }),\n      );\n      return slicedValue;\n    };\n\n    return (\n      <span\n        title={value}\n        style={{ color: getColumnDataColor(c), ...numericStyle, ...style }}\n      >\n        {getValue()}\n      </span>\n    );\n  }\n  if (c?.udt_name === \"interval\") {\n    return <>{getPGIntervalAsText(value)}</>;\n  }\n\n  if (value && [\"geography\", \"geometry\"].includes(c?.udt_name ?? \"\")) {\n    if (typeof value === \"object\" && !Array.isArray(value)) {\n      return <>{getSliced(JSON.stringify(value))}</>;\n    }\n    return <ShorterText style={style} value={value} column={c} />;\n  }\n\n  if (value && _PG_date.some((v) => v === c?.udt_name)) {\n    let val = value;\n\n    if (c?.udt_name !== \"timestamp\") {\n      try {\n        const date = new Date(value);\n        val =\n          dateAsYMD_Time(date) +\n          \".\" +\n          date.getMilliseconds().toString().padStart(3, \"0\");\n      } catch (e) {\n        console.error(e);\n      }\n    }\n\n    return (\n      <span style={{ color: getColumnDataColor(c), ...style }}>{val}</span>\n    );\n  }\n\n  if (value && (c?.udt_name.startsWith(\"json\") || isObject(value))) {\n    return (\n      <span style={{ color: getColumnDataColor(c), ...style }}>\n        {getSliced(JSON.stringify(value))}\n      </span>\n    );\n  }\n\n  if (typeof value === \"boolean\") {\n    return (\n      <span\n        style={{\n          color: getColumnDataColor({\n            ...c,\n            tsDataType: \"boolean\",\n            udt_name: \"bool\",\n          }),\n          ...style,\n        }}\n      >\n        {value.toString()}\n      </span>\n    );\n  }\n  if (typeof value === \"string\") {\n    if (value === \"\")\n      return (\n        <i\n          style={style}\n          className=\"text-2 noselect\"\n          title={showTitle ? \"&quot;&quot;\" : undefined}\n        >\n          Empty String\n        </i>\n      );\n    return <>{getSliced(value)}</>;\n  }\n\n  if (Array.isArray(value)) {\n    return (\n      <div className=\"flex-row-wrap gap-p25\">\n        {value.map((v, i) => (\n          <span key={i} className=\"chip gray font-14\" style={style}>\n            {getSliced(v)}\n          </span>\n        ))}\n      </div>\n    );\n  }\n\n  return <>{getSliced(value)}</>;\n};\n\nconst countDecimals = (num: number) => {\n  if (Math.floor(num.valueOf()) === num.valueOf()) return 0;\n  return num.toString().split(\".\")[1]?.length || 0;\n};\n\nexport const getColumnDataColor = (\n  c?: Pick<Partial<ValidatedColumnInfo>, \"udt_name\" | \"tsDataType\" | \"is_pkey\">,\n  fallBackColor?: string,\n) => {\n  if (c?.udt_name === \"uuid\" || c?.is_pkey) {\n    return \"var(--color-uuid)\";\n  }\n\n  if (c?.udt_name === \"geography\" || c?.udt_name === \"geometry\") {\n    return \"var(--color-geo)\";\n  }\n\n  if (\n    c?.udt_name === \"json\" ||\n    c?.udt_name === \"jsonb\" ||\n    c?.tsDataType === \"any\"\n  ) {\n    return \"var(--color-json)\";\n  }\n\n  if (_PG_date.some((v) => v === c?.udt_name)) {\n    return \"var(--color-date)\";\n  }\n\n  if (c && includes(_PG_numbers, c.udt_name)) {\n    return \"var(--color-number)\";\n  }\n\n  const TS_COL_TYPE_TO_COLOR = {\n    number: \"var(--color-number)\",\n    boolean: \"var(--color-boolean)\",\n  } as const;\n\n  return (\n    (c?.tsDataType ?\n      getProperty(TS_COL_TYPE_TO_COLOR, c.tsDataType)\n    : undefined) ?? fallBackColor\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/SmartFormField.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport type { FormFieldProps } from \"@components/FormField/FormField\";\nimport FormField from \"@components/FormField/FormField\";\nimport { FormFieldCodeEditor } from \"@components/FormField/FormFieldCodeEditor\";\nimport { JSONBSchemaA } from \"@components/JSONBSchema/JSONBSchema\";\nimport Loading from \"@components/Loader/Loading\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport { mdiDotsHorizontal } from \"@mdi/js\";\nimport { isObject, type AnyObject } from \"prostgles-types\";\nimport React, { useCallback, useState } from \"react\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type { DBSchemaTableColumn } from \"../../Dashboard/dashboardUtils\";\nimport { getPGIntervalAsText } from \"../../W_SQL/customRenderers\";\nimport type { ColumnDisplayConfig, SmartFormProps } from \"../SmartForm\";\nimport type {\n  ColumnData,\n  NewRowDataHandler,\n} from \"../SmartFormNewRowDataHandler\";\nimport { SmartFormFieldFileSection } from \"./SmartFormFieldFileSection\";\nimport {\n  SmartFormFieldForeignKey,\n  type SmartFormFieldForeignKeyProps,\n} from \"./SmartFormFieldForeignKey\";\nimport {\n  SmartFormFieldLinkedData,\n  useSmartFormFieldForeignDataState,\n} from \"./SmartFormFieldLinkedData\";\nimport { getSmartFormFieldRightButtons } from \"./SmartFormFieldRightButtons\";\nimport {\n  columnIsReadOnly,\n  getInputAutocomplete,\n  getInputType,\n  parseValue,\n  tsDataTypeFromUdtName,\n} from \"./fieldUtils\";\nimport { useSmartFormFieldAsJSON } from \"./useSmartFormFieldAsJSON\";\nimport { useSmartFormFieldOnChange } from \"./useSmartFormFieldOnChange\";\n\ntype SmartFormFieldValue =\n  | string\n  | number\n  | {\n      data: File;\n      name: string;\n    }\n  | null;\n\nexport type SmartFormFieldProps = Pick<\n  SmartFormProps,\n  \"db\" | \"methods\" | \"tableName\" | \"jsonbSchemaWithControls\"\n> & {\n  maxWidth?: string;\n  value: SmartFormFieldValue | undefined;\n  newValue: ColumnData | undefined;\n  row: AnyObject | undefined;\n  action?: \"update\" | \"insert\" | \"view\";\n  loading: boolean | undefined;\n  column: SmartColumnInfo;\n  style?: React.CSSProperties;\n  placeholder?: string;\n  multiSelect?: boolean;\n  error?: any;\n  rightContentAlwaysShow?: boolean;\n  rightContent?: React.ReactNode;\n  hideNullBtn?: boolean;\n  sectionHeader?: string;\n  tables: CommonWindowProps[\"tables\"];\n  table: CommonWindowProps[\"tables\"][number];\n  enableInsert: boolean;\n  newRowDataHandler: NewRowDataHandler;\n  someColumnsHaveIcons: boolean;\n};\nexport type SmartColumnInfo = DBSchemaTableColumn & ColumnDisplayConfig;\n\n/**\n * Allows displaying and editing a single column from a SmartForm based on table schema and config\n */\nexport const SmartFormField = (props: SmartFormFieldProps) => {\n  const {\n    action,\n    value,\n    placeholder = \"\",\n    multiSelect,\n    column,\n    hideNullBtn,\n    sectionHeader,\n    style,\n    tableName,\n    maxWidth = \"100vw\",\n    db,\n    row,\n    tables,\n    table,\n    rightContentAlwaysShow,\n    jsonbSchemaWithControls,\n    enableInsert,\n    newRowDataHandler,\n    someColumnsHaveIcons,\n    loading,\n  } = props;\n\n  const onChange = useCallback(\n    (newColData: ColumnData) => {\n      newRowDataHandler.setColumnData(column.name, newColData);\n    },\n    [newRowDataHandler, column.name],\n  );\n\n  const { onCheckAndChange, error } = useSmartFormFieldOnChange({\n    onChange,\n    column,\n    table,\n  });\n  const [showDateInput, setShowDateInput] = useState(true);\n\n  let rightIcons: React.ReactNode = getSmartFormFieldRightButtons({\n    ...props,\n    onChange: onCheckAndChange,\n    showDateInput: {\n      value: !!showDateInput,\n      onChange: setShowDateInput,\n    },\n  });\n  const [collapsed, setCollapsed] = useState(true);\n\n  const renderAsJSON = useSmartFormFieldAsJSON({\n    column,\n    tableName,\n    jsonbSchemaWithControls,\n    value,\n  });\n\n  const readOnly = columnIsReadOnly(action, column);\n  const foreignDataState = useSmartFormFieldForeignDataState({\n    readOnly,\n    row,\n    column,\n    action,\n    db,\n    enableInsert,\n    tables,\n  });\n\n  if (readOnly) rightIcons = null;\n  let hint = column.hint;\n  if (\n    !readOnly &&\n    column.has_default &&\n    column.is_pkey &&\n    action === \"insert\"\n  ) {\n    hint = hint || \"Column has default value. Can leave empty\";\n    if (collapsed) {\n      return (\n        <Btn\n          className=\"mt-1\"\n          title=\"Expand\"\n          iconPath={mdiDotsHorizontal}\n          onClick={() => {\n            setCollapsed(!collapsed);\n          }}\n        />\n      );\n    }\n  }\n\n  let parsedValue;\n  try {\n    parsedValue = parseValue(column, value);\n  } catch (e: any) {\n    parsedValue = value;\n  }\n  if (readOnly && column.udt_name === \"interval\" && isObject(value)) {\n    parsedValue = getPGIntervalAsText(value);\n  }\n\n  let type = getInputType(column).toLowerCase();\n  if (type.startsWith(\"date\") && !showDateInput) {\n    type = \"text\";\n  }\n\n  let arrayType: FormFieldProps<\"text\">[\"arrayType\"];\n  if (column.tsDataType.endsWith(\"[]\") && !column.tsDataType.includes(\"any\")) {\n    const elemTSType = tsDataTypeFromUdtName(column.element_udt_name as any);\n    arrayType = {\n      tsDataType: elemTSType as any,\n      udt_name: column.element_udt_name as any,\n    };\n  }\n\n  const cantUpdate = readOnly && action === \"update\";\n\n  const ftableIcon =\n    column.icon ?? foreignDataState?.insertAndSearchState?.ftableInfo?.icon;\n\n  return (\n    <>\n      {sectionHeader && (\n        <h4\n          className=\"noselect\"\n          style={{\n            marginBottom: \"0.5em\",\n          }}\n        >\n          {sectionHeader}\n        </h4>\n      )}\n      <FormField\n        id={tableName + \"-\" + column.name}\n        data-key={column.name}\n        leftIcon={\n          ftableIcon ?\n            <SvgIcon className=\"f-0 text-1 mr-p5\" icon={ftableIcon} />\n          : someColumnsHaveIcons && (\n              <div\n                className=\"mt-p25 mr-p5\"\n                style={{\n                  width: \"24px\",\n                  height: \"24px\",\n                }}\n              />\n            )\n        }\n        label={column.hideLabel ? \"\" : column.label}\n        data-command=\"SmartFormField\"\n        style={style}\n        className={cantUpdate ? \" cursor-default \" : \"\"}\n        inputClassName={cantUpdate ? \" cursor-default \" : \"\"}\n        maxWidth={maxWidth}\n        inputStyle={{\n          minWidth: 0,\n          ...(type === \"checkbox\" ? { padding: \"1px\" } : {}),\n        }}\n        asJSON={renderAsJSON?.component}\n        showFullScreenToggle={renderAsJSON?.component === \"codeEditor\"}\n        inputContent={\n          renderAsJSON?.component === \"JSONBSchema\" ?\n            <JSONBSchemaA\n              db={db}\n              schema={renderAsJSON.jsonbSchema}\n              tables={tables}\n              {...renderAsJSON.opts}\n              value={value}\n              onChange={onCheckAndChange}\n            />\n          : renderAsJSON?.component === \"codeEditor\" ?\n            loading ?\n              <Loading />\n            : <FormFieldCodeEditor\n                asJSON={renderAsJSON}\n                //@ts-ignore\n                value={value}\n                onChange={onCheckAndChange}\n                readOnly={readOnly}\n              />\n\n          : foreignDataState?.showSmartFormFieldForeignKey && (\n              <SmartFormFieldForeignKey\n                {...foreignDataState}\n                key={column.name}\n                column={column as SmartFormFieldForeignKeyProps[\"column\"]}\n                db={db}\n                readOnly={readOnly}\n                onChange={onChange}\n                tables={tables}\n                value={value}\n                row={row}\n                newRowDataHandler={newRowDataHandler}\n                table={table}\n              />\n            )\n\n        }\n        key={column.name}\n        placeholder={placeholder}\n        //@ts-ignore\n        type={type}\n        autoComplete={getInputAutocomplete(column)}\n        value={parsedValue ?? null}\n        rawValue={value}\n        title={cantUpdate ? \"You are not allowed to update this field\" : \"\"}\n        asTextArea={\n          column.tsDataType === \"string\" &&\n          typeof value === \"string\" &&\n          (value.length > 50 || value.split(\"\\n\").length > 1)\n        }\n        readOnly={readOnly}\n        multiSelect={multiSelect}\n        onChange={onCheckAndChange}\n        error={props.error || error}\n        arrayType={arrayType}\n        rightIcons={rightIcons}\n        rightContent={\n          foreignDataState?.insertAndSearchState &&\n          action &&\n          row && (\n            <SmartFormFieldLinkedData\n              {...props}\n              state={foreignDataState.insertAndSearchState}\n              action={action}\n              row={row}\n              column={column}\n              tableInfo={table.info}\n              jsonbSchemaWithControls={jsonbSchemaWithControls}\n              hideNullBtn={hideNullBtn}\n              newRowDataHandler={newRowDataHandler}\n              readOnly={readOnly}\n            />\n          )\n        }\n        rightContentAlwaysShow={rightContentAlwaysShow}\n        labelAsValue={true}\n        nullable={column.is_nullable}\n        inputProps={{ min: column.min, max: column.max }}\n        hint={hint}\n        hideClearButton={hideNullBtn}\n      />\n      {column.file ?\n        typeof value === \"number\" ?\n          <ErrorComponent error={\"Unexpected number data type\"} />\n        : <SmartFormFieldFileSection db={db} table={table} media={value} />\n      : null}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/SmartFormFieldFileSection.tsx",
    "content": "import type { LocalMedia } from \"@components/FileInput/FileInput\";\nimport { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport { usePromise } from \"prostgles-client\";\nimport { type DBSchemaTable } from \"prostgles-types\";\nimport React from \"react\";\nimport type { SmartFormProps } from \"../SmartForm\";\n\ntype P = {\n  table: DBSchemaTable;\n  media: string | LocalMedia | undefined | null;\n} & Pick<SmartFormProps, \"db\">;\n\nexport const SmartFormFieldFileSection = ({ db, table, media }: P) => {\n  const { fileTableName } = table.info;\n  const mediaFile = usePromise(async () => {\n    if (!fileTableName || !media) return undefined;\n    if (typeof media === \"string\") {\n      const mediaItem = await db[fileTableName]?.findOne?.({ id: media });\n      if (!mediaItem) return;\n      return {\n        url: mediaItem.url,\n        content_type: mediaItem.content_type,\n      };\n    }\n    const { data } = media;\n    const url = URL.createObjectURL(data);\n    const content_type = data.type;\n    return { url, content_type };\n  }, [media, db, fileTableName]);\n  if (!mediaFile) return null;\n  const { url, content_type } = mediaFile;\n  return (\n    <MediaViewer\n      style={{ maxHeight: \"250px\" }}\n      key={url}\n      url={url}\n      content_type={content_type}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/SmartFormFieldForeignKey.tsx",
    "content": "import { sliceText } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport { FileInput } from \"@components/FileInput/FileInput\";\nimport { FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { Select, type FullOption } from \"@components/Select/Select\";\nimport { mdiClose } from \"@mdi/js\";\nimport { useMemoDeep } from \"prostgles-client/dist/prostgles\";\nimport {\n  isDefined,\n  isObject,\n  pickKeys,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport {\n  type ColumnData,\n  NewRowDataHandler,\n} from \"../SmartFormNewRowDataHandler\";\nimport { RenderValue } from \"./RenderValue\";\nimport type { SmartColumnInfo, SmartFormFieldProps } from \"./SmartFormField\";\nimport { type SmartFormFieldLinkedDataInsertState } from \"./SmartFormFieldLinkedData\";\nimport { fetchForeignKeyOptions } from \"./fetchForeignKeyOptions\";\nimport { useIsMounted } from \"prostgles-client\";\n\nexport type SmartFormFieldForeignKeyProps = Pick<\n  SmartFormFieldProps,\n  \"value\" | \"db\" | \"tables\" | \"row\" | \"table\"\n> &\n  SmartFormFieldLinkedDataInsertState & {\n    column: SmartColumnInfo & {\n      references: NonNullable<ValidatedColumnInfo[\"references\"]>;\n    };\n    onChange: (newValue: ColumnData) => Promise<void> | void;\n    readOnly: boolean;\n    newRowDataHandler: NewRowDataHandler;\n  };\n\nexport const SmartFormFieldForeignKey = (\n  props: SmartFormFieldForeignKeyProps,\n) => {\n  const {\n    column,\n    db,\n    onChange,\n    tables,\n    value,\n    row,\n    readOnly,\n    newRowDataHandler,\n    table,\n    setShowNestedInsertForm,\n  } = props;\n\n  const [fullOptions, setFullOptions] = useState<FullOption[]>();\n  const getuseIsMounted = useIsMounted();\n  const newValue = newRowDataHandler.getNewRow()[column.name];\n\n  const isUpsertingFile = column.file && isObject(newValue);\n  const rowWithFKeyVals = useMemo(() => {\n    if (!row) return;\n    const fkeyColNames = column.references\n      .map((ref) => ref.cols)\n      .filter(isDefined)\n      .flat()\n      .filter((c) => {\n        /** Exclude file insert data */\n        return !isUpsertingFile || c !== column.name;\n      });\n    return pickKeys(row, fkeyColNames);\n  }, [row, column, isUpsertingFile]);\n\n  const rowWithFKeyValsMemo = useMemoDeep(\n    () => rowWithFKeyVals,\n    [rowWithFKeyVals],\n  );\n  const onSearchOptions = useCallback(\n    async (term: string) => {\n      const options = await fetchForeignKeyOptions({\n        column,\n        db,\n        table,\n        tables,\n        row: rowWithFKeyValsMemo,\n        term,\n      });\n      if (!getuseIsMounted()) return;\n      setFullOptions(options);\n    },\n    [column, db, table, tables, rowWithFKeyValsMemo, getuseIsMounted],\n  );\n\n  useEffect(() => {\n    void onSearchOptions(\"\");\n  }, [value, onSearchOptions]);\n\n  const valueStyle = {\n    fontSize: \"16px\",\n    fontWeight: 500,\n    paddingLeft: \"6px 0\",\n  };\n\n  const selectedOption = fullOptions?.find((o) => o.key === value);\n\n  const paddingValue = 0;\n  const isNullOrEmpty = value === null || value === undefined;\n\n  const displayValue = (\n    <FlexRowWrap\n      className={\"gap-p5 min-w-0\"}\n      style={{\n        /** Empty values too tall */\n        padding: isNullOrEmpty && !readOnly ? 0 : `${paddingValue} 0`,\n      }}\n    >\n      {column.file ? null : selectedOption?.leftContent}\n      <div className=\"text-ellipsis max-w-fit\" style={valueStyle}>\n        <RenderValue\n          value={value}\n          column={column}\n          showTitle={false}\n          maxLength={30}\n          getValues={undefined}\n        />\n      </div>\n      {isDefined(selectedOption?.subLabel) && (\n        <div\n          className=\"SmartFormFieldForeignKey.subLabel ta-left text-ellipsis\"\n          style={{\n            opacity: 0.75,\n            fontSize: \"14px\",\n            fontWeight: \"normal\",\n            maxWidth: \"300px\",\n          }}\n        >\n          {selectedOption.subLabel}\n        </div>\n      )}\n    </FlexRowWrap>\n  );\n\n  if (readOnly) {\n    return displayValue;\n  }\n\n  const referencedInsertData =\n    newValue?.type === \"nested-column\" ? newValue.value : undefined;\n\n  if (referencedInsertData) {\n    if (column.file) {\n      const media =\n        newValue?.type === \"nested-file-column\" && newValue.value ?\n          [newValue.value]\n        : [];\n      return (\n        <FileInput\n          className={\n            \"mt-p5 f-0 formfield-bg-color \" +\n            (table.info.isFileTable ? \"mt-2\" : \"\")\n          }\n          label={column.label}\n          media={media}\n          maxFileCount={1}\n          onAdd={([value]) => {\n            void onChange({\n              type: \"nested-file-column\",\n              value,\n            });\n          }}\n          onDelete={() => {\n            void onChange({\n              type: \"nested-file-column\",\n              value: undefined,\n            });\n          }}\n        />\n      );\n    }\n\n    const referencedInsertDataObj =\n      referencedInsertData instanceof NewRowDataHandler ?\n        referencedInsertData.getRow()\n      : referencedInsertData;\n    const newDataText = sliceText(\n      Object.entries(referencedInsertDataObj ?? {})\n        .map(([k, v]) =>\n          isObject(v) ? `{ ${k} }`\n          : Array.isArray(v) ? `[{ ${k} }]`\n          : v?.toString(),\n        )\n        .join(\", \"),\n      60,\n    );\n\n    return (\n      <FlexRow\n        className=\"gap-0 f-1\"\n        style={{ justifyContent: \"space-between\" }}\n      >\n        <Btn\n          className=\"formfield-bg-color\"\n          color=\"action\"\n          title=\"View insert data\"\n          onClick={() => {\n            setShowNestedInsertForm(true);\n          }}\n        >\n          {newDataText}\n        </Btn>\n        <Btn\n          title=\"Remove nested insert\"\n          iconPath={mdiClose}\n          onClick={() => {\n            onChange({ type: \"nested-column\", value: undefined });\n          }}\n        />\n      </FlexRow>\n    );\n  }\n\n  return (\n    <Select\n      className=\"SmartFormFieldForeignKey FormField_Select noselect formfield-bg-color\"\n      variant=\"div\"\n      fullOptions={fullOptions ?? []}\n      onSearch={onSearchOptions}\n      onChange={(newVal) => onChange({ type: \"column\", value: newVal })}\n      value={value}\n      labelAsValue={true}\n      btnProps={{\n        children: displayValue,\n        style: {\n          justifyContent: \"space-between\",\n          flex: 1,\n          paddingLeft: \"6px\",\n        },\n      }}\n    />\n  );\n};\n\nconsole.error(\"CANNOT ADD TIMECHART AI DASHBOARD SQL EDITOR\");\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/SmartFormFieldLinkedData.tsx",
    "content": "import type {\n  AnyObject,\n  TableInfo,\n  ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport { getPossibleNestedInsert, isObject } from \"prostgles-types\";\nimport React, { useMemo, useState } from \"react\";\nimport type { SmartFormProps } from \"../SmartForm\";\nimport { type NewRowDataHandler } from \"../SmartFormNewRowDataHandler\";\nimport type { SmartFormFieldProps } from \"./SmartFormField\";\nimport { SmartFormFieldLinkedDataInsert } from \"./SmartFormFieldLinkedDataInsert/SmartFormFieldLinkedDataInsert\";\nimport { SmartFormFieldLinkedDataSearch } from \"./SmartFormFieldLinkedDataSearch\";\n\nexport type SmartFormFieldLinkedDataProps = Pick<\n  SmartFormProps,\n  | \"db\"\n  | \"tables\"\n  | \"methods\"\n  | \"hideNullBtn\"\n  | \"enableInsert\"\n  | \"onSuccess\"\n  | \"tableName\"\n  | \"rowFilter\"\n  | \"jsonbSchemaWithControls\"\n> &\n  Pick<SmartFormFieldProps, \"newValue\"> & {\n    column: ValidatedColumnInfo;\n    row: AnyObject;\n    tableInfo: TableInfo;\n    action: \"update\" | \"insert\" | \"view\";\n    newRowDataHandler: NewRowDataHandler;\n  };\n\nexport const SmartFormFieldLinkedData = (\n  props: SmartFormFieldLinkedDataProps & {\n    state: SmartFieldForeignKeyOptionsState;\n    readOnly: boolean;\n  },\n) => {\n  const {\n    row,\n    state,\n    db,\n    tables,\n    methods,\n    readOnly,\n    column,\n    newRowDataHandler,\n    tableName,\n  } = props;\n\n  if (!state) return null;\n  return (\n    <div className=\"SmartFormFieldOptions flex-row\">\n      {state.showSearchState && (\n        <SmartFormFieldLinkedDataSearch\n          tables={tables}\n          methods={methods}\n          db={db}\n          readOnly={readOnly}\n          row={row}\n          column={column}\n          tableName={tableName}\n          {...state.showSearchState}\n          ftable={state.showSearchState.ftableInfo}\n          onChange={(value) => {\n            newRowDataHandler.setColumn(column.name, value);\n          }}\n        />\n      )}\n      {state.showInsertState && (\n        <SmartFormFieldLinkedDataInsert {...props} {...state.showInsertState} />\n      )}\n    </div>\n  );\n};\n\nconst useSmartFieldForeignKeyOptionsState = ({\n  column,\n  action,\n  db,\n  tables,\n  enableInsert,\n  setShowNestedInsertForm,\n  showNestedInsertForm,\n  row,\n}: Pick<\n  SmartFormFieldProps,\n  \"column\" | \"action\" | \"db\" | \"tables\" | \"enableInsert\" | \"row\"\n> &\n  SmartFormFieldLinkedDataInsertState) => {\n  const value = row ? row[column.name] : undefined;\n  const isUpsertingFile = column.file && action !== \"view\" && isObject(value);\n  return useMemo(() => {\n    const ref = getPossibleNestedInsert(column, tables);\n    const fileTableName = tables[0]?.info.fileTableName;\n    const ftable =\n      ref?.ftable ?? (column.file && fileTableName ? fileTableName : undefined);\n    const ftableInfo =\n      ftable ? tables.find((t) => t.name === ftable) : undefined;\n    const fTableCols = ftableInfo?.columns;\n    const ftableHandler =\n      ftable && column.references?.length ? db[ftable] : undefined;\n    const fcol = ref?.fcols[ref.cols.indexOf(column.name)];\n    if (!ftable || !fcol) return undefined;\n\n    /** Show insert if can insert to ftable AND can update column that references ftable */\n    const showInsertState =\n      (\n        (action === \"insert\" || (action === \"update\" && column.update)) &&\n        enableInsert &&\n        ftableHandler?.insert\n      ) ?\n        {\n          ftable,\n          ftableInfo,\n          fcol,\n          setShowNestedInsertForm,\n          showNestedInsertForm,\n        }\n      : undefined;\n\n    const hasMultipleCols = fTableCols && fTableCols.length > 1;\n    /** No point showing full table search when there is only 1 column */\n    const showSearchState =\n      (\n        action !== \"view\" &&\n        ftableHandler?.find &&\n        hasMultipleCols &&\n        !isUpsertingFile\n      ) ?\n        { ftable, ftableInfo, fcol }\n      : undefined;\n\n    if (!showInsertState && !showSearchState) {\n      return undefined;\n    }\n\n    return {\n      showSearchState,\n      showInsertState,\n      ftableInfo,\n    };\n  }, [\n    column,\n    tables,\n    db,\n    enableInsert,\n    action,\n    setShowNestedInsertForm,\n    showNestedInsertForm,\n    isUpsertingFile,\n  ]);\n};\nexport type SmartFieldForeignKeyOptionsState = ReturnType<\n  typeof useSmartFieldForeignKeyOptionsState\n>;\n\nexport const useSmartFormFieldForeignDataState = ({\n  column,\n  readOnly,\n  row,\n  action,\n  db,\n  tables,\n  enableInsert,\n}: Pick<\n  SmartFormFieldProps,\n  \"column\" | \"row\" | \"action\" | \"db\" | \"tables\" | \"enableInsert\"\n> & {\n  readOnly: boolean;\n}) => {\n  const [showNestedInsertForm, setShowNestedInsertForm] = useState(false);\n\n  const insertAndSearchState = useSmartFieldForeignKeyOptionsState({\n    column,\n    action,\n    db,\n    tables,\n    enableInsert,\n    setShowNestedInsertForm,\n    showNestedInsertForm,\n    row,\n  });\n  const showSmartFormFieldLinkedData = !readOnly && action && row;\n\n  const showSmartFormFieldForeignKey =\n    !!column.references?.length && !column.is_pkey;\n  if (!showSmartFormFieldLinkedData && !showSmartFormFieldForeignKey) return;\n\n  return {\n    insertAndSearchState,\n    showNestedInsertForm,\n    setShowNestedInsertForm,\n    showSmartFormFieldLinkedData,\n    showSmartFormFieldForeignKey,\n  };\n};\n\nexport type SmartFormFieldLinkedDataInsertState = {\n  showNestedInsertForm: boolean;\n  setShowNestedInsertForm: (show: boolean) => void;\n};\n// throw \"Must fix referenced image column nested chain. Ensure it doesn't need a press on Update on parent row\"\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/SmartFormFieldLinkedDataInsert/SmartFormFieldLinkedDataInsert.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { mdiFilePlusOutline, mdiPlus } from \"@mdi/js\";\nimport React from \"react\";\nimport { SmartForm } from \"../../SmartForm\";\nimport { NewRowDataHandler } from \"../../SmartFormNewRowDataHandler\";\nimport type {\n  SmartFormFieldLinkedDataInsertState,\n  SmartFormFieldLinkedDataProps,\n} from \"../SmartFormFieldLinkedData\";\nimport { useNestedInsertDefaultData } from \"../useNestedInsertDefaultData\";\nimport { useSmartFormFieldLinkedDataInsert } from \"./useSmartFormFieldLinkedDataInsert\";\n\ntype P = Pick<\n  SmartFormFieldLinkedDataProps,\n  | \"db\"\n  | \"column\"\n  | \"action\"\n  | \"newValue\"\n  | \"tables\"\n  | \"methods\"\n  | \"tableName\"\n  | \"row\"\n  | \"hideNullBtn\"\n  | \"jsonbSchemaWithControls\"\n  | \"onSuccess\"\n  | \"rowFilter\"\n> &\n  SmartFormFieldLinkedDataInsertState & {\n    ftable: string;\n    fcol: string;\n    newRowDataHandler: NewRowDataHandler;\n  };\n\nexport const SmartFormFieldLinkedDataInsert = ({\n  db,\n  ftable: canInsertFTableName,\n  fcol,\n  column,\n  action,\n  tables,\n  methods,\n  tableName,\n  row,\n  hideNullBtn,\n  jsonbSchemaWithControls,\n  onSuccess,\n  setShowNestedInsertForm,\n  showNestedInsertForm,\n  newValue,\n  rowFilter,\n  newRowDataHandler,\n}: P) => {\n  const { fileUpsertInsert } = useSmartFormFieldLinkedDataInsert({\n    column,\n    action,\n    newRowDataHandler,\n  });\n\n  const parentFormNewRowDataHandler =\n    (\n      newValue?.type === \"nested-column\" &&\n      newValue.value instanceof NewRowDataHandler\n    ) ?\n      newValue.value\n    : undefined;\n\n  const defaultData = useNestedInsertDefaultData({\n    ftable: canInsertFTableName,\n    tables,\n    tableName,\n    row,\n  });\n\n  if (fileUpsertInsert) {\n    return (\n      <div className=\"SmartFormFieldLinkedDataInsert w-fit h-fit\">\n        <Btn\n          iconPath={mdiFilePlusOutline}\n          color=\"action\"\n          title=\"Attach file\"\n          data-command=\"SmartFormFieldOptions.AttachFile\"\n          onClick={(e) => {\n            const input =\n              e.currentTarget.parentElement?.querySelector<HTMLInputElement>(\n                \"input\",\n              );\n            if (input) {\n              input.click();\n            }\n          }}\n        />\n        <input\n          accept={fileUpsertInsert.inputAccept}\n          type=\"file\"\n          className=\"m-0 p-0 hidden\"\n          autoCorrect=\"off\"\n          autoCapitalize=\"off\"\n          style={{ width: 0, height: 0, opacity: 0 }}\n          onChange={fileUpsertInsert.onInputChange}\n        />\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <Btn\n        data-command=\"SmartFormFieldOptions.NestedInsert\"\n        title=\"Insert new record\"\n        data-key={canInsertFTableName}\n        iconPath={mdiPlus}\n        onClick={() => {\n          setShowNestedInsertForm(true);\n        }}\n      />\n      {showNestedInsertForm && (\n        <SmartForm\n          key=\"referenced-insert\"\n          asPopup={true}\n          db={db}\n          tables={tables}\n          methods={methods}\n          hideNullBtn={hideNullBtn}\n          tableName={canInsertFTableName}\n          onClose={() => {\n            setShowNestedInsertForm(false);\n          }}\n          defaultData={defaultData}\n          jsonbSchemaWithControls={jsonbSchemaWithControls}\n          onInserted={(newRowOrRows) => {\n            const newRow =\n              Array.isArray(newRowOrRows) ? newRowOrRows[0] : newRowOrRows;\n            if (newRow) {\n              newRowDataHandler.setColumnData(column.name, newRow[fcol]);\n            }\n          }}\n          onSuccess={(r) => {\n            onSuccess?.(r);\n          }}\n          parentForm={{\n            table: tables.find((t) => t.name === tableName)!,\n            ...(action === \"insert\" ?\n              {\n                type: \"insert\",\n                newRowDataHandler: parentFormNewRowDataHandler,\n                setColumnData: (newRow) => {\n                  newRowDataHandler.setNestedColumn(column.name, newRow);\n                  setShowNestedInsertForm(false);\n                },\n              }\n            : {\n                type: \"insert-and-update\",\n                column: column,\n                rowFilter,\n                row,\n              }),\n          }}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/SmartFormFieldLinkedDataInsert/useSmartFormFieldLinkedDataInsert.tsx",
    "content": "import { CONTENT_TYPE_TO_EXT, getKeys } from \"prostgles-types\";\nimport type { NewRowDataHandler } from \"../../SmartFormNewRowDataHandler\";\nimport type { SmartFormFieldLinkedDataProps } from \"../SmartFormFieldLinkedData\";\nimport { act, useMemo } from \"react\";\n\ntype P = Pick<SmartFormFieldLinkedDataProps, \"column\" | \"action\"> & {\n  newRowDataHandler: NewRowDataHandler;\n};\n\nexport const useSmartFormFieldLinkedDataInsert = ({\n  column,\n  action,\n  newRowDataHandler,\n}: P) => {\n  const columnFile = column.file;\n  const fileUpsertInsert = useMemo(() => {\n    if (!(columnFile && [\"insert\", \"update\"].includes(action))) {\n      return;\n    }\n\n    let inputAccept: string | undefined;\n    if (\n      \"acceptedContent\" in columnFile &&\n      Array.isArray(columnFile.acceptedContent) &&\n      columnFile.acceptedContent.length\n    ) {\n      inputAccept =\n        columnFile.acceptedContent.map((c) => `${c}/*`).join() +\n        \",\" +\n        columnFile.acceptedContent\n          .flatMap((type) =>\n            getKeys(CONTENT_TYPE_TO_EXT)\n              .filter((k) => k.startsWith(type))\n              .flatMap((k) => CONTENT_TYPE_TO_EXT[k])\n              .flat(),\n          )\n          .map((type) => `.${type}`)\n          .join(\",\");\n    } else if (\n      \"acceptedContentType\" in columnFile &&\n      Array.isArray(columnFile.acceptedContentType) &&\n      columnFile.acceptedContentType.length\n    ) {\n      inputAccept = columnFile.acceptedContentType\n        .flatMap((type) =>\n          getKeys(CONTENT_TYPE_TO_EXT)\n            .filter((k) => k === type)\n            .flatMap((k) => CONTENT_TYPE_TO_EXT[k])\n            .flat(),\n        )\n        .map((type) => `.${type}`)\n        .join(\",\");\n    } else if (\n      \"acceptedFileTypes\" in columnFile &&\n      Array.isArray(columnFile.acceptedFileTypes) &&\n      columnFile.acceptedFileTypes.length\n    ) {\n      inputAccept = `${columnFile.acceptedFileTypes.map((type) => `.${type}`).join(\",\")}`;\n    }\n\n    const onInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {\n      const file = e.currentTarget.files?.[0];\n      newRowDataHandler.setNestedFileColumn(\n        column.name,\n        file && {\n          name: file.name,\n          data: file,\n        },\n      );\n    };\n\n    return { inputAccept, onInputChange };\n  }, [action, columnFile, newRowDataHandler, column.name]);\n\n  return { fileUpsertInsert };\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/SmartFormFieldLinkedDataSearch.tsx",
    "content": "import { mdiCheckCircle, mdiCheckCircleOutline } from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { tout } from \"../../../utils/utils\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\nimport type { SmartFormFieldLinkedDataProps } from \"./SmartFormFieldLinkedData\";\nimport {\n  ViewMoreSmartCardList,\n  type ViewMoreSmartCardListProps,\n} from \"./ViewMoreSmartCardList\";\n\ntype P = Pick<\n  SmartFormFieldLinkedDataProps,\n  \"db\" | \"methods\" | \"tables\" | \"row\" | \"column\" | \"tableName\"\n> & {\n  ftable: DBSchemaTableWJoins;\n  fcol: string;\n  readOnly: boolean;\n  onChange: (value: any) => void;\n};\n\nexport const SmartFormFieldLinkedDataSearch = ({\n  tables,\n  methods,\n  db,\n  ftable,\n  fcol,\n  row,\n  column,\n  readOnly,\n  onChange,\n  tableName,\n}: P) => {\n  const listProps = useMemo(() => {\n    const searchFilterValue = row[column.name];\n    return {\n      getActions:\n        readOnly ? undefined : (\n          (cardRow, onClosePopup) => {\n            const currentValue = row[column.name];\n            const newValue = cardRow[fcol];\n            /** Some numeric fields are returned as strings */\n            if (currentValue == newValue)\n              return (\n                <Btn\n                  title=\"Selected\"\n                  variant=\"icon\"\n                  disabledInfo=\"Already selected\"\n                  iconPath={mdiCheckCircle}\n                />\n              );\n            return (\n              <Btn\n                title=\"Select\"\n                variant=\"icon\"\n                color=\"action\"\n                iconPath={mdiCheckCircleOutline}\n                onClickPromise={async () => {\n                  onChange(newValue);\n                  await tout(1000);\n                  onClosePopup();\n                }}\n              />\n            );\n          }\n        ),\n      searchFilter:\n        searchFilterValue === null ?\n          []\n        : [\n            {\n              fieldName: fcol,\n              value: searchFilterValue,\n              minimised: true,\n            },\n          ],\n    } satisfies Pick<ViewMoreSmartCardListProps, \"searchFilter\" | \"getActions\">;\n  }, [row, column.name, fcol, onChange, readOnly]);\n  return (\n    <ViewMoreSmartCardList\n      db={db}\n      tables={tables}\n      methods={methods}\n      ftable={ftable}\n      rootTableName={tableName}\n      {...listProps}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/SmartFormFieldRightButtons.tsx",
    "content": "import { mdiCalendar, mdiCrosshairsGps, mdiFormatText } from \"@mdi/js\";\nimport { _PG_date } from \"prostgles-types\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport type { SmartFormFieldProps } from \"./SmartFormField\";\n\ntype P = Omit<SmartFormFieldProps, \"onChange\"> & {\n  showDateInput: {\n    value: boolean;\n    onChange: (newValue: boolean) => void;\n  };\n  onChange: (newValue: any) => void;\n};\nexport const getSmartFormFieldRightButtons = ({\n  column,\n  onChange,\n  showDateInput,\n}: P) => {\n  const isGeoData =\n    column.udt_name === \"geometry\" || column.udt_name === \"geography\";\n\n  if (_PG_date.some((v) => v === column.udt_name)) {\n    return (\n      <Btn\n        title={showDateInput.value ? \"Edit as text\" : \"Edit with picker\"}\n        iconPath={showDateInput.value ? mdiFormatText : mdiCalendar}\n        className=\"h-full\"\n        // color={showDateInput? \"action\" : undefined}\n        onClick={() => showDateInput.onChange(!showDateInput.value)}\n      />\n    );\n  } else if (isGeoData) {\n    return (\n      <>\n        {/* <PopupMenu \n        button={\n          <Btn iconPath={mdiMap} />\n        }\n        title=\"Edit\"\n        showFullscreenToggle={{ defaultValue: true }}\n        render={onClose => <div className=\"f-1 flex-row h-full w-full relative\">\n          <DeckGLMap\n            geoJsonLayers={[]}\n            onGetFullExtent={() => {\n              return [[1,1], [1,1]];\n            }}\n            options={{}}\n            tileURLs={DEFAULT_TILE_URLS}\n            edit={{ \n              dbMethods: methods, \n              dbProject: db, \n              dbTables: tables,\n              feature: value as any,\n              onStartEdit: () => {},\n              onInsertOrUpdate: () => {\n                onClose();\n              }\n            }}\n          />\n        </div>}\n      /> */}\n        <PopupMenu\n          style={{\n            height: \"100%\",\n          }}\n          button={\n            <Btn\n              title=\"Use my current location\"\n              iconPath={mdiCrosshairsGps}\n              className={`h-full rounded-${column.is_nullable ? 0 : \"r\"}`}\n              onClick={() => showDateInput.onChange(!showDateInput.value)}\n            />\n          }\n          content={\n            <p>You will have to allow your browser to use your location data</p>\n          }\n          footerButtons={[\n            {\n              color: \"action\",\n              label: \"OK\",\n              onClick: () => {\n                try {\n                  navigator.geolocation.getCurrentPosition((pos) => {\n                    const lat = pos.coords.latitude;\n                    const lng = pos.coords.longitude;\n                    if ((lat as any) === null) {\n                      alert(\"GPS not activated!\");\n                    } else {\n                      onChange({\n                        $ST_GeomFromEWKT: [`SRID=4326;POINT(${lng} ${lat})`],\n                      });\n                    }\n                  });\n                } catch (e: any) {\n                  alert(\"Something went wrong: \" + e.toString());\n                }\n              },\n            },\n            {\n              label: \"No thanks\",\n              onClickClose: true,\n            },\n          ]}\n        />\n      </>\n    );\n  }\n\n  return null;\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/ViewMoreSmartCardList.tsx",
    "content": "import type { DetailedFilter } from \"@common/filterUtils\";\nimport Btn from \"@components/Btn\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Popup from \"@components/Popup/Popup\";\nimport { mdiSearchWeb } from \"@mdi/js\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\nimport { SmartCardList } from \"../../SmartCardList/SmartCardList\";\nimport { useJoinedSectionFieldConfigs } from \"../JoinedRecords/useJoinedSectionFieldConfigs\";\nimport type { SmartFormFieldLinkedDataProps } from \"./SmartFormFieldLinkedData\";\n\nexport type ViewMoreSmartCardListProps = Pick<\n  SmartFormFieldLinkedDataProps,\n  \"db\" | \"methods\" | \"tables\"\n> & {\n  ftable: DBSchemaTableWJoins;\n  searchFilter: DetailedFilter[] | undefined;\n  getActions:\n    | ((row: AnyObject, onClosePopup: VoidFunction) => React.ReactNode)\n    | undefined;\n  rootTableName?: string;\n};\nexport const ViewMoreSmartCardList = ({\n  db,\n  methods,\n  ftable,\n  tables,\n  searchFilter,\n  getActions,\n  rootTableName,\n}: ViewMoreSmartCardListProps) => {\n  const [anchorEl, setAnchorEl] = useState<HTMLElement>();\n\n  const fieldConfigs = useJoinedSectionFieldConfigs({\n    sectionTable: ftable,\n    tables,\n    tableName: rootTableName,\n  });\n\n  return (\n    <>\n      <Btn\n        iconPath={mdiSearchWeb}\n        title=\"View more\"\n        data-command=\"ViewMoreSmartCardList\"\n        onClick={({ currentTarget }) => setAnchorEl(currentTarget)}\n      />\n      {anchorEl && (\n        <Popup\n          title={ftable.label}\n          onClose={() => setAnchorEl(undefined)}\n          anchorEl={anchorEl}\n          onClickClose={false}\n          positioning=\"fullscreen\"\n          showFullscreenToggle={\n            {\n              // getStyle: () => ({ maxWidth: \"800px\" }),\n            }\n          }\n          clickCatchStyle={{ opacity: 1 }}\n\n          // rootChildStyle={{\n          //   maxWidth: \"min(100vw, 800px)\",\n          // }}\n        >\n          <SmartCardList\n            showTopBar={true}\n            db={db}\n            methods={methods}\n            tables={tables}\n            tableName={ftable.name}\n            excludeNulls={true}\n            searchFilter={searchFilter}\n            getActions={\n              getActions ?\n                (row) => getActions(row, () => setAnchorEl(undefined))\n              : undefined\n            }\n            fieldConfigs={fieldConfigs}\n            noDataComponent={\n              <InfoRow className=\" \" color=\"info\" variant=\"filled\">\n                No records\n              </InfoRow>\n            }\n          />\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/fetchColumnValueSuggestions.ts",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { getComputedColumnSelect } from \"src/dashboard/W_Table/tableUtils/getTableSelect\";\nimport type { FilterColumn } from \"../../SmartFilter/smartFilterUtils\";\nimport { colIs } from \"./fieldUtils\";\nconst OPTIONS_LIMIT = 20;\n\nexport const fetchColumnValueSuggestions = async (args: {\n  table: string;\n  db: DBHandlerClient;\n  column: FilterColumn;\n  term?: string;\n  groupBy?: boolean;\n  filter?: AnyObject;\n}): Promise<(string | null)[]> => {\n  const { db, table, term: _term, column: col, groupBy = true, filter } = args;\n  const tableHandler = db[table];\n  if (!tableHandler?.find) {\n    console.error(\"Invalid column provided\");\n    return [];\n  }\n  const term = (_term || \"\").trimStart();\n\n  try {\n    const finalFilter = {\n      $and: [\n        filter,\n        !term ? {} : { [col.name]: { $ilike: `%${term}%` } },\n      ].filter((v) => v),\n    };\n    const select = {\n      [col.name]:\n        col.type === \"computed\" ?\n          getComputedColumnSelect(col.computedConfig)\n        : 1,\n      /** Sorting removed because it is expensive on big dataset */\n      // [`${col}_sort`]: {\n      //     $position_lower: [\n      //         term || '', col\n      //     ]\n      // },\n    } as const;\n\n    if (col.type === \"computed\" && col.computedConfig.funcDef.isAggregate) {\n      console.warn(\n        \"TOOD: implement groupBy other columns to ensure correct aggregate results\",\n      );\n    }\n\n    const res = await tableHandler.find(finalFilter, {\n      select,\n      groupBy,\n      returnType: \"values\",\n      limit: OPTIONS_LIMIT,\n      // orderBy: !term?\n      //     [{ key: col, asc: true, nulls: \"first\" } ]:\n\n      //     [\n\n      //         { key: `${col}_sort`, asc: true, nulls: \"first\" },\n      //         { key: col, asc: true, nulls: \"first\" }\n      //     ],\n    });\n\n    /**\n     * Prepend empty/null value options to the top if they exist\n     */\n    if (col.type === \"column\") {\n      if (\n        !res.includes(\"\") &&\n        !colIs(col, \"_PG_date\") &&\n        col.tsDataType === \"string\" &&\n        col.udt_name !== \"uuid\"\n      ) {\n        const empty = await tableHandler.findOne?.(\n          { [col.name]: \"\" },\n          { select: { [col.name]: \"$trim\" } },\n        );\n        if (empty) res.unshift(\"\");\n      }\n      if (!res.includes(null)) {\n        const c = (await tableHandler.count?.({ [col.name]: null })) ?? \"0\";\n        if (+c) res.unshift(null);\n      }\n    }\n\n    return res;\n  } catch (e) {\n    console.error(e);\n    return [];\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/fetchForeignKeyOptions.tsx",
    "content": "import type { FileTable } from \"@common/utils\";\nimport { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport { type FullOption } from \"@components/Select/Select\";\nimport { SvgIconFromURL } from \"@components/SvgIcon\";\nimport { type DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport {\n  _PG_numbers,\n  _PG_numbers_str,\n  includes,\n  isDefined,\n  isEmpty,\n  type AnyObject,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport React from \"react\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\nimport type { SmartFormFieldForeignKeyProps } from \"./SmartFormFieldForeignKey\";\n\ntype FetchForeignKeyOptionsArgs = Pick<\n  SmartFormFieldForeignKeyProps,\n  \"column\" | \"db\" | \"row\" | \"table\" | \"tables\"\n> & {\n  term: string;\n};\n\ntype GetRootFkeyTableArgs = Pick<\n  FetchForeignKeyOptionsArgs,\n  \"table\" | \"tables\" | \"db\"\n> & {\n  prevPath: { tableName: string; on: [string, string][] }[];\n  columnName: string;\n};\n\nexport const fetchForeignKeyOptions = async ({\n  column,\n  table,\n  db,\n  tables,\n  row,\n  term,\n}: FetchForeignKeyOptionsArgs): Promise<FullOption[]> => {\n  const rootFkeyTable = getFtableSearchOpts({\n    column,\n    table,\n    tables,\n    db,\n    term,\n    row,\n  });\n\n  if (!rootFkeyTable) {\n    return fetchSearchResults({\n      mainColumn: column.name,\n      textColumn: undefined,\n      db,\n      table,\n      term,\n      filter: undefined,\n    });\n  }\n\n  const { filterFilterWithoutCurrentValue } = rootFkeyTable;\n  const result = await fetchSearchResults({\n    ...rootFkeyTable,\n    db,\n    term,\n    filter: filterFilterWithoutCurrentValue,\n  });\n\n  /** We must add current value */\n  const currentValue = row?.[column.name];\n\n  const isNumericSoMightBeString = includes(_PG_numbers, column.udt_name);\n  if (\n    row &&\n    isDefined(currentValue) &&\n    currentValue !== null &&\n    !result.some((o) =>\n      isNumericSoMightBeString ? o.key == currentValue : o.key === currentValue,\n    )\n  ) {\n    const [currentValueOption] = await fetchSearchResults({\n      ...rootFkeyTable,\n      db,\n      term: \"\",\n      filter: {\n        [rootFkeyTable.mainColumn]: currentValue,\n      },\n    });\n    if (currentValueOption) {\n      result.unshift(currentValueOption);\n    }\n  }\n\n  return result;\n};\n\n/**\n * Given a column that is a foreign key, we want to find the best table from the reference chain to extract a suitable text column to help the user\n * pick a value from the foreign table.\n */\nconst getRootFkeyTable = ({\n  tables,\n  columnName,\n  table,\n  db,\n  prevPath = [],\n}: GetRootFkeyTableArgs):\n  | undefined\n  | (Pick<GetRootFkeyTableArgs, \"table\" | \"prevPath\"> & {\n      column: ValidatedColumnInfo;\n      bestTextCol: string | undefined;\n      table: DBSchemaTableWJoins;\n    }) => {\n  const column = table.columns.find((c) => c.name === columnName);\n  if (!column || !db[table.name]?.find) return;\n  if (column.is_pkey) {\n    const bestTextCols = getBestTextColumns(\n      table,\n      prevPath.flatMap(({ on }) => on.map((o) => o[1])),\n    );\n    return {\n      column,\n      table,\n      prevPath,\n      bestTextCol: bestTextCols[0]?.name,\n    };\n  }\n  const fcolsInfo =\n    column.references\n      ?.map((fk) => {\n        if (prevPath.some((p) => p.tableName === fk.ftable)) return;\n        const fcol = fk.fcols[fk.cols.indexOf(column.name)];\n        if (!fcol) return;\n        return {\n          ftable: fk.ftable,\n          fcol,\n          on: fk.cols.map(\n            (col, i) => [col, fk.fcols[i]!] satisfies [string, string],\n          ),\n        };\n      })\n      .filter(isDefined) ?? [];\n\n  return fcolsInfo\n    .flatMap((fcolInfo) => {\n      const ftable = tables.find((t) => t.name === fcolInfo.ftable);\n      if (!ftable) return;\n      const res = getRootFkeyTable({\n        tables,\n        columnName: fcolInfo.fcol,\n        table: ftable,\n        prevPath: [\n          ...prevPath,\n          { on: fcolInfo.on, tableName: fcolInfo.ftable },\n        ],\n        db,\n      });\n      return res;\n    })\n    .find((d) => d);\n};\n\nconst getFtableSearchOpts = ({\n  column,\n  table,\n  db,\n  tables,\n  row,\n}: FetchForeignKeyOptionsArgs) => {\n  const rootFkeyTable = getRootFkeyTable({\n    columnName: column.name,\n    prevPath: [],\n    table,\n    tables,\n    db,\n  });\n  if (!rootFkeyTable) return;\n\n  let filterFilterWithoutCurrentValue = {};\n\n  const tableJoin = rootFkeyTable.prevPath[0];\n\n  /**\n   * If we have a current row AND the fkey relationship is on multiple columns we add other column values into the filter\n   */\n  if (row && !isEmpty(row) && tableJoin && tableJoin.on.length > 1) {\n    const nextTableFilter = {};\n    tableJoin.on.forEach(([col, fcol]) => {\n      if (col !== column.name) {\n        nextTableFilter[fcol] = row[col];\n      }\n    });\n    const tableName = table.name;\n    const path = rootFkeyTable.prevPath\n      .map((p, i) => {\n        return {\n          ...p,\n          on: [Object.fromEntries(p.on.map(([col, fcol]) => [fcol, col]))],\n          tableName: rootFkeyTable.prevPath[i - 1]?.tableName ?? tableName,\n        };\n      })\n      .reverse()\n      .map((p) => ({ table: p.tableName, on: p.on }));\n\n    filterFilterWithoutCurrentValue =\n      path.length > 1 ?\n        {\n          $existsJoined: {\n            path: path.slice(0, -1),\n            filter: nextTableFilter,\n          },\n        }\n      : nextTableFilter;\n  }\n  return {\n    mainColumn: rootFkeyTable.column.name,\n    textColumn: rootFkeyTable.bestTextCol,\n    table: rootFkeyTable.table,\n    filterFilterWithoutCurrentValue,\n  };\n};\n\ntype Args = {\n  term: string;\n  mainColumn: string;\n  textColumn: string | undefined;\n  table: DBSchemaTableWJoins;\n  filter: AnyObject | undefined;\n  db: DBHandlerClient;\n};\nconst fetchSearchResults = async ({\n  mainColumn,\n  textColumn,\n  db,\n  filter,\n  table,\n  term,\n}: Args): Promise<FullOption[]> => {\n  const { name: tableName, rowIconColumn, info } = table;\n  const fileUrl = info.isFileTable ? \"url\" : undefined;\n  const extraColumns =\n    rowIconColumn ? [rowIconColumn]\n    : fileUrl ? [fileUrl]\n    : [];\n  const tableHandler = db[tableName];\n  if (!tableHandler?.find) return [];\n  const filterColumns = [mainColumn, textColumn].filter(isDefined);\n\n  const termFilter =\n    term ?\n      { $or: filterColumns.map((col) => ({ [col]: { $ilike: `%${term}%` } })) }\n    : {};\n  const finalFilter = {\n    $and: [filter, termFilter].filter((v) => !isEmpty(v)),\n  };\n\n  const res = await tableHandler.find(finalFilter, {\n    select: Array.from(new Set([...filterColumns, ...extraColumns])),\n    groupBy: true,\n    limit: OPTIONS_LIMIT,\n  });\n  return res.map((row) => {\n    const rowIconSrc = rowIconColumn && row[rowIconColumn];\n    return {\n      leftContent:\n        rowIconSrc ?\n          <SvgIconFromURL\n            className=\"mr-p5 text-0\"\n            url={rowIconSrc}\n            style={{\n              width: \"24px\",\n              height: \"24px\",\n            }}\n          />\n        : fileUrl ?\n          <MediaViewer\n            url={row[fileUrl]}\n            style={{\n              marginRight: \"1em\",\n              maxHeight: \"80px\",\n              maxWidth: \"80px\",\n              pointerEvents: \"none\",\n            }}\n          />\n        : null,\n      key: row[mainColumn],\n      subLabel: textColumn && row[textColumn],\n    } satisfies FullOption;\n  });\n};\n\nconst isTextColumn = (col: ValidatedColumnInfo) =>\n  ([\"text\", \"varchar\", \"citext\", \"char\"] as const).some(\n    (textType) => textType === col.udt_name,\n  );\n\n/**\n * When a non-text column is referencing another table,\n * we want to try and show the most representative text column of that table\n * to make it easier for the user to pick a value\n */\nexport const getBestTextColumns = (\n  table: DBSchemaTableWJoins,\n  excludeCols: string[],\n) => {\n  if (table.info.isFileTable) {\n    return table.columns.filter((c) =>\n      includes([\"original_name\"] satisfies (keyof FileTable)[], c.name),\n    );\n  }\n  const nonSelectableNullableTextCols = table.columns\n    .filter(isTextColumn)\n    .filter((c) => c.select && !excludeCols.includes(c.name));\n  const hasNonNullableTextCol = nonSelectableNullableTextCols.some(\n    (c) => !c.is_nullable,\n  );\n  const fTableTextColumns = nonSelectableNullableTextCols\n    .filter(\n      (c) =>\n        /** Allow non nullable text cols if we don't have other options */\n        !hasNonNullableTextCol || !c.is_nullable,\n    )\n    .map((column) => {\n      const shortestUnique = table.info.uniqueColumnGroups\n        ?.filter((g) => g.includes(column.name))\n        .sort((a, b) => a.length - b.length)[0];\n      return {\n        column,\n        shortestUnique,\n        shortestUniqueLength: shortestUnique?.length ?? 100,\n      };\n    })\n    .sort((a, b) => a.shortestUniqueLength - b.shortestUniqueLength);\n  return fTableTextColumns.map((c) => c.column);\n};\n\nconst OPTIONS_LIMIT = 20;\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/fieldUtils.ts",
    "content": "import type { ValidatedColumnInfo } from \"prostgles-types\";\nimport {\n  _PG_bool,\n  _PG_date,\n  _PG_geometric,\n  _PG_interval,\n  _PG_json,\n  _PG_numbers,\n  _PG_postgis,\n  _PG_strings,\n  isObject,\n  TS_PG_Types,\n} from \"prostgles-types\";\nimport type { FilterColumn } from \"../../SmartFilter/smartFilterUtils\";\nimport { getPGIntervalAsText } from \"../../W_SQL/customRenderers\";\n\n/**\n * Used in transforming a postgres/db value to a valid html <input /> OR <CodeEditor /> value\n */\nexport const parseValue = (\n  c: ValidatedColumnInfo | FilterColumn,\n  value: any,\n  reverseForServer = false,\n) => {\n  if (reverseForServer) {\n    if (\n      (c.udt_name === \"geography\" || c.udt_name === \"geometry\") &&\n      typeof value === \"string\" &&\n      value.trim().startsWith(\"{\")\n    ) {\n      try {\n        return JSON.parse(value);\n      } catch (e) {}\n    }\n\n    return value;\n  }\n\n  /** CodeEditor accepts only string */\n  if (c.udt_name.startsWith(\"json\")) {\n    if (!value) return \"\";\n    return JSON.stringify(value, null, 2);\n  }\n\n  if (value) {\n    if (c.udt_name === \"interval\" && typeof value !== \"string\") {\n      return getPGIntervalAsText(value);\n    }\n\n    const parseDateStr = (v: string | number, withTimeZone = false) => {\n      const wTz = new Date(v).toISOString();\n      if (!withTimeZone) return wTz.slice(0, -5);\n\n      /** datetime-local does not support timezone so we're slicing it out anyway */\n      return wTz.slice(0, 19);\n    };\n\n    if (c.udt_name.startsWith(\"geo\")) {\n      if (typeof value === \"string\") return value;\n\n      try {\n        return JSON.stringify(\n          typeof value === \"object\" ? value : JSON.parse(value),\n          null,\n          2,\n        );\n      } catch (err) {\n        return typeof value === \"string\" ? value : value + \"\";\n      }\n    }\n\n    if (\n      c.udt_name.startsWith(\"geo\") &&\n      typeof value === \"object\" &&\n      !Array.isArray(value)\n    ) {\n      return JSON.stringify(value);\n    }\n    const v = typeof value === \"string\" ? value : +value;\n    if (c.udt_name === \"date\") return new Date(v).toISOString().split(\"T\")[0];\n    if (c.udt_name.startsWith(\"timestamp\"))\n      return parseDateStr(v, c.udt_name === \"timestamptz\");\n    if (Array.isArray(value) && !value.some((v) => isObject(v))) {\n      if (c.udt_name.includes(\"timestamp\")) {\n        return value\n          .filter((v) => v !== \"\")\n          .map((v) => parseDateStr(v, c.udt_name.endsWith(\"z\")));\n      }\n    }\n  }\n\n  return value;\n};\n\nexport const parseDefaultValue = (\n  c: ValidatedColumnInfo,\n  value: any,\n  wasChanged: boolean,\n) => {\n  if (wasChanged) return value;\n\n  /* If value is provided then return it */\n  if (![null, undefined].includes(value)) return value;\n\n  /* If value is nullable and null then return it */\n  if (c.is_nullable && value === null) return value;\n\n  if (c.has_default && typeof c.column_default === \"string\") {\n    if (c.column_default.endsWith(\"::text\"))\n      return c.column_default.slice(1, -7);\n    // if ([\"now()\", \"CURRENT_TIMESTAMP\"].includes(c.column_default)) {\n    //   if (c.udt_name === \"date\") return (new Date()).toISOString().split('T')[0];\n    //   return (new Date()).toISOString().slice(0, -5);\n    // }\n    if (c.udt_name === \"jsonb\" && c.column_default.endsWith(\"::jsonb\")) {\n      try {\n        const val = JSON.parse(c.column_default.slice(1, -8));\n        return val;\n      } catch (e) {\n        console.error(\"Could not parse column_default\", e);\n      }\n    }\n    if (c.tsDataType === \"number\") return Number(c.column_default);\n  } else if (value) {\n    return parseValue(c, value);\n  }\n\n  return value;\n};\n\nexport const getInputType = (\n  c: Pick<ValidatedColumnInfo, \"udt_name\" | \"tsDataType\" | \"name\">,\n): string => {\n  return (\n    c.udt_name === \"date\" ? \"date\"\n    : c.udt_name.startsWith(\"timestamp\") ? \"datetime-local\"\n    : c.udt_name === \"time\" ? \"time\"\n    : c.tsDataType === \"boolean\" ? \"checkbox\"\n    : [\"email\"].includes(c.name) ? \"email\"\n    : [\"phone\", \"telephone\", \"phone_number\", \"tel\"].includes(c.name) ? \"tel\"\n    : [\"zip_code\", \"zipcode\", \"post_code\", \"postcode\"].includes(c.name) ?\n      \"postal-code\"\n    : [\"given-name\", \"first_name\", \"first-name\"].includes(c.name) ? \"given-name\"\n    : [\"family-name\", \"last_name\", \"last-name\"].includes(c.name) ? \"family-name\"\n    : [\"address_line1\", \"address_line\"].includes(c.name) ? \"address-line1\"\n    : [\"address_line2\"].includes(c.name) ? \"address-line2\"\n    : c.tsDataType === \"string\" ? \"text\"\n    : (c.tsDataType as string)\n  );\n};\n\nexport const getInputAutocomplete = (c: ValidatedColumnInfo): string => {\n  const _autocomplete_values = [\n    \"address-line1\",\n    \"address-line2\",\n    \"address-line3\",\n    \"street_address\",\n    \"street\",\n    \"address\",\n    \"address-level1\",\n    \"address-level2\",\n    \"address-level3\",\n    \"address-level4\",\n    \"organization\",\n    \"organization-title\",\n    \"country\",\n    \"country-name\",\n    \"postal-code\",\n    \"tel\",\n    \"given-name\",\n    \"family-name\",\n    \"email\",\n  ];\n  const autocomplete_values = _autocomplete_values.concat(\n    _autocomplete_values.map((v) => v.replaceAll(\"-\", \"_\")),\n  );\n  const noAutocomplete = [\"date\", \"timestamp\", \"timestamptz\"].includes(\n    c.udt_name,\n  );\n  return (\n    noAutocomplete ? \"off\"\n    : autocomplete_values.includes(c.name.toLowerCase()) ?\n      c.name.replaceAll(\"_\", \"-\").toLowerCase()\n    : \"on\"\n  );\n};\n\nexport const columnIsReadOnly = (\n  action: \"update\" | \"insert\" | \"view\" | undefined,\n  c: ValidatedColumnInfo,\n) => {\n  return (\n    action === \"view\" ||\n    (action === \"update\" && !c.update) ||\n    (action === \"insert\" && !c.insert)\n  );\n};\n\nexport const tsDataTypeFromUdtName = (\n  udtName: string,\n): ValidatedColumnInfo[\"tsDataType\"] => {\n  return (\n    Object.entries(TS_PG_Types).find(([ts, pgArr]) =>\n      (pgArr as any).includes(udtName.toLowerCase()),\n    )?.[0] ?? (\"string\" as any)\n  );\n};\n\nconst PG_Types = {\n  _PG_strings,\n  _PG_numbers,\n  _PG_json,\n  _PG_bool,\n  _PG_date,\n  _PG_interval,\n  _PG_postgis,\n  _PG_geometric,\n} as const;\ntype PG_T = keyof typeof PG_Types;\nexport const colIs = (\n  c: Partial<Pick<ValidatedColumnInfo, \"udt_name\">> | undefined,\n  type: PG_T | PG_T[],\n) => {\n  const types = Array.isArray(type) ? type : [type];\n  return types.some((t) => PG_Types[t].some((v) => c?.udt_name === v));\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/useFormData.ts",
    "content": "import type { TablesWithInfo } from \"../../DashboardMenu/useTableSizeInfo\";\n\n/**\n \n\ngiven this Typescript tree data structure used for inserting data into postgres:\n\ntype InsertData = {\n   tableName: string;\n   tableInfo: TableInfo;\n   row: Record<string, any>;\n   nestedInserts?: InsertData[];\n}\n\ntype TableInfo = {\n  name: string;\n  columns: {\n      name: string;\n      is_pkey: boolean;\n      references?: { ftable: string; cols: string[]; fcols: string[]; }[]\n   };\n}\n\nWrite a function that will be called from the nested insert row to identify which fields with fixed data because:\n1) they are referencing the parent\n2) they are referencing a sibling nested record from the parent row \n\ntype getFixedData = (parentRow: InsertData): Record<string, any>\n\n\n */\n\n/**\n * Identifies fixed data fields for nested inserts by analyzing parent and sibling references\n * @param parentInsert The parent InsertData object\n * @returns A record of field names and their fixed values\n */\nconst getFixedData = (parentInsert: InsertData): Record<string, any> => {\n  const fixedData: Record<string, any> = {};\n\n  // No need to analyze if there are no nested inserts\n  if (!parentInsert.nestedInserts || parentInsert.nestedInserts.length === 0) {\n    return fixedData;\n  }\n\n  // Get all primary keys from the parent\n  const parentPrimaryKeys: Record<string, any> = {};\n  parentInsert.tableInfo.columns\n    .filter((col) => col.is_pkey)\n    .forEach((col) => {\n      parentPrimaryKeys[col.name] = parentInsert.row[col.name];\n    });\n\n  // Create a map of all sibling tables and their data\n  const siblingTablesMap: Record<string, Record<string, any>> = {};\n  parentInsert.nestedInserts.forEach((nestedInsert) => {\n    const primaryData: Record<string, any> = {};\n    nestedInsert.tableInfo.columns\n      .filter((col) => col.is_pkey)\n      .forEach((col) => {\n        primaryData[col.name] = nestedInsert.row[col.name];\n      });\n    siblingTablesMap[nestedInsert.tableName] = {\n      ...nestedInsert.row,\n      ...primaryData,\n    };\n  });\n\n  // For each column in each nested insert, check if it references the parent or a sibling\n  parentInsert.nestedInserts.forEach((nestedInsert) => {\n    nestedInsert.tableInfo.columns.forEach((column) => {\n      if (column.references && column.references.length > 0) {\n        column.references.forEach((reference) => {\n          // Case 1: Column references the parent table\n          if (reference.ftable === parentInsert.tableName) {\n            for (let i = 0; i < reference.cols.length; i++) {\n              const localCol = reference.cols[i];\n              const foreignCol = reference.fcols[i]!;\n              if (parentInsert.row[foreignCol] !== undefined) {\n                if (!fixedData[nestedInsert.tableName]) {\n                  fixedData[nestedInsert.tableName] = {};\n                }\n                fixedData[nestedInsert.tableName][localCol!] =\n                  parentInsert.row[foreignCol];\n              }\n            }\n          }\n\n          // Case 2: Column references a sibling table\n          else if (siblingTablesMap[reference.ftable]) {\n            for (let i = 0; i < reference.cols.length; i++) {\n              const localCol = reference.cols[i]!;\n              const foreignCol = reference.fcols[i];\n              if (\n                siblingTablesMap[reference.ftable]![foreignCol!] !== undefined\n              ) {\n                if (!fixedData[nestedInsert.tableName]) {\n                  fixedData[nestedInsert.tableName] = {};\n                }\n                fixedData[nestedInsert.tableName][localCol] =\n                  siblingTablesMap[reference.ftable]![foreignCol!];\n              }\n            }\n          }\n        });\n      }\n    });\n  });\n\n  return fixedData;\n};\n\ntype InsertData = {\n  tableName: string;\n  tableInfo: TablesWithInfo[number];\n  row: Record<string, any>;\n  nestedInserts?: InsertData[];\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/useNestedInsertDefaultData.ts",
    "content": "import { useMemo } from \"react\";\nimport type { SmartFormProps } from \"../SmartForm\";\nimport type { AnyObject } from \"prostgles-types\";\n\nexport const useNestedInsertDefaultData = ({\n  tables,\n  tableName,\n  row,\n  ftable,\n}: Pick<SmartFormProps, \"tables\" | \"tableName\"> & {\n  row: AnyObject | undefined;\n  ftable: string;\n}) => {\n  const defaultData = useMemo(() => {\n    const table = tables.find((t) => t.name === tableName);\n    const joinConfig = table?.joinsV2.find((j) => j.tableName === ftable);\n    if (!joinConfig || !row) return;\n    const data = {};\n    joinConfig.on.map((fkey) => {\n      fkey.map(([pcol, fcol]) => {\n        data[fcol] = row[pcol];\n      });\n    });\n    return data;\n  }, [row, ftable, tables, tableName]);\n\n  return defaultData;\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/useSmartFormFieldAsJSON.tsx",
    "content": "import {\n  getJSONBSchemaAsJSONSchema,\n  isEmpty,\n  isObject,\n  type JSONB,\n} from \"prostgles-types\";\nimport { useMemo } from \"react\";\nimport type {\n  CodeEditorJsonSchema,\n  CodeEditorProps,\n} from \"../../CodeEditor/CodeEditor\";\nimport type { SmartFormFieldProps } from \"./SmartFormField\";\n\ntype P = Pick<\n  SmartFormFieldProps,\n  \"column\" | \"tableName\" | \"jsonbSchemaWithControls\" | \"value\"\n>;\n\nexport type AsJSON = {\n  schemas?: CodeEditorJsonSchema[];\n  options?: Omit<CodeEditorProps, \"language\" | \"value\">;\n} & (\n  | {\n      component: \"codeEditor\";\n      jsonbSchema?: JSONB.JSONBSchema;\n    }\n  | {\n      component: \"JSONBSchema\";\n      jsonbSchema: JSONB.JSONBSchema;\n      opts: Exclude<SmartFormFieldProps[\"jsonbSchemaWithControls\"], boolean>;\n    }\n);\n\n/**\n * Given a column, if it is:\n * - JSONB with a CHECK schema\n * or\n * - geography type\n * then render it using JSONBSchemaA or CodeEditor\n */\nexport const useSmartFormFieldAsJSON = (props: P): AsJSON | undefined => {\n  const { column, tableName, jsonbSchemaWithControls, value } = props;\n  const valueIsNonEmptyObject = useMemo(\n    () => isObject(value) && !isEmpty(value),\n    [value],\n  );\n\n  return useMemo(() => {\n    /** When a function was passed to the geo data (ST_...) */\n    if (column.udt_name === \"geography\" && valueIsNonEmptyObject) {\n      return {\n        component: \"codeEditor\",\n        options: {},\n      };\n    }\n\n    if (column.udt_name.startsWith(\"json\") && tableName) {\n      if (jsonbSchemaWithControls && column.jsonbSchema) {\n        const opts =\n          isObject(jsonbSchemaWithControls) ?\n            jsonbSchemaWithControls\n          : undefined;\n\n        return {\n          component: \"JSONBSchema\",\n          jsonbSchema: column.jsonbSchema,\n          opts,\n        };\n      }\n      const jsonSchema =\n        column.jsonbSchema &&\n        getJSONBSchemaAsJSONSchema(tableName, column.name, column.jsonbSchema);\n      return {\n        options: {},\n        ...(column.jsonbSchema && {\n          schemas: [\n            {\n              id: `${tableName}_${column.name}`,\n              schema: jsonSchema,\n            },\n          ],\n        }),\n        component: \"codeEditor\",\n      };\n    }\n  }, [column, tableName, jsonbSchemaWithControls, valueIsNonEmptyObject]);\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormField/useSmartFormFieldOnChange.ts",
    "content": "import { useCallback, useState } from \"react\";\nimport { parseValue } from \"./fieldUtils\";\nimport type { SmartFormFieldProps } from \"./SmartFormField\";\nimport type { AnyObject } from \"prostgles-types\";\nimport type { ColumnData } from \"../SmartFormNewRowDataHandler\";\n\nexport const useSmartFormFieldOnChange = (\n  props: Pick<SmartFormFieldProps, \"column\" | \"table\"> & {\n    onChange: (newColData: ColumnData) => void;\n  },\n) => {\n  const {\n    onChange,\n    column,\n    table: { info: tableInfo },\n  } = props;\n  const [error, setError] = useState<any>();\n\n  const onCheckAndChange = useCallback(\n    async (_newValue: File[] | string | number | null | AnyObject) => {\n      let newValue: string | number | null | { data: File; name: string }[] =\n        _newValue as any;\n\n      if (\n        _newValue === \"\" &&\n        [\"Date\", \"number\", \"boolean\", \"Object\"].includes(\n          column.tsDataType as string,\n        ) &&\n        column.is_nullable\n      ) {\n        newValue = null;\n      }\n\n      let error = null;\n      try {\n        newValue = parseValue(column, _newValue, true);\n      } catch (err: any) {\n        error = err;\n      }\n\n      if (!tableInfo.hasFiles) {\n        if (\n          typeof column.min === \"number\" &&\n          typeof newValue === \"number\" &&\n          newValue < column.min\n        ) {\n          newValue = Math.max(newValue, column.min);\n        } else if (\n          typeof column.max === \"number\" &&\n          typeof newValue === \"number\" &&\n          newValue > column.max\n        ) {\n          newValue = Math.min(newValue, column.max);\n        }\n      }\n\n      try {\n        await onChange({ type: \"column\", value: newValue });\n      } catch (err: any) {\n        error = err.toString();\n      }\n      setError(error);\n    },\n    [column, onChange, tableInfo.hasFiles],\n  );\n\n  return {\n    onCheckAndChange,\n    error,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormFieldList.tsx",
    "content": "import { classOverride, FlexCol } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport {\n  getPossibleNestedInsert,\n  isObject,\n  type AnyObject,\n} from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { DBSchemaTablesWJoins } from \"../Dashboard/dashboardUtils\";\nimport type { SmartFormProps } from \"./SmartForm\";\nimport {\n  SmartFormField,\n  type SmartColumnInfo,\n} from \"./SmartFormField/SmartFormField\";\nimport { SmartFormFileSection } from \"./SmartFormFileSection\";\nimport type { NewRow, NewRowDataHandler } from \"./SmartFormNewRowDataHandler\";\nimport type { SmartFormState } from \"./useSmartForm\";\nimport type { SmartFormModeState } from \"./useSmartFormMode\";\n\ntype P = Pick<\n  SmartFormProps,\n  | \"tableName\"\n  | \"tables\"\n  | \"db\"\n  | \"label\"\n  | \"contentClassname\"\n  | \"asPopup\"\n  | \"enableInsert\"\n  | \"jsonbSchemaWithControls\"\n  | \"hideNullBtn\"\n  | \"methods\"\n> &\n  SmartFormModeState &\n  Pick<SmartFormState, \"error\" | \"errors\"> & {\n    newRowDataHandler: NewRowDataHandler;\n    newRowData: NewRow | undefined;\n    row: AnyObject;\n    table: DBSchemaTablesWJoins[number];\n    displayedColumns: SmartColumnInfo[];\n  };\n\nexport const SmartFormFieldList = (props: P) => {\n  const {\n    tableName,\n    jsonbSchemaWithControls,\n    enableInsert = true,\n    db,\n    tables,\n    contentClassname,\n    newRowDataHandler,\n    table,\n    row,\n    mode,\n    displayedColumns,\n    error,\n    errors,\n    modeType,\n    methods,\n    newRowData,\n  } = props;\n\n  const hideNullBtn = mode.type === \"view\" || props.hideNullBtn;\n\n  const tableInfo = table.info;\n\n  const someColumnsHaveIcons = useMemo(() => {\n    return displayedColumns.some((c) => {\n      if (c.icon) return true;\n      const ref = getPossibleNestedInsert(c, tables);\n      return Boolean(\n        ref && tables.some((t) => t.name === ref.ftable && t.icon),\n      );\n    });\n  }, [displayedColumns, tables]);\n\n  return (\n    <ScrollFade\n      className={classOverride(\n        \"SmartFormFieldList flex-col f-1 o-auto min-h-0 min-w-0 pb-1 gap-1 px-2\",\n        contentClassname,\n      )}\n    >\n      {/* {!!geographyData.length && (\n        <DeckGLMap\n          geoJsonLayers={geographyData.map((d) => {\n            return {\n              features: d.value,\n              id: d.column.name,\n              label: d.column.name,\n            };\n          })}\n          basemapDesaturate={0}\n          basemapOpacity={1}\n          dataOpacity={1}\n          edit={undefined}\n          geoJsonLayersDataFilterSignature=\"\"\n        />\n      )} */}\n      {tableInfo.isFileTable && tableInfo.fileTableName && (\n        <SmartFormFileSection\n          {...props}\n          table={table}\n          newRowDataHandler={newRowDataHandler}\n          row={row}\n          mode={mode}\n          mediaTableName={tableInfo.fileTableName}\n        />\n      )}\n      {displayedColumns.map((c, i) => {\n        const rawValue = row[c.name];\n        const newValue = newRowData?.[c.name];\n        const formFieldStyle: React.CSSProperties =\n          !c.sectionHeader ? {} : { marginTop: \"1em\" };\n\n        if (c.onRender) {\n          const columnNode = c.onRender(rawValue, (newVal) =>\n            newRowDataHandler.setColumnData(c.name, {\n              type: \"column\",\n              value: newVal,\n            }),\n          );\n          return (\n            <FlexCol key={c.name} style={formFieldStyle} className=\"gap-p25\">\n              <Label variant=\"normal\">{c.label}</Label>\n              {columnNode}\n            </FlexCol>\n          );\n        }\n\n        return (\n          <SmartFormField\n            key={i}\n            tables={tables}\n            db={db}\n            tableName={tableName}\n            table={table}\n            action={modeType}\n            loading={\n              mode.type === \"update\" || mode.type === \"view\" ?\n                mode.loading\n              : undefined\n            }\n            column={c}\n            value={rawValue}\n            newValue={newValue}\n            row={row}\n            jsonbSchemaWithControls={jsonbSchemaWithControls}\n            error={\n              errors[c.name] ??\n              (isObject(error) && error.column === c.name ? error : undefined)\n            }\n            rightContentAlwaysShow={false}\n            methods={methods}\n            hideNullBtn={hideNullBtn}\n            sectionHeader={c.sectionHeader}\n            style={formFieldStyle}\n            enableInsert={enableInsert}\n            newRowDataHandler={newRowDataHandler}\n            someColumnsHaveIcons={someColumnsHaveIcons}\n          />\n        );\n      })}\n    </ScrollFade>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormFileSection.tsx",
    "content": "import type { AnyObject, DBSchemaTable } from \"prostgles-types\";\nimport { isEmpty } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { Media } from \"@components/FileInput/FileInput\";\nimport { FileInput } from \"@components/FileInput/FileInput\";\nimport type { SmartFormProps } from \"./SmartForm\";\nimport type { NewRow, NewRowDataHandler } from \"./SmartFormNewRowDataHandler\";\nimport type { SmartFormState } from \"./useSmartForm\";\n\ntype P = {\n  row: AnyObject;\n  mediaTableName: string;\n  newRowDataHandler: NewRowDataHandler | undefined;\n  table: DBSchemaTable;\n  newRowData: NewRow | undefined;\n} & Pick<SmartFormProps, \"defaultData\" | \"onSuccess\" | \"db\"> &\n  Pick<SmartFormState, \"mode\">;\n\n/**\n * Appears at the bottom of the form when the table is a file table.\n */\nexport const SmartFormFileSection = ({\n  db,\n  table,\n  newRowData,\n  defaultData,\n  mode: action,\n  onSuccess,\n  mediaTableName,\n  row,\n  newRowDataHandler,\n}: P) => {\n  const tableInfo = table.info;\n  const { isFileTable } = table.info;\n  const tableName = table.name;\n  const media: Media[] | undefined = useMemo(() => {\n    if (!isFileTable) throw \"Must be a file table\";\n    if (!isEmpty(row)) return [row as Media];\n    if (defaultData && !isEmpty(defaultData)) return [defaultData as Media];\n    return [];\n\n    // if (action.type === \"insert\") {\n    //   if (defaultData && isObject(defaultData) && !newRowData) {\n    //     return [defaultData as Media];\n    //   } else {\n    //     return row[mediaTableName] ?? [];\n    //   }\n    // } else {\n    //   return newRowData?.[tableName]?.value ?? [row as Media];\n    // }\n  }, [\n    row,\n    isFileTable,\n    defaultData,\n    // mediaTableName,\n    // action.type,\n    // newRowData,\n    // tableName,\n  ]);\n\n  if (\"loading\" in action && action.loading) return null;\n  if (!newRowDataHandler) return null;\n\n  return (\n    <FileInput\n      key={tableName}\n      className={\"mt-p5 f-0 \" + (tableInfo.isFileTable ? \" min-w-300\" : \"\")}\n      media={media}\n      // minSize={isFileTable ? 470 : 450}\n      maxFileCount={1}\n      onAdd={([file]) => {\n        // const currentRow = action.type === \"update\" ? action.currentRow : {};\n        // const currMedia = [\n        //   ...(newRowData?.[mediaTableName]?.value || []),\n        //   ...(currentRow?.[mediaTableName] || []),\n        // ].filter(isDefined);\n        // newRowDataHandler.setColumnData(mediaTableName, {\n        //   type: \"nested-table\",\n        //   value: [...currMedia, ...files],\n        // });\n        newRowDataHandler.setNewRow(\n          !file ?\n            {}\n          : {\n              name: { type: \"column\", value: file.name },\n              data: { type: \"column\", value: file.data },\n            },\n        );\n      }}\n      onDelete={async (media) => {\n        if (\"id\" in media && media.id) {\n          if (action.type === \"update\" && tableInfo.isFileTable) {\n            // ????\n            newRowDataHandler.setNewRow({\n              [tableName]: { type: \"nested-table\", value: [] },\n            });\n          } else {\n            const mediaTableHandler = db[mediaTableName];\n            if (mediaTableHandler?.update) {\n              const res = await mediaTableHandler.update(\n                { id: media.id },\n                { deleted: true },\n                onSuccess ? { returning: \"*\" } : {},\n              );\n              onSuccess?.(\"update\", res);\n            }\n          }\n        } else {\n          const currMedia: Media[] = newRowData?.[mediaTableName]?.value || [];\n          newRowDataHandler.setColumnData(mediaTableName, {\n            type: \"nested-table\",\n            value: currMedia.filter((m) => m.name !== media.name),\n          });\n        }\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormFooter/SmartFormFooterButtons.tsx",
    "content": "import { mdiContentCopy, mdiDelete } from \"@mdi/js\";\nimport { isEmpty } from \"prostgles-types\";\nimport React from \"react\";\nimport { dataCommand } from \"../../../Testing\";\nimport Btn from \"@components/Btn\";\nimport ConfirmationDialog from \"@components/ConfirmationDialog\";\nimport { Footer } from \"@components/Popup/Footer\";\nimport { type SmartFormProps } from \"../SmartForm\";\nimport type { SmartFormState } from \"../useSmartForm\";\nimport { type SmartFormActionsState } from \"./useSmartFormActions\";\nimport { getEntries } from \"@common/utils\";\n\ntype P = SmartFormState &\n  SmartFormActionsState &\n  Pick<SmartFormProps, \"onClose\" | \"parentForm\" | \"db\">;\n\nexport const SmartFormFooterButtons = (props: P): JSX.Element => {\n  const {\n    error,\n    errors,\n    onClose,\n    parentForm,\n    successMessage,\n    confirmPopup,\n    buttons,\n  } = props;\n\n  /** Showing Success animation. Will close soon */\n  if (successMessage) {\n    return <></>;\n  }\n\n  if (confirmPopup) {\n    return (\n      <>\n        <div\n          className=\"absolute \"\n          style={{\n            inset: 0,\n            opacity: 0.5,\n            background: \"gray\",\n            zIndex: 1, // needed to be on top of focused code editors\n          }}\n        />\n        <ConfirmationDialog\n          className=\"bg-color-0\"\n          style={{ zIndex: 2 }}\n          {...confirmPopup}\n        />\n      </>\n    );\n  }\n  if (!buttons || (getEntries(buttons).every(([_, b]) => !b) && !onClose)) {\n    return <></>;\n  }\n\n  const errorMsg =\n    error || !isEmpty(errors) ? \"Must fix error first\" : undefined;\n\n  const actionButtons = (\n    <>\n      {buttons.onClickUpdate && (\n        <Btn\n          {...dataCommand(\"SmartForm.update\")}\n          color=\"action\"\n          className=\"\"\n          variant=\"filled\"\n          disabledInfo={errorMsg}\n          onClick={buttons.onClickUpdate}\n        >\n          Update\n        </Btn>\n      )}\n      {buttons.onClickDelete && (\n        <Btn\n          {...dataCommand(\"SmartForm.delete\")}\n          title=\"Delete record\"\n          color=\"danger\"\n          disabledInfo={errorMsg}\n          iconPath={mdiDelete}\n          onClick={buttons.onClickDelete}\n        >\n          Delete\n        </Btn>\n      )}\n      {buttons.onClickClone && (\n        <Btn\n          color=\"action\"\n          {...dataCommand(\"SmartForm.clone\")}\n          iconPath={mdiContentCopy}\n          variant=\"filled\"\n          title=\"Prepare a duplicate insert that excludes primary key fields\"\n          className=\"ml-auto\"\n          onClick={buttons.onClickClone}\n        >\n          Clone\n        </Btn>\n      )}\n      {buttons.onClickInsert && (\n        <Btn\n          color=\"action\"\n          {...dataCommand(\"SmartForm.insert\")}\n          disabledInfo={errorMsg}\n          className=\" \"\n          variant=\"filled\"\n          onClick={buttons.onClickInsert}\n        >\n          {parentForm?.type === \"insert\" ? `Add` : `Insert`}\n        </Btn>\n      )}\n    </>\n  );\n\n  return (\n    <Footer style={{ padding: \"1em\" }}>\n      {onClose && (\n        <Btn\n          className=\" bg-color-0 mr-auto\"\n          {...dataCommand(\"SmartForm.close\")}\n          onClick={() => onClose(true)}\n        >\n          Close\n        </Btn>\n      )}\n      {actionButtons}\n    </Footer>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormFooter/useSmartFormActions.ts",
    "content": "import { useCallback, useMemo, useState } from \"react\";\nimport { getSmartGroupFilter } from \"@common/filterUtils\";\nimport type { ConfirmDialogProps } from \"@components/ConfirmationDialog\";\nimport type { SmartFormProps } from \"../SmartForm\";\nimport type { SmartFormNewRowState } from \"../useNewRowDataHandler\";\nimport type { SmartFormState } from \"../useSmartForm\";\nimport { areEqual } from \"../../../utils/utils\";\n\ntype ConfirmationPopup = Pick<\n  ConfirmDialogProps,\n  \"message\" | \"acceptBtn\" | \"onClose\" | \"onAccept\"\n>;\n\ntype Args = Pick<\n  SmartFormProps,\n  | \"fixedData\"\n  | \"parentForm\"\n  | \"disabledActions\"\n  | \"onSuccess\"\n  | \"onInserted\"\n  | \"db\"\n  | \"confirmUpdates\"\n> &\n  SmartFormNewRowState &\n  SmartFormState;\n\nexport const useSmartFormActions = ({\n  mode,\n  fixedData,\n  newRow,\n  parentForm,\n  disabledActions,\n  newRowDataHandler,\n  table,\n  db,\n  setLoading,\n  onSuccess,\n  onInserted,\n  setError,\n  setErrors,\n  getErrors,\n  parseError,\n  confirmUpdates,\n}: Args): {\n  successMessage: string | undefined;\n  setSuccessMessage: (msg: string | undefined) => void;\n  confirmPopup: ConfirmationPopup | undefined;\n  buttons:\n    | {\n        onClickInsert?: () => void;\n        onClickUpdate?: () => Promise<void>;\n        onClickDelete?: () => void;\n        onClickClone?: () => void;\n      }\n    | undefined;\n} => {\n  const [confirmPopup, setConfirmPopup] = useState<ConfirmationPopup>();\n  const [successMessage, setSuccessMessage] = useState<string>();\n\n  const performAction = useCallback(\n    async (action: () => Promise<void> | void) => {\n      setLoading(true);\n      try {\n        await action();\n        setError(undefined);\n        setErrors({});\n      } catch (error: any) {\n        console.error(error);\n        parseError(error);\n      }\n      setLoading(false);\n    },\n    [setLoading, setError, setErrors, parseError],\n  );\n\n  const newRowWithUpdates = useMemo(() => {\n    const res = newRow && {\n      ...newRow,\n      ...fixedData,\n    };\n\n    const nothingToUpdate =\n      mode.type === \"update\" &&\n      res &&\n      mode.currentRow &&\n      areEqual(mode.currentRow, res, Object.keys(res));\n\n    if (nothingToUpdate) {\n      return undefined;\n    }\n\n    return res;\n  }, [fixedData, newRow, mode]);\n\n  const buttons = useMemo(() => {\n    if (mode.type === \"manual\") {\n      return undefined;\n    }\n    if (mode.type === \"insert\") {\n      return {\n        onClickInsert: () => {\n          return getErrors(() => {\n            return performAction(async () => {\n              if (!newRow) throw \"No row data to insert\";\n              if (parentForm?.type === \"insert\") {\n                parentForm.setColumnData(newRowDataHandler);\n                return;\n              }\n\n              const doInsert = async () => {\n                if (table.info.isFileTable && parentForm) {\n                  const { table: parentTable, rowFilter: parentRowFilter } =\n                    parentForm;\n                  const pkeyColumns = parentTable.columns\n                    .filter((c) => c.is_pkey)\n                    .map((c) => c.name);\n                  if (!pkeyColumns.length) {\n                    throw (\n                      \"No primary key columns found in parent form table \" +\n                      parentForm.table.name\n                    );\n                  }\n                  const parentTableName = parentTable.name;\n                  const parentTableHandler = db[parentTableName];\n                  if (!parentTableHandler?.update) {\n                    throw \"Not allowed to update referenced table\";\n                  }\n                  const filter = getSmartGroupFilter(parentRowFilter);\n                  const count = await parentTableHandler.count?.(filter);\n                  if (+(count ?? 0) !== 1) {\n                    throw \"Could not match to exactly 1 row\";\n                  }\n                  await parentTableHandler.update(\n                    filter,\n                    {\n                      [parentForm.column.name]: newRow,\n                    },\n                    { returning: \"*\" },\n                  );\n                  return newRow;\n                }\n\n                await mode.tableHandlerInsert!(\n                  newRow,\n                  onInserted || onSuccess ? { returning: \"*\" } : {},\n                );\n\n                return newRow;\n              };\n              const result = await doInsert();\n\n              onSuccess?.(\"insert\", result);\n              onInserted?.(result);\n\n              setSuccessMessage(\"Inserted\");\n            });\n          });\n        },\n      };\n    }\n    const { tableHandlerUpdate, tableHandlerDelete } = mode;\n    if (mode.type === \"update\" || mode.type === \"multiUpdate\") {\n      if (\n        !(\n          !confirmUpdates ||\n          !newRowWithUpdates ||\n          !tableHandlerUpdate ||\n          disabledActions?.includes(\"update\")\n        )\n      ) {\n        return {\n          onClickUpdate: async () => {\n            return performAction(() => {\n              setConfirmPopup({\n                message: \"Are you sure you want to update?\",\n                acceptBtn: {\n                  dataCommand: \"SmartForm.update.confirm\",\n                  text: \"Update row!\",\n                  color: \"action\",\n                },\n                onAccept: async () => {\n                  setConfirmPopup(undefined);\n\n                  await performAction(async () => {\n                    const nr = await tableHandlerUpdate(\n                      mode.rowFilterObj,\n                      newRowWithUpdates,\n                      {\n                        returning: \"*\",\n                      },\n                    );\n                    if (!nr?.length) {\n                      throw \"No rows were updated. Access rules may not allow this update.\";\n                    }\n                    onSuccess?.(\"update\", nr);\n                    setSuccessMessage(\"Updated\");\n                  });\n                },\n                onClose: () => setConfirmPopup(undefined),\n              });\n            });\n          },\n        };\n      }\n    }\n    return {\n      onClickDelete:\n        !tableHandlerDelete || disabledActions?.includes(\"delete\") ?\n          undefined\n        : () => {\n            setConfirmPopup({\n              message: \"Are you sure you want to delete this?\",\n              acceptBtn: {\n                dataCommand: \"SmartForm.delete.confirm\",\n                text: \"Delete!\",\n                color: \"danger\",\n              },\n              onAccept: async () => {\n                setConfirmPopup(undefined);\n                await performAction(async () => {\n                  const nr = await tableHandlerDelete(mode.rowFilterObj, {\n                    returning: \"*\",\n                  });\n\n                  if (!nr?.length) {\n                    throw \"No rows were deleted. Access rules may not allow this update.\";\n                  }\n                  onSuccess?.(\"delete\");\n                  setSuccessMessage(\"Deleted\");\n                });\n              },\n              onClose: () => {\n                setConfirmPopup(undefined);\n              },\n            });\n          },\n      onClickClone:\n        (\n          mode.type !== \"update\" ||\n          !mode.clone ||\n          disabledActions?.includes(\"clone\")\n        ) ?\n          undefined\n        : mode.clone,\n    };\n  }, [\n    mode,\n    performAction,\n    newRowDataHandler,\n    parentForm,\n    disabledActions,\n    table,\n    newRow,\n    onInserted,\n    onSuccess,\n    db,\n    setSuccessMessage,\n    newRowWithUpdates,\n    confirmUpdates,\n    getErrors,\n  ]);\n\n  return { successMessage, setSuccessMessage, confirmPopup, buttons };\n};\nexport type SmartFormActionsState = ReturnType<typeof useSmartFormActions>;\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormNewRowDataHandler.ts",
    "content": "import {\n  getObjectEntries,\n  isDefined,\n  isEmpty,\n  isEqual,\n  type AnyObject,\n} from \"prostgles-types\";\nimport type { Media } from \"@components/FileInput/FileInput\";\n\nexport type NewRow = Record<string, ColumnData>;\n\ntype Opts = {\n  onChange: (\n    newRow: NewRow,\n    columnName: string,\n    newData: ColumnData,\n  ) => NewRow | Promise<NewRow>;\n  onChanged: (newRow: NewRow) => void;\n};\nexport class NewRowDataHandler {\n  private newRow: NewRow | undefined;\n  private onChange: Opts[\"onChange\"];\n  private onChanged: Opts[\"onChanged\"];\n  constructor(newRow: NewRow | undefined, opts: Opts) {\n    this.newRow = newRow;\n    this.onChanged = (newRow) => opts.onChanged({ ...newRow });\n    this.onChange = opts.onChange;\n  }\n\n  setHandlers = (opts: Opts) => {\n    this.onChanged = (newRow) => opts.onChanged({ ...newRow });\n    this.onChange = opts.onChange;\n  };\n\n  getNewRow = () => ({ ...this.newRow });\n\n  setNewRow = (newRow: NewRow) => {\n    this.newRow = newRow;\n    this.onChanged(this.newRow);\n  };\n\n  setColumnData = async (columnName: string, newData: ColumnData) => {\n    this.newRow ??= {};\n\n    if (newData.value === undefined) {\n      delete this.newRow[columnName];\n    } else if (isEqual(this.newRow[columnName], newData)) {\n      /** Exclude no changes */\n      return;\n    } else {\n      this.newRow[columnName] = newData;\n    }\n    this.newRow = await this.onChange(this.newRow, columnName, newData);\n    this.onChanged(this.newRow);\n  };\n\n  setNestedColumn = (\n    columnName: string,\n    value: Extract<ColumnData, { type: \"nested-column\" }>[\"value\"],\n  ) => {\n    this.setColumnData(columnName, {\n      type: \"nested-column\",\n      value,\n    });\n  };\n  setNestedFileColumn = (\n    columnName: string,\n    value: Extract<ColumnData, { type: \"nested-file-column\" }>[\"value\"],\n  ) => {\n    this.setColumnData(columnName, {\n      type: \"nested-file-column\",\n      value,\n    });\n  };\n  setNestedTable = (\n    columnName: string,\n    value: Extract<ColumnData, { type: \"nested-table\" }>[\"value\"],\n  ) => {\n    this.setColumnData(columnName, {\n      type: \"nested-table\",\n      value,\n    });\n  };\n\n  setColumn = (\n    columnName: string,\n    value: Extract<ColumnData, { type: \"column\" }>[\"value\"],\n  ) => {\n    this.setColumnData(columnName, {\n      type: \"column\",\n      value,\n    });\n  };\n\n  getNestedColumnData = (columnName: string): AnyObject | undefined => {\n    const colData = this.newRow?.[columnName];\n    if (colData?.type !== \"nested-column\") return undefined;\n    if (colData.value instanceof NewRowDataHandler) {\n      return colData.value.getRow();\n    }\n    return colData.value;\n  };\n\n  getNestedTableData = (columnName: string): AnyObject[] | undefined => {\n    const colData = this.newRow?.[columnName];\n    if (colData?.type !== \"nested-table\") return undefined;\n    return colData.value\n      .map((v) => (v instanceof NewRowDataHandler ? v.getRow() : v))\n      .filter(isDefined);\n  };\n\n  getRow = (\n    filterFunc?: (data: ColumnData) => boolean,\n  ): AnyObject | undefined => {\n    const result =\n      this.newRow &&\n      Object.fromEntries(\n        getObjectEntries(this.newRow)\n          .filter(([_, v]) => filterFunc?.(v) ?? true)\n          .map(([key, { type, value: valueOrNewRowData }]) => {\n            if (type === \"nested-table\" && Array.isArray(valueOrNewRowData)) {\n              return [\n                key,\n                valueOrNewRowData.map((v) =>\n                  v instanceof NewRowDataHandler ? v.getRow() : v,\n                ),\n              ];\n            }\n            if (\n              type === \"nested-column\" &&\n              valueOrNewRowData instanceof NewRowDataHandler\n            ) {\n              return [key, valueOrNewRowData.getRow()];\n            }\n            return [key, valueOrNewRowData];\n          }),\n      );\n    if (isEmpty(result)) return undefined;\n    return result;\n  };\n}\n\nexport type ColumnData =\n  | {\n      type: \"column\";\n      value: any;\n    }\n  | {\n      /**\n       * Added from the fkey column\n       */\n      type: \"nested-column\";\n      value: AnyObject | NewRowDataHandler | undefined;\n    }\n  | {\n      /**\n       * References the file table\n       */\n      type: \"nested-file-column\";\n      value: Media | undefined;\n    }\n  | {\n      /**\n       * Added from the JoinedRecords\n       */\n      type: \"nested-table\";\n      value: (AnyObject | NewRowDataHandler)[];\n    };\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormPopup/SmartFormPopupWrapper.tsx",
    "content": "import { mdiChevronLeft, mdiChevronRight } from \"@mdi/js\";\nimport {\n  type AnyObject,\n  getKeys,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { sliceText } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport type { PopupProps } from \"@components/Popup/Popup\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport type { SmartFormProps } from \"../SmartForm\";\nimport type { SmartFormState } from \"../useSmartForm\";\n\ntype P = Pick<SmartFormProps, \"onPrevOrNext\" | \"prevNext\" | \"asPopup\"> & {\n  rowFilterObj: AnyObject | undefined;\n  displayedColumns: Pick<\n    ValidatedColumnInfo,\n    \"name\" | \"is_pkey\" | \"references\"\n  >[];\n  headerText: string;\n  children: React.ReactNode;\n  maxWidth: string;\n  onClose: () => void;\n  table: SmartFormState[\"table\"];\n};\nexport const SmartFormPopupWrapper = ({\n  onPrevOrNext,\n  prevNext,\n  rowFilterObj,\n  displayedColumns,\n  headerText,\n  children,\n  maxWidth,\n  onClose,\n  asPopup,\n  table,\n}: P) => {\n  const prevNextClass = \"smartformprevnext\";\n\n  const autoFocusFirstIfIsInsert = !rowFilterObj;\n  const extraProps: Pick<\n    PopupProps,\n    \"onKeyDown\" | \"headerRightContent\" | \"autoFocusFirst\"\n  > = useMemo(() => {\n    return !onPrevOrNext ?\n        ({\n          autoFocusFirst: autoFocusFirstIfIsInsert ? \"content\" : undefined,\n        } satisfies Pick<PopupProps, \"autoFocusFirst\">)\n      : {\n          autoFocusFirst: \"header\",\n          onKeyDown: (e, section) => {\n            if (section !== \"header\") return;\n\n            if (e.key === \"ArrowLeft\") {\n              onPrevOrNext(-1);\n            }\n            if (e.key === \"ArrowRight\") {\n              onPrevOrNext(1);\n            }\n          },\n          headerRightContent: (\n            <div className={\"flex-row mx-1 \" + prevNextClass}>\n              <Btn\n                iconPath={mdiChevronLeft}\n                disabledInfo={\n                  prevNext?.prev === false ? \"Reached end\" : undefined\n                }\n                data-command=\"SmartForm.header.previousRow\"\n                onClick={({ currentTarget }) => {\n                  currentTarget.focus();\n                  onPrevOrNext(-1);\n                }}\n              />\n              <Btn\n                iconPath={mdiChevronRight}\n                data-command=\"SmartForm.header.nextRow\"\n                disabledInfo={\n                  prevNext?.next === false ? \"Reached end\" : undefined\n                }\n                onClick={({ currentTarget }) => {\n                  currentTarget.focus();\n                  onPrevOrNext(1);\n                }}\n              />\n            </div>\n          ),\n        };\n  }, [autoFocusFirstIfIsInsert, onPrevOrNext, prevNext?.next, prevNext?.prev]);\n\n  const { subTitle } = useMemo(() => {\n    const filterKeys =\n      rowFilterObj && \"$and\" in rowFilterObj ?\n        rowFilterObj.$and.flatMap((f) => getKeys(f))\n      : getKeys(rowFilterObj ?? {});\n    /** Do not show subTitle rowFilter if it's primary key and shows in columns */\n    const knownJoinColumns = displayedColumns\n      .filter((c) => c.is_pkey || c.references)\n      .map((c) => c.name);\n    const subTitle =\n      rowFilterObj ?\n        filterKeys.every((col) => knownJoinColumns.includes(col)) ?\n          undefined\n        : sliceText(\n            \" (\" +\n              Object.entries(rowFilterObj)\n                .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)\n                .join(\" AND \") +\n              \")\",\n            100,\n          )\n      : \"\";\n    return { subTitle };\n  }, [displayedColumns, rowFilterObj]);\n\n  if (!asPopup) {\n    return children;\n  }\n  return (\n    <Popup\n      title={\n        <FlexRow\n          data-command=\"SmartForm.header.tableIconAndName\"\n          className=\"gap-1\"\n        >\n          {table.icon && <SvgIcon size={34} icon={table.icon} />}\n          {headerText}\n        </FlexRow>\n      }\n      subTitle={subTitle}\n      {...extraProps}\n      contentClassName={`${maxWidth} pt-1`}\n      positioning=\"right-panel\"\n      onClose={onClose}\n      clickCatchStyle={{ opacity: 0.2 }}\n      showFullscreenToggle={{\n        getStyle: (fullscreen) =>\n          fullscreen ? {} : { width: \"min(600px, 100vw)\" },\n      }}\n    >\n      {children}\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormUpperFooter/SmartFormUpperFooter.tsx",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport { isDefined, isObject } from \"prostgles-types\";\nimport React, { useEffect, useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport Popup from \"@components/Popup/Popup\";\nimport { W_MethodControls } from \"../../W_Method/W_MethodControls\";\nimport { JoinedRecords } from \"../JoinedRecords/JoinedRecords\";\nimport { useActiveJoinedRecordsTab } from \"./useActiveJoinedRecordsTab\";\nimport type { SmartFormProps } from \"../SmartForm\";\nimport type { SmartFormState } from \"../useSmartForm\";\n\nexport type SmartFormUpperFooterProps = Omit<SmartFormProps, \"columns\"> &\n  SmartFormState;\n\nexport const SmartFormUpperFooter = (props: SmartFormUpperFooterProps) => {\n  const {\n    onChange,\n    rowFilter,\n    tableName,\n    methods,\n    showJoinedTables = true,\n    newRowDataHandler,\n    tables,\n    newRowData,\n    mode,\n    row,\n    db,\n    modeType,\n  } = props;\n\n  const dbMethodActions = Object.entries(methods)\n    .map(([methodName, _m]) => {\n      if (isObject(_m) && \"run\" in _m) {\n        const argEntries = Object.entries(_m.input);\n        const thisTableArgIdx = argEntries.findIndex(\n          ([_, arg]) =>\n            arg.lookup?.type === \"data\" &&\n            arg.lookup.isFullRow &&\n            arg.lookup.table === tableName,\n        );\n        if (thisTableArgIdx > -1) {\n          return {\n            methodName,\n            argName: argEntries[thisTableArgIdx]![0],\n            arg: argEntries[thisTableArgIdx]![1],\n          };\n        }\n      }\n\n      return undefined;\n    })\n    .filter(isDefined);\n\n  const [method, setMethod] = useState<{\n    name: string;\n    row: AnyObject;\n    argName: string;\n  }>();\n\n  const showChanges =\n    onChange && mode.type === \"update\" && Object.keys(newRowData ?? {}).length;\n\n  const [methodState, setMethodState] = useState<{\n    args?: AnyObject | undefined;\n    disabledArgs?: string[] | undefined;\n  }>({});\n\n  const rootDivRef = React.useRef<HTMLDivElement>(null);\n  const [isFocused, setIsFocused] = useState(false);\n  useEffect(() => {\n    const onClickListener = ({ target }: MouseEvent) => {\n      if (!rootDivRef.current) return;\n      if (rootDivRef.current.contains(target as Node)) {\n        setIsFocused(true);\n      } else {\n        setIsFocused(false);\n      }\n    };\n\n    document.addEventListener(\"click\", onClickListener);\n    return () => {\n      document.removeEventListener(\"click\", onClickListener);\n    };\n  }, [method]);\n\n  const { activeJoinedRecordsTab, setActiveJoinedRecordsTab } =\n    useActiveJoinedRecordsTab({\n      rootDivRef,\n      tableName,\n      tables,\n      actionType: mode.type,\n    });\n\n  if (\n    !(\n      showJoinedTables &&\n      tables.find((t) => t.name === tableName)?.joinsV2.length\n    ) &&\n    !showChanges &&\n    !dbMethodActions.length\n  ) {\n    return null;\n  }\n\n  const showMethods = dbMethodActions.length > 0 && row;\n\n  if (!(method || showJoinedTables || showMethods)) return null;\n\n  return (\n    <div\n      className={\n        \"SmartFormUpperFooter bt b-color-0 max-h-fit flex-col o-auto min-h-0 min-w-0 w-full f-0 bg-popup-content\"\n      }\n      ref={rootDivRef}\n      style={{\n        /** Replaced shadow with ScrollFade */\n        // boxShadow: \"0px 3px 9px 0px var(--shadow0)\",\n        // clipPath: \"inset(-10px 1px 0px 1px)\",\n        minHeight: \"1px\",\n        flex: isFocused ? 1 : 0.3,\n        /** Expand full allowed height to prevent size change when toggling joined records sections */\n        // ...(activeJoinedRecordsTab && {\n        //   flex: 1,\n        // }),\n      }}\n    >\n      {method && (\n        <Popup\n          onClose={() => setMethod(undefined)}\n          title={method.name}\n          showFullscreenToggle={{\n            defaultValue: true,\n          }}\n        >\n          <W_MethodControls\n            method_name={method.name}\n            fixedRowArgument={{\n              argName: method.argName,\n              row: method.row,\n              tableName,\n            }}\n            db={db}\n            tables={tables}\n            methods={methods}\n            state={methodState}\n            setState={setMethodState}\n            w={undefined}\n          />\n        </Popup>\n      )}\n      {showJoinedTables && (\n        <JoinedRecords\n          modeType={modeType}\n          db={db}\n          tables={tables}\n          tablesToShow={\n            isObject(showJoinedTables) ? showJoinedTables : undefined\n          }\n          methods={methods}\n          rowFilter={rowFilter}\n          newRowData={newRowData}\n          tableName={tableName}\n          newRowDataHandler={newRowDataHandler}\n          onTabChange={setActiveJoinedRecordsTab}\n          activeTabKey={activeJoinedRecordsTab}\n          onSuccess={props.onSuccess}\n          parentForm={props.parentForm}\n          errors={props.errors}\n          row={row}\n        />\n      )}\n      {showMethods && (\n        <div className=\"dbMethodActions flex-row-wrap gap-p5 p-1\">\n          {dbMethodActions.map(({ methodName, arg, argName }, i) => {\n            const { lookup } = arg;\n            const showInRowCard =\n              lookup?.type !== \"data\" ? undefined : lookup.showInRowCard;\n            return (\n              <Btn\n                key={methodName}\n                color={showInRowCard?.actionColor ?? \"action\"}\n                variant=\"filled\"\n                onClick={() => {\n                  setMethod({ name: methodName, row, argName });\n                }}\n              >\n                {showInRowCard?.actionLabel ?? methodName}\n              </Btn>\n            );\n          })}\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/SmartFormUpperFooter/useActiveJoinedRecordsTab.ts",
    "content": "import { useEffect, useState } from \"react\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\nimport type { SmartFormMode } from \"../useSmartFormMode\";\n\nexport const useActiveJoinedRecordsTab = ({\n  rootDivRef,\n  tableName,\n  tables,\n  actionType,\n}: {\n  actionType: SmartFormMode[\"type\"];\n  tableName: string;\n  tables: DBSchemaTableWJoins[];\n  rootDivRef: React.RefObject<HTMLDivElement>;\n}) => {\n  const [activeJoinedRecordsTab, setActiveJoinedRecordsTab] =\n    useState<string>();\n\n  /**\n   * For convenience, close joined records tab when clicking on the form fields section\n   */\n  const joinedRecordsIsOpened = !!activeJoinedRecordsTab;\n  useEffect(() => {\n    if (!joinedRecordsIsOpened) return;\n    const clickAwayListener: EventListener = (e) => {\n      const rootDiv = rootDivRef.current;\n      if (!rootDiv) return;\n      const parentFormFields = rootDiv\n        .closest(\".SmartForm\")\n        ?.querySelector(\".SmartFormFieldList\");\n      const clickPath = e.composedPath();\n      if (\n        parentFormFields &&\n        !clickPath.includes(rootDiv) &&\n        clickPath.includes(parentFormFields)\n      ) {\n        setActiveJoinedRecordsTab(\"\");\n      }\n    };\n    document.addEventListener(\"click\", clickAwayListener);\n    return () => {\n      document.removeEventListener(\"click\", clickAwayListener);\n    };\n  }, [setActiveJoinedRecordsTab, joinedRecordsIsOpened, rootDivRef]);\n\n  /** If is insert and a table record is required then show it */\n  useEffect(() => {\n    if (isDefined(activeJoinedRecordsTab) || actionType !== \"insert\") return;\n    const table = tables.find((t) => t.name === tableName);\n    const requiredNestedInsert = table?.info.requiredNestedInserts?.[0];\n    if (requiredNestedInsert) {\n      setActiveJoinedRecordsTab(requiredNestedInsert.ftable);\n    }\n  }, [\n    activeJoinedRecordsTab,\n    actionType,\n    tables,\n    setActiveJoinedRecordsTab,\n    tableName,\n  ]);\n\n  return { activeJoinedRecordsTab, setActiveJoinedRecordsTab };\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/useNewRowDataHandler.ts",
    "content": "import {\n  getSerialisableError,\n  isDefined,\n  isEmpty,\n  isObject,\n  omitKeys,\n  pickKeys,\n  type AnyObject,\n  type ProstglesError,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { getKeys } from \"../../utils/utils\";\nimport type { DBSchemaTableWJoins } from \"../Dashboard/dashboardUtils\";\nimport type { getErrorsHook, SmartFormProps } from \"./SmartForm\";\nimport { parseDefaultValue } from \"./SmartFormField/fieldUtils\";\nimport type { SmartColumnInfo } from \"./SmartFormField/SmartFormField\";\nimport {\n  NewRowDataHandler,\n  type ColumnData,\n  type NewRow,\n} from \"./SmartFormNewRowDataHandler\";\nimport type { useSmartFormMode } from \"./useSmartFormMode\";\n\ntype Args = {\n  columns: ValidatedColumnInfo[];\n  table: DBSchemaTableWJoins | undefined;\n  displayedColumns: SmartColumnInfo[];\n} & ReturnType<typeof useSmartFormMode> &\n  Pick<\n    SmartFormProps,\n    | \"defaultData\"\n    | \"rowFilter\"\n    | \"fixedData\"\n    | \"confirmUpdates\"\n    | \"onChange\"\n    | \"onSuccess\"\n    | \"parentForm\"\n  >;\n\nexport const useNewRowDataHandler = (args: Args) => {\n  const {\n    rowFilter,\n    fixedData,\n    defaultData,\n    confirmUpdates,\n    displayedColumns,\n    onChange,\n    onSuccess,\n    columns,\n    mode,\n    table,\n    setLocalRowFilter,\n    parentForm,\n  } = args;\n  const [error, setError] = useState<undefined | string>();\n  const [errors, setErrors] = useState<AnyObject>({});\n  const columnMap = useMemo(() => {\n    const colMap: Map<string, ValidatedColumnInfo> = new Map();\n    columns.forEach((c) => {\n      colMap.set(c.name, c);\n    });\n    return colMap;\n  }, [columns]);\n\n  const [newRowData, setNewRowData] = useState<NewRow>();\n  const parseError = useCallback(\n    (error: ProstglesError) => {\n      let newError: string =\n        typeof error === \"string\" ? error : (\n          [\n            error.table ? `${error.table}: ` : \"\",\n            error.detail ? error.detail + \"\\n\" : \"\",\n            error.message || error.txt,\n          ]\n            .filter(Boolean)\n            .join(\"\\n\") ||\n          JSON.stringify(getSerialisableError(error)) ||\n          \"Unknown error\"\n        );\n      const newErrors: AnyObject = {};\n      if (isObject(error) && error.code === \"23503\" && error.table) {\n        console.log(error);\n        newError =\n          error.detail ||\n          `Table ${error.table} has rows that reference this record (foreign_key_violation)\\n\\n${error.message || \"\"}`;\n      } else if (Object.keys(error).length && error.constraint) {\n        let cols: string[] = [];\n        if (error.columns) {\n          cols = error.columns;\n        } else if (error.column) {\n          cols = [error.column];\n        }\n        cols.forEach((c) => {\n          if (columns.find((col) => col.name === c)) {\n            let message = error.constraint;\n            if (error.code_info === \"unique_violation\") {\n              message =\n                \"Value already exists. \\nConstraint: \" + error.constraint;\n            }\n            newErrors[c] = message;\n          }\n        });\n      }\n\n      if (Object.keys(newErrors).length) {\n        newError = error.message || error.detail || \"Unknown error\";\n        setErrors(newErrors);\n      }\n      setError(newError);\n    },\n    [columns],\n  );\n  const onSetColumnData = useCallback(\n    async (newRow: NewRow, columnName: string, newVal: ColumnData) => {\n      const column = columnMap.get(columnName);\n\n      if (!mode) throw \"unexpected\";\n      /* Remove updates that change nothing */\n      if (mode.type === \"update\" && mode.currentRow) {\n        const { currentRow } = mode;\n        getKeys(newRow).forEach((key) => {\n          if (newRow[key] === currentRow[key] && key in currentRow) {\n            delete newRow[key];\n          }\n        });\n      }\n\n      /* Remove empty updates (user deleted a non text column) */\n      if (\n        column &&\n        newVal.type === \"column\" &&\n        newVal.value === \"\" &&\n        column.tsDataType !== \"string\"\n      ) {\n        delete newRow[column.name];\n      }\n\n      if (\n        !onChange &&\n        (mode.type === \"update\" || mode.type === \"multiUpdate\") &&\n        !confirmUpdates\n      ) {\n        try {\n          const newRow = await mode.tableHandlerUpdate?.(\n            mode.rowFilterObj,\n            { [columnName]: newVal.value },\n            { returning: \"*\" },\n          );\n          onSuccess?.(\"update\", newRow as any);\n        } catch (_e: any) {\n          parseError(_e);\n          throw _e;\n          // This triggered clearing the error before it could be shown\n          // return newRow;\n        }\n      }\n\n      /** Update rowFilter to ensure the record does not dissapear after updating */\n      if (\n        !confirmUpdates &&\n        rowFilter &&\n        column?.is_pkey &&\n        rowFilter.find((f) => f.fieldName === column.name)\n      ) {\n        setLocalRowFilter(\n          rowFilter.map((f) =>\n            f.fieldName === column.name ? { ...f, value: newVal } : f,\n          ),\n        );\n      }\n      onChange?.(newRow);\n\n      if (columnName in errors) {\n        setErrors(omitKeys(errors, [columnName]));\n      }\n\n      return newRow;\n    },\n    [\n      columnMap,\n      confirmUpdates,\n      errors,\n      mode,\n      onChange,\n      onSuccess,\n      rowFilter,\n      setErrors,\n      setLocalRowFilter,\n      parseError,\n    ],\n  );\n\n  const [newRowDataHandler] = useState(\n    new NewRowDataHandler(undefined, {\n      onChange: onSetColumnData,\n      onChanged: setNewRowData,\n    }),\n  );\n  newRowDataHandler.setHandlers({\n    onChange: onSetColumnData,\n    onChanged: (newRow) => setNewRowData({ ...newRow }),\n  });\n\n  const [newRow, setNewRow] = useState<AnyObject>();\n  useEffect(() => {\n    const newRow = newRowDataHandler.getRow();\n    setNewRow(newRow);\n    setErrors({});\n    setError(undefined);\n  }, [newRowData, newRowDataHandler]);\n\n  const maybeClonedRow = mode?.type === \"insert\" ? mode.clonedRow : undefined;\n  const clonedRow = useMemo(() => {\n    if (!maybeClonedRow) return;\n    const insertableFields = displayedColumns\n      .filter((c) => c.insert)\n      .map((c) => c.name);\n    return pickKeys(maybeClonedRow, insertableFields);\n  }, [displayedColumns, maybeClonedRow]);\n\n  useEffect(() => {\n    if (mode?.type !== \"insert\") {\n      return;\n    }\n    /** Set existing data from parentForm */\n    if (parentForm?.type === \"insert\") {\n      const existingData = parentForm.newRowDataHandler?.getNewRow();\n      if (existingData) {\n        newRowDataHandler.setNewRow(existingData);\n        return;\n      }\n    }\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n    const defaultColumnData = Object.fromEntries(\n      columns\n        .map((c) => {\n          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n          const value = parseDefaultValue(c, undefined, false);\n          if (value === undefined) return;\n          // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n          return [c.name, value];\n        })\n        .filter(isDefined),\n    );\n    const newRowWithDefaultData = Object.fromEntries(\n      Object.entries({\n        ...defaultColumnData,\n        ...clonedRow,\n        ...defaultData,\n        ...fixedData,\n      }).map(([key, value]) => [\n        key,\n        { type: \"column\", value } satisfies ColumnData,\n      ]),\n    );\n    newRowDataHandler.setNewRow(newRowWithDefaultData);\n  }, [\n    mode?.type,\n    columns,\n    fixedData,\n    defaultData,\n    clonedRow,\n    newRowDataHandler,\n    parentForm,\n  ]);\n\n  const row = useMemo(() => {\n    if (!mode) return {};\n    if (mode.type === \"insert\") {\n      return {\n        ...mode.clonedRow,\n        ...newRow,\n        ...fixedData,\n      };\n    }\n    if (mode.type === \"view\") {\n      return {\n        ...mode.currentRow,\n      };\n    }\n    return {\n      ...(mode.type === \"update\" ? mode.currentRow : {}),\n      ...newRow,\n      ...fixedData,\n    };\n  }, [newRow, mode, fixedData]);\n\n  const getErrors: getErrorsHook = useCallback(\n    async (cb) => {\n      const cannotBeNullMessage = \"Must not be empty\";\n      const data = {\n        ...newRow,\n        ...fixedData,\n      };\n      let _errors: AnyObject | undefined;\n\n      const tableInfo = table?.info;\n\n      displayedColumns\n        .filter((c) => c.insert || c.update)\n        .forEach((c) => {\n          const val = data[c.name];\n\n          /* Check against not null rules */\n          if (!c.is_nullable) {\n            const isNull = (v) => [undefined, null].includes(v);\n\n            const willInsertMedia =\n              tableInfo?.hasFiles &&\n              tableInfo.fileTableName &&\n              c.references?.some((r) => r.ftable === tableInfo.fileTableName) &&\n              data[tableInfo.fileTableName]?.length;\n            if (\n              /* If it's an insert then ensure all non nullable cols are filled */\n              (!rowFilter &&\n                isNull(val) &&\n                !c.has_default &&\n                !willInsertMedia) ||\n              /* If update then ensure not updating non nullable with null  */\n              val === null\n            ) {\n              _errors ??= {};\n              _errors[c.name] = cannotBeNullMessage;\n            }\n          }\n\n          /** Ensure json fields are not string */\n          if (c.udt_name.startsWith(\"json\") && typeof val === \"string\") {\n            try {\n              data[c.name] = JSON.parse(val);\n            } catch (error) {\n              _errors ??= {};\n              _errors[c.name] = \"Must be a valid json\";\n            }\n          }\n        });\n\n      table?.info.requiredNestedInserts?.forEach(\n        ({ ftable, maxRows, minRows }) => {\n          const ftableData = data[ftable];\n          if (!ftableData || !Array.isArray(ftableData) || !ftableData.length) {\n            _errors ??= {};\n            _errors[ftable] = \"Required\";\n          } else if (minRows && ftableData.length < minRows) {\n            _errors ??= {};\n            _errors[ftable] = `Must have at least ${minRows} rows`;\n          } else if (maxRows && ftableData.length > maxRows) {\n            _errors ??= {};\n            _errors[ftable] = `Must have at most ${maxRows} rows`;\n          }\n        },\n      );\n\n      if (!_errors) {\n        const errors = await cb(data);\n\n        if (errors && typeof errors !== \"string\" && !isEmpty(errors)) {\n          setErrors(errors);\n        }\n      } else {\n        setErrors(_errors);\n      }\n    },\n    [fixedData, rowFilter, table, newRow, displayedColumns],\n  );\n\n  return {\n    error,\n    errors,\n    setError,\n    setErrors,\n    parseError,\n    getErrors,\n    newRowDataHandler,\n    newRowData,\n    newRow,\n    row,\n  };\n};\n\nexport type SmartFormNewRowState = ReturnType<typeof useNewRowDataHandler>;\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/useSmartForm.ts",
    "content": "import { type TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { type AnyObject } from \"prostgles-types\";\nimport { useMemo, useState } from \"react\";\nimport type { DBSchemaTableWJoins } from \"../Dashboard/dashboardUtils\";\nimport type { SmartFormProps } from \"./SmartForm\";\nimport {\n  useNewRowDataHandler,\n  type SmartFormNewRowState,\n} from \"./useNewRowDataHandler\";\nimport {\n  useSmartFormColumns,\n  type SmartFormColumnState,\n} from \"./useSmartFormColumns\";\nimport { useSmartFormMode, type SmartFormModeState } from \"./useSmartFormMode\";\n\nexport type SmartFormState = {\n  table: DBSchemaTableWJoins;\n} & SmartFormModeState &\n  SmartFormColumnState &\n  SmartFormNewRowState;\nexport const useSmartForm = (props: SmartFormProps) => {\n  const { db, tables, tableName } = props;\n\n  const table = useMemo(() => {\n    return tables.find((t) => t.name === tableName);\n  }, [tables, tableName]);\n\n  const tableHandler = db[tableName] as Partial<TableHandlerClient>;\n\n  const modeResult = useSmartFormMode({ ...props, table });\n\n  const { columns, displayedColumns } = useSmartFormColumns({\n    ...props,\n    table,\n    ...modeResult,\n  });\n\n  const [referencedInsertData, setReferencedInsertData] = useState<AnyObject>(\n    {},\n  );\n\n  const mediaTableInfo = useMemo(() => {\n    const tableInfo = table?.info;\n    if (tableInfo?.hasFiles && tableInfo.fileTableName) {\n      return tables.find((t) => t.info.isFileTable)?.info;\n    }\n  }, [table, tables]);\n\n  const newRowState = useNewRowDataHandler({\n    ...modeResult,\n    ...props,\n    displayedColumns,\n    columns,\n    table,\n  });\n\n  return {\n    ...modeResult,\n    ...newRowState,\n    table,\n    tableHandler,\n    referencedInsertData,\n    setReferencedInsertData,\n    columns,\n    displayedColumns,\n    mediaTableInfo,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/useSmartFormColumns.ts",
    "content": "import { usePromise } from \"prostgles-client\";\nimport { quickClone } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport { getKeys, isDefined } from \"prostgles-types\";\nimport { useMemo } from \"react\";\nimport type { DBSchemaTableWJoins } from \"../Dashboard/dashboardUtils\";\nimport type { SmartFormProps } from \"./SmartForm\";\nimport type { SmartColumnInfo } from \"./SmartFormField/SmartFormField\";\nimport type { useSmartFormMode } from \"./useSmartFormMode\";\n\ntype UseSmartFormColumnsProps = Pick<\n  SmartFormProps,\n  \"fixedData\" | \"columnFilter\" | \"columns\" | \"hideNonUpdateableColumns\" | \"lang\"\n> &\n  Pick<ReturnType<typeof useSmartFormMode>, \"mode\"> & {\n    table: DBSchemaTableWJoins | undefined;\n  };\nexport const useSmartFormColumns = (props: UseSmartFormColumnsProps) => {\n  const {\n    fixedData,\n    columns: columnsConfig,\n    columnFilter,\n    table,\n    mode,\n    lang,\n    hideNonUpdateableColumns,\n  } = props;\n\n  const dynamicValidatedColumns = usePromise(async () => {\n    if (!mode) return undefined;\n    /** TODO: merge with display_options?.prettyTableAndColumnNames */\n    const result =\n      mode.type !== \"update\" ?\n        table?.columns\n      : (\n          await mode.tableHandlerGetColumns(lang, {\n            rule: \"update\",\n            filter: mode.rowFilterObj,\n          })\n        )\n          .map((vc) => {\n            const col = table?.columns.find((c) => c.name === vc.name);\n            if (!col) return undefined;\n            return {\n              ...col,\n              ...vc,\n            };\n          })\n          .filter(isDefined);\n\n    const invalidColumns =\n      columnsConfig &&\n      result &&\n      Object.keys(columnsConfig).filter(\n        (colName) => !result.some((_c) => _c.name === colName),\n      );\n    const warning =\n      invalidColumns?.length ?\n        \"Some requested columns not found in the table: \" +\n        JSON.stringify(invalidColumns)\n      : undefined;\n    if (warning) {\n      console.error(warning);\n    }\n    return result;\n  }, [mode, table?.columns, lang, columnsConfig]);\n\n  const smartCols: SmartColumnInfo[] = useMemo(() => {\n    if (!mode) return [];\n    let validatedCols = quickClone(dynamicValidatedColumns || []);\n    if (fixedData) {\n      const fixedFields = getKeys(fixedData);\n      validatedCols = validatedCols.map((c) => ({\n        ...c,\n        insert: fixedFields.includes(c.name) ? false : c.insert,\n      }));\n    }\n    let displayedCols = validatedCols as SmartColumnInfo[];\n\n    if (columnsConfig) {\n      /** Add headers */\n      displayedCols = Object.entries(columnsConfig)\n        .map(([colName, colConf]) => {\n          if (colConf === 1) {\n            return validatedCols.find((c) => c.name === colName);\n          } else {\n            const ec = validatedCols.find((c) => c.name === colName);\n            if (!ec) return undefined;\n\n            return { ...ec, ...colConf };\n          }\n        })\n        .filter(isDefined);\n    } else if (columnFilter) {\n      displayedCols = displayedCols.filter(columnFilter);\n    }\n\n    if (mode.type === \"multiUpdate\") {\n      displayedCols = displayedCols.filter((c) => c.update);\n    }\n\n    return displayedCols;\n  }, [columnsConfig, columnFilter, dynamicValidatedColumns, mode, fixedData]);\n\n  const modeType = mode?.type;\n  const displayedColumns = useMemo(() => {\n    if (table?.info.isFileTable && modeType === \"insert\") {\n      return [];\n    }\n\n    const validatedCols = smartCols.slice(0);\n    const displayedCols = validatedCols;\n\n    return displayedCols.filter(\n      (c) =>\n        (modeType === \"view\" && c.select) ||\n        ((modeType === \"update\" || modeType === \"multiUpdate\") &&\n          (c.update || (!hideNonUpdateableColumns && c.select))) ||\n        (modeType === \"insert\" && c.insert),\n    );\n  }, [smartCols, modeType, hideNonUpdateableColumns, table?.info.isFileTable]);\n\n  return {\n    columns: smartCols,\n    displayedColumns,\n  };\n};\nexport type SmartFormColumnState = ReturnType<typeof useSmartFormColumns>;\n"
  },
  {
    "path": "client/src/dashboard/SmartForm/useSmartFormMode.ts",
    "content": "import {\n  useAsyncEffectQueue,\n  useEffectDeep,\n  type TableHandlerClient,\n} from \"prostgles-client\";\nimport { type AnyObject } from \"prostgles-types\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n  getSmartGroupFilter,\n  type DetailedFilterBase,\n} from \"@common/filterUtils\";\nimport type { DBSchemaTableWJoins } from \"../Dashboard/dashboardUtils\";\nimport type { SmartFormProps } from \"./SmartForm\";\n\nexport type SmartFormMode =\n  | {\n      type: \"view\" | \"update\";\n      currentRow: AnyObject | undefined;\n      loading: boolean;\n      rowFilter: DetailedFilterBase[];\n      rowFilterObj: AnyObject;\n      select: AnyObject;\n      clone: VoidFunction | undefined;\n      tableHandlerFindOne: TableHandlerClient[\"findOne\"];\n      tableHandlerSubscribeOne: undefined | TableHandlerClient[\"subscribeOne\"];\n      tableHandlerUpdate: undefined | TableHandlerClient[\"update\"];\n      tableHandlerDelete: undefined | TableHandlerClient[\"delete\"];\n      tableHandlerGetColumns: TableHandlerClient[\"getColumns\"];\n    }\n  | {\n      type: \"multiUpdate\";\n      rowFilter: DetailedFilterBase[];\n      rowFilterObj: AnyObject;\n      tableHandlerUpdate: undefined | TableHandlerClient[\"update\"];\n      tableHandlerDelete: undefined | TableHandlerClient[\"delete\"];\n    }\n  | ({\n      type: \"insert\";\n      tableHandlerInsert: TableHandlerClient[\"insert\"] | undefined;\n    } & (\n      | {\n          clonedRow: AnyObject;\n          cancelClone: VoidFunction;\n        }\n      | {\n          clonedRow?: undefined;\n          cancelClone?: undefined;\n        }\n    ))\n  | {\n      type: \"manual\";\n    };\n\ntype ModeOrError = SmartFormMode | string;\n\nexport type SmartFormModeState = Omit<\n  ReturnType<typeof useSmartFormMode>,\n  \"mode\"\n> & {\n  mode: SmartFormMode;\n  modeType: \"view\" | \"update\" | \"insert\" | undefined;\n};\nexport const useSmartFormMode = (\n  props: Pick<\n    SmartFormProps,\n    \"tableName\" | \"db\" | \"rowFilter\" | \"onChange\" | \"parentForm\" | \"onLoaded\"\n  > & {\n    table: DBSchemaTableWJoins | undefined;\n  },\n) => {\n  const { tableName, rowFilter, db, table, onChange, parentForm, onLoaded } =\n    props;\n  const [loading, setLoading] = useState(false);\n  const tableHandler = db[tableName] as Partial<TableHandlerClient> | undefined;\n  const tableInfo = table?.info;\n  const [localRowFilter, setLocalRowFilter] = useState(rowFilter);\n  useEffectDeep(() => {\n    setLocalRowFilter(rowFilter);\n  }, [rowFilter, tableName]);\n  const [clonedRow, setClonedRow] = useState<AnyObject>();\n  const [currentRowInfo, setCurrentRowInfo] = useState<{\n    row: AnyObject;\n    tableName: string;\n  }>();\n\n  useEffect(() => {\n    if (currentRowInfo) {\n      onLoaded?.();\n    }\n  }, [currentRowInfo, onLoaded]);\n\n  const tableHandlerUpdate = tableHandler?.update;\n  const tableHandlerDelete = tableHandler?.delete;\n  const tableHandlerInsert = tableHandler?.insert;\n  const tableHandlerGetInfo = tableHandler?.getInfo;\n  const tableHandlerFindOne = tableHandler?.findOne;\n  const tableHandlerSubscribeOne = tableHandler?.subscribeOne;\n  const tableHandlerGetColumns = tableHandler?.getColumns;\n  const isManuallyControlled = Boolean(onChange || parentForm);\n  const activeRowFilter = localRowFilter || rowFilter;\n\n  const modeOrError: ModeOrError = useMemo(() => {\n    if (!tableHandlerGetInfo || !tableHandlerGetColumns || !tableInfo) {\n      return (\"Table getInfo/getColumns hooks not available/published: \" +\n        tableName) satisfies ModeOrError;\n    }\n    if (rowFilter) {\n      if (clonedRow) {\n        if (!tableHandlerInsert) return \"Not allowed to insert\";\n        return {\n          type: \"insert\",\n          clonedRow,\n          tableHandlerInsert,\n          cancelClone: () => {\n            setClonedRow(undefined);\n          },\n        } satisfies ModeOrError;\n      }\n      if (!rowFilter.length) {\n        if (!tableHandlerUpdate && !tableHandlerDelete) {\n          return \"Now allowed to update or delete records from this table\";\n        }\n        return {\n          type: \"multiUpdate\",\n          tableHandlerDelete,\n          tableHandlerUpdate,\n          rowFilter: [],\n          rowFilterObj: {},\n        } satisfies ModeOrError;\n      }\n\n      if (!tableHandlerFindOne) {\n        return \"Not allowed to view data\";\n      }\n\n      const currentRow =\n        currentRowInfo?.tableName === tableName ?\n          currentRowInfo.row\n        : undefined;\n      const select = { \"*\": 1 } as const;\n\n      if (\n        tableInfo.fileTableName &&\n        tableInfo.fileTableName !== tableName &&\n        tableInfo.hasFiles &&\n        db[tableInfo.fileTableName]?.find\n      ) {\n        select[tableInfo.fileTableName] = \"*\";\n      }\n\n      return {\n        type: tableHandlerUpdate ? \"update\" : \"view\",\n        clone:\n          tableHandlerInsert && currentRow ?\n            () => setClonedRow(currentRow)\n          : undefined,\n        currentRow,\n        rowFilter: activeRowFilter!,\n        rowFilterObj: getSmartGroupFilter(activeRowFilter),\n        select,\n        loading: !currentRow,\n        tableHandlerFindOne,\n        tableHandlerSubscribeOne,\n        tableHandlerDelete,\n        tableHandlerUpdate,\n        tableHandlerGetColumns,\n      } satisfies ModeOrError;\n    } else {\n      if (isManuallyControlled) {\n        return {\n          type: \"insert\",\n          tableHandlerInsert,\n        } satisfies ModeOrError;\n      }\n      if (!tableHandlerInsert) {\n        return \"Cannot insert. Check longs\";\n      }\n      return {\n        type: \"insert\",\n        tableHandlerInsert,\n      } satisfies ModeOrError;\n    }\n    //@ts-ignore\n  }, [\n    tableInfo,\n    rowFilter,\n    tableName,\n    isManuallyControlled,\n    db,\n    tableHandlerFindOne,\n    tableHandlerGetInfo,\n    tableHandlerSubscribeOne,\n    tableHandlerGetColumns,\n    tableHandlerUpdate,\n    tableHandlerDelete,\n    tableHandlerInsert,\n    clonedRow,\n    activeRowFilter,\n    currentRowInfo,\n  ]);\n\n  const mode = typeof modeOrError === \"string\" ? undefined : modeOrError;\n  const select =\n    mode?.type === \"view\" || mode?.type === \"update\" ? mode.select : undefined;\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useAsyncEffectQueue(async () => {\n    if (!select) return;\n    const filter = getSmartGroupFilter(activeRowFilter);\n    if (tableHandlerSubscribeOne) {\n      const sub = await tableHandlerSubscribeOne(filter, { select }, (row) => {\n        row &&\n          setCurrentRowInfo({\n            row,\n            tableName,\n          });\n      });\n      return sub.unsubscribe;\n    } else if (tableHandlerFindOne) {\n      const row = await tableHandlerFindOne(filter, {\n        select,\n      });\n      row &&\n        setCurrentRowInfo({\n          row,\n          tableName,\n        });\n    }\n  }, [\n    select,\n    tableName,\n    activeRowFilter,\n    tableHandlerFindOne,\n    tableHandlerSubscribeOne,\n  ]);\n\n  const error = typeof modeOrError === \"string\" ? modeOrError : undefined;\n  return {\n    mode,\n    modeType: mode?.type === \"multiUpdate\" ? \"update\" : mode?.type,\n    error,\n    setLocalRowFilter,\n    localRowFilter,\n    loading,\n    setLoading,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartSelect.tsx",
    "content": "import { mdiAlertCircleOutline, mdiPencil, mdiPlus } from \"@mdi/js\";\nimport type { TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { isDefined } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { TestSelectors } from \"../Testing\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport type { LabelProps } from \"@components/Label\";\nimport { Label } from \"@components/Label\";\nimport Loading from \"@components/Loader/Loading\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport {\n  SearchList,\n  type SearchListItemContent,\n  type SearchListItem,\n} from \"@components/SearchList/SearchList\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { useIsMounted } from \"prostgles-client\";\n\ntype SmartSelectProps<\n  THandler extends TableHandlerClient = TableHandlerClient,\n> = {\n  popupTitle?: string;\n  label?: LabelProps;\n  values: string[];\n  tableHandler: THandler;\n  filter?: Parameters<THandler[\"count\"]>[0];\n  onChange: (newValues: string[]) => void;\n  /**\n   * Must be unique */\n  fieldName: string;\n  displayField?: string;\n  getLabel?: (\n    value: string,\n  ) => Pick<Partial<SearchListItem>, \"subLabel\" | \"disabledInfo\"> &\n    Partial<SearchListItemContent>;\n  disabledInfo?: string;\n  allowCreate?: boolean;\n  placeholder?: string;\n} & TestSelectors;\n\nexport const SmartSelect = <\n  THandler extends TableHandlerClient = TableHandlerClient,\n>(\n  props: SmartSelectProps<THandler>,\n) => {\n  const {\n    values,\n    tableHandler: tableHandlerRaw,\n    placeholder,\n    onChange,\n    fieldName,\n    getLabel,\n    popupTitle: title,\n    label,\n    displayField,\n    filter = {},\n    disabledInfo,\n    allowCreate = true,\n    id,\n  } = props;\n  const limit = 21;\n  const tableHandler = tableHandlerRaw as Partial<typeof tableHandlerRaw>;\n  const { data: rows } = tableHandler.useSubscribe!(filter, {\n    limit,\n    select: [fieldName, displayField].filter(isDefined),\n    groupBy: true,\n  });\n  const items = rows?.map((r) => ({\n    key: r[fieldName],\n    label: displayField ? r[displayField] : r[fieldName],\n  }));\n  const displayValues = values\n    .map((value) => items?.find((d) => value === d.key)?.label)\n    .filter(isDefined);\n  const [noExactSearchMatch, setNoExactSearchMatch] = useState<\n    string | undefined\n  >();\n  const getIsMounted = useIsMounted();\n\n  if (!items?.length) return null;\n\n  return (\n    <PopupMenu\n      title={title}\n      positioning=\"beneath-left\"\n      button={\n        <FlexCol\n          id={id}\n          className={`gap-p5 ${disabledInfo ? \"disabled\" : \"\"}`}\n          title={disabledInfo}\n          onClick={disabledInfo ? (e) => e.stopPropagation() : undefined}\n        >\n          {label && <Label {...label} onClick={(e) => e.stopPropagation()} />}\n\n          <FlexRowWrap className=\"SmartSelect relative  ai-center\">\n            {!items.length && <Loading variant=\"cover\" />}\n            <Values values={displayValues} />\n\n            {values.length === 1 && values[0] === \"admin\" ?\n              <InfoRow variant=\"naked\" color=\"warning\">\n                <Icon path={mdiAlertCircleOutline} size={1} />\n                <div>\n                  There are no unnassigned user types left. Update current\n                  access rules to free up user types or create new user types\n                </div>\n              </InfoRow>\n            : <div className=\"flex-row gap-1\">\n                <Btn\n                  key={values.length.toString()}\n                  data-command={props[\"data-command\"]}\n                  iconPath={!values.length ? mdiPlus : mdiPencil}\n                  variant={!values.length ? \"filled\" : \"icon\"}\n                  size={!values.length ? \"small\" : undefined}\n                  // data-command=\"SmartSelect.Edit\"\n                  color=\"action\"\n                />\n              </div>\n            }\n          </FlexRowWrap>\n        </FlexCol>\n      }\n      footerButtons={[\n        !noExactSearchMatch || !tableHandler.insert || !allowCreate ?\n          undefined\n        : {\n            label: \"Create\",\n            variant: \"outline\",\n            iconPath: mdiPlus,\n            color: \"action\",\n            onClickMessage: async (_, setM) => {\n              setM({ loading: 1 });\n              await tableHandler.insert!({\n                ...filter,\n                [displayField ?? fieldName]: noExactSearchMatch,\n              }).catch((err) => setM({ err }));\n              setM({ ok: \"Created!\" }, () => {\n                if (!getIsMounted()) return;\n                setNoExactSearchMatch(undefined);\n              });\n            },\n          },\n        {\n          label: \"Done\",\n          variant: \"filled\",\n          color: \"action\",\n          onClickClose: true,\n          \"data-command\": \"SmartSelect.Done\",\n          className: \"ml-auto\",\n        },\n      ]}\n      onClickClose={false}\n      contentStyle={{\n        padding: \"1em\",\n      }}\n    >\n      <SearchList\n        onMultiToggle={(items) => {\n          onChange(items.filter((d) => d.checked).map((d) => d.key as string));\n        }}\n        style={{\n          maxHeight: \"500px\",\n          background: \"var(--bg-popup)\",\n        }}\n        placeholder={placeholder}\n        onType={(sTerm) => {\n          const newNoExactSearchMatch =\n            sTerm && items.some(({ label }) => label === sTerm) ?\n              undefined\n            : sTerm;\n          if (noExactSearchMatch !== newNoExactSearchMatch) {\n            setNoExactSearchMatch(newNoExactSearchMatch);\n          }\n        }}\n        autoFocus={true}\n        noSearchLimit={0}\n        items={items\n          .map(({ key, label }) => {\n            return {\n              key,\n              label,\n              checked: values.includes(key),\n              disabledInfo: undefined,\n              ...getLabel?.(key),\n              onPress: () => {\n                onChange(\n                  values.includes(key) ?\n                    values.filter((v) => v !== key)\n                  : values.concat([key]),\n                );\n              },\n            };\n          })\n          .sort((a, b) => +!!a.disabledInfo - +!!b.disabledInfo)}\n        endOfResultsContent={\n          <div className=\"flex-row ai-center\">\n            <div className=\"p-p5\">Not found</div>\n          </div>\n        }\n      />\n    </PopupMenu>\n  );\n};\n\nconst Values = ({ values }: Pick<SmartSelectProps, \"values\">) => {\n  if (!values.length) return null;\n\n  return (\n    <div className=\"flex-row-wrap gap-p5\">\n      {values.map((value) => (\n        <Chip\n          key={value}\n          value={value}\n          variant=\"outline\"\n          style={{\n            padding: \"8px\",\n            // background: `var(--blue-100)`\n          }}\n        />\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/SmartTable.tsx",
    "content": "import {\n  getSmartGroupFilter,\n  type DetailedFilter,\n  type DetailedFilterBase,\n} from \"@common/filterUtils\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { PaginationProps } from \"@components/Table/Pagination\";\nimport { Table } from \"@components/Table/Table\";\nimport { type AnyObject, type SubscriptionHandler } from \"prostgles-types\";\nimport React from \"react\";\nimport type { Prgl } from \"../App\";\nimport { quickClone } from \"../utils/utils\";\nimport RTComp from \"./RTComp\";\nimport { SmartFilterBar } from \"./SmartFilterBar/SmartFilterBar\";\nimport { SmartForm } from \"./SmartForm/SmartForm\";\nimport { isNumericColumn } from \"./W_SQL/getSQLResultTableColumns\";\nimport type { ColumnSort } from \"./W_Table/ColumnMenu/ColumnMenu\";\nimport { getEditColumn } from \"./W_Table/tableUtils/getEditColumn\";\nimport { onRenderColumn } from \"./W_Table/tableUtils/onRenderColumn\";\nimport type { ProstglesColumn } from \"./W_Table/W_Table\";\n\ntype SmartTableProps = Pick<Prgl, \"db\" | \"tables\" | \"methods\"> & {\n  filter?: DetailedFilter[];\n  tableName: string;\n  tableCols?: ProstglesColumn[];\n  onClosePopup?: () => void;\n  onClickRow?: (row?: AnyObject) => void;\n  title?:\n    | React.ReactNode\n    | ((dataCounts: {\n        totalRows: number;\n        filteredRows: number;\n      }) => React.ReactNode);\n  titlePrefix?: string;\n  showInsert?: boolean;\n  allowEdit?: boolean;\n  className?: string;\n  noDataComponent?: React.ReactNode;\n  onFilterChange?: (filter: DetailedFilter[]) => void;\n  filterOperand?: \"and\" | \"or\";\n  realtime?: { throttle?: number };\n};\n\ntype S = {\n  error?: any;\n  rows: AnyObject[];\n  sort: ColumnSort[];\n  filter?: DetailedFilter[];\n  editRowFilter?: DetailedFilterBase[];\n  loadedData: boolean;\n  filteredRows: number;\n  columns?: ProstglesColumn[];\n} & Pick<Required<PaginationProps>, \"page\" | \"pageSize\" | \"totalRows\">;\n\nexport default class SmartTable extends RTComp<SmartTableProps, S> {\n  state: S = {\n    rows: [],\n    page: 0,\n    pageSize: 25,\n    totalRows: 0,\n    filteredRows: 0,\n    sort: [],\n    loadedData: false,\n  };\n\n  realtimeOpt?: {\n    filter: AnyObject;\n    realtime: SmartTableProps[\"realtime\"];\n  };\n  realtime?: {\n    filter?: AnyObject;\n    sub: SubscriptionHandler;\n  };\n\n  get columns(): ProstglesColumn[] {\n    if (this.state.columns) return this.state.columns;\n\n    const { tableName, db, tableCols, tables, allowEdit = true } = this.props;\n    const tableHandler = db[tableName];\n    let _tableCols = tableCols ?? [];\n    if (!tableCols) {\n      const onClickEditRow = (editRowFilter) => {\n        this.setState({ editRowFilter });\n      };\n      const table = tables.find((t) => t.name === tableName);\n      const cols = table?.columns ?? [];\n      _tableCols = cols\n        .filter((c) => c.select)\n        .map((c) => {\n          const isNumeric = isNumericColumn(c);\n          return {\n            key: c.name,\n            sortable: true,\n            subLabel: c.data_type,\n            ...c,\n            /* Align numbers to right for an easier read */\n            headerClassname: isNumeric ? \" jc-end  \" : \" \",\n            className: isNumeric ? \" ta-right \" : \" \",\n            onRender: onRenderColumn({\n              c,\n              table,\n              tables,\n              barchartVals: undefined,\n              getValues: () => {\n                return this.state.rows.map((r) => r[c.name]);\n              },\n            }),\n          };\n        });\n\n      if (allowEdit && tableHandler && table) {\n        _tableCols.unshift(\n          getEditColumn({\n            table,\n            columnConfig: cols,\n            tableHandler: tableHandler as any,\n            onClickRow: onClickEditRow,\n          }),\n        );\n      }\n    }\n\n    return _tableCols;\n  }\n\n  onMount() {\n    this.getData();\n  }\n\n  async onUnmount() {\n    await this.realtime?.sub.unsubscribe();\n  }\n\n  loading = true;\n  onDelta(deltaP: Partial<SmartTableProps> | undefined): void {\n    const { filter = {}, tableName, db, realtime } = this.props;\n\n    void (async () => {\n      const tableHandler = db[tableName];\n      if (\n        tableHandler?.subscribe &&\n        (JSON.stringify(realtime) !==\n          JSON.stringify(this.realtimeOpt?.realtime) ||\n          JSON.stringify(filter) !== JSON.stringify(this.realtimeOpt?.filter))\n      ) {\n        this.realtimeOpt = quickClone({ filter, realtime });\n        await this.realtime?.sub.unsubscribe();\n        this.realtime = realtime && {\n          sub: await tableHandler.subscribe(\n            filter,\n            {\n              select: \"\",\n              limit: 0,\n              throttle: this.props.realtime?.throttle ?? 100,\n            },\n            () => {\n              void this.getData();\n            },\n          ),\n          filter,\n        };\n      } else if (deltaP?.filter) {\n        void this.getData();\n      }\n    })();\n  }\n\n  get filter() {\n    return this.props.filter ?? this.state.filter ?? [];\n  }\n\n  getData = async (\n    filter: DetailedFilter[] = this.filter,\n    sort: ColumnSort[] = this.state.sort,\n    page: number = this.state.page,\n    pageSize: PaginationProps[\"pageSize\"] = this.state.pageSize,\n  ) => {\n    try {\n      const { tableName, db } = this.props;\n      const tableHandler = db[tableName];\n      if (!tableHandler) return;\n\n      const _filter = getSmartGroupFilter(\n        filter,\n        undefined,\n        this.props.filterOperand,\n      );\n      const totalRows = await tableHandler.count!();\n      const filteredRows = await tableHandler.count!(_filter);\n      const rows = await tableHandler.find!(_filter, {\n        limit: pageSize,\n        orderBy: sort,\n        offset: page * pageSize,\n      });\n      this.setState({\n        rows,\n        filter,\n        sort,\n        page,\n        pageSize,\n        totalRows,\n        filteredRows,\n        loadedData: true,\n        error: undefined,\n      });\n    } catch (error) {\n      this.setState({ error, loadedData: true });\n    }\n  };\n\n  render() {\n    const {\n      tableName,\n      db,\n      tables,\n      onClickRow,\n      onClosePopup,\n      className,\n      noDataComponent,\n      titlePrefix,\n      title,\n    } = this.props;\n    const {\n      filter,\n      rows,\n      sort,\n      page,\n      filteredRows,\n      totalRows,\n      editRowFilter,\n      loadedData,\n      error,\n    } = this.state;\n    const titleNode =\n      typeof title === \"function\" ?\n        title({ filteredRows, totalRows })\n      : (title ?? (\n          <span className=\"text-1 px-1 py-p5\">\n            {titlePrefix ?? tableName}\n            <span>{` (${filteredRows.toLocaleString()}/${totalRows.toLocaleString()})`}</span>\n          </span>\n        ));\n\n    if (error) {\n      return <ErrorComponent error={error} />;\n    }\n\n    if (!loadedData) {\n      return <Loading />;\n    }\n\n    if (noDataComponent && !this.state.filter?.length && !rows.length) {\n      return noDataComponent;\n    }\n\n    const tableCols = this.columns.slice(0);\n\n    const content = (\n      <FlexCol\n        className={\n          \"gap-0 f-1 min-h-0 relative \" + (onClosePopup ? \"\" : className)\n        }\n      >\n        {!onClosePopup && titleNode}\n        {editRowFilter && (\n          <SmartForm\n            asPopup={true}\n            confirmUpdates={true}\n            db={db}\n            methods={this.props.methods}\n            tables={tables}\n            tableName={tableName}\n            rowFilter={editRowFilter}\n            onSuccess={() => {\n              void this.getData();\n            }}\n            onClose={() => {\n              this.setState({ editRowFilter: undefined });\n            }}\n          />\n        )}\n\n        <SmartFilterBar\n          className=\"p-1 bg-color-2 min-h-fit\"\n          rowCount={totalRows}\n          db={db}\n          methods={this.props.methods}\n          table_name={tableName}\n          tables={tables}\n          filter={filter}\n          onChange={(filter) => {\n            this.props.onFilterChange?.(filter);\n            this.getData(filter);\n          }}\n          onHavingChange={() => {\n            console.warn(\"Having change not implemented\");\n          }}\n          onSortChange={undefined}\n          hideSort={true}\n          showInsertUpdateDelete={{\n            onSuccess: () => this.getData(),\n          }}\n        />\n        <Table\n          rows={rows}\n          cols={tableCols}\n          className={\"pb -1 \"}\n          onRowClick={onClickRow}\n          sort={sort}\n          onSort={(sort) => {\n            this.getData(undefined, sort);\n          }}\n          onColumnReorder={(newCols) => {\n            const nIdxes = newCols\n              .filter((c) => !(c.computed && c.key === \"edit_row\"))\n              .map((c) => c.name);\n            this.setState({\n              columns: tableCols\n                .slice(0)\n                .sort(\n                  (a, b) => nIdxes.indexOf(a.name) - nIdxes.indexOf(b.name),\n                ),\n            });\n          }}\n          pagination={{\n            page,\n            pageSize: 10,\n            totalRows,\n            onPageChange: (page) => {\n              this.getData(undefined, undefined, page);\n            },\n            onPageSizeChange: (pageSize) => {\n              this.getData(undefined, undefined, undefined, pageSize);\n            },\n          }}\n        />\n      </FlexCol>\n    );\n\n    if (!onClosePopup) return content;\n\n    return (\n      <Popup\n        title={titleNode}\n        positioning=\"right-panel\"\n        onClose={onClosePopup}\n        contentStyle={{\n          maxWidth: \"calc(100vw - 20px)\",\n          padding: 0,\n        }}\n        contentClassName={className}\n      >\n        {content}\n      </Popup>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/StatusMonitor/StatusMonitor.tsx",
    "content": "import React, { useState } from \"react\";\nimport type { PrglState } from \"../../App\";\nimport { FlexCol } from \"@components/Flex\";\nimport type { DBSMethods } from \"../Dashboard/DBS\";\nimport { StatusMonitorInfoHeader } from \"./StatusMonitorInfoHeader/StatusMonitorInfoHeader\";\nimport { StatusMonitorProcList } from \"./StatusMonitorProcList\";\n\nexport type StatusMonitorProps = Pick<\n  PrglState,\n  \"dbs\" | \"dbsMethods\" | \"dbsTables\"\n> & {\n  connectionId: string;\n  getStatus: Required<DBSMethods>[\"getStatus\"];\n  runConnectionQuery: Required<DBSMethods>[\"runConnectionQuery\"];\n};\n\nexport const StatusMonitor = (props: StatusMonitorProps) => {\n  const [samplingRate, setSamplingRate] = useState(0.5);\n  const [statusError, setStatusError] = useState<any>();\n  const [noBash, setNoBash] = useState(false);\n\n  // const [shellResult, setShellResult] = useState(\"\");\n  // const setShell = async (v: string) => {\n  //   const res = await execPSQLBash(dbs.sql!, connectionId, v);\n  //   console.log(res);\n  //   setShellResult(res.join(\"\\n\"));\n  //   getPidStats(dbs.sql!, connectionId);\n  // }\n\n  return (\n    <FlexCol className=\"StatusMonitor w-fit min-w-0 jc-start \">\n      <StatusMonitorInfoHeader\n        {...props}\n        samplingRate={samplingRate}\n        statusError={statusError}\n        setStatusError={setStatusError}\n        setNoBash={setNoBash}\n        setSamplingRate={setSamplingRate}\n      />\n\n      <StatusMonitorProcList\n        {...props}\n        samplingRate={samplingRate}\n        noBash={noBash}\n      />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/StatusMonitor/StatusMonitorConnections.tsx",
    "content": "import { mdiFilter, mdiStopCircleOutline } from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport type { ConnectionStatus } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Table } from \"@components/Table/Table\";\nimport type { ProstglesColumn } from \"../W_SQL/W_SQL\";\nimport type { StatusMonitorProps } from \"./StatusMonitor\";\n\ntype P = Pick<StatusMonitorProps, \"dbsMethods\" | \"connectionId\"> & {\n  c: ConnectionStatus;\n  datidFilter: number | undefined;\n  onSetDatidFilter: (datid: number) => void;\n};\nexport const StatusMonitorConnections = ({\n  c,\n  dbsMethods,\n  connectionId,\n  onSetDatidFilter,\n  datidFilter,\n}: P) => {\n  const connectionsColumns: ProstglesColumn[] = useMemo(\n    () => [\n      {\n        key: \"kill-conneciton\",\n        label: \"\",\n        name: \"kill-conneciton\",\n        tsDataType: \"string\",\n        udt_name: \"text\",\n        filter: false,\n        computed: false,\n        sortable: false,\n        width: 60,\n        onRender: ({ row: { datid } }) => (\n          <Btn\n            title=\"Kill connection\"\n            iconPath={mdiStopCircleOutline}\n            color=\"danger\"\n            onClickPromise={async () => {\n              const query = `\n            SELECT *, pg_terminate_backend(pid)\n            FROM pg_stat_activity \n            WHERE pid <> pg_backend_pid()\n            AND datid = \\${datid};\n          `;\n              await dbsMethods.runConnectionQuery!(connectionId, query, {\n                datid,\n              });\n            }}\n          />\n        ),\n      },\n      {\n        key: \"show-conneciton-queries\",\n        label: \"\",\n        name: \"show-conneciton-queries\",\n        tsDataType: \"string\",\n        udt_name: \"text\",\n        filter: false,\n        computed: false,\n        sortable: false,\n        width: 60,\n        onRender: ({ row: { datid } }) => (\n          <Btn\n            title=\"Filter queries by this connection\"\n            iconPath={mdiFilter}\n            color=\"action\"\n            variant={datidFilter === datid ? \"filled\" : undefined}\n            onClick={() => {\n              onSetDatidFilter(datid);\n            }}\n          />\n        ),\n      },\n      ...Object.keys(c.connections[0] ?? {}).map(\n        (key) =>\n          ({\n            key,\n            name: key,\n            tsDataType: \"string\",\n            udt_name: \"text\",\n            filter: false,\n            sortable: false,\n            label: key,\n            computed: false,\n          }) satisfies ProstglesColumn,\n      ),\n    ],\n    [\n      c.connections,\n      connectionId,\n      datidFilter,\n      dbsMethods.runConnectionQuery,\n      onSetDatidFilter,\n    ],\n  );\n\n  const connNum = c.connections.length;\n  const maxConnNum = c.maxConnections;\n\n  return (\n    <PopupMenu\n      className=\"f-0\"\n      title=\"Connections\"\n      clickCatchStyle={{ opacity: 0.5 }}\n      onClickClose={false}\n      button={\n        <Chip\n          className=\"noselect pointer\"\n          label={\"Connections\"}\n          variant=\"header\"\n          color={(maxConnNum - connNum) / maxConnNum > 0.5 ? \"green\" : \"yellow\"}\n        >\n          {c.connections.length}/{c.maxConnections}\n        </Chip>\n      }\n    >\n      <Table cols={connectionsColumns} rows={c.connections} />\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/StatusMonitor/StatusMonitorInfoHeader/StatusMonitorInfoHeader.tsx",
    "content": "import type { ConnectionStatus } from \"@common/utils\";\nimport Chip from \"@components/Chip\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexRow } from \"@components/Flex\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport React, { useEffect, useState } from \"react\";\nimport { getServerCoreInfoStr } from \"../../../pages/Connections/useConnectionServersList\";\nimport { isEmpty } from \"../../../utils/utils\";\nimport type { StatusMonitorProps } from \"../StatusMonitor\";\nimport { StatusMonitorConnections } from \"../StatusMonitorConnections\";\nimport { StatusMonitorInfoHeaderCpu } from \"./StatusMonitorInfoHeaderCpu\";\nimport { StatusMonitorInfoHeaderMemory } from \"./StatusMonitorInfoHeaderMemory\";\nimport { useIsMounted } from \"prostgles-client\";\n\nexport const StatusMonitorInfoHeader = (\n  props: StatusMonitorProps & {\n    samplingRate: number;\n    statusError: any;\n    setStatusError: (e: any) => void;\n    setNoBash: (noBash: boolean) => void;\n    setSamplingRate: (rate: number) => void;\n  },\n) => {\n  const {\n    getStatus,\n    connectionId,\n    dbs,\n    dbsMethods,\n    samplingRate,\n    statusError,\n    setStatusError,\n    setNoBash,\n    setSamplingRate,\n  } = props;\n\n  const [c, setc] = useState<ConnectionStatus>();\n  const noBash = c?.noBash;\n  useEffect(() => {\n    setNoBash(!!noBash);\n  }, [noBash, setNoBash]);\n  const getIsMounted = useIsMounted();\n  useEffect(() => {\n    const interval = setInterval(async () => {\n      try {\n        const c = await getStatus(connectionId);\n        if (!getIsMounted()) {\n          return;\n        }\n        setc(c);\n        if (!isEmpty(c.getPidStatsErrors)) {\n          console.error(c.getPidStatsErrors);\n        }\n        setStatusError(undefined);\n      } catch (e) {\n        console.error(e);\n        setStatusError(e);\n      }\n    }, samplingRate * 1e3);\n\n    return () => clearInterval(interval);\n  }, [samplingRate, connectionId, getStatus, getIsMounted, setStatusError]);\n\n  // const [shellResult, setShellResult] = useState(\"\");\n  // const setShell = async (v: string) => {\n  //   const res = await execPSQLBash(dbs.sql!, connectionId, v);\n  //   console.log(res);\n  //   setShellResult(res.join(\"\\n\"));\n  //   getPidStats(dbs.sql!, connectionId);\n  // }\n  {\n    /* <FormFieldDebounced onChange={setShell} />\n      <div className=\"ws-pre\">{shellResult}</div> */\n  }\n\n  const { data: connection } = dbs.connections.useFindOne({ id: connectionId });\n  const [datidFilter, setDatidFilter] = useState<number | undefined>();\n\n  return (\n    <>\n      {statusError && <ErrorComponent error={statusError} />}\n      <FlexRow>\n        {connection && (\n          <Chip variant=\"header\" label=\"Server\">\n            {getServerCoreInfoStr(connection)}\n          </Chip>\n        )}\n        <StatusMonitorInfoHeaderMemory serverStatus={c?.serverStatus} />\n        {c && (\n          <StatusMonitorConnections\n            c={c}\n            datidFilter={datidFilter}\n            dbsMethods={dbsMethods}\n            connectionId={connectionId}\n            onSetDatidFilter={setDatidFilter}\n          />\n        )}\n        <StatusMonitorInfoHeaderCpu serverStatus={c?.serverStatus} />\n\n        <FormFieldDebounced\n          label={\"Sampling rate (s)\"}\n          variant=\"row\"\n          type=\"number\"\n          className=\"w-fit f-0  ml-auto\"\n          value={samplingRate}\n          inputStyle={{\n            maxWidth: \"4rem\",\n          }}\n          wrapperStyle={{ flexDirection: \"row\" }}\n          onChange={(v) => {\n            if (v < 0.1) return;\n            setSamplingRate(v);\n          }}\n          inputProps={{ min: 0.1, max: 100, step: 0.1 }}\n        />\n      </FlexRow>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/StatusMonitor/StatusMonitorInfoHeader/StatusMonitorInfoHeaderCpu.tsx",
    "content": "import type { ConnectionStatus } from \"@common/utils\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { mdiChip } from \"@mdi/js\";\nimport React from \"react\";\nimport { bytesToSize } from \"../../BackupAndRestore/BackupsControls\";\n\nexport const StatusMonitorInfoHeaderCpu = ({\n  serverStatus,\n}: Pick<ConnectionStatus, \"serverStatus\">) => {\n  if (!serverStatus) {\n    return;\n  }\n  const { cpu_model, cpu_mhz, cpu_cores_mhz, disk_space, ioInfo } =\n    serverStatus;\n\n  return (\n    <PopupMenu\n      title=\"Server info\"\n      className=\"f-0\"\n      positioning=\"center\"\n      clickCatchStyle={{ opacity: 0.5 }}\n      contentClassName=\"flex-col gap-1 p-1\"\n      button={\n        <Btn title=\"Server information\" iconPath={mdiChip} variant=\"faded\">\n          Server info\n        </Btn>\n      }\n    >\n      <Chip label={\"CPU Model\"} variant=\"header\">\n        <span className=\"ws-pre\">\n          {cpu_model}\n          <br></br>\n          {cpu_mhz}\n        </span>\n      </Chip>\n      <Chip label={\"CPU Frequency\"} variant=\"header\">\n        <div className=\"ws-pre ta-right\">{cpu_cores_mhz}</div>\n      </Chip>\n      <Chip label={\"Disk usage\"} variant=\"header\">\n        <span className=\"ws-pre\">{disk_space}</span>\n      </Chip>\n      {(ioInfo?.length ?? 0) > 0 && (\n        <FlexCol className=\"gap-0 p-p5\">\n          <span className=\"text-1 font-14 ta-left\">IO: </span>\n          <table className=\"ta-left\" style={{ borderSpacing: 0 }}>\n            <thead>\n              <tr>\n                <th>Device</th>\n                <th>Reads</th>\n                <th>Writes</th>\n              </tr>\n            </thead>\n            <tbody>\n              {ioInfo?.map((r) => (\n                <tr key={r.deviceName}>\n                  <td>{r.deviceName}</td>\n                  <td>{bytesToSize(r.readsCompletedSuccessfully)}</td>\n                  <td>{bytesToSize(r.writesCompleted)}</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </FlexCol>\n      )}\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/StatusMonitor/StatusMonitorInfoHeader/StatusMonitorInfoHeaderMemory.tsx",
    "content": "import type { ConnectionStatus } from \"@common/utils\";\nimport Chip from \"@components/Chip\";\nimport React from \"react\";\nimport { bytesToSize } from \"../../BackupAndRestore/BackupsControls\";\n\nexport const StatusMonitorInfoHeaderMemory = ({\n  serverStatus,\n}: Pick<ConnectionStatus, \"serverStatus\">) => {\n  if (!serverStatus) {\n    return;\n  }\n  const { total_memoryKb, memAvailable } = serverStatus;\n  return (\n    <Chip className=\"f-0\" variant=\"header\" label=\"Memory used\">\n      {((100 * (total_memoryKb - memAvailable)) / total_memoryKb)\n        .toFixed(1)\n        .padStart(2, \"0\")}\n      % ({bytesToSize(1024 * (total_memoryKb - memAvailable))})\n    </Chip>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/StatusMonitor/StatusMonitorProcList.tsx",
    "content": "import { mdiCancel, mdiStopCircleOutline } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useMemo, useState } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { PrglState } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport CodeExample from \"../CodeExample\";\nimport type { SmartCardListProps } from \"../SmartCardList/SmartCardList\";\nimport { SmartCardList } from \"../SmartCardList/SmartCardList\";\nimport { StyledInterval } from \"../W_SQL/customRenderers\";\nimport type { StatusMonitorProps } from \"./StatusMonitor\";\nimport { StatusMonitorProcListControlsHeader } from \"./StatusMonitorProcListControlsHeader\";\nimport { STATUS_MONITOR_IGNORE_QUERY } from \"@common/utils\";\n\nexport const StatusMonitorViewTypes = [\n  { key: \"All Queries\", subLabel: \"No filtering applied\" },\n  {\n    key: \"Active queries\",\n    subLabel:\n      \"All queries with 'active' state excluding the queries used to create this view\",\n  },\n  {\n    key: \"Blocked queries\",\n    subLabel: \"Queries that have are blocked by other queries\",\n  },\n] as const;\nexport type StatusMonitorViewType =\n  (typeof StatusMonitorViewTypes)[number][\"key\"];\n\nconst orderByCPU = {\n  key: \"cpu\",\n  asc: false,\n} as const;\nexport const StatusMonitorProcList = (\n  props: StatusMonitorProps & {\n    samplingRate: number;\n    noBash: boolean | undefined;\n  },\n) => {\n  const {\n    connectionId,\n    dbs,\n    dbsMethods,\n    dbsTables,\n    runConnectionQuery,\n    samplingRate,\n    noBash,\n  } = props;\n  const [viewType, setViewType] = useState<StatusMonitorViewType>(\n    StatusMonitorViewTypes[1].key,\n  );\n\n  const [toggledFields, setToggledFields] = useState<string[]>([]);\n  const { fieldConfigs, excludedFields } = useStatusMonitorProcListProps(\n    dbsMethods,\n    toggledFields,\n    connectionId,\n    noBash ?? true,\n  );\n  const allToggledFields = useMemo(\n    () =>\n      fieldConfigs\n        .filter(\n          (f) =>\n            typeof f === \"string\" || (!(f as any).hide && !(f as any).hideIf),\n        )\n        .map((f) => (typeof f === \"string\" ? f : f.name)),\n    [fieldConfigs],\n  );\n\n  const [datidFilter, setDatidFilter] = useState<number | undefined>();\n\n  const databaseId = usePromise(async () => {\n    const datids = await runConnectionQuery(\n      connectionId,\n      `SELECT datid\n      FROM pg_catalog.pg_stat_database\n      WHERE datname = current_database()\n    `,\n    );\n    if (datids.length === 1) {\n      const datid = datids[0]?.datid as number;\n      setDatidFilter(datid);\n      return datid;\n    }\n  }, [runConnectionQuery, connectionId]);\n\n  const filter = useMemo(() => {\n    return {\n      $and: [\n        {\n          ...(datidFilter ? { datid: datidFilter } : {}),\n          ...(viewType === \"Blocked queries\" ? { blocked_by_num: { \">\": 0 } }\n          : viewType === \"Active queries\" ?\n            {\n              state: \"active\",\n              query: { $nilike: `%${STATUS_MONITOR_IGNORE_QUERY}%` },\n            }\n          : {}),\n        },\n      ],\n    };\n  }, [datidFilter, viewType]);\n\n  return (\n    <SmartCardList\n      db={dbs as DBHandlerClient}\n      methods={dbsMethods}\n      tables={dbsTables}\n      tableName=\"stats\"\n      showEdit={false}\n      showTopBar={{\n        sort: true,\n        leftContent: (\n          <StatusMonitorProcListControlsHeader\n            {...props}\n            allToggledFields={allToggledFields}\n            excludedFields={excludedFields}\n            setToggledFields={setToggledFields}\n            dbsTables={dbsTables}\n            datidFilter={datidFilter}\n            setDatidFilter={setDatidFilter}\n            databaseId={databaseId}\n            viewType={viewType}\n            setViewType={setViewType}\n            samplingRate={samplingRate}\n          />\n        ),\n      }}\n      orderBy={orderByCPU}\n      rowProps={{\n        style: {\n          borderRadius: 0,\n        },\n      }}\n      noDataComponent={\n        <InfoRow color=\"info\" variant=\"filled\">\n          No {viewType}\n        </InfoRow>\n      }\n      realtime={true}\n      throttle={500}\n      filter={filter}\n      fieldConfigs={fieldConfigs}\n    />\n  );\n};\n\ntype FieldConfigs = Required<SmartCardListProps>[\"fieldConfigs\"];\n\nconst useStatusMonitorProcListProps = (\n  dbsMethods: PrglState[\"dbsMethods\"],\n  toggledFields: string[],\n  connectionId: string,\n  noBash: boolean,\n) => {\n  return useMemo(() => {\n    const actionRow = {\n      name: \"id_query_hash\",\n      className: \"ml-auto show-on-parent-hover\",\n      label: \"\",\n      renderMode: \"valueNode\",\n      render: (id_query_hash, row) =>\n        !dbsMethods.killPID ?\n          <div></div>\n        : <FlexRow className=\"\">\n            <Btn\n              title=\"Cancel this query\"\n              iconPath={mdiStopCircleOutline}\n              color=\"danger\"\n              onClickPromise={() =>\n                dbsMethods.killPID!(connectionId, id_query_hash, \"cancel\")\n              }\n            />\n            <Btn\n              title=\"Terminate this query\"\n              iconPath={mdiCancel}\n              color=\"danger\"\n              onClickPromise={() =>\n                dbsMethods.killPID!(connectionId, id_query_hash, \"terminate\")\n              }\n            />\n          </FlexRow>,\n    } satisfies FieldConfigs[number];\n\n    const hideOverflowStyle = { style: { overflow: \"hidden\" } };\n    const fixedFields = [\n      {\n        name: \"cpu\",\n        hide: noBash,\n        ...hideOverflowStyle,\n        render: (v: number) => <span className=\"\">{v}%</span>,\n      },\n      {\n        name: \"mem\",\n        hide: noBash,\n        renderMode: \"value\",\n        ...hideOverflowStyle,\n        render: (v: number) => <span className=\"\">{v}%</span>,\n      },\n      {\n        name: \"state\",\n        ...hideOverflowStyle,\n        label: \"State\",\n        renderMode: \"valueNode\",\n        render: (state) => {\n          return (\n            <Chip\n              className=\"mt-p25\"\n              color={\n                state === \"idle\" ? \"yellow\"\n                : state === \"active\" ?\n                  \"blue\"\n                : undefined\n              }\n            >\n              {state ?? \"unknown\"}\n            </Chip>\n          );\n        },\n      },\n      {\n        name: \"blocked_by\",\n        ...hideOverflowStyle,\n        label: \"Blocked by pids\",\n        hideIf: (value) => !value?.length,\n        renderMode: \"valueNode\",\n        render: (pids, row) => (\n          <FlexRow>\n            {pids?.map((pid, i) => (\n              <Chip key={pid} className=\"mt-p25\" color=\"red\">\n                {pid}\n              </Chip>\n            ))}\n          </FlexRow>\n        ),\n      },\n      {\n        name: \"running_time\",\n        ...hideOverflowStyle,\n        label: \"Running time\",\n        select: { $ageNow: [\"query_start\"] },\n        render: (value) => <StyledInterval value={value} mode=\"pg_stat\" />,\n      },\n      { name: \"pid\" },\n      { name: \"backend_xid\" },\n      actionRow,\n      {\n        name: \"query\",\n        className: \"w-full\",\n        render: (query, row) => (\n          <PopupMenu\n            key={row.pid}\n            button={<div>{query}</div>}\n            positioning=\"center\"\n            title={`PID: ${row.pid}`}\n          >\n            <CodeExample\n              value={query}\n              language=\"sql\"\n              style={{\n                minWidth: \"450px\",\n                minHeight: \"450px\",\n              }}\n            />\n          </PopupMenu>\n        ),\n      },\n    ] satisfies FieldConfigs;\n\n    const excludedFields: (keyof DBSSchema[\"stats\"])[] = fixedFields\n      .filter((f) => (f as any).render)\n      .map((f) => f.name)\n      .concat([\"connection_id\"]) as any;\n\n    const fieldConfigs = [\n      ...fixedFields.filter((ff) => !toggledFields.includes(ff.name)),\n      ...toggledFields,\n    ];\n\n    return { fieldConfigs, excludedFields };\n  }, [dbsMethods, toggledFields, connectionId, noBash]);\n};\n"
  },
  {
    "path": "client/src/dashboard/StatusMonitor/StatusMonitorProcListControlsHeader.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiInformationOutline } from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport type { StatusMonitorProps } from \"./StatusMonitor\";\nimport {\n  StatusMonitorViewTypes,\n  type StatusMonitorViewType,\n} from \"./StatusMonitorProcList\";\n\ntype P = StatusMonitorProps & {\n  viewType: StatusMonitorViewType;\n  setViewType: (viewType: StatusMonitorViewType) => void;\n  allToggledFields: string[];\n  excludedFields: string[];\n  datidFilter: number | undefined;\n  setDatidFilter: (datid: number | undefined) => void;\n  databaseId: number | undefined;\n  setToggledFields: (fields: string[]) => void;\n  samplingRate: number;\n};\n\nexport const StatusMonitorProcListControlsHeader = (props: P) => {\n  const {\n    dbsTables,\n    excludedFields,\n    allToggledFields,\n    setToggledFields,\n    datidFilter,\n    setDatidFilter,\n    viewType,\n    setViewType,\n    databaseId,\n  } = props;\n\n  const statColumns = useMemo(\n    () => dbsTables.find((t) => t.name === \"stats\")?.columns ?? [],\n    [dbsTables],\n  );\n\n  return (\n    <>\n      <FlexRowWrap>\n        <Select\n          btnProps={{\n            children: \"Fields...\",\n          }}\n          multiSelect={true}\n          fullOptions={statColumns.map((c) => ({\n            key: c.name,\n            label: c.label,\n            subLabel: c.hint,\n            disabledInfo:\n              excludedFields.includes(c.name) ?\n                \"Cannot toggle this field\"\n              : undefined,\n          }))}\n          value={allToggledFields}\n          onChange={setToggledFields}\n        />\n        <Select\n          value={viewType}\n          fullOptions={StatusMonitorViewTypes}\n          onChange={setViewType}\n        />\n        {viewType === \"Active queries\" && (\n          <Btn\n            title=\"Some queries used for this view have been hidden\"\n            iconPath={mdiInformationOutline}\n            color=\"warn\"\n            variant=\"faded\"\n          />\n        )}\n      </FlexRowWrap>\n      <Select\n        options={[\"Current database\", \"All databases\"]}\n        value={datidFilter ? \"Current database\" : \"All databases\"}\n        onChange={(val) =>\n          setDatidFilter(val === \"Current database\" ? databaseId : undefined)\n        }\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/TableConfig/ProcessLogs.tsx",
    "content": "import { useIsMounted } from \"prostgles-client\";\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { ProcStats } from \"@common/utils\";\nimport { getAgeFromDiff } from \"@common/utils\";\nimport type { Prgl } from \"../../App\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport { CodeEditorWithSaveButton } from \"../CodeEditor/CodeEditorWithSaveButton\";\nimport { getPGIntervalAsText } from \"../W_SQL/customRenderers\";\nimport type { editor } from \"../W_SQL/monacoEditorTypes\";\nimport type { FilterItem } from \"prostgles-types\";\nimport { LOG_LANGUAGE_ID } from \"../CodeEditor/registerLogLang\";\n\ntype P = Pick<Prgl, \"dbsMethods\" | \"connectionId\" | \"dbs\"> & {\n  type: \"tableConfig\" | \"onMount\" | \"methods\";\n  noMaxHeight?: boolean;\n};\nexport const ProcessLogs = (props: P) => {\n  const { dbsMethods, connectionId, dbs, type } = props;\n  const { data: conn } = dbs.connections.useSubscribeOne({ id: connectionId });\n  const { data: dbConf } = dbs.database_configs.useSubscribeOne({\n    $existsJoined: { connections: { id: connectionId } },\n  } as FilterItem);\n  const { data: dbConfLogs } = dbs.database_config_logs.useSubscribeOne({\n    $existsJoined: {\n      \"database_configs.connections\": { id: connectionId },\n    },\n  } as FilterItem);\n  const getIsMounted = useIsMounted();\n  const [editorKey, setEditorKey] = useState(Date.now().toString());\n  const [procStats, setProcStats] = useState<ProcStats & { error?: any }>();\n  const editorRef = useRef<editor.IStandaloneCodeEditor>();\n  const hasCode =\n    type === \"tableConfig\" ? !!dbConf?.table_config_ts\n    : type === \"onMount\" ? !!conn?.on_mount_ts\n    : true;\n  const isDisabled =\n    (type === \"tableConfig\" ? dbConf?.table_config_ts_disabled\n    : type === \"onMount\" ? conn?.on_mount_ts_disabled\n    : false) || !hasCode;\n\n  useEffect(() => {\n    if (isDisabled) return;\n    const interval = setInterval(async () => {\n      try {\n        const stats = await dbsMethods.getForkedProcStats?.(connectionId);\n        if (!getIsMounted()) return;\n        setProcStats(\n          type === \"tableConfig\" ? stats?.tableConfigRunner\n          : type === \"onMount\" ? stats?.onMountRunner\n          : stats?.methodRunner,\n        );\n      } catch (error) {\n        if (!getIsMounted()) return;\n        setProcStats({\n          cpu: 0,\n          mem: 0,\n          pid: 0,\n          uptime: 0,\n          error,\n        });\n      }\n    }, 1000);\n\n    return () => {\n      clearInterval(interval);\n    };\n  }, [dbsMethods, type, connectionId, getIsMounted, isDisabled]);\n\n  const logs =\n    type === \"tableConfig\" ? dbConfLogs?.table_config_logs\n    : type === \"onMount\" ? dbConfLogs?.on_mount_logs\n    : dbConfLogs?.on_run_logs;\n\n  /* Fix bug where logs are not rendered */\n  useEffect(() => {\n    const timeout = setTimeout(() => {\n      if (!editorRef.current || !getIsMounted()) return;\n      if (!editorRef.current.getDomNode()?.innerText.length && logs?.length) {\n        setEditorKey(Date.now().toString());\n      }\n    }, 1000);\n\n    return () => {\n      clearTimeout(timeout);\n    };\n  }, [editorRef, logs, setEditorKey, getIsMounted]);\n\n  const onMonacoEditorMount = useCallback(\n    (editor: editor.IStandaloneCodeEditor) => {\n      editorRef.current = editor;\n    },\n    [],\n  );\n\n  return (\n    <FlexCol className=\"f-1 relative\">\n      <CodeEditorWithSaveButton\n        key={editorKey}\n        label={\n          <FlexRow className=\"px-p5\">\n            {isDisabled || !procStats ?\n              <div>Process not started.</div>\n            : <>\n                <Chip variant=\"naked\" label=\"PID\">\n                  {procStats.pid}\n                </Chip>\n                <Chip variant=\"naked\" label=\"Cpu\">\n                  {procStats.cpu.toFixed(1)}%\n                </Chip>\n                <Chip variant=\"naked\" label=\"Mem\">\n                  {Math.round(procStats.mem / 1e6).toLocaleString() + \" MB\"}\n                </Chip>\n                <Chip variant=\"naked\" label=\"Uptime\">\n                  {getPGIntervalAsText(\n                    getAgeFromDiff(Math.round(procStats.uptime) * 1e3),\n                    true,\n                    undefined,\n                    true,\n                  )}\n                </Chip>\n              </>\n            }\n            <Label variant=\"normal\">\n              {isDisabled || !procStats ? \"Log history:\" : \"Logs:\"}\n            </Label>\n          </FlexRow>\n        }\n        onMount={onMonacoEditorMount}\n        options={options}\n        language={LOG_LANGUAGE_ID}\n        value={logs ?? \"\"}\n      />\n    </FlexCol>\n  );\n};\n\nconst options = { readOnly: true };\n"
  },
  {
    "path": "client/src/dashboard/TableConfig/TableConfig.tsx",
    "content": "import React from \"react\";\nimport type { Prgl } from \"../../App\";\nimport { FlexCol } from \"@components/Flex\";\nimport { CodeEditorWithSaveButton } from \"../CodeEditor/CodeEditorWithSaveButton\";\nimport { ProcessLogs } from \"./ProcessLogs\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\n\ntype P = {\n  prgl: Prgl;\n};\n\nexport const TableConfig = ({ prgl: { dbs, connectionId, dbsMethods } }: P) => {\n  const { data: dbConf } = dbs.database_configs.useSubscribeOne({\n    $existsJoined: { connections: { id: connectionId } },\n  });\n\n  if (!dbConf) return null;\n\n  return (\n    <FlexCol className=\"f-1\">\n      <p className=\"m-0 p-0\">\n        Table definitions and lifecycle methods that will be synced to schema\n      </p>\n      <SwitchToggle\n        label=\"Enabled\"\n        checked={!!dbConf.table_config_ts && !dbConf.table_config_ts_disabled}\n        onChange={async (checked) => {\n          await dbsMethods.setTableConfig?.(connectionId, {\n            table_config_ts_disabled: !checked,\n          });\n        }}\n      />\n      <CodeEditorWithSaveButton\n        key={\"tableConfig\"}\n        label=\"Table Config\"\n        language={{\n          lang: \"typescript\",\n          modelFileName: \"TableConfig.ts\",\n          tsLibraries: [\n            {\n              filePath: \"TableConfig.ts\",\n              content: TableConfigts,\n            },\n          ],\n        }}\n        codePlaceholder={exampleConfig}\n        value={dbConf.table_config_ts}\n        onSave={async (value) => {\n          await dbsMethods.setTableConfig?.(connectionId, {\n            table_config_ts: value,\n            table_config_ts_disabled: !value,\n          });\n        }}\n      />\n      <ProcessLogs\n        type=\"tableConfig\"\n        connectionId={connectionId}\n        dbsMethods={dbsMethods}\n        dbs={dbs}\n      />\n    </FlexCol>\n  );\n};\n\nconst TableConfigts = `\ntype TableConfig = Record<\n  string, { \n    /**\n     * Column names and sql definitions\n     * */\n    columns: Record<string, string>; \n  }\n>;\n`;\n\nconst exampleConfig = `/* Example */\nexport const tableConfig: TableConfig = {\n  my_table: {\n    columns: {\n      some_column: \"TEXT\",\n    },\n  },\n};\n\n`;\n"
  },
  {
    "path": "client/src/dashboard/TimeSeries.tsx",
    "content": "import React from \"react\";\nimport RTComp from \"./RTComp\";\n\ntype P = {\n  layers: {\n    label: string;\n    color: string;\n    width: number;\n    data: {\n      time: number;\n      value: number;\n    }[];\n  }[];\n};\n\nexport default class TimeSeries extends RTComp<P, any> {\n  ref?: HTMLDivElement;\n  canv?: HTMLCanvasElement;\n  deck?: any;\n  onMount() {\n    if (this.ref) {\n    }\n  }\n\n  render() {\n    const { layers } = this.props;\n    return <div className={\"timeseries-comp f-1 relative bg-color-0\"}></div>;\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/UserManager.tsx",
    "content": "import React from \"react\";\nimport type { ExtraProps } from \"../App\";\nimport RTComp from \"./RTComp\";\n\nimport type { SubscriptionHandler } from \"prostgles-types\";\nimport SmartTable from \"./SmartTable\";\nimport { PasswordlessSetup } from \"./AccessControl/PasswordlessSetup\";\nimport { t } from \"../i18n/i18nUtils\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\n\nexport type Users = {\n  created?: Date;\n  id?: string;\n  last_updated?: number;\n  password?: string;\n  status?: string;\n  type?: string;\n  username?: string;\n};\n\ntype S = {\n  collapsed: boolean;\n  loading: boolean;\n};\n\nexport default class UserManager extends RTComp<ExtraProps, S> {\n  state: S = {\n    collapsed: true,\n    loading: true,\n  };\n\n  loaded = false;\n  sub?: SubscriptionHandler;\n  onDelta = () => {\n    const { dbs, user } = this.props;\n    // if(dbs && !this.loaded){\n    //   this.loaded = true;\n    //   this.sub = await dbs.users.subscribe({ username: \"prostgles-no-auth-user\" }, { orderBy: { created: -1 } }, users => {\n    //     this.setState({ users })\n    //   })\n    // }\n  };\n\n  onUnmount() {}\n\n  render() {\n    const { dbs, dbsTables, user, dbsMethods, theme } = this.props;\n\n    let content: React.ReactNode;\n    if (user?.type !== \"admin\") {\n      content = <div>Must be admin to access this section</div>;\n    } else if (user.passwordless_admin) {\n      content = <PasswordlessSetup {...this.props} />;\n    } else {\n      content = (\n        <SmartTable\n          className=\"w-full\"\n          db={dbs as DBHandlerClient}\n          methods={dbsMethods}\n          titlePrefix={t.Users[\"Prostgles UI users\"]}\n          tableName=\"users\"\n          tables={dbsTables}\n          showInsert={true}\n          allowEdit={true}\n          realtime={{}}\n        />\n      );\n    }\n\n    return (\n      <div\n        className={\n          \"flex-col relative w-full f-1 min-h-0 pt-1 \" +\n          (window.isLowWidthScreen ? \"\" : \" px-2 \")\n        }\n      >\n        {content}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Barchart/W_Barchart.tsx",
    "content": "import ErrorComponent from \"@components/ErrorComponent\";\nimport Loading from \"@components/Loader/Loading\";\nimport { CellBarchart } from \"@components/ProgressBar\";\nimport { Table } from \"@components/Table/Table\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { kFormatter, type ActiveRow } from \"../W_Table/W_Table\";\nimport Window from \"../Window\";\nimport { useBarchartData } from \"./useBarchartData\";\n\nexport type W_BarchartProps = Omit<CommonWindowProps, \"w\"> & {\n  onClickRow: (\n    row: AnyObject | undefined,\n    tableName: string,\n    values: ActiveRow[\"barChart\"],\n  ) => void;\n  myActiveRow: ActiveRow | undefined;\n  activeRowColor: string | undefined;\n  w: WindowSyncItem<\"barchart\">;\n};\n\nexport const W_Barchart = ({\n  w,\n  prgl,\n  myLinks,\n  workspace,\n  getLinksAndWindows,\n}: W_BarchartProps) => {\n  const { barChartData, setSort, sort } = useBarchartData({\n    myLinks,\n    prgl,\n    getLinksAndWindows,\n  });\n  return (\n    <Window\n      getMenu={undefined}\n      w={w}\n      layoutMode={workspace.layout_mode ?? \"editable\"}\n    >\n      {!barChartData ?\n        <Loading />\n      : barChartData.type === \"error\" ?\n        <ErrorComponent error={barChartData.message} />\n      : <Table\n          rows={barChartData.rows}\n          sort={\n            sort && [\n              {\n                key: sort.column,\n                asc: sort.direction === \"asc\",\n              },\n            ]\n          }\n          onSort={([newSort]) => {\n            setSort(\n              newSort && {\n                column: newSort.key,\n                direction: newSort.asc ? \"asc\" : \"desc\",\n              },\n            );\n          }}\n          cols={[\n            {\n              key: \"label\",\n              name: \"label\",\n              label: barChartData.labelColumn,\n              width: barChartData.labelMaxWidth,\n              sortable: true,\n              filter: false,\n              tsDataType: \"string\",\n              udt_name: \"text\",\n            },\n            {\n              key: \"value\",\n              name: \"value\",\n              label:\n                barChartData.statType?.funcName.slice(1).toUpperCase() ||\n                \"Count\",\n              sortable: true,\n              filter: false,\n              tsDataType: \"number\",\n              udt_name: \"int4\",\n              onRender: ({ value }) => (\n                <CellBarchart\n                  style={{ marginTop: \"6px\" }}\n                  min={barChartData.min}\n                  max={barChartData.max}\n                  barColor={`rgb(${barChartData.colorArr.slice(0, 3).join(\",\")})`}\n                  textColor={\"var(--text-0)\"}\n                  value={Number(value)}\n                  message={kFormatter(Number(value))}\n                />\n              ),\n            },\n          ]}\n        />\n      }\n    </Window>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Barchart/fetchSQLBarchartData.ts",
    "content": "import type { SQLHandler } from \"prostgles-types\";\nimport type { LinkSyncItem } from \"../Dashboard/dashboardUtils\";\n\nexport const fetchSQLBarchartData = async (\n  sql: SQLHandler,\n  linkOpts: Extract<LinkSyncItem[\"options\"], { type: \"barchart\" }>,\n) => {};\n"
  },
  {
    "path": "client/src/dashboard/W_Barchart/useBarchartData.ts",
    "content": "import { useState } from \"react\";\nimport type { W_BarchartProps } from \"./W_Barchart\";\nimport { usePromise } from \"prostgles-client\";\nimport {\n  getSerialisableError,\n  includes,\n  type AnyObject,\n} from \"prostgles-types\";\n\nexport const useBarchartData = ({\n  myLinks,\n  prgl,\n  getLinksAndWindows,\n}: Pick<W_BarchartProps, \"myLinks\" | \"prgl\" | \"getLinksAndWindows\">) => {\n  const [sort, setSort] = useState<\n    { column: string; direction: \"asc\" | \"desc\" } | undefined\n  >(undefined);\n  const { db } = prgl;\n  const { windows } = getLinksAndWindows();\n  const barChartData = usePromise(async () => {\n    const firstLink = myLinks[0];\n    if (!firstLink) {\n      return {\n        type: \"error\" as const,\n        message: \"No links found for barchart\",\n      };\n    }\n\n    const linkOpts = firstLink.options;\n    if (linkOpts.type !== \"barchart\") {\n      return {\n        type: \"error\" as const,\n        message: `Invalid link type for barchart: ${firstLink.options.type}`,\n      };\n    }\n    const { sql } = db;\n    const { columns, statType } = linkOpts;\n    const [column] = columns;\n    if (!column) {\n      return {\n        type: \"error\" as const,\n        message: \"No label column defined for barchart\",\n      };\n    }\n    const { colorArr, name: labelColumn } = column;\n    const { dataSource } = linkOpts;\n    if (dataSource?.type === \"sql\") {\n      // const { dataSource } = linkOpts;\n      if (!sql) {\n        return {\n          type: \"error\" as const,\n          message: \"Running SQL not allowed \",\n        };\n      }\n\n      try {\n        const sorting =\n          sort ?\n            `ORDER BY ${sort.column === \"label\" ? 1 : 2} ${sort.direction === \"asc\" ? \"ASC\" : \"DESC\"}`\n          : \"\";\n        const result = await sql(\n          `\n          SELECT ${[\n            labelColumn + \" AS label\",\n            statType ?\n              `${statType.funcName}(${statType.numericColumn}) AS value`\n            : \"COUNT(*) AS value\",\n          ].join(\", \")}\n          FROM (\n            ${dataSource.sql}\n          ) prostgles_barchart_table\n          GROUP BY 1\n          ${sorting}\n        `,\n          undefined,\n          {\n            returnType: \"default-with-rollback\",\n          },\n        );\n        return {\n          type: \"data\" as const,\n          linkOpts,\n          rows: result.rows,\n          labelColumn,\n          colorArr,\n          statType,\n          ...getMinMax(result.rows),\n        };\n      } catch (error) {\n        return {\n          type: \"error\" as const,\n          // message: `Error fetching barchart data: ${error}`,\n          message: getSerialisableError(error),\n        };\n      }\n    } else if (\n      dataSource?.type === \"local-table\" ||\n      dataSource?.type === \"table\"\n    ) {\n      const { w1_id, w2_id } = firstLink;\n      const linkTable =\n        w1_id !== w2_id ?\n          windows.find(\n            (w) => w.type === \"table\" && [w1_id, w2_id].includes(w.id),\n          )\n        : undefined;\n      const tableName =\n        dataSource.type === \"local-table\" ?\n          dataSource.localTableName\n        : linkTable?.table_name;\n      const filter =\n        dataSource.type === \"local-table\" ?\n          dataSource.smartGroupFilter\n        : undefined;\n      const tableHandler = !tableName ? undefined : db[tableName];\n      if (!tableHandler?.find) {\n        return {\n          type: \"error\" as const,\n          message: `Local table ${tableName} not found/allowed`,\n        };\n      }\n\n      const funcName = statType ? statType.funcName : \"$countAll\";\n      const numericColumn = statType?.numericColumn;\n      const rows = await tableHandler.find(filter, {\n        select: {\n          label: { $column: [labelColumn] },\n          value: {\n            [funcName]: numericColumn ? [numericColumn] : [],\n          },\n        },\n        orderBy: sort && {\n          key: sort.column,\n          asc: sort.direction === \"asc\",\n        },\n\n        limit: 1000,\n      });\n      return {\n        type: \"data\" as const,\n        linkOpts,\n        rows,\n        labelColumn,\n        colorArr,\n        statType,\n        ...getMinMax(rows),\n      };\n    }\n  }, [myLinks, db, sort, windows]);\n\n  return { barChartData, sort, setSort };\n};\n\nconst getMinMax = (rows: AnyObject[]) => {\n  let min = Number.POSITIVE_INFINITY;\n  let max = Number.NEGATIVE_INFINITY;\n  let labelMaxWidth = 50;\n  rows.forEach(({ value, label }) => {\n    const val = value;\n    if (typeof val === \"number\") {\n      if (val < min) min = val;\n      if (val > max) max = val;\n    }\n    const labelStr =\n      includes([\"string\", \"number\"], typeof label) ?\n        label.toString()\n      : JSON.stringify(label || \"\");\n    const labelLength = labelStr.length * 8;\n    if (labelLength > labelMaxWidth) labelMaxWidth = labelLength;\n  });\n  if (min === Number.POSITIVE_INFINITY) min = 0;\n  if (max === Number.NEGATIVE_INFINITY) max = 0;\n  return { min, max, labelMaxWidth };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/OSM/OverpassQuery.tsx",
    "content": "import React from \"react\";\nimport { CodeEditorWithSaveButton } from \"../../CodeEditor/CodeEditorWithSaveButton\";\n\ntype P = {\n  query: string;\n  onChange: (value: string) => void;\n  autoSave?: boolean;\n};\nexport const OverpassQuery = ({ query, onChange, autoSave }: P) => {\n  return (\n    <div style={{ minWidth: \"500px\" }}>\n      <CodeEditorWithSaveButton\n        language=\"text\"\n        label=\"Overpass Query\"\n        autoSave={autoSave}\n        value={query}\n        onSave={onChange}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/OSM/fetchOSMCountryBoundary.ts",
    "content": "const fetchCountryBoundary = async (countryName: string) => {\n  // Define the Overpass API query to get the boundary of a specific country in GeoJSON format\n  const overpassQuery = `\n      [out:json];\n      relation[\"type\"=\"boundary\"][\"boundary\"=\"administrative\"][\"admin_level\"=\"2\"][\"name:en\"=\"${countryName}\"];\n      out body;\n      >;\n      out skel qt;\n  `;\n\n  // Encode the query for the Overpass API request\n  const overpassUrl =\n    \"https://overpass-api.de/api/interpreter?data=\" +\n    encodeURIComponent(overpassQuery);\n\n  try {\n    // Fetch data from the Overpass API\n    const response = await fetch(overpassUrl);\n    const data = await response.json();\n\n    // Filter for the relation that contains the boundary and the name in English\n    const relation = data.elements.find(\n      (element) => element.type === \"relation\" && element.tags[\"name:en\"],\n    );\n\n    if (!relation) {\n      throw new Error(`Boundary for country '${countryName}' not found`);\n    }\n\n    // Create a mapping of node IDs to coordinates (lat, lon)\n    const nodeMap: Record<string, [number, number]> = {};\n    data.elements.forEach((element) => {\n      if (element.type === \"node\") {\n        nodeMap[element.id] = [element.lon, element.lat]; // Store longitude and latitude as a pair\n      }\n    });\n\n    // Extract the ways that form the polygon and their respective nodes\n    const coordinates = relation.members\n      .filter((member) => member.type === \"way\")\n      .map((way) => way.ref)\n      .map((ref) =>\n        data.elements\n          .find((element) => element.type === \"way\" && element.id === ref)\n          .nodes.map((nodeId) => nodeMap[nodeId]),\n      );\n\n    // Construct the GeoJSON object\n    const geojson = {\n      type: \"Feature\",\n      properties: {\n        name: relation.tags[\"name:en\"] || relation.tags.name || \"Unknown\",\n      },\n      geometry: {\n        type: \"Polygon\",\n        coordinates: [coordinates.flat()],\n      },\n    };\n\n    return geojson;\n  } catch (error) {\n    console.error(\"Error fetching country boundary:\", error);\n  }\n};\n\n// Example usage:\nfetchCountryBoundary(\"Germany\")\n  .then((geojson) => {\n    console.log(\"Country Boundary GeoJSON:\", geojson);\n  })\n  .catch((error) => {\n    console.error(error);\n  });\n"
  },
  {
    "path": "client/src/dashboard/W_Map/OSM/getOSMData.ts",
    "content": "import { isDefined } from \"../../../utils/utils\";\nimport type { GeoJSONFeature } from \"../../Map/DeckGLMap\";\nimport { osmRelationToGeoJSON } from \"./osmToGeoJSON\";\n\nconst OVERPASS_URL = \"https://overpass-api.de/api/interpreter\";\n\ntype OSMElementType = \"node\" | \"way\" | \"relation\";\n\ntype OSMElementBase = {\n  id: number;\n  type: OSMElementType;\n  tags?: Partial<{ [key: string]: string }>;\n};\n\nexport type OSMNode = OSMElementBase & {\n  type: \"node\";\n  lat: number;\n  lon: number;\n};\n\nexport type OSMWay = Omit<OSMElementBase, \"type\"> & {\n  type: \"way\";\n  nodes: number[]; // Array of node IDs\n};\n\nexport type OSMRelationMemberWay = {\n  ref: number; // Reference ID of the member element\n  type: \"way\";\n  role: \"outer\" | \"inner\";\n};\nexport type OSMRelationMemberNode = {\n  ref: number; // Reference ID of the member element\n  type: \"node\";\n  role: \"admin_centre\" | \"label\";\n};\nexport type OSMRelationMember = OSMRelationMemberWay | OSMRelationMemberNode;\n\nexport type OSMRelation = OSMElementBase & {\n  type: \"relation\";\n  members: OSMRelationMember[];\n};\n\nexport type OSMElement = OSMNode | OSMWay | OSMRelation;\n\nconst checkIfFailedResponse = (response: Response) => {\n  if (!response.ok) {\n    throw new Error(\n      `Overpass API request failed: ${response.status} ${response.statusText}`,\n    );\n  }\n};\n\nconst cachedFeatures: Map<number, GeoJSONFeature> = new Map();\nexport const getOSMData = async (query: string, bbox: string) => {\n  const response = await fetch(OVERPASS_URL, {\n    method: \"POST\",\n    body: query.replace(/\\${bbox}/g, bbox),\n    cache: \"default\",\n  });\n  checkIfFailedResponse(response);\n  const data: {\n    elements: OSMElement[];\n  } = await response.json();\n  const cache = data.elements.map((d) => cachedFeatures.get(d.id));\n  const nonCached = data.elements.filter((d, i) => !cache[i]);\n  const features = await getOSMDataAsGeoJson({\n    elements: nonCached,\n  });\n  console.log(data.elements, features);\n  return {\n    data,\n    features: [...cache.filter(isDefined), ...features],\n  };\n};\n\nconst cachedElements: Map<number, OSMElement> = new Map();\nconst fetchElements = async <T extends \"node\" | \"way\">(\n  type: T,\n  ids: number[],\n): Promise<Map<number, T extends \"node\" ? OSMNode : OSMWay>> => {\n  if (!ids.length) return new Map();\n  const limit = 10000;\n  let batchNodeIds: number[];\n  let step = 0;\n  const nonCachedIds = ids.filter((id) => !cachedElements.has(id));\n  let elements: any[] = ids\n    .map((id) => cachedElements.get(id))\n    .filter(isDefined);\n  do {\n    batchNodeIds = nonCachedIds.splice(0, limit);\n    step++;\n    if (!batchNodeIds.length) break;\n    const response = await fetch(OVERPASS_URL, {\n      method: \"POST\",\n      body: `[out:json];${type}(id:${batchNodeIds.join(\",\")});out ${limit};`,\n    });\n    checkIfFailedResponse(response);\n    const nodeBatch = await response.json().then((res) => res.elements);\n    elements = [...elements, ...nodeBatch];\n    nodeBatch.forEach((node: any) => {\n      cachedElements.set(node.id, node);\n    });\n  } while (batchNodeIds.length);\n\n  const map = new Map<number, any>();\n  elements.forEach((node: any) => {\n    map.set(node.id, node);\n  });\n  return map;\n};\n\nconst wayToLineString = (\n  way: OSMWay,\n  nodes: Map<number, OSMNode>,\n): GeoJSONFeature[\"geometry\"] => {\n  const coordinates: [number, number][] = way.nodes\n    .map((nodeId: number) => {\n      const node = nodes.get(nodeId);\n      return node ?\n          ([node.lon, node.lat] satisfies [number, number])\n        : undefined;\n    })\n    .filter(isDefined);\n\n  return {\n    type: \"LineString\",\n    coordinates,\n  };\n};\n\nexport const getOSMDataAsGeoJson = async (responseData: {\n  elements: OSMElement[];\n}): Promise<GeoJSONFeature[]> => {\n  const unique = <T>(arr: T[]) => Array.from(new Set(arr));\n  const wayIds = unique(\n    responseData.elements\n      .flatMap((e) =>\n        e.type === \"relation\" ?\n          e.members\n            .flatMap((m) => (m.type === \"way\" ? m.ref : undefined))\n            .filter(isDefined)\n        : undefined,\n      )\n      .filter(isDefined),\n  );\n  const _nodeIds = unique(\n    responseData.elements.flatMap((e) => {\n      if (e.type === \"node\") return [e.id];\n      if (e.type === \"way\") return e.nodes;\n      // if(e.type === \"relation\") return e.members.map(member => member.ref);\n      return [];\n    }),\n  );\n  const ways = await fetchElements(\"way\", wayIds);\n  const wayNodeIds = unique(\n    ways\n      .values()\n      //@ts-ignore\n      .toArray()\n      .flatMap((w) => w.nodes),\n  );\n  const nodeIds = unique([..._nodeIds, ...wayNodeIds]);\n  const nodes = await fetchElements(\"node\", nodeIds);\n\n  const features: GeoJSONFeature[] = responseData.elements\n    .flatMap((element) => {\n      const commonProps: Pick<GeoJSONFeature, \"id\" | \"properties\" | \"type\"> = {\n        id: element.id,\n        type: \"Feature\",\n        properties: {\n          _is_from_osm: true,\n          ...element.tags,\n        },\n      };\n      if (element.type === \"node\") {\n        return {\n          ...commonProps,\n          geometry: {\n            type: \"Point\",\n            coordinates: [element.lon, element.lat],\n          },\n        } satisfies GeoJSONFeature;\n      } else if (element.type === \"way\") {\n        return {\n          ...commonProps,\n          geometry: wayToLineString(element, nodes),\n        } satisfies GeoJSONFeature;\n\n        /*\n      A relation can be a multipolygon or a route, node, etc.\n    */\n      } else if ((element as any).type === \"relation\") {\n        // return ways.values().toArray().map(way => ({\n        //   ...commonProps,\n        //   geometry: wayToLineString(way, nodes),\n        // }))\n\n        return osmRelationToGeoJSON(element, nodes, ways) ?? [];\n        // // Convert ways to GeoJSON coordinates\n        // const convertWayToCoordinates = (m: OSMRelationMemberWay) => {\n        //   const way = ways.find((el) => el.id === m.ref);\n        //   if(!way) {\n        //     console.warn(`Way with ID ${m.ref} not found`);\n        //     return undefined;\n        //   }\n        //   if(!way.nodes.length) {\n        //     console.warn(`Way with ID ${m.ref} has no nodes`);\n        //     return undefined;\n        //   }\n        //   const polygon: [number,number][] = way.nodes.map(nodeId => {\n        //     const node = nodes.find((el) => el.id === nodeId);\n        //     if(!node) return;\n        //     const point: [number,number] = [node.lon, node.lat];\n        //     return point;\n        //   }).filter(isDefined);\n\n        //   return polygon;\n        // };\n\n        // const outerWays = element.members.filter((m): m is OSMRelationMemberWay => m.type === \"way\" && m.role === \"outer\");\n        // const innerWays = element.members.filter((m): m is OSMRelationMemberWay => m.type === \"way\" && m.role === \"inner\");\n\n        // const coordinates: [number,number][][] = [];\n\n        // const outerPolygon: Polygon[\"coordinates\"] = outerWays.map(ow => convertWayToCoordinates(ow)).filter(isDefined);\n        // // const innerPolygons\n        // return {\n        //   ...commonProps,\n        //   geometry: {\n        //     type: \"Polygon\",\n        //     coordinates: outerPolygon,\n        //   },\n        // } satisfies GeoJSONFeature;\n      }\n      return undefined;\n    })\n    .filter(isDefined);\n\n  return features;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/OSM/osmToGeoJSON.ts",
    "content": "import type { Feature, Geometry } from \"geojson\";\nimport { MultiPolygon, Polygon } from \"geojson\";\nimport { cloneDeep } from \"lodash\";\nimport type { OSMNode, OSMRelation, OSMWay } from \"./getOSMData\";\n\nexport const osmRelationToGeoJSON = (\n  relation: OSMRelation,\n  nodeMap: Map<number, OSMNode>,\n  wayMap: Map<number, OSMWay>,\n): Feature<Geometry, { [key: string]: any }> | null => {\n  const relationMap = new Map<number, OSMRelation>();\n\n  const getWayCoordinates = (wayId: number): [number, number][] => {\n    const way = wayMap.get(wayId);\n    if (!way || !way.nodes.length) {\n      throw new Error(`Way ${wayId} not found`);\n    }\n    const coords: [number, number][] = way.nodes.map((nodeId) => {\n      const node = nodeMap.get(nodeId);\n      if (!node) {\n        throw new Error(`Node ${nodeId} not found`);\n      }\n      return [node.lon, node.lat];\n    });\n    return coords;\n  };\n\n  // Assemble rings\n  const rings: { role: string; coordinates: [number, number][] }[] = [];\n\n  for (const member of relation.members) {\n    if (member.type === \"way\") {\n      const coords = getWayCoordinates(member.ref);\n      rings.push({ role: member.role, coordinates: coords }); //  || \"outer\"\n    } else if ((member as any).type === \"relation\") {\n      console.log(\"Nested relation not handled\");\n      // Handle nested relations (optional, depends on your needs)\n      // const nestedRelation = relationMap.get(member.ref);\n      // if (nestedRelation) {\n      //   const nestedFeature = osmRelationToGeoJSON(nestedRelation, nodeMap, wayMap);\n      //   console.log(\"Nested feature:\", nestedFeature);\n      //   // Process nested features (e.g., merge polygons)\n      //   // For simplicity, this code does not handle nested relations deeply\n      // }\n    }\n  }\n\n  // Function to assemble rings into complete rings (handle way fragments)\n  const assembleRings = (\n    rings: { role: string; coordinates: [number, number][] }[],\n  ): { outer: [number, number][][]; inner: [number, number][][] } => {\n    const outerRings: [number, number][][] = [];\n    const innerRings: [number, number][][] = [];\n\n    const ringMap: { [key: string]: [number, number][][] } = {};\n\n    // Group rings by role\n    for (const ring of rings) {\n      if (!ringMap[ring.role]) {\n        ringMap[ring.role] = [];\n      }\n      ringMap[ring.role]!.push(ring.coordinates);\n    }\n\n    // Assemble outer rings\n    if (ringMap[\"outer\"]) {\n      outerRings.push(...assembleWays(ringMap[\"outer\"]));\n    }\n\n    // Assemble inner rings\n    if (ringMap[\"inner\"]) {\n      innerRings.push(...assembleWays(ringMap[\"inner\"]));\n    }\n\n    return { outer: outerRings, inner: innerRings };\n  };\n\n  // Function to assemble ways into complete rings\n  const assembleWays = (ways: [number, number][][]): [number, number][][] => {\n    const rings: [number, number][][] = [];\n    const used = new Set<number>();\n    let ring: [number, number][] = [];\n\n    while (ways.length > used.size) {\n      let connected = false;\n      for (let i = 0; i < ways.length; i++) {\n        if (used.has(i)) continue;\n        const way = ways[i]!;\n        if (ring.length === 0) {\n          ring = cloneDeep(way);\n          used.add(i);\n          connected = true;\n          break;\n        } else {\n          const ringStart = ring[0]!;\n          const ringEnd = ring[ring.length - 1]!;\n          const wayStart = way[0]!;\n          const wayEnd = way[way.length - 1]!;\n\n          if (coordinatesEqual(ringEnd, wayStart)) {\n            ring = ring.concat(way.slice(1));\n            used.add(i);\n            connected = true;\n            break;\n          } else if (coordinatesEqual(ringEnd, wayEnd)) {\n            ring = ring.concat(way.slice(0, -1).reverse());\n            used.add(i);\n            connected = true;\n            break;\n          } else if (coordinatesEqual(ringStart, wayEnd)) {\n            ring = way.slice(0, -1).concat(ring);\n            used.add(i);\n            connected = true;\n            break;\n          } else if (coordinatesEqual(ringStart, wayStart)) {\n            ring = way.slice(1).reverse().concat(ring);\n            used.add(i);\n            connected = true;\n            break;\n          }\n        }\n      }\n\n      if (!connected) {\n        // Ring is complete\n        if (ring.length > 0) {\n          // Ensure the ring is closed\n          if (!coordinatesEqual(ring[0]!, ring[ring.length - 1]!)) {\n            ring.push(ring[0]!);\n          }\n          rings.push(ring);\n        }\n        ring = [];\n      }\n    }\n\n    // Add the last ring if any\n    if (ring.length > 0) {\n      if (!coordinatesEqual(ring[0]!, ring[ring.length - 1]!)) {\n        ring.push(ring[0]!);\n      }\n      rings.push(ring);\n    }\n\n    return rings;\n  };\n\n  // Helper function to compare coordinates\n  const coordinatesEqual = (\n    a: [number, number],\n    b: [number, number],\n  ): boolean => {\n    return a[0] === b[0] && a[1] === b[1];\n  };\n\n  // Assemble the rings into polygons\n  const { outer: outerRings, inner: innerRings } = assembleRings(rings);\n\n  if (outerRings.length === 0) {\n    console.error(\"No outer rings found for the relation.\");\n    return null;\n  }\n\n  const polygons: [[number, number][]][] = outerRings.map((outerRing) => {\n    const polygon: [[number, number][]] = [outerRing];\n\n    // Assign inner rings to this outer ring if needed\n    // For simplicity, we're adding all inner rings to all outer rings\n    // In practice, you should check which inner rings are within each outer ring\n    for (const innerRing of innerRings) {\n      polygon.push(innerRing);\n    }\n\n    return polygon;\n  });\n\n  // Create the GeoJSON geometry\n  // @ts-ignore\n  const geometry: Geometry =\n    polygons.length > 1 ?\n      {\n        type: \"MultiPolygon\",\n        coordinates: polygons,\n      }\n    : {\n        type: \"Polygon\",\n        coordinates: polygons[0],\n      };\n\n  // Construct the GeoJSON Feature\n  const feature: Feature<Geometry, { [key: string]: any }> = {\n    type: \"Feature\",\n    properties: relation.tags || {},\n    geometry: geometry,\n  };\n\n  return feature;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/OSM/osmTypes.ts",
    "content": "/** \n * https://wiki.openstreetmap.org/wiki/Key:highway\n * https://wiki.openstreetmap.org/wiki/Key:amenity\n * https://wiki.openstreetmap.org/wiki/Key:building\n * https://wiki.openstreetmap.org/wiki/Key:natural\n * \n \n  let oTable = $0;\n  let data = [...oTable.rows].map(t => {\n    const [key, value, elemIcon, info] = [...t.children].map(u => u.innerText);\n    return { key, value, info }\n  }).filter(t => t.key === \"highway\");\n  console.log(data);\n\n */\nconst highway = [\n  {\n    key: \"highway\",\n    value: \"motorway\",\n    info: \"A restricted access major divided highway, normally with 2 or more running lanes plus emergency hard shoulder. Equivalent to the Freeway, Autobahn, etc..\",\n  },\n  {\n    key: \"highway\",\n    value: \"trunk\",\n    info: \"The most important roads in a country's system that aren't motorways. (Need not necessarily be a divided highway.)\",\n  },\n  {\n    key: \"highway\",\n    value: \"primary\",\n    info: \"The next most important roads in a country's system. (Often link larger towns.)\",\n  },\n  {\n    key: \"highway\",\n    value: \"secondary\",\n    info: \"The next most important roads in a country's system. (Often link towns.)\",\n  },\n  {\n    key: \"highway\",\n    value: \"tertiary\",\n    info: \"The next most important roads in a country's system. (Often link smaller towns and villages)\",\n  },\n  {\n    key: \"highway\",\n    value: \"unclassified\",\n    info: \"The least important through roads in a country's system – i.e. minor roads of a lower classification than tertiary, but which serve a purpose other than access to properties. (Often link villages and hamlets.)\\n\\nThe word 'unclassified' is a historical artefact of the UK road system and does not mean that the classification is unknown; you can use highway=road for that.\",\n  },\n  {\n    key: \"highway\",\n    value: \"residential\",\n    info: \"Roads which serve as an access to housing, without function of connecting settlements. Often lined with housing.\",\n  },\n  {\n    key: \"highway\",\n    value: \"motorway_link\",\n    info: \"The link roads (sliproads/ramps) leading to/from a motorway from/to a motorway or lower class highway. Normally with the same motorway restrictions.\",\n  },\n  {\n    key: \"highway\",\n    value: \"trunk_link\",\n    info: \"The link roads (sliproads/ramps) leading to/from a trunk road from/to a trunk road or lower class highway.\",\n  },\n  {\n    key: \"highway\",\n    value: \"primary_link\",\n    info: \"The link roads (sliproads/ramps) leading to/from a primary road from/to a primary road or lower class highway.\",\n  },\n  {\n    key: \"highway\",\n    value: \"secondary_link\",\n    info: \"The link roads (sliproads/ramps) leading to/from a secondary road from/to a secondary road or lower class highway.\",\n  },\n  {\n    key: \"highway\",\n    value: \"tertiary_link\",\n    info: \"The link roads (sliproads/ramps) leading to/from a tertiary road from/to a tertiary road or lower class highway.\",\n  },\n  {\n    key: \"highway\",\n    value: \"living_street\",\n    info: \"For living streets, which are residential streets where pedestrians have legal priority over cars, speeds are kept very low and this is can use for narrow roads that usually using for motorcycle roads.\",\n  },\n  {\n    key: \"highway\",\n    value: \"service\",\n    info: \"For access roads to, or within an industrial estate, camp site, business park, car park, alleys, etc. Can be used in conjunction with service=* to indicate the type of usage and with access=* to indicate who can use it and in what circumstances.\",\n  },\n  {\n    key: \"highway\",\n    value: \"pedestrian\",\n    info: \"For roads used mainly/exclusively for pedestrians in shopping and some residential areas which may allow access by motorised vehicles only for very limited periods of the day. To create a 'square' or 'plaza' create a closed way and tag as pedestrian and also with area=yes.\",\n  },\n  {\n    key: \"highway\",\n    value: \"track\",\n    info: \"Roads for mostly agricultural or forestry uses. To describe the quality of a track, see tracktype=*. Note: Although tracks are often rough with unpaved surfaces, this tag is not describing the quality of a road but its use. Consequently, if you want to tag a general use road, use one of the general highway values instead of track.\",\n  },\n  {\n    key: \"highway\",\n    value: \"bus_guideway\",\n    info: \"A busway where the vehicle guided by the way (though not a railway) and is not suitable for other traffic. Please note: this is not a normal bus lane, use access=no, psv=yes instead!\",\n  },\n  {\n    key: \"highway\",\n    value: \"escape\",\n    info: \"For runaway truck ramps, runaway truck lanes, emergency escape ramps, or truck arrester beds. It enables vehicles with braking failure to safely stop.\",\n  },\n  {\n    key: \"highway\",\n    value: \"raceway\",\n    info: \"A course or track for (motor) racing\",\n  },\n  {\n    key: \"highway\",\n    value: \"road\",\n    info: \"A road/way/street/motorway/etc. of unknown type. It can stand for anything ranging from a footpath to a motorway. This tag should only be used temporarily until the road/way/etc. has been properly surveyed. If you do know the road type, do not use this value, instead use one of the more specific highway=* values.\",\n  },\n  {\n    key: \"highway\",\n    value: \"busway\",\n    info: \"A dedicated roadway for bus rapid transit systems\",\n  },\n  {\n    key: \"highway\",\n    value: \"footway\",\n    info: \"For designated footpaths; i.e., mainly/exclusively for pedestrians. This includes walking tracks and gravel paths. If bicycles are allowed as well, you can indicate this by adding a bicycle=yes tag. Should not be used for paths where the primary or intended usage is unknown. Use highway=pedestrian for pedestrianised roads in shopping or residential areas and highway=track if it is usable by agricultural or similar vehicles. For ramps (sloped paths without steps), combine this tag with incline=*.\",\n  },\n  {\n    key: \"highway\",\n    value: \"bridleway\",\n    info: \"For horse riders. Pedestrians are usually also permitted, cyclists may be permitted depending on local rules/laws. Motor vehicles are forbidden.\",\n  },\n  {\n    key: \"highway\",\n    value: \"steps\",\n    info: \"For flights of steps (stairs) on footways. Use with step_count=* to indicate the number of steps\",\n  },\n  {\n    key: \"highway\",\n    value: \"corridor\",\n    info: \"For a hallway inside of a building.\",\n  },\n  {\n    key: \"highway\",\n    value: \"path\",\n    info: \"A non-specific path. Use highway=footway for paths mainly for walkers, highway=cycleway for one also usable by cyclists, highway=bridleway for ones available to horse riders as well as walkers and highway=track for ones which is passable by agriculture or similar vehicles.\",\n  },\n  {\n    key: \"highway\",\n    value: \"via_ferrata\",\n    info: \"A via ferrata is a route equipped with fixed cables, stemples, ladders, and bridges in order to increase ease and security for climbers. These via ferrata require equipment : climbing harness, shock absorber and two short lengths of rope, but do not require a long rope as for climbing.\",\n  },\n  {\n    key: \"highway\",\n    value: \"cycleway\",\n    info: \"For designated cycleways. Add foot=*, though it may be avoided if default-access-restrictions do apply.\",\n  },\n  {\n    key: \"highway\",\n    value: \"proposed\",\n    info: \"For planned roads, use with proposed=* and a value of the proposed highway value.\",\n  },\n  {\n    key: \"highway\",\n    value: \"construction\",\n    info: \"For roads under construction. Use construction=* to hold the value for the completed road.\",\n  },\n  {\n    key: \"highway\",\n    value: \"bus_stop\",\n    info: \"A small bus stop. Optionally one may also use public_transport=stop_position for the position where the vehicle stops and public_transport=platform for the place where passengers wait.\",\n  },\n  {\n    key: \"highway\",\n    value: \"crossing\",\n    info: \"A.k.a. crosswalk. Pedestrians can cross a street here; e.g., zebra crossing\",\n  },\n  {\n    key: \"highway\",\n    value: \"cyclist_waiting_aid\",\n    info: \"Street furniture for cyclists that are intended to make waiting at esp. traffic lights more comfortable.\",\n  },\n  {\n    key: \"highway\",\n    value: \"elevator\",\n    info: \"An elevator or lift, used to travel vertically, providing passenger and freight access between pathways at different floor levels.\",\n  },\n  {\n    key: \"highway\",\n    value: \"emergency_bay\",\n    info: \"An area beside a highway where you can safely stop your car in case of breakdown or emergency.\",\n  },\n  {\n    key: \"highway\",\n    value: \"emergency_access_point\",\n    info: \"Sign number which can be used to define your current position in case of an emergency. Use with ref=NUMBER_ON_THE_SIGN. See also emergency=access_point\",\n  },\n  {\n    key: \"highway\",\n    value: \"give_way\",\n    info: 'A \"give way,\" or \"Yield\" sign',\n  },\n  {\n    key: \"highway\",\n    value: \"ladder\",\n    info: \"A vertical or inclined set of steps or rungs intended for climbing or descending of a person with the help of hands.\",\n  },\n  {\n    key: \"highway\",\n    value: \"milestone\",\n    info: \"Highway location marker\",\n  },\n  {\n    key: \"highway\",\n    value: \"mini_roundabout\",\n    info: \"Similar to roundabouts, but at the center there is either a painted circle or a fully traversable island. In case of an untraversable center island, junction=roundabout should be used.\\n\\nRendered as anti-clockwise by default direction=anticlockwise. To render clockwise add the tag direction=clockwise.\",\n  },\n  {\n    key: \"highway\",\n    value: \"motorway_junction\",\n    info: \"Indicates a junction (UK) or exit (US). ref=* should be set to the exit number or junction identifier. (Some roads – e.g., the A14 – also carry junction numbers, so the tag may be encountered elsewhere despite its name)\",\n  },\n  {\n    key: \"highway\",\n    value: \"passing_place\",\n    info: \"The location of a passing space\",\n  },\n  {\n    key: \"highway\",\n    value: \"platform\",\n    info: \"A platform at a bus stop or station.\",\n  },\n  {\n    key: \"highway\",\n    value: \"rest_area\",\n    info: \"Place where drivers can leave the road to rest, but not refuel.\",\n  },\n  {\n    key: \"highway\",\n    value: \"services\",\n    info: \"A service station to get food and eat something, often found at motorways\",\n  },\n  {\n    key: \"highway\",\n    value: \"speed_camera\",\n    info: \"A fixed road-side or overhead speed camera.\",\n  },\n  {\n    key: \"highway\",\n    value: \"stop\",\n    info: \"A stop sign\",\n  },\n  {\n    key: \"highway\",\n    value: \"street_lamp\",\n    info: \"A street light, lamppost, street lamp, light standard, or lamp standard is a raised source of light on the edge of a road, which is turned on or lit at a certain time every night\",\n  },\n  {\n    key: \"highway\",\n    value: \"toll_gantry\",\n    info: \"A toll gantry is a gantry suspended over a way, usually a motorway, as part of a system of electronic toll collection. For a toll booth with any kind of barrier or booth see: barrier=toll_booth\",\n  },\n  {\n    key: \"highway\",\n    value: \"traffic_mirror\",\n    info: \"Mirror that reflects the traffic on one road when direct view is blocked.\",\n  },\n  {\n    key: \"highway\",\n    value: \"traffic_signals\",\n    info: \"Lights that control the traffic\",\n  },\n  {\n    key: \"highway\",\n    value: \"trailhead\",\n    info: \"Designated place to start on a trail or route\",\n  },\n  {\n    key: \"highway\",\n    value: \"turning_circle\",\n    info: \"A turning circle is a rounded, widened area usually, but not necessarily, at the end of a road to facilitate easier turning of a vehicle. Also known as a cul de sac.\",\n  },\n  {\n    key: \"highway\",\n    value: \"turning_loop\",\n    info: \"A widened area of a highway with a non-traversable island for turning around, often circular and at the end of a road.\",\n  },\n  {\n    key: \"highway\",\n    value: \"User Defined\",\n    info: \"All commonly used values according to Taginfo\",\n  },\n];\n\nconst amenities = [\n  {\n    key: \"amenity\",\n    value: \"bar\",\n    info: \"Bar is a purpose-built commercial establishment that sells alcoholic drinks to be consumed on the premises. They are characterised by a noisy and vibrant atmosphere, similar to a party and usually don't sell food. See also the description of the tags amenity=pub;bar;restaurant for a distinction between these.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"biergarten\",\n    info: \"Biergarten or beer garden is an open-air area where alcoholic beverages along with food is prepared and served. See also the description of the tags amenity=pub;bar;restaurant. A biergarten can commonly be found attached to a beer hall, pub, bar, or restaurant. In this case, you can use biergarten=yes additional to amenity=pub;bar;restaurant.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"cafe\",\n    info: \"Cafe is generally an informal place that offers casual meals and beverages; typically, the focus is on coffee or tea. Also known as a coffeehouse/shop, bistro or sidewalk cafe. The kind of food served may be mapped with the tags cuisine=* and diet:*=*. See also the tags amenity=restaurant;bar;fast_food.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"fast_food\",\n    info: \"Fast food restaurant (see also amenity=restaurant). The kind of food served can be tagged with cuisine=* and diet:*=*.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"food_court\",\n    info: \"An area with several different restaurant food counters and a shared eating area. Commonly found in malls, airports, etc.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"ice_cream\",\n    info: \"Ice cream shop or ice cream parlour. A place that sells ice cream and frozen yoghurt over the counter\",\n  },\n  {\n    key: \"amenity\",\n    value: \"pub\",\n    info: \"A place selling beer and other alcoholic drinks; may also provide food or accommodation (UK). See description of amenity=bar and amenity=pub for distinction between bar and pub\",\n  },\n  {\n    key: \"amenity\",\n    value: \"restaurant\",\n    info: \"Restaurant (not fast food, see amenity=fast_food). The kind of food served can be tagged with cuisine=* and diet:*=*.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"college\",\n    info: \"Campus or buildings of an institute of Further Education (aka continuing education)\",\n  },\n  {\n    key: \"amenity\",\n    value: \"dancing_school\",\n    info: \"A dancing school or dance studio\",\n  },\n  {\n    key: \"amenity\",\n    value: \"driving_school\",\n    info: \"Driving School which offers motor vehicle driving lessons\",\n  },\n  {\n    key: \"amenity\",\n    value: \"first_aid_school\",\n    info: \"A place where people can go for first aid courses.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"kindergarten\",\n    info: \"For children too young for a regular school (also known as preschool, playschool or nursery school), in some countries including afternoon supervision of primary school children.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"language_school\",\n    info: \"Language School: an educational institution where one studies a foreign language.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"library\",\n    info: \"A public library (municipal, university, …) to borrow books from.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"surf_school\",\n    info: \"A surf school is an establishment that teaches surfing.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"toy_library\",\n    info: \"A place to borrow games and toys, or play with them on site.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"research_institute\",\n    info: \"An establishment endowed for doing research.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"training\",\n    info: \"Public place where you can get training.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"music_school\",\n    info: \"A music school, an educational institution specialized in the study, training, and research of music.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"school\",\n    info: \"School and grounds - primary, middle and seconday schools\",\n  },\n  {\n    key: \"amenity\",\n    value: \"traffic_park\",\n    info: \"Juvenile traffic schools\",\n  },\n  {\n    key: \"amenity\",\n    value: \"university\",\n    info: \"An university campus: an institute of higher education\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bicycle_parking\",\n    info: \"Parking for bicycles\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bicycle_repair_station\",\n    info: \"General tools for self-service bicycle repairs, usually on the roadside; no service\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bicycle_rental\",\n    info: \"Rent a bicycle\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bicycle_wash\",\n    info: \"Clean a bicycle\",\n  },\n  {\n    key: \"amenity\",\n    value: \"boat_rental\",\n    info: \"Rent a Boat\",\n  },\n  {\n    key: \"amenity\",\n    value: \"boat_sharing\",\n    info: \"Share a Boat\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bus_station\",\n    info: \"May also be tagged as public_transport=station.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"car_rental\",\n    info: \"Rent a car\",\n  },\n  {\n    key: \"amenity\",\n    value: \"car_sharing\",\n    info: \"Share a car\",\n  },\n  {\n    key: \"amenity\",\n    value: \"car_wash\",\n    info: \"Wash a car\",\n  },\n  {\n    key: \"amenity\",\n    value: \"compressed_air\",\n    info: \"A device to inflate tires/tyres (e.g. motorcar, bicycle)\",\n  },\n  {\n    key: \"amenity\",\n    value: \"vehicle_inspection\",\n    info: \"Government vehicle inspection\",\n  },\n  {\n    key: \"amenity\",\n    value: \"charging_station\",\n    info: \"Charging facility for electric vehicles\",\n  },\n  {\n    key: \"amenity\",\n    value: \"driver_training\",\n    info: \"A place for driving training on a closed course\",\n  },\n  {\n    key: \"amenity\",\n    value: \"ferry_terminal\",\n    info: \"Ferry terminal/stop. A place where people/cars/etc. can board and leave a ferry.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"fuel\",\n    info: \"Petrol station; gas station; marine fuel; … Streets to petrol stations are often tagged highway=service.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"grit_bin\",\n    info: \"A container that holds grit or a mixture of salt and grit.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"motorcycle_parking\",\n    info: \"Parking for motorcycles\",\n  },\n  {\n    key: \"amenity\",\n    value: \"parking\",\n    info: \"Car park. Nodes and areas (without access tag) will get a parking symbol. Areas will be coloured. Streets on car parking are often tagged highway=service and service=parking_aisle.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"parking_entrance\",\n    info: \"An entrance or exit to an underground or multi-storey parking facility. Group multiple parking entrances together with a relation using the tags type=site and site=parking. Do not mix with amenity=parking.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"parking_space\",\n    info: \"A single parking space. Group multiple parking spaces together with a relation using the tags type=site and site=parking. Do not mix with amenity=parking.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"taxi\",\n    info: \"A place where taxis wait for passengers.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"weighbridge\",\n    info: \"A large weight scale to weigh vehicles and goods\",\n  },\n  {\n    key: \"amenity\",\n    value: \"atm\",\n    info: \"ATM or cash point: a device that provides the clients of a financial institution with access to financial transactions.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"payment_terminal\",\n    info: \"Self-service payment kiosk/terminal\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bank\",\n    info: \"Bank or credit union: a financial establishment where customers can deposit and withdraw money, take loans, make investments and transfer funds.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bureau_de_change\",\n    info: \"Bureau de change, money changer, currency exchange, Wechsel, cambio – a place to change foreign bank notes and travellers cheques.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"money_transfer\",\n    info: \"A place that offers money transfers, especially cash to cash\",\n  },\n  {\n    key: \"amenity\",\n    value: \"payment_centre\",\n    info: \"A non-bank place, where people can pay bills of public and private services and taxes.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"baby_hatch\",\n    info: \"A place where a baby can be, out of necessity, anonymously left to be safely cared for and perhaps adopted.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"clinic\",\n    info: \"A medium-sized medical facility or health centre.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"dentist\",\n    info: \"A dentist practice / surgery.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"doctors\",\n    info: \"A doctor's practice / surgery.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"hospital\",\n    info: \"A hospital providing in-patient medical treatment. Often used in conjunction with emergency=* to note whether the medical centre has emergency facilities (A&E (brit.) or ER (am.))\",\n  },\n  {\n    key: \"amenity\",\n    value: \"nursing_home\",\n    info: \"Discouraged tag for a home for disabled or elderly persons who need permanent care. Use amenity=social_facility + social_facility=nursing_home now.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"pharmacy\",\n    info: \"Pharmacy: a shop where a pharmacist sells medications\\ndispensing=yes/no - availability of prescription-only medications\",\n  },\n  {\n    key: \"amenity\",\n    value: \"social_facility\",\n    info: \"A facility that provides social services: group & nursing homes, workshops for the disabled, homeless shelters, etc.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"veterinary\",\n    info: \"A place where a veterinary surgeon, also known as a veterinarian or vet, practices.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"arts_centre\",\n    info: \"A venue where a variety of arts are performed or conducted\",\n  },\n  {\n    key: \"amenity\",\n    value: \"brothel\",\n    info: \"An establishment specifically dedicated to prostitution\",\n  },\n  {\n    key: \"amenity\",\n    value: \"casino\",\n    info: \"A gambling venue with at least one table game(e.g. roulette, blackjack) that takes bets on sporting and other events at agreed upon odds.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"cinema\",\n    info: \"A place where films are shown (US: movie theater)\",\n  },\n  {\n    key: \"amenity\",\n    value: \"community_centre\",\n    info: \"A place mostly used for local events, festivities and group activities; including special interest and special age groups. .\",\n  },\n  {\n    key: \"amenity\",\n    value: \"conference_centre\",\n    info: \"A large building that is used to hold a convention\",\n  },\n  {\n    key: \"amenity\",\n    value: \"events_venue\",\n    info: \"A building specifically used for organising events\",\n  },\n  {\n    key: \"amenity\",\n    value: \"exhibition_centre\",\n    info: \"An exhibition centre\",\n  },\n  {\n    key: \"amenity\",\n    value: \"fountain\",\n    info: \"A fountain for cultural / decorational / recreational purposes.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"gambling\",\n    info: \"A place for gambling, not being a shop=bookmaker, shop=lottery, amenity=casino, or leisure=adult_gaming_centre.\\n\\nGames that are covered by this definition include bingo and pachinko.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"love_hotel\",\n    info: \"A love hotel is a type of short-stay hotel operated primarily for the purpose of allowing guests privacy for sexual activities.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"music_venue\",\n    info: \"An indoor place to hear contemporary live music.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"nightclub\",\n    info: 'A place to drink and dance (nightclub). The German word is \"Disco\" or \"Discothek\". Please don\\'t confuse this with the German \"Nachtclub\" which is most likely amenity=stripclub.',\n  },\n  {\n    key: \"amenity\",\n    value: \"planetarium\",\n    info: \"A planetarium.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"public_bookcase\",\n    info: \"A street furniture containing books. Take one or leave one.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"social_centre\",\n    info: \"A place for free and not-for-profit activities.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"stage\",\n    info: \"A raised platform for performers.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"stripclub\",\n    info: \"A place that offers striptease or lapdancing (for sexual services use amenity=brothel).\",\n  },\n  {\n    key: \"amenity\",\n    value: \"studio\",\n    info: \"TV radio or recording studio\",\n  },\n  {\n    key: \"amenity\",\n    value: \"swingerclub\",\n    info: \"A club where people meet to have a party and group sex.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"theatre\",\n    info: \"A theatre or opera house where live performances occur, such as plays, musicals and formal concerts. Use amenity=cinema for movie theaters.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"courthouse\",\n    info: \"A building home to a court of law, where justice is dispensed\",\n  },\n  {\n    key: \"amenity\",\n    value: \"fire_station\",\n    info: \"A station of a fire brigade\",\n  },\n  {\n    key: \"amenity\",\n    value: \"police\",\n    info: \"A police station where police officers patrol from and that is a first point of contact for civilians\",\n  },\n  {\n    key: \"amenity\",\n    value: \"post_box\",\n    info: \"A box for the reception of mail. Alternative mail-carriers can be tagged via operator=*\",\n  },\n  {\n    key: \"amenity\",\n    value: \"post_depot\",\n    info: \"Post depot or delivery office, where letters and parcels are collected and sorted prior to delivery.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"post_office\",\n    info: \"Post office building with postal services\",\n  },\n  {\n    key: \"amenity\",\n    value: \"prison\",\n    info: \"A prison or jail where people are incarcerated before trial or after conviction\",\n  },\n  {\n    key: \"amenity\",\n    value: \"ranger_station\",\n    info: \"National Park visitor headquarters: official park visitor facility with police, visitor information, permit services, etc\",\n  },\n  {\n    key: \"amenity\",\n    value: \"townhall\",\n    info: \"Building where the administration of a village, town or city may be located, or just a community meeting place\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bbq\",\n    info: \"BBQ or Barbecue is a permanently built grill for cooking food, which is most typically used outdoors by the public. For example these may be found in city parks or at beaches. Use the tag fuel=* to specify the source of heating, such as fuel=wood;electric;charcoal. For mapping nearby table and chairs, see also the tag tourism=picnic_site. For mapping campfires and firepits, instead use the tag leisure=firepit.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"bench\",\n    info: \"A bench to sit down and relax a bit\",\n  },\n  {\n    key: \"amenity\",\n    value: \"dog_toilet\",\n    info: \"Area designated for dogs to urinate and excrete.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"dressing_room\",\n    info: \"Area designated for changing clothes.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"drinking_water\",\n    info: \"Drinking water is a place where humans can obtain potable water for consumption. Typically, the water is used for only drinking. Also known as a drinking fountain or bubbler.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"give_box\",\n    info: \"A small facility where people drop off and pick up various types of items in the sense of free sharing and reuse.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"mailroom\",\n    info: \"A mailroom for receiving packages or letters.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"parcel_locker\",\n    info: \"Machine for picking up and sending parcels\",\n  },\n  {\n    key: \"amenity\",\n    value: \"shelter\",\n    info: \"A small shelter against bad weather conditions. To additionally describe the kind of shelter use shelter_type=*.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"shower\",\n    info: \"Public shower.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"telephone\",\n    info: \"Public telephone\",\n  },\n  {\n    key: \"amenity\",\n    value: \"toilets\",\n    info: \"Public toilets (might require a fee)\",\n  },\n  {\n    key: \"amenity\",\n    value: \"water_point\",\n    info: \"Place where you can get large amounts of drinking water\",\n  },\n  {\n    key: \"amenity\",\n    value: \"watering_place\",\n    info: \"Place where water is contained and animals can drink\",\n  },\n  {\n    key: \"amenity\",\n    value: \"sanitary_dump_station\",\n    info: \"A place for depositing human waste from a toilet holding tank.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"recycling\",\n    info: \"Recycling facilities (bottle banks, etc.). Combine with recycling_type=container for containers or recycling_type=centre for recycling centres.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"waste_basket\",\n    info: \"A single small container for depositing garbage that is easily accessible for pedestrians.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"waste_disposal\",\n    info: \"A medium or large disposal bin, typically for bagged up household or industrial waste.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"waste_transfer_station\",\n    info: \"A waste transfer station is a location that accepts, consolidates and transfers waste in bulk.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"animal_boarding\",\n    info: \"A facility where you, paying a fee, can bring your animal for a limited period of time (e.g. for holidays)\",\n  },\n  {\n    key: \"amenity\",\n    value: \"animal_breeding\",\n    info: \"A facility where animals are bred, usually to sell them\",\n  },\n  {\n    key: \"amenity\",\n    value: \"animal_shelter\",\n    info: \"A shelter that recovers animals in trouble\",\n  },\n  {\n    key: \"amenity\",\n    value: \"animal_training\",\n    info: \"A facility used for non-competitive animal training\",\n  },\n  {\n    key: \"amenity\",\n    value: \"baking_oven\",\n    info: \"An oven used for baking bread and similar, for example inside a building=bakehouse.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"clock\",\n    info: \"A public visible clock\",\n  },\n  {\n    key: \"amenity\",\n    value: \"crematorium\",\n    info: \"A place where dead human bodies are burnt\",\n  },\n  {\n    key: \"amenity\",\n    value: \"dive_centre\",\n    info: \"A dive center is the base location where sports divers usually start scuba diving or make dive guided trips at new locations.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"funeral_hall\",\n    info: \"A place for holding a funeral ceremony, other than a place of worship.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"grave_yard\",\n    info: \"A (smaller) place of burial, often you'll find a church nearby. Large places should be landuse=cemetery instead.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"hunting_stand\",\n    info: \"A hunting stand: an open or enclosed platform used by hunters to place themselves at an elevated height above the terrain\",\n  },\n  {\n    key: \"amenity\",\n    value: \"internet_cafe\",\n    info: \"A place whose principal role is providing internet services to the public.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"kitchen\",\n    info: \"A public kitchen in a facility to use by everyone or customers\",\n  },\n  {\n    key: \"amenity\",\n    value: \"kneipp_water_cure\",\n    info: \"Outdoor foot bath facility. Usually this is a pool with cold water and handrail. Popular in German speaking countries.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"lounger\",\n    info: \"An object for people to lie down.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"marketplace\",\n    info: \"A marketplace where goods and services are traded daily or weekly.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"monastery\",\n    info: \"Monastery is the location of a monastery or a building in which monks and nuns live.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"mortuary\",\n    info: \"A morgue or funeral home, used for the storage of human corpses.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"photo_booth\",\n    info: \"A stand to create instant photos.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"place_of_mourning\",\n    info: \"A room or building where families and friends can come, before the funeral, and view the body of the person who has died.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"place_of_worship\",\n    info: \"A church, mosque, or temple, etc. Note that you also need religion=*, usually denomination=* and preferably name=* as well as amenity=place_of_worship. See the article for details.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"public_bath\",\n    info: \"A location where the public may bathe in common, etc. japanese onsen, turkish bath, hot spring\",\n  },\n  {\n    key: \"amenity\",\n    value: \"public_building\",\n    info: \"A generic public building. Don't use! See office=government.\",\n  },\n  {\n    key: \"amenity\",\n    value: \"refugee_site\",\n    info: \"A human settlement sheltering refugees or internally displaced persons\",\n  },\n  {\n    key: \"amenity\",\n    value: \"vending_machine\",\n    info: \"A machine selling goods – food, tickets, newspapers, etc. Add type of goods using vending=*\",\n  },\n  {\n    key: \"amenity\",\n    value: \"user defined\",\n    info: \"All commonly used values according to Taginfo\",\n  },\n];\n\nconst buildings = [\n  {\n    key: \"building\",\n    value: \"apartments\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"barracks\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"bungalow\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"cabin\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"detached\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"dormitory\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"farm\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"ger\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"hotel\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"house\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"houseboat\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"residential\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"semidetached_house\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"static_caravan\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"stilt_house\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"terrace\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"tree_house\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"trullo\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"commercial\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"industrial\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"kiosk\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"office\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"retail\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"supermarket\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"warehouse\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"religious\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"cathedral\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"chapel\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"church\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"kingdom_hall\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"monastery\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"mosque\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"presbytery\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"shrine\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"synagogue\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"temple\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"bakehouse\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"bridge\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"civic\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"college\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"fire_station\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"government\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"gatehouse\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"hospital\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"kindergarten\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"museum\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"public\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"school\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"toilets\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"train_station\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"transportation\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"university\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"barn\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"conservatory\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"cowshed\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"farm_auxiliary\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"greenhouse\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"slurry_tank\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"stable\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"sty\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"livestock\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"grandstand\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"pavilion\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"riding_hall\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"sports_hall\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"sports_centre\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"stadium\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"allotment_house\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"boathouse\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"hangar\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"hut\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"shed\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"carport\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"garage\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"garages\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"parking\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"digester\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"service\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"tech_cab\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"transformer_tower\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"water_tower\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"storage_tank\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"silo\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"beach_hut\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"bunker\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"castle\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"construction\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"container\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"guardhouse\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"military\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"outbuilding\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"pagoda\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"quonset_hut\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"roof\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"ruins\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"tent\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"tower\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"windmill\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"yes\",\n    info: \"\",\n  },\n  {\n    key: \"building\",\n    value: \"user defined\",\n    info: \"\",\n  },\n];\n\nconst natural = [\n  {\n    key: \"natural\",\n    value: \"fell\",\n    info: \"Habitat above the tree line covered with grass, dwarf shrubs and mosses.\",\n  },\n  {\n    key: \"natural\",\n    value: \"grassland\",\n    info: \"Areas where the vegetation is dominated by grasses (Poaceae) and other herbaceous (non-woody) plants. For mown/managed grass see landuse=grass, for hay/pasture see landuse=meadow.\",\n  },\n  {\n    key: \"natural\",\n    value: \"heath\",\n    info: 'A dwarf-shrub habitat, characterised by open, low growing woody vegetation, often dominated by plants of the Ericaceae.\\nNote. This is not for parks whose name contains the word \"heath\".',\n  },\n  {\n    key: \"natural\",\n    value: \"moor\",\n    info: \"Don't use, see wikipage. Upland areas, characterised by low-growing vegetation on acidic soils.\",\n  },\n  {\n    key: \"natural\",\n    value: \"scrub\",\n    info: \"Uncultivated land covered with shrubs, bushes or stunted trees.\",\n  },\n  {\n    key: \"natural\",\n    value: \"shrubbery\",\n    info: \"An area of shrubbery that is actively maintained or pruned by humans. A slightly wilder look is also possible\",\n  },\n  {\n    key: \"natural\",\n    value: \"tree\",\n    info: \"A single tree.\",\n  },\n  {\n    key: \"natural\",\n    value: \"tree_row\",\n    info: \"A line of trees.\",\n  },\n  {\n    key: \"natural\",\n    value: \"tree_stump\",\n    info: \"A tree stump, the remains of a cut down or broken tree.\",\n  },\n  {\n    key: \"natural\",\n    value: \"tundra\",\n    info: \"Habitat above tree line in alpine and subpolar regions, principally covered with uncultivated grass, low growing shrubs and mosses and sometimes grazed.\",\n  },\n  {\n    key: \"natural\",\n    value: \"wood\",\n    info: \"Tree-covered area (a 'forest' or 'wood'). Also see landuse=forest. For more detail, one can use leaf_type=* and leaf_cycle=*.\",\n  },\n  {\n    key: \"natural\",\n    value: \"bay\",\n    info: \"An area of water mostly surrounded by land but with a level connection to the ocean or a lake.\",\n  },\n  {\n    key: \"natural\",\n    value: \"beach\",\n    info: \"landform along a body of water which consists of sand, shingle or other loose material\",\n  },\n  {\n    key: \"natural\",\n    value: \"blowhole\",\n    info: \"An opening to a sea cave which has grown landwards resulting in blasts of water from the opening due to the wave action\",\n  },\n  {\n    key: \"natural\",\n    value: \"cape\",\n    info: \"A piece of elevated land sticking out into the sea or large lake. Includes capes, heads, headlands and (water) promontories.\",\n  },\n  {\n    key: \"natural\",\n    value: \"coastline\",\n    info: \"The mean high water springs line between the sea and land (with the water on the right side of the way.)\",\n  },\n  {\n    key: \"natural\",\n    value: \"crevasse\",\n    info: \"A large crack in a glacier\",\n  },\n  {\n    key: \"natural\",\n    value: \"geyser\",\n    info: \"A spring characterized by intermittent discharge of water ejected turbulently and accompanied by steam.\",\n  },\n  {\n    key: \"natural\",\n    value: \"glacier\",\n    info: \"A permanent body of ice formed naturally from snow that is moving under its own weight.\",\n  },\n  {\n    key: \"natural\",\n    value: \"hot_spring\",\n    info: \"A spring of geothermally heated groundwater\",\n  },\n  {\n    key: \"natural\",\n    value: \"isthmus\",\n    info: \"A narrow strip of land, bordered by water on both sides and connecting two larger land masses.\",\n  },\n  {\n    key: \"natural\",\n    value: \"mud\",\n    info: \"Area covered with mud: water saturated fine grained soil without significant plant growth. Also see natural=wetland + wetland=*.\",\n  },\n  {\n    key: \"natural\",\n    value: \"peninsula\",\n    info: \"A piece of land projecting into water from a larger land mass, nearly surrounded by water\",\n  },\n  {\n    key: \"natural\",\n    value: \"reef\",\n    info: \"A feature (rock, sandbar, coral, etc) lying beneath the surface of the water\",\n  },\n  {\n    key: \"natural\",\n    value: \"shingle\",\n    info: \"An accumulation of rounded rock fragments on a beach or riverbed\",\n  },\n  {\n    key: \"natural\",\n    value: \"shoal\",\n    info: \"An area of the sea floor near the sea surface (literally, becomes shallow) and exposed at low tide. See natural=sand as well.\",\n  },\n  {\n    key: \"natural\",\n    value: \"spring\",\n    info: \"A place where ground water flows naturally from the ground (Other languages).\",\n  },\n  {\n    key: \"natural\",\n    value: \"strait\",\n    info: \"A narrow area of water surrounded by land on two sides and by water on two other sides.\",\n  },\n  {\n    key: \"natural\",\n    value: \"water\",\n    info: \"Any body of water, from natural such as a lake or pond to artificial like moat or canal. Also see waterway=riverbank\",\n  },\n  {\n    key: \"natural\",\n    value: \"wetland\",\n    info: \"A natural area subject to inundation or with waterlogged ground, further specified with wetland=*\",\n  },\n  {\n    key: \"natural\",\n    value: \"arch\",\n    info: \"A rock arch naturally formed by erosion, with an opening underneath.\",\n  },\n  {\n    key: \"natural\",\n    value: \"arete\",\n    info: \"An arête, a thin, almost knife-like, ridge of rock which is typically formed when two glaciers erode parallel U-shaped valleys.\",\n  },\n  {\n    key: \"natural\",\n    value: \"bare_rock\",\n    info: \"An area with sparse/no soil or vegetation, so that the bedrock becomes visible.\",\n  },\n  {\n    key: \"natural\",\n    value: \"natural=blockfield\",\n    info: \"A surface covered with boulders or block-sized rocks, usually the result of volcanic activity or associated with alpine and subpolar climates and ice ages.\",\n  },\n  {\n    key: \"natural\",\n    value: \"cave_entrance\",\n    info: \"The entrance to a cave: a natural underground space large enough for a human to enter.\",\n  },\n  {\n    key: \"natural\",\n    value: \"cliff\",\n    info: \"A vertical or almost vertical natural drop in terrain, usually with a bare rock surface (leave the lower face to the right of the way).\",\n  },\n  {\n    key: \"natural\",\n    value: \"dune\",\n    info: \"A hill of sand formed by wind, covered with no or very little vegetation. See also natural=sand and natural=beach\",\n  },\n  {\n    key: \"natural\",\n    value: \"earth_bank\",\n    info: \"Large erosion gully or steep earth bank\",\n  },\n  {\n    key: \"natural\",\n    value: \"fumarole\",\n    info: \"A fumarole is an opening in a planet's crust, which emits steam and gases\",\n  },\n  {\n    key: \"natural\",\n    value: \"gully\",\n    info: \"Small scale cut in relief created by water erosion\",\n  },\n  {\n    key: \"natural\",\n    value: \"hill\",\n    info: \"A hill.\",\n  },\n  {\n    key: \"natural\",\n    value: \"peak\",\n    info: \"The top (summit) of a hill or mountain.\",\n  },\n  {\n    key: \"natural\",\n    value: \"ridge\",\n    info: \"A mountain or hill linear landform with a continuous elevated crest\",\n  },\n  {\n    key: \"natural\",\n    value: \"rock\",\n    info: \"A notable rock or group of rocks attached to the underlying bedrock.\",\n  },\n  {\n    key: \"natural\",\n    value: \"saddle\",\n    info: \"The lowest point along a ridge or between two mountain tops\",\n  },\n  {\n    key: \"natural\",\n    value: \"sand\",\n    info: \"An area covered by sand with no or very little vegetation. See natural=beach and natural=dune as well.\",\n  },\n  {\n    key: \"natural\",\n    value: \"scree\",\n    info: \"Unconsolidated angular rocks formed by rockfall and weathering from adjacent rockfaces.\",\n  },\n  {\n    key: \"natural\",\n    value: \"sinkhole\",\n    info: \"A natural depression or hole in the surface topography.\",\n  },\n  {\n    key: \"natural\",\n    value: \"stone\",\n    info: \"A single notable freestanding rock, which may differ from the composition of the terrain it lies in.; e.g., glacial erratic.\",\n  },\n  {\n    key: \"natural\",\n    value: \"valley\",\n    info: \"A natural depression flanked by ridges or ranges of mountains or hills\",\n  },\n  {\n    key: \"natural\",\n    value: \"volcano\",\n    info: \"An opening exposed on the earth's surface where volcanic material is emitted.\",\n  },\n  {\n    key: \"natural\",\n    value: \"user defined\",\n    info: \"All commonly used values according to Taginfo\",\n  },\n];\n\nconst boundaries = [\n  {\n    key: \"boundary\",\n    value: \"aboriginal_lands\",\n    info: \"A boundary representing official reservation boundaries of recognized aboriginal / indigenous / native peoples.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"administrative\",\n    info: \"An administrative boundary. Subdivisions of areas/territories/jurisdictions recognised by governments or other organisations for administrative purposes. These range from large groups of nation states right down to small administrative districts and suburbs, as indicated by the 'admin_level=*' combo tag.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"border_zone\",\n    info: \"A border zone is an area near the border where special restrictions on movement apply. Usually a permit is required for visiting.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"forest\",\n    info: \"A delimited forest is a land which is predominantly wooded and which is, for this reason, given defined boundaries. It may cover different tree stands, non-wooded areas, highways… but all the area within the boundaries are considered and managed as a single forest.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"forest_compartment\",\n    info: \"A forest compartment is a numbered sub-division within a delimited forest, physically materialized with visible, typically cleared, boundaries.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"hazard\",\n    info: \"A designated hazardous area, with a potential source of damage to health, life, property, or any other interest of value.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"maritime\",\n    info: \"Maritime boundaries which are not administrative boundaries: the Baseline, Contiguous Zone and EEZ (Exclusive Economic Zone).\",\n  },\n  {\n    key: \"boundary\",\n    value: \"marker\",\n    info: \"A boundary marker, border marker, boundary stone, or border stone is a robust physical marker that identifies the start of a land boundary or the change in a boundary, especially a change in direction of a boundary. See also historic=boundary_stone\",\n  },\n  {\n    key: \"boundary\",\n    value: \"national_park\",\n    info: \"Area of outstanding natural beauty, set aside for conservation and for recreation (Other languages).\",\n  },\n  {\n    key: \"boundary\",\n    value: \"place\",\n    info: \"boundary=place is commonly used to map the boundaries of a place=*, when these boundaries can be defined but these are not administrative boundaries.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"political\",\n    info: \"Electoral boundaries\",\n  },\n  {\n    key: \"boundary\",\n    value: \"postal_code\",\n    info: \"Postal code boundaries\",\n  },\n  {\n    key: \"boundary\",\n    value: \"protected_area\",\n    info: \"Protected areas, such as for national parks, marine protection areas, heritage sites, wilderness, cultural assets and similar.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"special_economic_zone\",\n    info: \"A government-defined area in which business and trade laws are different.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"disputed\",\n    info: \"An area of landed claimed by two or more parties (use with caution). See also Disputed territories.\",\n  },\n  {\n    key: \"boundary\",\n    value: \"user defined\",\n    info: \"All commonly used values according to Taginfo\",\n  },\n];\n\nexport const predefinedOsmQueries = {\n  amenities: {\n    nodeType: `node`,\n    subTypeTag: `amenity`,\n    subTypes: amenities,\n  },\n  roads: {\n    nodeType: \"way\",\n    subTypeTag: \"highway\",\n    subTypes: highway,\n  },\n  buildings: {\n    nodeType: `way`,\n    subTypeTag: `building`,\n    subTypes: buildings,\n  },\n  natural: {\n    nodeType: `node`,\n    subTypeTag: `natural`,\n    subTypes: natural,\n  },\n  boundaries: {\n    nodeType: `relation`,\n    subTypeTag: `boundary`,\n    subTypes: boundaries,\n  },\n  country_boundary: `relation[\"boundary\"=\"administrative\"][\"admin_level\"=\"2\"]`,\n  boundary_3: `relation[\"boundary\"=\"administrative\"][\"admin_level\"=\"3\"]`,\n  boundary_4: `relation[\"boundary\"=\"administrative\"][\"admin_level\"=\"4\"]`,\n  boundary_5: `relation[\"boundary\"=\"administrative\"][\"admin_level\"=\"5\"]`,\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/W_Map.tsx",
    "content": "import { isObject } from \"@common/publishUtils\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { AnyObject, ParsedJoinPath } from \"prostgles-types\";\nimport {\n  getKeys,\n  isDefined,\n  isEmpty,\n  reverseParsedPath,\n} from \"prostgles-types\";\nimport React from \"react\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { WindowData, WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport type {\n  DeckGlColor,\n  GeoJSONFeature,\n  GeoJsonLayerProps,\n  HoverCoords,\n  MapHandler,\n} from \"../Map/DeckGLMap\";\nimport { DeckGLMap } from \"../Map/DeckGLMap\";\nimport type { DeltaOfData } from \"../RTComp\";\nimport RTComp from \"../RTComp\";\nimport { SmartForm } from \"../SmartForm/SmartForm\";\nimport type { ActiveRow } from \"../W_Table/W_Table\";\nimport W_Table from \"../W_Table/W_Table\";\nimport Window from \"../Window\";\nimport { DataLayerManager } from \"../WindowControls/DataLayerManager/DataLayerManager\";\nimport { W_MapMenu } from \"./W_MapMenu\";\nimport { MapInfoSection } from \"./controls/MapInfoSection\";\nimport { fetchMapLayerData } from \"./fetchData/fetchMapLayerData\";\nimport { getMapFilter } from \"./fetchData/getMapData\";\nimport { getMapDataExtent } from \"./fetchData/getMapDataExtent\";\nimport type { HoveredObject } from \"./onMapHover\";\nimport { onMapHover } from \"./onMapHover\";\nimport type { SingleSyncHandles } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\n\nexport type LayerBase = {\n  /**\n   * Link id + layer col index\n   * */\n  _id: string;\n\n  /**\n   * Link id\n   * */\n  linkId: string;\n\n  color: string;\n\n  /** If missing then it's a local layer */\n  wid?: string;\n  fillColor: DeckGlColor;\n  lineColor: DeckGlColor;\n  getLineColor?: () => DeckGlColor;\n  elevation?: number;\n  geomColumn: string;\n\n  /**\n   * Columns to show on hover. Will be selected\n   * Defaults to all columns\n   */\n  tooltipColumns?: string[];\n  tooltipRender?: (row: AnyObject) => React.ReactNode;\n\n  disabled: boolean;\n};\n\nexport type LayerOSM = LayerBase & {\n  type: \"osm\";\n  query: string;\n};\n\nexport type LayerSQL = LayerBase & {\n  type: \"sql\";\n  sql: string;\n  withStatement: string;\n  parameters?: any;\n};\n\nexport type LayerTable = LayerBase & {\n  type: \"table\";\n  tableName: string;\n  externalFilters: AnyObject[];\n  tableFilter?: AnyObject;\n  joinFilter?: AnyObject;\n} & (\n    | {\n        joinStartTable: string;\n        path: ParsedJoinPath[];\n      }\n    | {\n        joinStartTable: undefined;\n        path: undefined;\n      }\n  );\n\nexport type LayerQuery = LayerTable | LayerSQL | LayerOSM;\n\nexport type W_MapProps = CommonWindowProps<\"map\"> & {\n  layerQueries?: LayerQuery[];\n  onClickRow: (row: AnyObject | undefined, tableName: string) => any;\n  myActiveRow: ActiveRow | undefined;\n};\n\nexport type MapLayerExtras = Record<\n  string,\n  { colorStr: string; color: [number, number, number] }\n>;\n\nexport type ClickedItem = GeoJSONFeature & {\n  properties: GeoJSONFeature[\"properties\"] & {\n    $rowhash?: string;\n    geomColumn: string;\n    tableName: string;\n    l: GeoJSONFeature[\"geometry\"];\n    layer: {\n      _id: string;\n    };\n  } & (\n      | {\n          i: AnyObject | string;\n        }\n      | {\n          /** Aggregation size */\n          c: string;\n          radius: number;\n          i?: undefined;\n        }\n    );\n};\n\nexport type W_MapState = {\n  loading: boolean;\n  loadingLayers: boolean;\n  wSync: SingleSyncHandles<Required<WindowData<\"map\">>, true> | null;\n  minimised: boolean;\n  layers?: GeoJsonLayerProps[];\n  lqs: LayerQuery[];\n  error: unknown;\n  hoverObj?: any;\n  hoverCoords?: HoverCoords;\n  hovData?: any;\n\n  bytesPerSec: number;\n  dataAge: number;\n\n  drawingShape?: GeoJSONFeature;\n\n  isDrawing?: boolean;\n\n  clickedItem?: ClickedItem;\n};\n\ntype D = {\n  w: WindowSyncItem<\"map\">;\n};\n\nexport default class W_Map extends RTComp<W_MapProps, W_MapState, D> {\n  refHeader?: HTMLDivElement;\n  refResize?: HTMLElement;\n  ref?: HTMLElement;\n\n  state: W_MapState = {\n    loadingLayers: false,\n    loading: false,\n    wSync: null,\n    minimised: false,\n    // layers: [],\n    lqs: [],\n    error: null,\n    hoverObj: undefined,\n    hoverCoords: undefined,\n    hovData: undefined,\n    bytesPerSec: 0,\n    dataAge: 0,\n  };\n\n  getDataSignature(\n    args: Parameters<typeof W_Table.getTableDataRequestSignature>[0],\n    dataAge: number,\n    layer: LayerQuery,\n    other: any,\n  ): { signature: string; cachedLayer?: GeoJsonLayerProps } {\n    const signature = W_Table.getTableDataRequestSignature(args, dataAge, [\n      layer,\n      other,\n    ]);\n    const cachedLayer = this.state.layers?.find(\n      (l) => l.dataSignature === signature,\n    );\n\n    return { signature, cachedLayer };\n  }\n\n  onMount() {\n    const { w } = this.props;\n    if (!this.state.wSync) {\n      const wSync = w.$cloneSync((w, delta) => {\n        this.setData({ w }, { w: delta });\n      });\n      this.setState({ wSync });\n    }\n  }\n  onUnmount() {\n    this.state.wSync?.$unsync();\n    this.layerSubs.map((s) => {\n      s.sub.unsubscribe();\n    });\n  }\n\n  goToDataExtent = async () => {\n    let extent;\n    try {\n      extent = await this.getDataExtent();\n      if (extent && this.d.w) {\n        this.d.w.$update(\n          { options: { extent: extent.flat() } },\n          { deepMerge: true },\n        );\n        this.map?.fitBounds(extent);\n      } else {\n        // return\n      }\n    } catch (error: any) {\n      if (this.state.error?.toString() !== error.toString()) {\n        this.setState({ error });\n      }\n      console.error(error);\n      return;\n    }\n  };\n\n  autoRefreshInterval: NodeJS.Timeout | undefined;\n  gettingExtent = false;\n  onDelta = (\n    dp: Partial<W_MapProps>,\n    ds: Partial<W_MapState>,\n    dd: DeltaOfData<D>,\n  ) => {\n    const delta = { ...dp, ...ds, ...dd };\n\n    const ns: any = {};\n\n    if (this.d.w) {\n      if (\n        !this.gettingExtent &&\n        !this.d.w.options.extent &&\n        this.props.layerQueries?.length\n      ) {\n        this.gettingExtent = true;\n        // this.setState({ loadingExtent: true })\n        setTimeout(() => {\n          if (this.mounted) {\n            this.goToDataExtent();\n            this.gettingExtent = false;\n            // this.setState({ loadingExtent: false })\n          }\n        }, 600);\n      }\n      const deltaOpts =\n        delta.w?.options ?? ({} as WindowData<\"map\">[\"options\"]);\n      const changedOpts = getKeys(deltaOpts || {});\n      const changedWkeys = getKeys(delta.w || {});\n\n      if (\n        delta.dataAge ||\n        delta.layerQueries ||\n        changedOpts.length ||\n        changedOpts.includes(\"refresh\") ||\n        changedOpts.includes(\"aggregationMode\")\n      ) {\n        void this.setLayerData(this.state.dataAge);\n\n        if (changedOpts.includes(\"refresh\")) {\n          const { refresh } = this.d.w.options;\n\n          if (this.autoRefreshInterval) {\n            clearInterval(this.autoRefreshInterval);\n            this.autoRefreshInterval = undefined;\n          }\n          if (refresh?.type === \"Interval\" && refresh.intervalSeconds) {\n            this.autoRefreshInterval = setInterval(() => {\n              this.setLayerData(Date.now());\n            }, 1000 * refresh.intervalSeconds);\n          }\n        }\n      }\n\n      if (\n        (this.d.w.options.extentBehavior === \"filterToMapBounds\" &&\n          changedOpts.includes(\"extent\")) ||\n        changedOpts.includes(\"extentBehavior\")\n      ) {\n        this.props.onForceUpdate();\n      }\n    }\n\n    if (!isEmpty(ns)) {\n      this.setState(ns);\n    }\n  };\n\n  layerSubs: {\n    filter: string;\n    tableName: string;\n    sub: any;\n  }[] = [];\n\n  /**\n   * Deck GL values seems to go over limits\n   */\n  static extentToFilter = (\n    [x1, y1, x2, y2]: [number, number, number, number],\n    geomColumn: string,\n  ): AnyObject => {\n    const xMin = -180;\n    const yMin = -90;\n    const xMax = 180;\n    const yMax = 90;\n    const bboxCoords = [\n      Math.min(xMax, Math.max(xMin, x1)),\n      Math.min(yMax, Math.max(yMin, y1)),\n      Math.max(xMin, Math.min(xMax, x2)),\n      Math.max(yMin, Math.min(yMax, y2)),\n    ];\n    return {\n      [`${geomColumn}.&&.st_makeenvelope`]: bboxCoords,\n    };\n  };\n\n  getFilter = (\n    lTable: LayerTable,\n    ext4326: [number, number, number, number],\n  ): {\n    finalFilter: AnyObject;\n    finalFilterWOextent: AnyObject;\n    isJoin: boolean;\n  } => {\n    const { geomColumn, externalFilters, path } = lTable;\n\n    const isJoin = !!path?.length;\n\n    /** Extent filter must be outside exists */\n    // const finalFilterWOextent = wrapFilterIfJoined({ $and: [tableFilter, ...externalFilters].filter(isDefined) }, path, lTable.tableName)!;\n    const finalFilterWOextent = {\n      $and: [...externalFilters].filter(isDefined),\n    };\n\n    /* We use a current extent filter UNLESS a joinFilter (activeRow) filter is applied because the items might be outside of our extent */\n    const currentExtentFilter = W_Map.extentToFilter(ext4326, geomColumn);\n    // const finalFilter = { $and: [finalFilterWOextent, wrapFilterIfJoined(joinFilter, path, lTable.tableName) || currentExtentFilter].filter(isDefined) };\n    const finalFilter = {\n      $and: [...externalFilters, currentExtentFilter].filter(isDefined),\n    };\n\n    return {\n      finalFilter,\n      finalFilterWOextent,\n      isJoin,\n    };\n  };\n\n  getSQL = (\n    { sql, withStatement, parameters, geomColumn }: LayerSQL,\n    select: string,\n    limit = 1000,\n  ): { sql: string; args: AnyObject } => {\n    if (!sql) throw \"No SQL\";\n\n    const finalSql = `\n      ${withStatement}\n      SELECT ${select} \n      FROM ( \n        ${sql}\n      ) prostgles_chart_data\n      LIMIT ${limit}`;\n    const args = { ...parameters, geomColumn };\n\n    return { sql: finalSql, args };\n  };\n\n  getDataExtent = getMapDataExtent.bind(this);\n\n  dataSignature = \"\";\n\n  lastDataRequest = Date.now();\n  loadAgain = false;\n\n  /**\n   * Used to fetch and draw layer data\n   */\n  setLayerData = fetchMapLayerData.bind(this);\n\n  hoveredObj?: HoveredObject;\n  hovering?: {\n    hoverObj: HoveredObject;\n    hoverObjStr: string;\n    timeout?: NodeJS.Timeout;\n  };\n  onHover = onMapHover.bind(this);\n\n  getMenu = (w: D[\"w\"]) => {\n    const { bytesPerSec } = this.state;\n    return <W_MapMenu {...this.props} w={w} bytesPerSec={bytesPerSec} />;\n  };\n\n  map?: MapHandler;\n  render() {\n    const {\n      minimised = false,\n      layers: fetchedLayers = [],\n      loadingLayers,\n      clickedItem,\n      hoverCoords,\n      hovData,\n      drawingShape,\n      isDrawing,\n      error,\n    } = this.state;\n    const { w } = this.d;\n    const { layerQueries, onClickRow, prgl, active_row } = this.props;\n\n    if (!w) return null;\n\n    const layers: typeof fetchedLayers = [\n      ...fetchedLayers,\n      ...(drawingShape ?\n        [\n          {\n            dataSignature: \"drawingSHAPE\",\n            features: [drawingShape],\n            filled: true,\n            id: \"drawingSHAPE\",\n            getFillColor: (f) => [131, 56, 236, 255] satisfies DeckGlColor,\n            getLineWidth: (f) => 1211,\n            layerColor: [131, 56, 236, 255] satisfies DeckGlColor,\n            getLineColor: (f) => [131, 56, 236, 255] satisfies DeckGlColor,\n            lineWidth: 1222,\n            pickable: true,\n            stroked: true,\n            display: undefined,\n          },\n        ]\n      : []),\n    ];\n\n    let tooltipPopup: React.ReactNode = null;\n    if (hovData && hoverCoords && this.ref) {\n      const { screenCoordinates = [20, 20] } = hoverCoords;\n      const mapRect = this.ref.getBoundingClientRect();\n      const x = mapRect.x + screenCoordinates[0];\n      const y = mapRect.y + screenCoordinates[1];\n      tooltipPopup = (\n        <Popup\n          clickCatchStyle={{ display: \"none\" }}\n          anchorXY={{ x, y }}\n          positioning=\"tooltip\"\n          contentClassName=\"p-p5\"\n        >\n          <div className=\"bg-color-0 o-auto flex-col\">\n            {Object.entries(hovData).map(([k, v]) => {\n              //\n              let txt =\n                [\"string\", \"number\"].includes(typeof v) ?\n                  `${v}`\n                : (JSON.stringify(v, null, 2) as string | undefined);\n              if (txt && txt.length > 20) {\n                txt = txt.slice(0, 30) + \"...\";\n              }\n\n              return (\n                <div\n                  key={k}\n                  className=\"flex-row ai-center\"\n                  style={{ marginBottom: \"4px\" }}\n                >\n                  <div\n                    className=\"text-medium text-gray font-12\"\n                    style={{ color: \"var(--gray-400)\", fontWeight: \"bold\" }}\n                  >\n                    {k}:\n                  </div>\n                  <div className=\"ml-p5\">{txt}</div>\n                </div>\n              );\n            })}\n          </div>\n        </Popup>\n      );\n    }\n\n    const geoJsonLayers = layers;\n\n    const geoJsonLayersDataFilterSignature = JSON.stringify([layerQueries]);\n    let form: React.ReactNode = null;\n    if (w.options.showCardOnClick && clickedItem?.properties.i) {\n      const table = this.props.tables.find(\n        (t) => t.name === clickedItem.properties.tableName,\n      );\n      if (table) {\n        const filter = getMapFilter(\n          {\n            geomColumn: clickedItem.properties.geomColumn,\n            linkId: clickedItem.properties.layer._id,\n          },\n          table.columns,\n          clickedItem.properties,\n          this.props.myLinks,\n        );\n        form =\n          !filter ? null : (\n            <SmartForm\n              asPopup={true}\n              confirmUpdates={true}\n              db={prgl.db}\n              methods={prgl.methods}\n              tables={prgl.tables}\n              tableName={clickedItem.properties.tableName}\n              rowFilter={filter.detailedFilter}\n              onSuccess={() => {\n                this.setState({ dataAge: Date.now() });\n              }}\n              onClose={() => this.setState({ clickedItem: undefined })}\n            />\n          );\n      }\n    }\n\n    const content = (\n      <>\n        {form}\n        {tooltipPopup}\n        {minimised ? null : (\n          <div\n            className=\"relative f-1 flex-col o-hidden\"\n            ref={(r) => {\n              if (r) this.ref = r;\n            }}\n            onPointerLeave={() => {\n              // this.ref!.style.cursor = \"default\";\n              this.onHover();\n            }}\n            style={isDrawing ? { cursor: \"crosshair\" } : {}}\n          >\n            <MapInfoSection\n              fetchedLayers={fetchedLayers}\n              error={error}\n              active_row={active_row}\n              loadingLayers={loadingLayers}\n              w={w}\n            />\n            <DeckGLMap\n              onLoad={(map) => {\n                void this.setLayerData(this.state.dataAge);\n                this.map = map;\n              }}\n              geoJsonLayersDataFilterSignature={\n                geoJsonLayersDataFilterSignature\n              }\n              topLeftContent={\n                !w.options.hideLayersBtn && (\n                  <DataLayerManager\n                    {...this.props}\n                    w={w}\n                    type=\"map\"\n                    asMenuBtn={{\n                      color: \"action\",\n                      variant: \"filled\",\n                      size: \"small\",\n                      className: \"shadow\",\n                    }}\n                  />\n                )\n              }\n              basemapImage={w.options.basemapImage}\n              projection={w.options.projection}\n              onClick={(e) => {\n                const object: ClickedItem | undefined = e.object as any;\n                let rowFilter: AnyObject | undefined;\n                const filterOrHash: string | AnyObject | undefined =\n                  object?.properties.i;\n                if (object && filterOrHash) {\n                  if (isObject(filterOrHash)) {\n                    rowFilter = filterOrHash;\n                  } else {\n                    const table = this.props.tables.find(\n                      (t) => t.name === object.properties.tableName,\n                    );\n                    if (table) {\n                      rowFilter = getMapFilter(\n                        {\n                          geomColumn: object.properties.geomColumn,\n                          linkId: object.properties.layer._id,\n                        },\n                        table.columns,\n                        object.properties,\n                        this.props.myLinks,\n                      )?.filterValue;\n                    }\n                  }\n                }\n                onClickRow(\n                  rowFilter ? rowFilter : undefined,\n                  e.object?.properties?.tableName,\n                );\n\n                const newClickedItem = e.object as any;\n                if (\n                  JSON.stringify(newClickedItem) !==\n                  JSON.stringify(this.state.clickedItem)\n                ) {\n                  this.setState({ clickedItem: newClickedItem });\n                }\n              }}\n              tileURLs={(w.options.tileURLs ?? []).map((v) =>\n                v\n                  .replaceAll(\"{Z}\", \"{z}\")\n                  .replaceAll(\"{X}\", \"{x}\")\n                  .replaceAll(\"{Y}\", \"{y}\"),\n              )}\n              tileSize={w.options.tileSize || 256}\n              tileAttribution={w.options.tileAttribution}\n              basemapOpacity={w.options.basemapOpacity ?? 0.2}\n              basemapDesaturate={w.options.basemapDesaturate ?? 0}\n              dataOpacity={w.options.dataOpacity ?? 0.5}\n              initialState={(w.options as any) || {}}\n              geoJsonLayers={geoJsonLayers}\n              options={{\n                extentBehavior: w.options.extentBehavior,\n              }}\n              onOptionsChange={(newOpts) => {\n                w.$update({ options: newOpts }, { deepMerge: true });\n              }}\n              onMapStateChange={({\n                extent,\n                latitude,\n                longitude,\n                zoom,\n                pitch,\n                bearing,\n                target,\n              }) => {\n                /**\n                 * IS THIS STILL NEEDED?\n                 * Ensure the first extent is from data */\n                // if(w.options.extent){\n                // }\n                w.$update(\n                  {\n                    options: {\n                      target,\n                      extent,\n                      latitude,\n                      longitude,\n                      zoom,\n                      pitch,\n                      bearing,\n                    },\n                  },\n                  { deepMerge: true },\n                );\n              }}\n              onHover={this.onHover}\n              onGetFullExtent={this.getDataExtent}\n              edit={\n                !w.options.showAddShapeBtn ?\n                  undefined\n                : {\n                    /** Exclude clicked aggregated shapes from edit feature */\n                    feature:\n                      this.state.clickedItem?.properties.$rowhash ?\n                        this.state.clickedItem\n                      : undefined,\n                    dbProject: this.props.prgl.db,\n                    theme: this.props.prgl.theme,\n                    dbTables: this.props.tables,\n                    dbMethods: this.props.prgl.methods,\n                    layerQueries,\n                    onInsertOrUpdate: () => {\n                      this.setState({\n                        dataAge: Date.now(),\n                        clickedItem: undefined,\n                      });\n                    },\n                    onStartEdit: () => {\n                      this.setState({\n                        // editFeature: this.state.clickedItem,\n                        hovData: undefined,\n                        hoverCoords: undefined,\n                        hoverObj: undefined,\n                      });\n                    },\n                  }\n              }\n            />\n          </div>\n        )}\n      </>\n    );\n\n    return (\n      <Window\n        w={w}\n        getMenu={this.getMenu}\n        layoutMode={this.props.workspace.layout_mode ?? \"editable\"}\n      >\n        {content}\n      </Window>\n    );\n  }\n}\n\nexport const wrapFilterIfJoined = <F extends AnyObject | undefined>(\n  filter: F,\n  path: ParsedJoinPath[] | undefined,\n  rootTable: string,\n): F extends AnyObject ? AnyObject : undefined => {\n  const joinPath =\n    path?.length ? reverseParsedPath(path, rootTable) : undefined;\n  if (filter === undefined) return filter as any;\n\n  if (joinPath) {\n    return {\n      $existsJoined: {\n        filter,\n        path: joinPath,\n      },\n    } as any;\n  }\n\n  return filter as any;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/W_MapMenu.tsx",
    "content": "import React from \"react\";\n\nimport {\n  mdiCog,\n  mdiLayersOutline,\n  mdiMap,\n  mdiPalette,\n  mdiSyncCircle,\n} from \"@mdi/js\";\nimport { FlexRow } from \"@components/Flex\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { Select } from \"@components/Select/Select\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { TabItem } from \"@components/Tabs\";\nimport Tabs from \"@components/Tabs\";\nimport type { WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport type { ColumnConfig } from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport { AutoRefreshMenu } from \"../W_Table/TableMenu/AutoRefreshMenu\";\nimport { DataLayerManager } from \"../WindowControls/DataLayerManager/DataLayerManager\";\nimport { MapBasemapOptions } from \"./controls/MapBasemapOptions\";\nimport type { W_MapProps } from \"./W_Map\";\nexport const MAP_PROJECTIONS = [\"mercator\", \"orthographic\"] as const;\n\ntype ProstglesMapMenuProps = W_MapProps & {\n  w: WindowSyncItem<\"map\">;\n  bytesPerSec: number;\n};\n\nexport const W_MapMenu = (props: ProstglesMapMenuProps) => {\n  const { w } = props;\n\n  let coloredCols: ColumnConfig[] = [];\n  if (Array.isArray(w.columns)) {\n    coloredCols = w.columns.filter((c) => c.style?.type !== \"None\");\n  }\n\n  let colorMenu: Record<string, TabItem> = {};\n  if (coloredCols.length) {\n    colorMenu = {\n      Color: {\n        leftIconPath: mdiPalette,\n        content: (\n          <Select\n            label=\"Bin size\"\n            variant=\"div\"\n            className=\"w-fit b b-color mb-1\"\n            options={coloredCols.map(({ name }) => name)}\n            value={w.options.colorField}\n            onChange={(colorField: string) => {\n              w.$update({ options: { colorField } }, { deepMerge: true });\n            }}\n          />\n        ),\n      },\n    };\n  }\n\n  return (\n    <Tabs\n      variant=\"vertical\"\n      compactMode={window.isMobileDevice ? \"hide-inactive\" : undefined}\n      contentClass=\"p-1\"\n      items={{\n        \"Data refresh\": {\n          leftIconPath: mdiSyncCircle,\n          style:\n            (w.options.refresh?.type || \"None\") === \"None\" ?\n              {}\n            : { color: \"var(--active)\" },\n          content: <AutoRefreshMenu w={w} />,\n        },\n        Basemap: {\n          leftIconPath: mdiMap,\n          content: <MapBasemapOptions w={w} />,\n        },\n        Layers: {\n          leftIconPath: mdiLayersOutline,\n          content: <DataLayerManager {...props} type=\"map\" />,\n        },\n        ...colorMenu,\n        Settings: {\n          leftIconPath: mdiCog,\n          content: (\n            <div className=\"flex-col gap-1\">\n              <FlexRow className=\"gap-2 ai-start\">\n                <Select\n                  className=\"w-fit mt-p25\"\n                  label=\"Aggregation mode\"\n                  value={w.options.aggregationMode?.type ?? \"wait\"}\n                  fullOptions={[\n                    {\n                      key: \"limit\",\n                      label: \"Limit\",\n                      subLabel:\n                        \"Will aggregate if the total number of features higher than specified\",\n                    },\n                    {\n                      key: \"wait\",\n                      label: \"Download time\",\n                      subLabel:\n                        \"Will aggregate/simplify if data load takes longer than specified\",\n                    },\n                  ]}\n                  onChange={(type) => {\n                    w.$update(\n                      { options: { aggregationMode: { type } } },\n                      { deepMerge: true },\n                    );\n                  }}\n                />\n                {w.options.aggregationMode?.type === \"limit\" ?\n                  <FormFieldDebounced\n                    type=\"number\"\n                    style={{ width: \"200px\" }}\n                    label=\"Result count limit\"\n                    value={w.options.aggregationMode.limit || 1000}\n                    inputProps={{ min: 1, step: 1 }}\n                    onChange={(e) => {\n                      w.$update(\n                        { options: { aggregationMode: { limit: +e } } },\n                        { deepMerge: true },\n                      );\n                    }}\n                  />\n                : <FormFieldDebounced\n                    type=\"number\"\n                    style={{ width: \"200px\" }}\n                    label=\"Wait time in seconds\"\n                    hint={`Current connection is approx. ${(props.bytesPerSec / 1000).toFixed(1)} MB/s`}\n                    value={w.options.aggregationMode?.wait ?? 2}\n                    inputProps={{ min: 0 }}\n                    onChange={(e) => {\n                      w.$update(\n                        { options: { aggregationMode: { wait: +e } } },\n                        { deepMerge: true },\n                      );\n                    }}\n                  />\n                }\n              </FlexRow>\n              <SwitchToggle\n                label={`Show Add/Edit shape button`}\n                checked={!!w.options.showAddShapeBtn}\n                onChange={(showAddShapeBtn) => {\n                  w.$update(\n                    { options: { showAddShapeBtn } },\n                    { deepMerge: true },\n                  );\n                }}\n              />\n              <SwitchToggle\n                label={`Hide layers button`}\n                checked={!!w.options.hideLayersBtn}\n                onChange={(hideLayersBtn) => {\n                  w.$update(\n                    { options: { hideLayersBtn } },\n                    { deepMerge: true },\n                  );\n                }}\n              />\n              <SwitchToggle\n                label={`Show row card on click`}\n                checked={!!w.options.showCardOnClick}\n                onChange={(showCardOnClick) => {\n                  w.$update(\n                    { options: { showCardOnClick } },\n                    { deepMerge: true },\n                  );\n                }}\n              />\n            </div>\n          ),\n        },\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/controls/MapBasemapOptions.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport ButtonGroup from \"@components/ButtonGroup\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Label } from \"@components/Label\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { mdiDelete, mdiMap, mdiPlus, mdiSearchWeb } from \"@mdi/js\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport { isDefined, isEqual } from \"prostgles-types\";\nimport React, { useCallback, useState } from \"react\";\nimport type { WindowData } from \"../../Dashboard/dashboardUtils\";\nimport { DEFAULT_TILE_URLS } from \"../../Map/mapUtils\";\nimport SmartTable from \"../../SmartTable\";\nimport { MAP_PROJECTIONS } from \"../W_MapMenu\";\n\ntype P = {\n  w: SyncDataItem<Required<WindowData<\"map\">>, true>;\n  className?: string;\n  asPopup?: boolean;\n};\nexport const MapBasemapOptions = ({ w, className, asPopup }: P) => {\n  const prgl = usePrgl();\n  const { tables } = prgl;\n  const mediaTable = tables.find((t) => t.info.isFileTable);\n  const [localOptions, setLocalOptions] = useState(w.options);\n  const [newTileUrl, setNewTileUrl] = useState(\"\");\n  const {\n    tileURLs,\n    projection = \"mercator\",\n    basemapImage,\n    tileAttribution,\n    tileSize,\n  } = asPopup ? localOptions : w.options;\n  const tileURLsOrDefaults = tileURLs?.length ? tileURLs : DEFAULT_TILE_URLS;\n  const updateOptions = useCallback(\n    (options: Partial<(typeof w)[\"options\"]>) => {\n      setLocalOptions({ ...localOptions, ...options });\n      if (asPopup) return;\n      w.$update({ options }, { deepMerge: true });\n    },\n    [w, localOptions, asPopup],\n  );\n  const setBaseImageURL = useCallback(\n    (url: string) => {\n      const setBounds = (height = 100, width = 100) => {\n        updateOptions({ basemapImage: { url, bounds: [0, 0, width, height] } });\n      };\n\n      try {\n        const img = new Image();\n        img.onload = function () {\n          const height = img.height;\n          const width = img.width;\n          setBounds(height, width);\n        };\n        img.onerror = (e) => {\n          console.error(e);\n          setBounds();\n        };\n        img.src = url;\n      } catch (e) {\n        setBounds();\n        console.error(e);\n      }\n    },\n    [updateOptions],\n  );\n\n  const content = (\n    <FlexCol\n      className={className}\n      data-command={!asPopup ? \"MapBasemapOptions\" : undefined}\n    >\n      <ButtonGroup\n        data-command=\"MapBasemapOptions.Projection\"\n        label={{ label: \"Projection\", variant: \"normal\" }}\n        options={MAP_PROJECTIONS}\n        value={projection}\n        onChange={(projection) => {\n          updateOptions({ projection });\n        }}\n      />\n      {projection !== \"mercator\" ?\n        <>\n          <FormField\n            type=\"text\"\n            label=\"Basemap Image URL\"\n            autoComplete=\"off\"\n            value={basemapImage?.url}\n            onChange={setBaseImageURL}\n            rightIcons={\n              mediaTable && (\n                <PopupMenu\n                  button={\n                    <Btn\n                      iconPath={mdiSearchWeb}\n                      title=\"From data\"\n                      size={\"small\"}\n                    />\n                  }\n                  render={(pClose) => {\n                    return (\n                      <SmartTable\n                        title=\"Click row to select\"\n                        db={prgl.db}\n                        tableName={mediaTable.name}\n                        tables={tables}\n                        methods={prgl.methods}\n                        filter={[\n                          {\n                            fieldName: \"content_type\",\n                            type: \"$ilike\",\n                            value: \"%image%\",\n                            minimised: true,\n                          },\n                        ]}\n                        onClickRow={(row) => {\n                          if (row) {\n                            setBaseImageURL(row.url);\n                            pClose();\n                          }\n                        }}\n                      />\n                    );\n                  }}\n                />\n              )\n            }\n          />\n        </>\n      : <>\n          <FormField\n            type=\"text\"\n            label=\"Attribution\"\n            autoComplete=\"off\"\n            value={tileAttribution?.title}\n            onChange={(title) => {\n              updateOptions({\n                tileAttribution: {\n                  url: \"\",\n                  ...tileAttribution,\n                  title,\n                },\n              });\n            }}\n          />\n          <FormField\n            type=\"url\"\n            label=\"Attribution URL\"\n            autoComplete=\"off\"\n            value={tileAttribution?.url}\n            onChange={(url) => {\n              updateOptions({\n                tileAttribution: {\n                  title: \"\",\n                  ...tileAttribution,\n                  url,\n                },\n              });\n            }}\n          />\n          <Label label=\"Tile URLs\" variant=\"normal\" />\n          {tileURLsOrDefaults.map((turl, i) => (\n            <div key={i} className=\"flex-row ai-center py-p5 o-auto\">\n              <Btn\n                iconPath={mdiDelete}\n                color=\"danger\"\n                onClick={() => {\n                  updateOptions({\n                    tileURLs: tileURLsOrDefaults.filter((t) => t !== turl),\n                  });\n                }}\n              />\n              <Chip variant=\"naked\" value={turl} />\n            </div>\n          ))}\n          <InfoRow color=\"info\">Delete all tiles to restore default</InfoRow>\n          <FormField\n            label={\"New tile URL\"}\n            type=\"text\"\n            asTextArea={true}\n            value={newTileUrl}\n            onChange={setNewTileUrl}\n            rightContentAlwaysShow={true}\n            rightIcons={\n              <Btn\n                title=\"Add new tile url\"\n                color=\"action\"\n                iconPath={mdiPlus}\n                onClick={() => {\n                  const urls = Array.from(\n                    new Set([...tileURLsOrDefaults, newTileUrl]),\n                  ).filter(isDefined);\n                  updateOptions({ tileURLs: urls });\n                }}\n              />\n            }\n          />\n          <FormField\n            label=\"Tile size\"\n            value={tileSize || 256}\n            options={[16, 32, 64, 128, 256, 512, 1024]}\n            onChange={(tileSize) => {\n              updateOptions({ tileSize });\n            }}\n          />\n        </>\n      }\n    </FlexCol>\n  );\n\n  if (!asPopup) {\n    return content;\n  }\n\n  return (\n    <PopupMenu\n      title=\"Basemap config\"\n      className={className}\n      clickCatchStyle={{ opacity: 0.5 }}\n      onClickClose={false}\n      positioning=\"center\"\n      contentClassName=\"p-1\"\n      data-command=\"MapBasemapOptions\"\n      button={\n        <Btn iconPath={mdiMap} color=\"action\" variant=\"faded\">\n          Basemap config\n        </Btn>\n      }\n      footerButtons={[\n        {\n          label: \"Cancel\",\n          onClickClose: true,\n        },\n        {\n          label: \"Update\",\n          variant: \"filled\",\n          color: \"action\",\n          disabledInfo:\n            isEqual(w.options, localOptions) ? \"Nothing to update\" : undefined,\n          onClickPromise: async () => {\n            await w.$update({ options: localOptions }, { deepMerge: true });\n          },\n        },\n      ]}\n    >\n      {content}\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/controls/MapInfoSection.tsx",
    "content": "import ErrorComponent from \"@components/ErrorComponent\";\nimport Loading from \"@components/Loader/Loading\";\nimport React, { useEffect, useState } from \"react\";\nimport type { GeoJsonLayerProps } from \"src/dashboard/Map/DeckGLMap\";\nimport type { W_MapProps } from \"../W_Map\";\nimport { classOverride } from \"@components/Flex\";\n\ntype P = {\n  loadingLayers: boolean;\n  fetchedLayers: GeoJsonLayerProps[];\n  error: unknown;\n} & Pick<W_MapProps, \"active_row\" | \"w\">;\nexport const MapInfoSection = (props: P) => {\n  const { loadingLayers, w, active_row, fetchedLayers, error } = props;\n  const [show, setShow] = useState(false);\n  const { extentBehavior } = w.options;\n  /** Debounce show */\n  useEffect(() => {\n    const timeout = setTimeout(() => {\n      setShow(true);\n    }, 1000);\n    return () => {\n      setShow(false);\n      clearTimeout(timeout);\n    };\n  }, [props]);\n\n  if (error) {\n    return (\n      <Wrapper className=\"relative flex-row m-2 p-2 bg-color-0 rounded max-h-fit \">\n        <ErrorComponent title=\"Map error\" withIcon={true} error={error} />\n      </Wrapper>\n    );\n  }\n\n  if (loadingLayers && w.options.refresh?.type !== \"Realtime\") {\n    return (\n      <Wrapper>\n        <Loading delay={100} />\n      </Wrapper>\n    );\n  }\n\n  if (!show) return null;\n  if (!loadingLayers && fetchedLayers.every((l) => !l.features.length)) {\n    const message =\n      active_row ?\n        extentBehavior === \"autoZoomToData\" ?\n          \"No location data found for the selected row.\"\n        : \"Data not within the current bounds. Set map extent behaviour to 'Follow data'\"\n      : \"No location data to display.\";\n    return (\n      <Wrapper className=\"w-full h-full \">\n        <div\n          className=\"p-p5 rounded\"\n          style={{\n            background: \"rgb(255 255 255 / 20%)\",\n            backdropFilter: \"blur(4px)\",\n          }}\n        >\n          {message}\n        </div>\n      </Wrapper>\n    );\n  }\n\n  return null;\n};\n\nconst Wrapper = ({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) => {\n  return (\n    <div\n      className={classOverride(\n        \"MapInfoSection f-1 flex-col jc-center ai-center absolute pl-2 mt-1 ml-2\",\n        className,\n      )}\n      style={{\n        zIndex: 1,\n        /** No dark tiles */\n        color: \"black\",\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/controls/MapOSMQuery.tsx",
    "content": "import { useEffectDeep } from \"prostgles-client\";\nimport React, { useState } from \"react\";\nimport { isObject } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { Select } from \"@components/Select/Select\";\nimport { getKeys } from \"../../../utils/utils\";\nimport { CodeEditor } from \"../../CodeEditor/CodeEditor\";\nimport type { GeoJSONFeature } from \"../../Map/DeckGLMap\";\nimport { download } from \"../../W_SQL/W_SQL\";\nimport { getOSMData } from \"../OSM/getOSMData\";\nimport { predefinedOsmQueries } from \"../OSM/osmTypes\";\nimport { mdiDownload, mdiPlus } from \"@mdi/js\";\nimport { OverpassQuery } from \"../OSM/OverpassQuery\";\n\ntype DataType = keyof typeof predefinedOsmQueries;\n\nconst getQuery = (\n  type: DataType,\n  subTypeValues: string[] | undefined,\n  limit: number,\n) => {\n  const query = predefinedOsmQueries[type];\n  let mainQuery =\n    (isObject(query) ? `${query.nodeType}[${query.subTypeTag}]` : query) +\n    `(\\${bbox});`;\n  if (\n    subTypeValues?.length &&\n    isObject(query) &&\n    subTypeValues.length < query.subTypes.length\n  ) {\n    mainQuery =\n      \"(\" +\n      subTypeValues\n        .map(\n          (v) =>\n            `${query.nodeType}[${[query.subTypeTag, v].map((s) => JSON.stringify(s)).join(\"=\")}](\\${bbox});`,\n        )\n        .join(\"\") +\n      \");\";\n  }\n  return `[out:json];${mainQuery}out ${limit};`;\n};\n\ntype MapOSMQueryOSMQueryProps = {\n  bbox: string;\n  onData: (features: GeoJSONFeature[], query: string) => void;\n};\nexport const MapOSMQuery = ({ bbox, onData }: MapOSMQueryOSMQueryProps) => {\n  const [query, setQuery] = useState(\"\");\n  const [dataType, setDataType] = useState<DataType>(\"roads\");\n  const defaultQuery = predefinedOsmQueries[dataType];\n  const subTypes = isObject(defaultQuery) ? defaultQuery.subTypes : undefined;\n  const [selectedSubTypes, setSelectedSubTypes] = useState<\n    string[] | undefined\n  >();\n  const [data, setData] = useState<any>(null);\n  const [loading, setLoading] = useState<boolean>(false);\n  const [error, setError] = useState<string | null>(null);\n  const [features, setFeatures] = useState<GeoJSONFeature[]>([]);\n  const [limit, setLimit] = useState(1e5);\n\n  useEffectDeep(() => {\n    setSelectedSubTypes(subTypes?.map((s) => s.value));\n  }, [subTypes]);\n  useEffectDeep(() => {\n    setQuery(getQuery(dataType, selectedSubTypes, limit));\n  }, [dataType, limit, selectedSubTypes]);\n\n  const handleQueryChange = (value: string) => {\n    setQuery(value);\n  };\n\n  const onSave = ([f, d] = [features, dataType]) => {\n    download(JSON.stringify(f, null, 2), `${d}.geojson`, \"text/json\");\n  };\n  const onSearch = async (saveAsFile = false) => {\n    setLoading(true);\n    setError(null);\n\n    try {\n      const { data, features } = await getOSMData(query, bbox);\n      setData(data);\n      setFeatures(features);\n      if (saveAsFile) {\n        onSave([features, dataType]);\n      } else {\n        onData(features, query);\n      }\n    } catch (err: any) {\n      setError(err.message);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <FlexCol>\n      <FlexRow>\n        <Select\n          label={\"Data Type:\"}\n          options={getKeys(predefinedOsmQueries)}\n          value={dataType}\n          onChange={setDataType}\n        />\n        <FormField\n          label={\"Limit\"}\n          type=\"number\"\n          value={limit}\n          style={{ width: \"12ch\" }}\n          onChange={(e) => setLimit(e)}\n        />\n        {subTypes && (\n          <Select\n            label={\"Subtypes\"}\n            multiSelect={true}\n            fullOptions={subTypes.map((s) => ({\n              key: s.value,\n              subLabel: s.info,\n            }))}\n            value={selectedSubTypes}\n            onChange={(values) => {\n              setSelectedSubTypes(values);\n            }}\n          />\n        )}\n      </FlexRow>\n      <OverpassQuery\n        autoSave={true}\n        query={query}\n        onChange={handleQueryChange}\n      />\n      <FlexRow>\n        <Btn\n          iconPath={mdiPlus}\n          loading={loading}\n          variant=\"filled\"\n          color=\"action\"\n          onClick={() => onSearch()}\n        >\n          Add layer\n        </Btn>\n\n        <Btn\n          iconPath={mdiDownload}\n          loading={loading}\n          variant=\"faded\"\n          color=\"action\"\n          onClick={() => onSearch(true)}\n        >\n          Save as GeoJSON\n        </Btn>\n      </FlexRow>\n      {error && <ErrorComponent error={error} />}\n      {data && (\n        <>\n          <CodeEditor\n            style={{\n              minWidth: \"500px\",\n              minHeight: \"500px\",\n            }}\n            value={JSON.stringify(data, null, 2)}\n            language=\"json\"\n          />\n          <Btn color=\"action\" variant=\"filled\" onClick={() => onSave()}>\n            Save as GeoJSON\n          </Btn>\n        </>\n      )}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/controls/MapOpacityMenu.tsx",
    "content": "import type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport React from \"react\";\nimport { classOverride } from \"@components/Flex\";\nimport { Slider } from \"@components/Slider\";\nimport type { WindowData } from \"../../Dashboard/dashboardUtils\";\n\ntype P = {\n  w: SyncDataItem<Required<WindowData<\"map\">>, true>;\n  className?: string;\n};\n\nexport const MapOpacityMenu = ({ w, className }: P) => {\n  const {\n    basemapOpacity = 0.25,\n    basemapDesaturate = 0,\n    dataOpacity = 1,\n  } = w.options;\n  const updateOptions = (options: Partial<(typeof w)[\"options\"]>) => {\n    w.$update({ options }, { deepMerge: true });\n  };\n\n  return (\n    <div\n      data-command=\"MapOpacityMenu\"\n      className={classOverride(\"p-1 shadow bg-color-0 rounded\", className)}\n    >\n      <Slider\n        label={`Basemap opacity ${basemapOpacity.toFixed(1)}`}\n        min={0}\n        max={1}\n        value={basemapOpacity}\n        defaultValue={0.25}\n        onChange={(opacity) => {\n          updateOptions({ basemapOpacity: opacity });\n        }}\n      />\n      <Slider\n        label={`Basemap desaturate ${basemapDesaturate.toFixed(1)}`}\n        min={-15}\n        max={15}\n        value={basemapDesaturate}\n        defaultValue={0}\n        onChange={(desaturate) => {\n          updateOptions({ basemapDesaturate: desaturate });\n        }}\n      />\n\n      <Slider\n        label={`Data opacity ${dataOpacity.toFixed(1)}`}\n        min={0.001}\n        max={1}\n        value={dataOpacity}\n        defaultValue={0.5}\n        onChange={(dataOpacity) => {\n          updateOptions({ dataOpacity });\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/fetchData/fetchMapLayerData.ts",
    "content": "import type { AnyObject, SelectParams } from \"prostgles-types\";\nimport { pickKeys } from \"prostgles-types\";\nimport { getIcon } from \"@components/SvgIcon\";\nimport type {\n  Extent,\n  GeoJSONFeature,\n  GeoJsonLayerProps,\n} from \"../../Map/DeckGLMap\";\nimport { getOSMData } from \"../OSM/getOSMData\";\nimport type W_Map from \"../W_Map\";\nimport type { W_MapState } from \"../W_Map\";\nimport { MAP_SELECT_COLUMNS, getMapSelect, getSQLData } from \"./getMapData\";\nimport { getMapFeatureStyle } from \"../getMapFeatureStyle\";\nimport { scaleLinear } from \"d3\";\n\nexport const DEFAULT_GET_COLOR: Pick<\n  GeoJsonLayerProps,\n  \"getFillColor\" | \"getLineColor\"\n> = {\n  getLineColor: (f: GeoJSONFeature) =>\n    f.geometry.type === \"Polygon\" ? [200, 0, 80, 55] : [0, 129, 167, 255],\n  getFillColor: (f: GeoJSONFeature) =>\n    f.geometry.type === \"Polygon\" ? [200, 0, 80, 255] : [0, 129, 167, 255],\n};\n\nexport const fetchMapLayerData = async function (this: W_Map, dataAge: number) {\n  const {\n    prgl: { db },\n    layerQueries = [],\n    tables,\n  } = this.props;\n  const { w } = this.d;\n  if (!w) return;\n\n  const ext4326: Extent = (w.options.extent as Extent | undefined) || [\n    -180, -90, 180, 90,\n  ];\n\n  let result: Pick<W_MapState, \"layers\"> = {},\n    error;\n  const AGG_LIMIT = 100000;\n\n  let { bytesPerSec } = this.state;\n  if (this.state.loadingLayers) {\n    this.loadAgain = true;\n    return;\n  }\n\n  for (const l of this.props.myLinks) {\n    const opts = l.options;\n    if (opts.type === \"map\" && opts.mapIcons) {\n      if (opts.mapIcons.type === \"fixed\") {\n        await getIcon(opts.mapIcons.iconPath);\n      } else {\n        for (const c of opts.mapIcons.conditions) {\n          await getIcon(c.iconPath);\n        }\n      }\n    }\n  }\n\n  if (w.options.extent) {\n    try {\n      this.setState({ loadingLayers: true });\n\n      this.lastDataRequest = Date.now();\n\n      /** Remove subscriptions that are not used anymore */\n      this.layerSubs\n        .filter(\n          (s) =>\n            !layerQueries.find(\n              (l) => \"tableName\" in l && l.tableName === s.tableName,\n            ),\n        )\n        .map((s) => {\n          s.sub.unsubscribe();\n        });\n\n      const layers: GeoJsonLayerProps[] = [];\n      await Promise.all(\n        layerQueries\n          .filter((q) => !q.disabled)\n          .map(async (q, i) => {\n            let rows: AnyObject[] | undefined,\n              aggs: { c: string; l: any; radius: number }[] | undefined,\n              _tableName,\n              _geomColumn,\n              dataSignature = \"\",\n              tableFilterWOExtent: AnyObject = {},\n              opts: SelectParams = {};\n\n            let willAggregate = false;\n\n            if (q.type === \"osm\") {\n              const [b, a, b1, a1] = w.options.extent ?? [];\n              const osmBbox = w.options.extent ? [a, b, a1, b1].join(\",\") : \"\";\n              const { features } =\n                !osmBbox ?\n                  { features: [] }\n                : await getOSMData(q.query, osmBbox);\n              layers.push({\n                dataSignature,\n                id: \"osm\" + Date.now(),\n                features,\n                pickable: true,\n                stroked: true,\n                filled: true,\n                ...DEFAULT_GET_COLOR,\n                display: undefined,\n              });\n            } else if (\"tableName\" in q) {\n              const { tableName, geomColumn } = { ...q };\n              const { ...f } = this.getFilter(q, ext4326);\n\n              _tableName = tableName;\n              _geomColumn = geomColumn;\n\n              const columns = tables.find((t) => t.name === tableName)?.columns;\n              if (!columns) {\n                this.setState({\n                  error: \"Could not find columns for table: \" + tableName,\n                });\n                return;\n              }\n\n              const tableHandler = db[tableName];\n              if (\n                !tableHandler?.find ||\n                !tableHandler.findOne ||\n                !tableHandler.size\n              ) {\n                throw `db.${tableName} handler is missing one of the required permissions: find, findOne, size`;\n              }\n\n              const zoom = w.options.zoom || 1;\n              tableFilterWOExtent = f.finalFilterWOextent;\n\n              const subFilter = tableFilterWOExtent;\n              const subFilterStr = JSON.stringify(subFilter);\n              if (\n                tableHandler.subscribe &&\n                w.options.refresh?.type === \"Realtime\" &&\n                !this.layerSubs.find(\n                  (s) => s.filter === subFilterStr && s.tableName === tableName,\n                )\n              ) {\n                try {\n                  const sub = await tableHandler.subscribe(\n                    subFilter,\n                    {\n                      limit: 2,\n                      throttle: (w.options.refresh.throttleSeconds || 0) * 1000,\n                    },\n                    () => {\n                      this.setLayerData(Date.now());\n                    },\n                  );\n                  this.layerSubs.push({\n                    tableName,\n                    filter: subFilterStr,\n                    sub,\n                  });\n                } catch (e: any) {\n                  console.error(e);\n                  alert(\"Could not subscribe. Check logs \");\n                }\n              }\n\n              /** Get client download speed */\n              const downloadStart = Date.now();\n\n              const select = getMapSelect(q, columns, this.props.myLinks);\n              const selectGeoJson = { select: pickKeys(select, [\"l\"]) };\n              const oneRow = await tableHandler.findOne(\n                f.finalFilter,\n                selectGeoJson,\n              );\n              const seconds = (Date.now() - downloadStart) / 1000;\n              bytesPerSec = (JSON.stringify(oneRow || {}).length * 4) / seconds;\n              opts = { select: select as any, limit: AGG_LIMIT };\n\n              if (!oneRow || !this.ref) {\n                layers.push({\n                  dataSignature,\n                  id: \"tbl\" + Date.now(),\n                  features: [],\n                  pickable: true,\n                  stroked: true,\n                  filled: true,\n                  ...DEFAULT_GET_COLOR,\n                  display: undefined,\n                });\n                return;\n              }\n\n              const xDelta = Math.abs(ext4326[0] - ext4326[2]);\n              const yDelta = Math.abs(ext4326[1] - ext4326[3]);\n              const minDelta = Math.min(xDelta, yDelta);\n\n              /** Simplify Polygon and LineString shapes */\n              if (!oneRow.l?.type.endsWith(\"Point\")) {\n                // && (oneRow?.c?.coordinates || []).flat().flat().flat().length > 30){\n                const scale =\n                  zoom > 7 ?\n                    scaleLinear().range([0, 0.005]).domain([9, 7]).clamp(true)\n                  : scaleLinear()\n                      .range([0.005, 0.05])\n                      .domain([7, 1])\n                      .clamp(true);\n                const size = scale(zoom);\n                if (size > 0) {\n                  opts = {\n                    select: {\n                      ...pickKeys(select, [\"i\"]),\n                      [MAP_SELECT_COLUMNS.geoJson]: {\n                        $ST_Simplify: [geomColumn, size],\n                      },\n                    } as any,\n                    limit: AGG_LIMIT,\n                  };\n                }\n              }\n\n              const { signature, cachedLayer } = this.getDataSignature(\n                { filter: f.finalFilter, ...opts },\n                dataAge,\n                q,\n                w.options.aggregationMode,\n              );\n              dataSignature = signature;\n              if (cachedLayer) {\n                layers.push(cachedLayer);\n                return;\n              }\n\n              if (oneRow.l?.type === \"Point\") {\n                const { aggregationMode } = this.d.w?.options ?? {};\n                if (aggregationMode?.type === \"limit\") {\n                  const count = parseInt(\n                    (await tableHandler.count?.(f.finalFilter)) as any,\n                  );\n                  willAggregate =\n                    Number.isFinite(count) &&\n                    count > (aggregationMode.limit || 1000);\n                } else {\n                  // const count = await db[tableName].count(f.finalFilter);\n                  const size = await tableHandler.size(\n                    f.finalFilter,\n                    selectGeoJson,\n                  );\n                  const actualWait = +size / 1000 / bytesPerSec;\n                  willAggregate = actualWait > (aggregationMode?.wait ?? 2);\n                }\n              }\n\n              if (willAggregate) {\n                const radiusRangeScale = scaleLinear()\n                  .range([20, 250])\n                  .domain([0.001, 0.1]);\n                const scale = scaleLinear()\n                  .range([0.07, 0.0005])\n                  .domain([0.886, 0.007166]);\n                /** TODO: fix point bad agg cluster positioning at low zoom  */\n                // const scale = d3.scaleLinear().range([0.07, 0.0005]).domain([0.2, 0.007166]).clamp(true);\n                const size = scale(minDelta);\n                const opts = {\n                  select: {\n                    c: { $countAll: [] },\n                    [MAP_SELECT_COLUMNS.geoJson]: {\n                      $ST_SnapToGrid: [geomColumn, size],\n                    },\n                  },\n                  limit: AGG_LIMIT,\n                } as const;\n\n                const aggRows = (await tableHandler.find(\n                  f.finalFilter,\n                  opts,\n                )) as Record<keyof (typeof opts)[\"select\"], any>[];\n                let minCount, maxCount;\n                aggRows.forEach(({ c }) => {\n                  minCount = Math.min(minCount ?? +c, +c);\n                  maxCount = Math.max(maxCount ?? +c, +c);\n                });\n                const maxRange = radiusRangeScale(minDelta);\n                const radiusScale = scaleLinear()\n                  .range([1, maxRange])\n                  .domain([minCount, maxCount]);\n\n                aggs = aggRows.map((a) => ({\n                  ...a,\n                  radius: radiusScale(+a.c),\n                }));\n              } else {\n                const scale = scaleLinear()\n                  .range([1, 10, 80, 100])\n                  .domain([20, 14, 10, 1])\n                  .clamp(true);\n\n                rows = await tableHandler.find(f.finalFilter, opts);\n\n                const radius = scale(zoom);\n                rows = rows.map((r) => ({ ...r, type: \"table\", radius }));\n              }\n              if (aggs)\n                aggs = aggs.filter(\n                  (r) => r[MAP_SELECT_COLUMNS.geoJson]?.coordinates?.length,\n                );\n              if (rows)\n                rows = rows.filter(\n                  (r) => r[MAP_SELECT_COLUMNS.geoJson]?.coordinates?.length,\n                );\n            } else if (\"sql\" in q) {\n              if (!db.sql) {\n                console.error(\"Not enough privileges to run query\");\n                alert(\n                  \"Could not show data: sql privilege not allowed for current user\",\n                );\n                return;\n              }\n              const { sql } = q;\n              const lsig = this.getDataSignature({ sql }, dataAge, q, []);\n\n              dataSignature = lsig.signature;\n              if (lsig.cachedLayer) {\n                layers.push(lsig.cachedLayer);\n                return;\n              }\n\n              rows = await getSQLData(q, db, AGG_LIMIT);\n              rows = rows.map((r) => ({ ...r, type: \"sql\" }));\n            }\n\n            let data = rows || aggs || ([] as any);\n            data = data.filter((r) => r[MAP_SELECT_COLUMNS.geoJson]);\n            const badRow = data.find(\n              (r) => typeof r[MAP_SELECT_COLUMNS.geoJson] !== \"object\",\n            );\n            if (badRow) {\n              console.error(\n                \"Bad GeoJSON data. Expecting type object but got -> \" +\n                  typeof badRow.l,\n                badRow,\n              );\n              data = [];\n            }\n\n            const { fillColor, lineColor } = q;\n            const layerColor = lineColor;\n\n            layers.push({\n              id: q._id + Date.now(),\n              dataSignature,\n              features: data.map((r) => ({\n                type: \"Feature\",\n                geometry: r[MAP_SELECT_COLUMNS.geoJson],\n                properties: {\n                  radius: +r.radius || 1,\n                  ...r,\n                  layer: q,\n                  tableName: _tableName,\n                  geomColumn: _geomColumn,\n                },\n              })),\n              pickable: true,\n              stroked: true,\n              filled: true,\n              elevation: q.elevation,\n              ...getMapFeatureStyle(\n                q,\n                this.state.clickedItem,\n                this.props.myLinks,\n              ),\n              getLineWidth: (f) => 1211,\n              layerColor,\n              lineWidth: 1222,\n            });\n          }),\n      );\n\n      result = { layers };\n    } catch (err) {\n      error = err;\n    }\n  }\n\n  if (!result.layers) {\n    delete result.layers;\n  }\n\n  if (this.loadAgain) {\n    this.loadAgain = false;\n    setTimeout(() => this.setLayerData(this.state.dataAge), 0);\n  }\n\n  this.setState({\n    ...result,\n    loadingLayers: false,\n    error,\n    bytesPerSec,\n    dataAge,\n  });\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/fetchData/getMapData.ts",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type {\n  AnyObject,\n  ValidatedColumnInfo,\n  SelectParams,\n} from \"prostgles-types\";\nimport type { LayerSQL, LayerTable } from \"../W_Map\";\nimport type { GeoJSONFeature } from \"../../Map/DeckGLMap\";\nimport type { DetailedFilterBase } from \"@common/filterUtils\";\nimport type { LinkSyncItem } from \"../../Dashboard/dashboardUtils\";\nimport type { DBS } from \"../../Dashboard/DBS\";\n\nexport const WITH_LAST_SELECT_ALIAS = \"prostgles_chart_data\";\nconst rowHashQuery =\n  `md5(((${WITH_LAST_SELECT_ALIAS}.*))::text) as \"$rowhash\"` as const;\nconst getSQLQuery = ({\n  sql,\n  withStatement,\n  limit,\n  whereClause = \"\",\n  selectList,\n}: Pick<LayerSQL, \"sql\" | \"withStatement\"> & {\n  selectList: string;\n  whereClause?: string;\n  limit?: number;\n}) => {\n  const limitQuery = Number.isInteger(limit) ? ` LIMIT ${limit} ` : \"\";\n  return `\n    ${withStatement}\n    SELECT \n      ${selectList}\n      , ST_AsGeoJSON(ST_SetSRID(\\${geomColumn:name}, 4326))::json as l \n    FROM (\n      ${sql}\n    ) prostgles_chart_data\n    ${whereClause}\n    ${limitQuery}\n  `;\n};\n\nexport const MAP_SELECT_COLUMNS = {\n  geoJson: \"l\",\n  idObj: \"i\",\n  props: \"p\",\n} as const;\nexport type MapDataResult = {\n  l: AnyObject;\n  i: AnyObject | string;\n};\n\nexport const getSQLData = async (\n  layer: LayerSQL,\n  db: DBHandlerClient,\n  AGG_LIMIT: number,\n): Promise<{ $rowhash: string; l: AnyObject }[]> => {\n  const { parameters, geomColumn } = layer;\n\n  const query = getSQLQuery({\n    ...layer,\n    selectList: [\n      rowHashQuery,\n      \"ST_AsGeoJSON(ST_SetSRID(${geomColumn:name}, 4326))::json as l\",\n    ].join(\", \"),\n    limit: AGG_LIMIT,\n  });\n\n  const result = (await db.sql!(\n    query,\n    { ...parameters, geomColumn },\n    { returnType: \"rows\" },\n  )) as any;\n\n  return result;\n};\n\nexport const getMapSelect = (\n  { geomColumn, linkId }: Pick<LayerTable, \"geomColumn\" | \"linkId\">,\n  columns: ValidatedColumnInfo[],\n  myLinks: LinkSyncItem[],\n) => {\n  const idColumns = columns.filter((c) => c.is_pkey).map((c) => c.name);\n  const link = myLinks.find((l) => l.id === linkId);\n  const opts = link?.options;\n  const select = {\n    [MAP_SELECT_COLUMNS.idObj]:\n      idColumns.length ?\n        { $jsonb_build_object: idColumns }\n      : { $md5_multi: [geomColumn] },\n    [MAP_SELECT_COLUMNS.geoJson]: { $ST_AsGeoJSON: [geomColumn] },\n    ...(opts?.type === \"map\" && opts.mapShowText?.columnName ?\n      {\n        [MAP_SELECT_COLUMNS.props]: {\n          $jsonb_build_object: [opts.mapShowText.columnName],\n        },\n      }\n    : {}),\n  } as const satisfies SelectParams[\"select\"];\n\n  return select;\n};\nexport const getMapFilter = (\n  lt: Pick<LayerTable, \"geomColumn\" | \"linkId\">,\n  columns: ValidatedColumnInfo[],\n  fProps: GeoJSONFeature[\"properties\"],\n  myLinks: LinkSyncItem[],\n) => {\n  const select = getMapSelect(lt, columns, myLinks);\n  if (fProps.type !== \"table\") {\n    console.error(\"Only table type is supported\");\n    return undefined;\n  }\n  if (select.i.$jsonb_build_object) {\n    const filterValue = fProps.i;\n    return {\n      filterValue,\n      detailedFilter: Object.entries(filterValue).map(([fieldName, value]) => ({\n        fieldName,\n        value,\n      })) satisfies DetailedFilterBase[],\n    };\n  }\n  const filterValue = {\n    $filter: [select.i, \"=\", fProps.i],\n  };\n\n  return {\n    filterValue,\n    detailedFilter: [\n      {\n        fieldName: \"i\",\n        type: \"=\",\n        value: fProps.i,\n        complexFilter: {\n          type: \"$filter\",\n          leftExpression: select.i as any,\n        },\n      },\n    ] satisfies DetailedFilterBase[],\n  };\n};\n\nexport const getSQLHoverRow = async (\n  q: LayerSQL,\n  db: DBHandlerClient,\n  $rowhash: string,\n): Promise<{ $rowhash: string; d: AnyObject } | undefined> => {\n  const { parameters, geomColumn } = q;\n\n  const query = getSQLQuery({\n    ...q,\n    selectList: [\n      rowHashQuery,\n      `row_to_json((${WITH_LAST_SELECT_ALIAS}.*)) as d`,\n    ].join(\", \"),\n    whereClause: `WHERE \"$rowhash\" = \\${$rowhash} `,\n  });\n  return (await db.sql!(\n    query,\n    { ...parameters, geomColumn, $rowhash },\n    { returnType: \"row\" },\n  )) as any;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/fetchData/getMapDataExtent.ts",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport type { DecKGLMapProps } from \"../../Map/DeckGLMap\";\nimport type W_Map from \"../W_Map\";\nimport { defaultWorldExtent } from \"../../WindowControls/AddChartLayer\";\n\n/**\n *\n * @returns data extent for all given filters except map current extent filter\n */\nexport const getMapDataExtent: DecKGLMapProps[\"onGetFullExtent\"] =\n  async function (this: W_Map, fromUserClick = false) {\n    const {\n      prgl: { db },\n      layerQueries = [],\n    } = this.props;\n\n    let minLat,\n      minLng,\n      maxLat,\n      maxLng,\n      _xyExtent: { e: string } | AnyObject | undefined;\n    for (const layer of layerQueries) {\n      if (layer.type === \"osm\") {\n        return undefined;\n      } else if (\"tableName\" in layer) {\n        const { geomColumn, tableName } = layer;\n        const { finalFilterWOextent } = this.getFilter(\n          layer,\n          defaultWorldExtent,\n        );\n        if (!db[tableName]?.findOne) throw `db.${tableName}.find not allowed`;\n        _xyExtent =\n          (await db[tableName].findOne(finalFilterWOextent, {\n            select: { e: { $ST_Extent: [geomColumn] } },\n          })) ?? [];\n      } else {\n        if (!db.sql) throw \"SQL not allowed\";\n        const q = this.getSQL(\n          layer,\n          \"ST_Extent(${geomColumn:name}::geometry) as e\",\n        );\n        // console.log( await db.sql(q.sql, q.args, { returnType: \"statement\" }))\n        try {\n          _xyExtent =\n            (await db.sql(q.sql, q.args, { returnType: \"row\" })) ?? undefined;\n        } catch (error) {\n          console.error(error);\n        }\n      }\n\n      if (!_xyExtent?.e) {\n        if (fromUserClick) {\n          alert(\"No data to zoom to\");\n        }\n        // console.error(\"No extent\")\n        return undefined;\n      }\n\n      const [[_minLat, _minLng], [_maxLat, _maxLng]] =\n        _xyExtent.e\n          ?.slice(4, -1)\n          ?.split(\",\")\n          .map((v) => v.split(\" \").map((v) => +v)) ?? [];\n\n      minLat = Math.min(minLat ?? _minLat, _minLat);\n      maxLat = Math.max(maxLat ?? _maxLat, _maxLat);\n      minLng = Math.min(minLng ?? _minLng, _minLng);\n      maxLng = Math.max(maxLng ?? _maxLng, _maxLng);\n    }\n\n    if (minLat === undefined) return undefined;\n\n    return [\n      [minLat, minLng],\n      [maxLat, maxLng],\n    ];\n  };\n"
  },
  {
    "path": "client/src/dashboard/W_Map/fetchData/getMapLayerQueries.ts",
    "content": "import { isDefined } from \"prostgles-types\";\nimport { parseFullFilter } from \"@common/publishUtils\";\nimport type {\n  Link,\n  LinkSyncItem,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport { windowIs } from \"../../Dashboard/dashboardUtils\";\nimport type { CrossFilters } from \"../../joinUtils\";\nimport { getCrossFilters } from \"../../joinUtils\";\nimport type { LayerOSM, LayerQuery, LayerSQL, LayerTable } from \"../W_Map\";\nimport type { ActiveRow } from \"../../W_Table/W_Table\";\nimport type { DeckGlColor } from \"../../Map/DeckGLMap\";\nimport { getSmartGroupFilter } from \"@common/filterUtils\";\nimport { PALETTE } from \"src/dashboard/Dashboard/PALETTE\";\n\ntype Args = {\n  links: LinkSyncItem[];\n  myLinks: LinkSyncItem[];\n  windows: WindowSyncItem[];\n  active_row: ActiveRow | undefined;\n  w: WindowSyncItem<\"map\">;\n};\n\nexport const getLinkColorV2 = (l?: Link, opacity?: number) => {\n  const colorArr =\n    l?.options.type !== \"table\" ?\n      l?.options.columns[0]?.colorArr\n    : l.options.colorArr;\n\n  return getLinkColor(colorArr, opacity);\n};\n\nexport const getLinkColor = (\n  colorValue: number[] | undefined,\n  opacity?: number,\n) => {\n  const value = colorValue as [number, number, number, number] | undefined;\n  const parseVal = (v: number) => {\n    if (typeof v === \"number\" && v >= 0 && v <= 255) return v;\n    return 99;\n  };\n  const defaultVal = PALETTE.c1.getDeckRGBA();\n  const colorArrRaw = value ?? defaultVal;\n  const colorArrParsed = colorArrRaw.map(parseVal) as typeof colorArrRaw;\n  const [r, g, b, a] = colorArrParsed;\n  const colorOpacity =\n    Number.isFinite(opacity) ? opacity!\n    : Number.isFinite(a) ? a\n    : 1;\n  const colorArr: DeckGlColor = [r, g, b, colorOpacity * 255];\n  const colorArrStr = [r, g, b, colorOpacity];\n  const colorStr = `rgba(${colorArrStr.join()})`;\n\n  return {\n    colorArr,\n    colorStr,\n  };\n};\n\nexport const getMapLayerQueries = ({\n  links,\n  myLinks,\n  windows,\n  active_row,\n  w,\n}: Args) => {\n  const layerQueries: LayerQuery[] = myLinks\n    .flatMap((l: Link) => {\n      const lOpts = l.options;\n      if (\n        lOpts.type !== \"map\" ||\n        (!lOpts.columns.length &&\n          (lOpts.dataSource?.type !== \"osm\" || !lOpts.dataSource.osmLayerQuery))\n      ) {\n        throw \"columns/OSM query missing from link\";\n      }\n      const isLocalLayerLink = l.w1_id === l.w2_id;\n      const linkW =\n        isLocalLayerLink ? undefined : (\n          windows.find(\n            (_w) =>\n              (windowIs(_w, \"table\") || windowIs(_w, \"sql\")) &&\n              _w.id !== w.id &&\n              [l.w1_id, l.w2_id].includes(_w.id),\n          )\n        );\n      const _joinEndTable =\n        lOpts.dataSource?.type === \"table\" ?\n          lOpts.dataSource.joinPath\n        : undefined;\n      const joinEndTable = _joinEndTable?.at(-1);\n      const _localTableName =\n        lOpts.dataSource?.type === \"local-table\" ?\n          lOpts.dataSource.localTableName\n        : undefined;\n      const tableName =\n        isLocalLayerLink ? _localTableName : (\n          (joinEndTable?.table ?? linkW?.table_name)\n        );\n\n      if (lOpts.dataSource?.type === \"osm\") {\n        const { colorArr, colorStr } = getLinkColor(\n          lOpts.mapColorMode?.type === \"fixed\" ?\n            lOpts.mapColorMode.colorArr\n          : [0, 0, 0, 1],\n        );\n        const fillColor = colorArr;\n        const lineColor = colorArr;\n        const color = colorStr;\n        const query = lOpts.dataSource.osmLayerQuery;\n        return {\n          ...l.options,\n          disabled: !!l.disabled,\n          _id: `${l.id}`,\n          linkId: l.id,\n          fillColor,\n          lineColor,\n          color,\n          geomColumn: \"\",\n          type: \"osm\",\n          query,\n        } satisfies LayerOSM;\n      }\n\n      const columnLayerQueries: LayerQuery[] = lOpts.columns\n        .flatMap(({ colorArr: _colorArr, name: geomColumn }, columnIndex) => {\n          const { colorArr, colorStr } = getLinkColor(_colorArr);\n\n          const fillColor = colorArr;\n          const lineColor = colorArr;\n          const color = colorStr;\n          const commonOpts = {\n            ...l.options,\n            disabled: !!l.disabled,\n            _id: `${l.id}-${columnIndex}`,\n            linkId: l.id,\n            fillColor,\n            lineColor,\n            color,\n            geomColumn,\n            wid: linkW?.id,\n          };\n\n          if (tableName) {\n            const jf: CrossFilters =\n              !linkW ?\n                {\n                  all: [],\n                  crossFilters: [],\n                  activeRowFilter: undefined,\n                }\n              : getCrossFilters(w, active_row, links, windows);\n\n            const smartGroupFilter =\n              lOpts.dataSource?.type === \"local-table\" ?\n                lOpts.dataSource.smartGroupFilter\n              : undefined;\n            const localLayerFilter =\n              (!smartGroupFilter ? undefined : (\n                parseFullFilter(smartGroupFilter, undefined, undefined)\n              )) ?? {};\n            const joinPath =\n              lOpts.dataSource?.type === \"table\" ?\n                lOpts.dataSource.joinPath\n              : undefined;\n            const joinInfo =\n              joinPath && w.table_name && joinEndTable ?\n                {\n                  joinStartTable: w.table_name,\n                  path: joinPath,\n                }\n              : ({\n                  joinStartTable: undefined,\n                  path: undefined,\n                } satisfies Pick<LayerTable, \"joinStartTable\" | \"path\">);\n\n            const lt: LayerTable = {\n              ...commonOpts,\n              type: \"table\",\n              tableName,\n              ...joinInfo,\n              externalFilters: [...jf.all, localLayerFilter],\n              tableFilter: getSmartGroupFilter(linkW?.filter || []),\n              joinFilter: jf.activeRowFilter,\n              // elevation: 1000\n            };\n            return lt;\n\n            /** Must be sql */\n          } else if (linkW?.type === \"sql\") {\n            const latestW = linkW.$get();\n            const sql =\n              lOpts.dataSource?.type === \"sql\" ?\n                lOpts.dataSource.sql\n              : undefined;\n            if (!sql) {\n              throw \"Unexpected: sql missing\";\n            }\n            const lsql: LayerSQL = {\n              ...commonOpts,\n              type: \"sql\",\n              sql,\n              withStatement:\n                lOpts.dataSource?.type === \"sql\" ?\n                  lOpts.dataSource.withStatement\n                : \"\",\n            };\n            if (!lsql.sql) {\n              return undefined;\n            }\n            return lsql;\n          } else {\n            console.error(\"linkW is missing?\");\n          }\n        })\n        .filter(isDefined);\n\n      return columnLayerQueries;\n    })\n    .filter(isDefined);\n\n  return layerQueries;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/getMapFeatureStyle.ts",
    "content": "import { cachedSvgs } from \"@components/SvgIcon\";\nimport type { LinkSyncItem } from \"../Dashboard/dashboardUtils\";\nimport type {\n  DeckGlColor,\n  GeoJSONFeature,\n  GeoJsonLayerProps,\n} from \"../Map/DeckGLMap\";\nimport { blend } from \"../W_Table/colorBlend\";\nimport { MAP_SELECT_COLUMNS } from \"./fetchData/getMapData\";\nimport type { ClickedItem, LayerQuery } from \"./W_Map\";\nimport { asRGB } from \"src/utils/colorUtils\";\n\nexport const rgbaToString = (rgba: DeckGlColor) => {\n  const [r, g, b, a = 1] = rgba.map((v) => (Number.isInteger(v) ? v : 1));\n  const alpha = a > 1 ? a / 255 : a;\n  return `rgba(${[r, g, b, alpha]})`;\n};\n\nconst parseFeatureColor = (\n  f: GeoJSONFeature,\n  link: LinkSyncItem | undefined,\n  { geomColumn }: LayerQuery,\n): DeckGlColor | undefined => {\n  if (!link) return;\n  const opts = link.options;\n  if (opts.type !== \"map\") return;\n  const { mapColorMode, columns } = opts;\n  const colorArr = columns.find((c) => c.name === geomColumn)?.colorArr;\n  if (!mapColorMode) return colorArr as DeckGlColor;\n  if (mapColorMode.type === \"fixed\")\n    return mapColorMode.colorArr as DeckGlColor;\n  if (mapColorMode.type === \"conditional\") {\n    const { columnName, conditions } = mapColorMode;\n    const val = f.properties[columnName];\n    const condition = conditions.find((c) => c.value === val);\n    return condition?.colorArr as DeckGlColor;\n  }\n\n  const { minColorArr, maxColorArr, max, min, columnName } = mapColorMode;\n  const val = f.properties[columnName];\n  if ([val, max, min].every((v) => Number.isFinite(v))) {\n    const perc = (val - min) / (max - min);\n    const minColor = rgbaToString(minColorArr as DeckGlColor);\n    const maxColor = rgbaToString(maxColorArr as DeckGlColor);\n    const color = blend(minColor, maxColor, perc);\n    return asRGB(color);\n  }\n};\n\nexport const getMapFeatureStyle = (\n  layerQuery: LayerQuery,\n  clickedItem: ClickedItem | undefined,\n  links: LinkSyncItem[],\n): Pick<\n  GeoJsonLayerProps,\n  | \"getFillColor\"\n  | \"getLineColor\"\n  | \"getText\"\n  | \"getTextSize\"\n  | \"getIcon\"\n  | \"display\"\n> => {\n  const getIsClickedFeature = (f: GeoJSONFeature) => {\n    return (\n      clickedItem &&\n      \"type\" in f.properties &&\n      ((f.properties.type === \"sql\" &&\n        clickedItem.properties.$rowhash &&\n        f.properties.$rowhash === clickedItem.properties.$rowhash) ||\n        (f.properties.type === \"table\" &&\n          clickedItem.properties.i &&\n          f.properties.i === clickedItem.properties.i))\n    );\n  };\n  const { linkId } = layerQuery;\n  const link = links.find((l) => l.id === linkId);\n  const opts = link?.options;\n  if (!opts || opts.type !== \"map\") {\n    throw new Error(\"Invalid map link type\");\n  }\n\n  const { mapIcons, mapShowText } = opts;\n\n  return {\n    getFillColor: (f) => {\n      /** TODO maybe color items based on any table styled columns  \n          let fill = fillColor;\n          if(!willAggregate && sourceW.table_name && sourceW.columns){\n            const styledCols = sourceW.columns.filter(c => c.style?.type && c.style?.type !== \"None\");\n\n          }\n        */\n      if (getIsClickedFeature(f)) {\n        return [0, 0, 0, 255] as DeckGlColor;\n      }\n      const fillColor =\n        parseFeatureColor(f, link, layerQuery) || layerQuery.fillColor;\n      if (f.geometry.type.includes(\"Polygon\")) {\n        return fillColor.slice(0, 3).concat([200]) as DeckGlColor;\n      }\n      return fillColor;\n    },\n    getLineColor: (f) => {\n      const lineColor =\n        parseFeatureColor(f, link, layerQuery) || layerQuery.lineColor;\n      if (getIsClickedFeature(f)) {\n        return [0, 0, 0, 255] as DeckGlColor;\n      }\n      return lineColor;\n    },\n    getText:\n      mapShowText ?\n        (f) => {\n          const { columnName } = mapShowText;\n          return (\n            f.properties[MAP_SELECT_COLUMNS.props]?.[columnName]?.toString() ??\n            \"\"\n          );\n        }\n      : undefined,\n    getTextSize:\n      mapShowText ?\n        (f) => {\n          const { columnName } = mapShowText;\n          const txt = `${f.properties[MAP_SELECT_COLUMNS.props]?.[columnName]?.toString() ?? \"\"}`;\n          return 12; // txt.length * 4;\n        }\n      : undefined,\n    getIcon:\n      !mapIcons ? undefined : (\n        (f) => {\n          const icon =\n            mapIcons.type === \"fixed\" ?\n              mapIcons.iconPath\n            : (mapIcons.conditions.find(\n                (c) => c.value === f.properties[mapIcons.columnName],\n              )?.iconPath ?? \"\");\n          const iconPath = `/icons/${icon}.svg`;\n          const rawSvg = cachedSvgs.get(iconPath) ?? \"\";\n          const lineColor =\n            parseFeatureColor(f, link, layerQuery) || layerQuery.lineColor;\n          const svg = rawSvg.replace(\n            \"<svg \",\n            `<svg width=\"24\" height=\"24\" style=\"color:${rgbaToString(lineColor)};\" `,\n          );\n          return {\n            id: `${iconPath}-${lineColor}`,\n            // Maybe load directly to avoid the flickering? `${location.origin}${iconPath}`, //\n            url: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`,\n            width: 24,\n            height: 24,\n          };\n        }\n      ),\n    display: mapIcons?.display,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Map/onMapHover.ts",
    "content": "import type { HoverCoords } from \"../Map/DeckGLMap\";\nimport type { LayerQuery, LayerSQL } from \"./W_Map\";\nimport type W_Map from \"./W_Map\";\nimport type { MapDataResult } from \"./fetchData/getMapData\";\nimport { getMapFilter, getSQLHoverRow } from \"./fetchData/getMapData\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { isObject } from \"prostgles-types\";\n\nexport type HoveredObject = {\n  properties: MapDataResult & {\n    geomColumn: string;\n    radius: number;\n    tableName: string;\n    /**\n     * Aggregated count\n     */\n    c?: string;\n    layer?: LayerQuery;\n  };\n};\n\nexport function onMapHover(\n  this: W_Map,\n  hoverObj?: AnyObject & HoveredObject,\n  hoverCoords?: HoverCoords,\n) {\n  const { prgl, tables } = this.props;\n  if (!hoverObj && !this.hovering && !this.state.hoverObj) {\n    //  || this.state.clickedItem\n    return;\n  }\n\n  this.hoveredObj = hoverObj;\n  const hoverObjStr = JSON.stringify(hoverObj);\n  if (this.hovering) {\n    if (this.hovering.hoverObjStr === hoverObjStr) {\n      return;\n    } else {\n      clearTimeout(this.hovering.timeout);\n    }\n  }\n\n  /** If no hover then imediatelly remove existing existing hover data */\n  if (!hoverObj) {\n    this.hovering = undefined;\n    this.setState({ hoverObj, hoverCoords, hovData: undefined });\n\n    /** If hovering then wait for the cursor to settle on an object before firing data request */\n  } else {\n    if ((hoverObj.properties as any)?._is_from_osm === true) {\n      this.setState({ hoverObj, hoverCoords, hovData: hoverObj.properties });\n      return;\n    }\n\n    /** Throttle  */\n    this.hovering = {\n      hoverObj: { ...hoverObj },\n      hoverObjStr,\n      timeout: setTimeout(async () => {\n        /**\n         * Check if not stale hover and set data\n         */\n        if (\n          this.hovering?.hoverObjStr === hoverObjStr &&\n          isObject(hoverObj.properties) &&\n          hoverObj.properties.layer\n        ) {\n          const { tableName, i, layer, c } = hoverObj.properties;\n          let hovData;\n          if (layer.type === \"table\") {\n            if (c) {\n              hovData = { Count: c };\n            } else if (tableName) {\n              const table = tables.find((t) => t.name === tableName);\n              if (table) {\n                const selectCols = table.columns.filter((c) =>\n                  [\"geography\", \"geometry\"].includes(c.udt_name),\n                );\n                if (!selectCols.length) {\n                  return;\n                }\n                const select = selectCols.reduce(\n                  (a, v) => ({ ...a, [v.name]: 0 }),\n                  {},\n                );\n                const filter = getMapFilter(\n                  layer,\n                  table.columns,\n                  hoverObj.properties as any,\n                  this.props.myLinks,\n                )?.filterValue;\n                // const filter = selectData.i.$jsonb_build_object? (i as AnyObject) : {\n                //   $filter: [\n                //     selectData.i,\n                //     \"=\",\n                //     i\n                //   ]\n                // };\n                hovData = await prgl.db[tableName]?.findOne?.(filter, {\n                  select,\n                });\n              }\n            }\n          } else if (i && typeof i === \"string\") {\n            hovData = (await getSQLHoverRow(layer as LayerSQL, prgl.db, i))?.d;\n          }\n          this.hovering = undefined;\n\n          this.setState({ hoverObj, hoverCoords, hovData });\n        }\n      }, 300),\n    };\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Method/FunctionLabel.tsx",
    "content": "import React from \"react\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport type { DBSSchema } from \"@common/publishUtils\";\n\nexport const FunctionLabel = ({\n  name,\n  description,\n  arguments: args,\n}: DBSSchema[\"published_methods\"]) => {\n  return (\n    <FlexCol className=\"gap-p5 m-auto\">\n      <FlexRow className=\"gap-p25\">\n        <div>{name}</div>\n        <div className=\"text-2\">\n          {!args.length ?\n            \" ()\"\n          : ` ({ ${args.map((a) => `${a.name}: ${a.type === \"Lookup\" ? `${a.lookup.table}.${a.lookup.column}` : \"\"}`).join(\"; \")} })`\n          }\n        </div>\n      </FlexRow>\n      {!!description.trim() && <div className=\"text-2\">{description}</div>}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Method/NewMethod.tsx",
    "content": "import { omitKeys } from \"prostgles-types\";\nimport React, { useEffect, useState } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Prgl } from \"../../App\";\nimport { pageReload } from \"@components/Loader/Loading\";\nimport Popup from \"@components/Popup/Popup\";\nimport { MethodDefinition } from \"../AccessControl/Methods/MethodDefinition\";\n\nexport type Method = DBSSchema[\"published_methods\"] & {\n  access_control_methods: DBSSchema[\"access_control_methods\"][];\n};\n\ntype P = Pick<\n  Prgl,\n  \"dbs\" | \"db\" | \"tables\" | \"dbsTables\" | \"dbsMethods\" | \"dbKey\"\n> & {\n  /** If undefined then it's a new method */\n  methodId: number | undefined;\n  access_rule_id: number | undefined;\n  connectionId: string;\n  onClose: VoidFunction;\n};\nexport const NewMethod = ({\n  dbKey,\n  db,\n  dbs,\n  methodId,\n  connectionId,\n  onClose,\n  dbsTables,\n  tables,\n  dbsMethods,\n  access_rule_id,\n}: P) => {\n  const [newMethod, setNewMethod] = useState<Partial<Omit<Method, \"id\">>>({\n    name: \"my_new_func\",\n    arguments: [],\n    run: \"export const run: ProstglesMethod = async (args, { db, dbo, user, callMCPServerTool }) => {\\n  \\n}\",\n    connection_id: connectionId,\n    description: \"\",\n    outputTable: null,\n  });\n\n  const { data: existingMethod } = dbs.published_methods.useFindOne(\n    {\n      id: methodId,\n    },\n    undefined,\n    { skip: !methodId },\n  );\n  useEffect(() => {\n    if (existingMethod && existingMethod.id === methodId) {\n      setNewMethod(existingMethod);\n    }\n  }, [existingMethod, methodId]);\n\n  const isNewMethod = methodId === undefined;\n\n  return (\n    <Popup\n      title={isNewMethod ? \"Add function\" : `Update ${methodId}`}\n      positioning=\"fullscreen\"\n      onClickClose={false}\n      onClose={onClose}\n      footerButtons={[\n        {\n          onClickClose: true,\n          label: \"Close\",\n          className: \"mr-auto\",\n        },\n        {\n          label: !isNewMethod ? \"Update function\" : \"Add function\",\n          color: \"action\",\n          variant: \"filled\",\n          onClickPromise: async () => {\n            if (methodId) {\n              await dbs.published_methods.update(\n                { id: methodId },\n                omitKeys(newMethod, [\"access_control_methods\"]),\n              );\n            } else {\n              const { id } = await dbs.published_methods.insert(\n                {\n                  ...newMethod,\n                  connection_id: connectionId,\n                },\n                { returning: { id: 1 } },\n              );\n              if (access_rule_id) {\n                await dbs.access_control_methods.insert({\n                  access_control_id: access_rule_id,\n                  published_method_id: id,\n                });\n              }\n              pageReload(\"inserted published_methods\");\n            }\n            onClose();\n          },\n        },\n      ]}\n      contentClassName=\"flex-col gap-1 p-2\"\n    >\n      <MethodDefinition\n        dbKey={dbKey}\n        dbs={dbs}\n        connectionId={connectionId}\n        dbsMethods={dbsMethods}\n        method={newMethod}\n        tables={tables}\n        dbsTables={dbsTables}\n        db={db}\n        onChange={setNewMethod}\n      />\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Method/PublishedMethods.tsx",
    "content": "import { mdiDelete, mdiLanguageTypescript, mdiPencil, mdiPlus } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { isDefined } from \"prostgles-types\";\nimport React, { useMemo, useState } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport ConfirmationDialog from \"@components/ConfirmationDialog\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { SectionHeader } from \"../AccessControl/AccessControlRuleEditor\";\nimport type { ValidEditedAccessRuleState } from \"../AccessControl/useEditedAccessRule\";\nimport { SmartCardList } from \"../SmartCardList/SmartCardList\";\nimport { ProcessLogs } from \"../TableConfig/ProcessLogs\";\nimport { NewMethod } from \"./NewMethod\";\nimport { FunctionLabel } from \"./FunctionLabel\";\nimport type { DBS } from \"../Dashboard/DBS\";\nimport type { FieldConfig } from \"../SmartCard/SmartCard\";\n\ntype P = {\n  className?: string;\n  style?: React.CSSProperties;\n  accessRuleId: number | undefined;\n  prgl: Prgl;\n  editedRule: ValidEditedAccessRuleState | undefined;\n};\n\nexport const PublishedMethods = ({\n  className,\n  style,\n  prgl,\n  accessRuleId,\n  editedRule,\n}: P) => {\n  const { dbsMethods, dbsTables, dbs, connectionId } = prgl;\n  const { listProps, action, setAction } = useSmartCardListProps({\n    dbs,\n    connectionId,\n    editedRule,\n  });\n\n  return (\n    <FlexCol className={className} style={style}>\n      <SectionHeader icon={mdiLanguageTypescript}>Functions</SectionHeader>\n      <div className={`flex-col gap-1 pl-2 `}>\n        <p className=\"p-0 m-0\">Server-side user triggered functions</p>\n        <SmartCardList\n          db={dbs as DBHandlerClient}\n          methods={dbsMethods}\n          tables={dbsTables}\n          tableName=\"published_methods\"\n          realtime={true}\n          noDataComponent={\n            <InfoRow color=\"info\" variant=\"filled\">\n              No functions\n            </InfoRow>\n          }\n          showEdit={false}\n          {...listProps}\n          footer={\n            <FlexRow className=\"mt-1\">\n              <Btn\n                iconPath={mdiPlus}\n                variant={isDefined(accessRuleId) ? \"faded\" : \"filled\"}\n                color=\"action\"\n                title=\"Create new method\"\n                onClick={() => {\n                  setAction({ type: \"create\" });\n                }}\n              >\n                Create function\n              </Btn>\n              <PopupMenu\n                title=\"Logs\"\n                onClickClose={false}\n                button={<Btn variant=\"faded\">Show logs</Btn>}\n                showFullscreenToggle={{ defaultValue: true }}\n                clickCatchStyle={{ opacity: 0.5 }}\n                positioning=\"center\"\n              >\n                <ProcessLogs\n                  noMaxHeight={true}\n                  type=\"methods\"\n                  connectionId={connectionId}\n                  dbs={dbs}\n                  dbsMethods={dbsMethods}\n                />\n              </PopupMenu>\n            </FlexRow>\n          }\n        />\n        <div className=\"flex-col f-1\">\n          {action && (\n            <NewMethod\n              {...prgl}\n              connectionId={connectionId}\n              access_rule_id={accessRuleId}\n              dbs={dbs}\n              onClose={() => setAction(undefined)}\n              methodId={\n                action.type === \"update\" ? action.existingMethodId : undefined\n              }\n            />\n          )}\n        </div>\n      </div>\n    </FlexCol>\n  );\n};\n\nconst useSmartCardListProps = ({\n  dbs,\n  connectionId,\n  editedRule,\n}: {\n  dbs: DBS;\n  connectionId: string;\n  editedRule: P[\"editedRule\"];\n}) => {\n  const [action, setAction] = useState<\n    { type: \"update\"; existingMethodId: number } | { type: \"create\" }\n  >();\n\n  const listProps = useMemo(() => {\n    const filter = { connection_id: connectionId };\n    const style = { width: \"fit-content\" };\n    const rowProps = {\n      className: \"trigger-hover\",\n    };\n    const fieldConfigs: FieldConfig<DBSSchema[\"published_methods\"]>[] = [\n      { name: \"description\" as const, hide: true },\n      { name: \"arguments\" as const, hide: true },\n      {\n        name: \"name\",\n        renderMode: \"full\",\n        render: (v, row: DBSSchema[\"published_methods\"]) =>\n          !editedRule ?\n            <FunctionLabel {...row} />\n          : <SwitchToggle\n              checked={\n                !!editedRule.newRule?.access_control_methods?.some(\n                  (m) => m.published_method_id === row.id,\n                )\n              }\n              label={{\n                children: <FunctionLabel {...row} />,\n              }}\n              onChange={(checked) => {\n                const access_control_methods =\n                  editedRule.newRule?.access_control_methods ?? [];\n\n                editedRule.onChange({\n                  access_control_methods:\n                    checked ?\n                      [\n                        ...access_control_methods,\n                        { published_method_id: row.id },\n                      ]\n                    : access_control_methods.filter(\n                        (a) => a.published_method_id !== row.id,\n                      ),\n                });\n              }}\n            />,\n      } satisfies FieldConfig<DBSSchema[\"published_methods\"]>,\n      {\n        name: \"id\" as const,\n        label: \" \",\n        className: \"f-1 \",\n        render: (v, row: DBSSchema[\"published_methods\"]) => (\n          <FlexRow className=\"noselect w-full\">\n            <div className=\"ml-auto flex-row ai-center show-on-trigger-hover\">\n              <Btn\n                title=\"Edit function\"\n                iconPath={mdiPencil}\n                onClick={() => {\n                  setAction({\n                    type: \"update\",\n                    existingMethodId: row.id,\n                  });\n                }}\n              />\n              <PopupMenu\n                button={\n                  <Btn\n                    title=\"Delete function...\"\n                    color=\"danger\"\n                    iconPath={mdiDelete}\n                  />\n                }\n                onClickClose={false}\n                clickCatchStyle={{ opacity: 1 }}\n                render={(pClose) => (\n                  <ConfirmationDialog\n                    acceptBtn={{\n                      color: \"danger\",\n                      text: \"Delete function\",\n                      dataCommand: \"PublishedMethods.deleteFunction\",\n                    }}\n                    message=\"Are you sure you want to delete this function?\"\n                    onAccept={async () => {\n                      await dbs.published_methods.delete({ id: row.id });\n                    }}\n                    onClose={pClose}\n                  />\n                )}\n              />\n            </div>\n          </FlexRow>\n        ),\n      },\n    ].filter(isDefined);\n    return {\n      filter,\n      fieldConfigs,\n      style,\n      rowProps,\n    };\n  }, [connectionId, editedRule, dbs.published_methods]);\n\n  return {\n    listProps,\n    action,\n    setAction,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Method/W_Method.tsx",
    "content": "import React, { useState } from \"react\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { useEffectAsync } from \"../DashboardMenu/DashboardMenuSettings\";\nimport Window from \"../Window\";\nimport { W_MethodMenu } from \"./W_MethodMenu\";\nimport { W_MethodControls } from \"./W_MethodControls\";\nimport { useIsMounted } from \"prostgles-client\";\n\nexport type W_MethodProps = Omit<CommonWindowProps, \"w\"> & {\n  w: WindowSyncItem<\"method\">;\n};\nexport const W_Method = (allProps: W_MethodProps) => {\n  const { tables, ...props } = allProps;\n\n  const [w, setW] = useState(allProps.w);\n\n  const getIsMounted = useIsMounted();\n  useEffectAsync(async () => {\n    const wSync = await props.w.$cloneSync((newW, deltaW) => {\n      if (!getIsMounted()) return;\n      setW(newW);\n    });\n\n    return wSync.$unsync;\n  }, [props.w, getIsMounted]);\n\n  const setOpts = (newOpts: Partial<(typeof w)[\"options\"]>) => {\n    w.$update({ options: { ...w.options, ...newOpts } }, { deepMerge: false });\n  };\n\n  return (\n    <Window\n      w={w as any}\n      layoutMode={props.workspace.layout_mode ?? \"editable\"}\n      getMenu={(w, closeMenu) => (\n        <W_MethodMenu {...allProps} w={w} closeMenu={closeMenu} />\n      )}\n    >\n      <W_MethodControls\n        {...allProps.prgl}\n        method_name={w.method_name}\n        w={w}\n        setState={setOpts}\n        state={w.options}\n      />\n    </Window>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Method/W_MethodControls.tsx",
    "content": "import { mdiChevronDown, mdiPlay } from \"@mdi/js\";\nimport type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { AnyObject, JSONB, MethodFullDef } from \"prostgles-types\";\nimport { isEmpty } from \"prostgles-types\";\nimport { getKeys } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { useReactiveState, type Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { isCompleteJSONB } from \"@components/JSONBSchema/isCompleteJSONB\";\nimport { JSONBSchema } from \"@components/JSONBSchema/JSONBSchema\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { omitKeys } from \"prostgles-types\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport type { WindowData } from \"../Dashboard/dashboardUtils\";\nimport SmartTable from \"../SmartTable\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { MethodDefinition } from \"../AccessControl/Methods/MethodDefinition\";\nimport { prgl_R } from \"../../WithPrgl\";\nimport { ProcessLogs } from \"../TableConfig/ProcessLogs\";\n\ntype P = Pick<Prgl, \"db\" | \"methods\" | \"tables\"> & {\n  method_name: string;\n  state: {\n    args?: AnyObject;\n    disabledArgs?: string[];\n    hiddenArgs?: string[];\n  };\n  setState: (newState: P[\"state\"]) => void;\n  fixedRowArgument?: {\n    row: AnyObject;\n    argName: string;\n    tableName: string;\n  };\n  w: SyncDataItem<Required<WindowData<\"method\">>, true> | undefined;\n};\n\nexport const W_MethodControls = ({\n  w,\n  db,\n  tables,\n  methods,\n  method_name,\n  fixedRowArgument,\n  ...otherProps\n}: P) => {\n  const { state: prgl } = useReactiveState(prgl_R);\n\n  const [result, setResult] = useState<AnyObject | void>();\n  const [showResults, setShowResults] = useState(true);\n  const m: MethodFullDef | undefined =\n    method_name && typeof methods[method_name] !== \"function\" ?\n      (methods[method_name] as any)\n    : undefined;\n  const [error, setError] = useState<any>(\n    !m ? `Method named \"${method_name}\" not found` : undefined,\n  );\n  const [expandControls, setExpandControls] = useState(true);\n  const [showJSONBErrors, setshowJSONBErrors] = useState(false);\n  const { dbs, connectionId } = prgl!;\n  const { data: method } = dbs.published_methods.useSubscribeOne(\n    { name: w?.method_name, connection_id: connectionId },\n    { limit: w?.method_name ? 1 : 0 },\n  );\n\n  const [loading, setLoading] = useState(false);\n\n  const argDefaults: AnyObject = {};\n  const disabledArgsDefaults: string[] = [];\n  if (m) {\n    getKeys(m.input).map((argName) => {\n      const arg = m.input[argName]!;\n      const ref = arg.lookup?.type === \"data\" ? arg.lookup : undefined;\n      if ((arg as any).optional) {\n        disabledArgsDefaults.push(argName);\n      }\n      if (fixedRowArgument?.argName === argName) {\n        argDefaults[argName] =\n          ref?.isFullRow ? fixedRowArgument.row\n          : ref?.column ? fixedRowArgument.row[ref.column]\n          : undefined;\n      } else if (arg.defaultValue !== undefined && !ref?.isFullRow) {\n        argDefaults[argName] = arg.defaultValue;\n      }\n    });\n  }\n\n  const args = otherProps.state.args ?? argDefaults;\n  const disabledArgs = otherProps.state.disabledArgs ?? disabledArgsDefaults;\n  const hiddenArgs = otherProps.state.hiddenArgs ?? [];\n  const setArgs = (newArgs) => {\n    setError(undefined);\n    otherProps.setState({ args: newArgs });\n  };\n\n  const inputSchema = m?.input ?? {};\n  const mArgs = getKeys(inputSchema).reduce((a, k) => {\n    const v: keyof typeof inputSchema = k;\n    const arg = inputSchema[v];\n    return {\n      ...a,\n      [v]:\n        arg?.lookup?.type === \"data-def\" ?\n          {\n            ...arg,\n            lookup: {\n              ...arg.lookup,\n              type: \"data\",\n            },\n          }\n        : inputSchema[v],\n    };\n  }, {});\n\n  const argSchema: JSONB.JSONBSchema = {\n    type: {\n      ...omitKeys(mArgs as any, hiddenArgs),\n    },\n    defaultValue: Object.entries(m?.input ?? {})\n      .filter(([k, v]) => v.defaultValue !== undefined)\n      .reduce((a, [k, v]) => ({ ...a, [k]: v.defaultValue }), {}),\n  };\n\n  const hasErrors = !isCompleteJSONB(args, argSchema);\n\n  const outputTableInfo =\n    m?.outputTable ? tables.find((t) => t.name === m.outputTable) : undefined;\n  const { showCode = false, showLogs } = w?.options ?? {};\n\n  if (!prgl) {\n    return <>prgl missing</>;\n  }\n  return (\n    <FlexCol\n      className=\"W_MethodControls f-1  min-s-0 o-auto bg-color-2\"\n      style={{ gap: \"2px\" }}\n    >\n      {showCode && method && (\n        <MethodDefinition\n          renderMode=\"Code\"\n          {...prgl}\n          db={db}\n          tables={tables}\n          method={method}\n          onChange={(code) => {\n            dbs.published_methods.update({ id: method.id }, { run: code.run });\n          }}\n        />\n      )}\n      {m && (\n        <div\n          className=\"flex-col gap-1 p-1 shadow bg-color-0\"\n          style={{\n            /** Used to ensure the \"Show code\" minimap is beneath clickcatch when typing method arguments autocomplete */\n            zIndex: 5,\n          }}\n        >\n          <div\n            className={\n              \"flex-row-wrap gap-1 \" + (expandControls ? \"\" : \" hidden \")\n            }\n          >\n            <JSONBSchema\n              schema={argSchema}\n              value={args as any}\n              onChange={(v) => {\n                setArgs(v) as any;\n              }}\n              db={db}\n              tables={tables}\n              allowIncomplete={true}\n              showErrors={showJSONBErrors}\n            />\n          </div>\n          <FlexRow className=\"gap-2\">\n            <Btn\n              loading={loading}\n              iconPath={mdiPlay}\n              onClick={async () => {\n                try {\n                  const params = omitKeys(args, [\n                    ...disabledArgs,\n                    ...hiddenArgs,\n                  ]);\n                  if (hasErrors) {\n                    setshowJSONBErrors(true);\n                    return;\n                  }\n\n                  setLoading(true);\n                  setError(undefined);\n                  const res = await m.run(params);\n                  if (m.outputTable) {\n                    w?.$update({\n                      name: `${method_name} - ${await db[m.outputTable]?.count?.()}`,\n                    });\n                  }\n\n                  setshowJSONBErrors(false);\n                  setResult(res);\n                } catch (err: any) {\n                  setError(err);\n                  setResult(undefined);\n                }\n                setLoading(false);\n              }}\n              color=\"action\"\n              variant=\"filled\"\n              disabledInfo={\n                hasErrors && showJSONBErrors ?\n                  \"Errors/invalid values found\"\n                : undefined\n              }\n            >\n              Run\n            </Btn>\n\n            {!isEmpty(inputSchema) && (\n              <Btn\n                iconPath={mdiChevronDown}\n                iconStyle={{\n                  transform: `rotate(${!expandControls ? 0 : 180}deg)`,\n                  transition: \".3s all\",\n                }}\n                onClick={() => setExpandControls((v) => !v)}\n              >\n                {!expandControls ? \"Expand\" : \"Collapse\"} input\n              </Btn>\n            )}\n\n            {w && (\n              <>\n                <SwitchToggle\n                  label=\"Show code\"\n                  className=\"ml-auto\"\n                  checked={!!showCode}\n                  onChange={(showCode) => {\n                    w.$update({ options: { showCode } }, { deepMerge: true });\n                  }}\n                  variant=\"row\"\n                />\n                <SwitchToggle\n                  label=\"Show logs\"\n                  checked={!!showLogs}\n                  onChange={(showLogs) => {\n                    w.$update({ options: { showLogs } }, { deepMerge: true });\n                  }}\n                  variant=\"row\"\n                />\n              </>\n            )}\n\n            <SwitchToggle\n              label=\"Show results\"\n              checked={showResults}\n              onChange={setShowResults}\n              variant=\"row\"\n              disabledInfo={\n                result || m.outputTable ? undefined : \"No results to show\"\n              }\n            />\n          </FlexRow>\n        </div>\n      )}\n      {m?.outputTable && !outputTableInfo && (\n        <ErrorComponent\n          error={`Results table ( ${m.outputTable} ) missing or not allowed`}\n          className=\"m-1\"\n        />\n      )}\n      {showResults && !error && (\n        <div className=\"flex-row f-1\">\n          {m?.outputTable && outputTableInfo ?\n            <SmartTable\n              title=\"\"\n              db={db}\n              methods={methods}\n              tableName={m.outputTable}\n              tables={tables}\n              realtime={{}}\n            />\n          : result ?\n            <CodeEditor\n              className=\"b-unset\"\n              style={{ flex: 1 }}\n              language=\"json\"\n              value={JSON.stringify(result, null, 2)}\n            />\n          : null}\n        </div>\n      )}\n      {showLogs && (\n        <ProcessLogs\n          connectionId={connectionId}\n          dbs={dbs}\n          dbsMethods={prgl.dbsMethods}\n          type=\"methods\"\n        />\n      )}\n      <ErrorComponent\n        error={error}\n        findMsg={true}\n        className=\"o-auto min-h-0 m-1 ai-start f-1\"\n      />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Method/W_MethodMenu.tsx",
    "content": "import { mdiFormatListCheckbox, mdiPencil } from \"@mdi/js\";\nimport { isEmpty } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { pageReload } from \"@components/Loader/Loading\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport Tabs from \"@components/Tabs\";\nimport { MethodDefinition } from \"../AccessControl/Methods/MethodDefinition\";\nimport type { W_MethodProps } from \"./W_Method\";\n\nexport const W_MethodMenu = (\n  props: W_MethodProps & { closeMenu: () => void },\n) => {\n  const {\n    prgl: { dbs, dbsTables, user, connectionId },\n    w,\n    closeMenu,\n  } = props;\n  const { data: method } = dbs.published_methods.useFindOne({\n    name: w.method_name,\n    connection_id: connectionId,\n  });\n  const [editedMethod, setEditedMethod] =\n    useState<DBSSchema[\"published_methods\"]>();\n\n  const isAdmin = user?.type === \"admin\";\n  const { hiddenArgs = [] } = w.options;\n\n  if (!method || isEmpty(method)) return null;\n\n  return (\n    <Tabs\n      variant={\"vertical\"}\n      contentClass=\"o-auto f-1 p-p25\"\n      compactMode={window.isMobileDevice ? \"hide-inactive\" : undefined}\n      // defaultActiveKey={isAdmin? \"edit\" : undefined}\n      defaultActiveKey={\"args\"}\n      items={{\n        args: {\n          label: \"Arguments\",\n          leftIconPath: mdiFormatListCheckbox,\n          content: (\n            <div className=\"flex-col \">\n              <SearchList\n                onMultiToggle={(v) => {\n                  const hiddenArgs = v\n                    .filter((d) => !d.checked)\n                    .map((d) => d.key) as string[];\n                  w.$update(\n                    { options: { ...w.options, hiddenArgs } },\n                    { deepMerge: true },\n                  );\n                }}\n                items={method.arguments.map((a) => {\n                  const checked = !hiddenArgs.includes(a.name);\n                  return {\n                    key: a.name,\n                    subLabel:\n                      a.type.startsWith(\"Lookup\") ?\n                        `references ${(a as any).table}`\n                      : a.type,\n                    checked,\n                    disabledInfo: !a.optional ? \"Is required\" : undefined,\n                    onPress: () => {\n                      w.$update(\n                        {\n                          options: {\n                            ...w.options,\n                            hiddenArgs:\n                              !checked ?\n                                hiddenArgs.filter((da) => da !== a.name)\n                              : [...hiddenArgs, a.name],\n                          },\n                        },\n                        { deepMerge: true },\n                      );\n                    },\n                  };\n                })}\n              />\n            </div>\n          ),\n        },\n        edit: {\n          label: \"Edit definition\",\n          leftIconPath: mdiPencil,\n          disabledText: !isAdmin ? \"Not allowed\" : undefined,\n          content: (\n            <div className=\"flex-col o-auto f-1 min-s-0 p-1 gap-1\">\n              <MethodDefinition\n                dbKey={props.prgl.dbKey}\n                dbs={props.prgl.dbs}\n                connectionId={connectionId}\n                dbsMethods={props.prgl.dbsMethods}\n                method={{ ...(editedMethod ?? method) }}\n                dbsTables={props.prgl.dbsTables}\n                tables={props.tables}\n                onChange={(v) => setEditedMethod(v as any)}\n                db={props.prgl.db}\n              />\n              <div className=\"p-1 flex-row ai-center\">\n                <Btn onClick={closeMenu} variant=\"faded\">\n                  {!editedMethod ? \"Close\" : \"Cancel\"}\n                </Btn>\n\n                {editedMethod && (\n                  <Btn\n                    color=\"action\"\n                    variant=\"filled\"\n                    className=\" ml-auto\"\n                    onClickPromise={async () => {\n                      const oldMethod = await dbs.published_methods.findOne({\n                        id: method.id,\n                      });\n                      if (oldMethod) {\n                        await dbs.published_methods.update(\n                          { id: method.id },\n                          editedMethod,\n                        );\n                        w.$update({ method_name: editedMethod.name });\n                        setTimeout(() => {\n                          pageReload(\"edited published_methods\");\n                        }, 500);\n                      }\n                    }}\n                  >\n                    Update\n                  </Btn>\n                )}\n              </div>\n            </div>\n          ),\n        },\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_QuickMenu.tsx",
    "content": "import {\n  mdiChartBoxMultipleOutline,\n  mdiFilter,\n  mdiMagnify,\n  mdiSetLeftCenter,\n} from \"@mdi/js\";\n\nimport Btn from \"@components/Btn\";\nimport React from \"react\";\nimport type { CommonWindowProps } from \"./Dashboard/Dashboard\";\nimport type { OnAddChart, WindowSyncItem } from \"./Dashboard/dashboardUtils\";\n\nimport { isJoinedFilter } from \"@common/filterUtils\";\nimport { classOverride } from \"@components/Flex\";\nimport { t } from \"../i18n/i18nUtils\";\nimport type { DBS } from \"./Dashboard/DBS\";\nimport { getLinkColorV2 } from \"./W_Map/fetchData/getMapLayerQueries\";\nimport type { ChartableSQL } from \"./W_SQL/getChartableSQL\";\nimport { AddChartMenu } from \"./W_Table/TableMenu/AddChartMenu\";\n\nexport type ProstglesQuickMenuProps = Pick<\n  CommonWindowProps,\n  \"tables\" | \"prgl\" | \"myLinks\" | \"childWindows\"\n> & {\n  w: WindowSyncItem<\"table\"> | WindowSyncItem<\"sql\">;\n  dbs: DBS;\n  setLinkMenu?: (args: {\n    w: WindowSyncItem<\"table\">;\n    anchorEl: HTMLElement | Element;\n  }) => any;\n  onAddChart?: OnAddChart;\n  /**\n   * If undefined then will show all\n   */\n  show?: { filter?: boolean; link?: boolean };\n  chartableSQL: ChartableSQL | undefined;\n};\n\nexport const W_QuickMenu = (props: ProstglesQuickMenuProps) => {\n  const {\n    w,\n    setLinkMenu,\n    onAddChart,\n    show,\n    chartableSQL,\n    prgl,\n    myLinks,\n    childWindows,\n  } = props;\n  const { theme, tables } = prgl;\n  const table = tables.find((t) => t.name === w.table_name);\n  const showLinks =\n    (!show || show.link) &&\n    Boolean(\n      (setLinkMenu && w.table_name && table?.joinsV2.length) ||\n      (w.type !== \"sql\" && !!myLinks.length),\n    );\n\n  const [firstLink] = myLinks;\n  const divRef = React.useRef<HTMLDivElement>(null);\n\n  if (!table && !showLinks && w.type === \"table\") {\n    return null;\n  }\n\n  const bgColorClass = theme === \"light\" ? \"bg-color-3\" : \"bg-color-0\";\n\n  const addChartProps =\n    w.type === \"sql\" && chartableSQL ? { w, type: w.type, chartableSQL }\n    : w.type === \"table\" ? { w, type: w.type }\n    : undefined;\n\n  const minimisedCharts = childWindows.filter(\n    (w) => (w.type === \"timechart\" || w.type === \"map\") && w.minimised,\n  );\n  const hasMinimisedCharts = minimisedCharts.length > 0;\n\n  return (\n    <>\n      <div\n        data-command=\"Window.W_QuickMenu\"\n        className={classOverride(\n          \"W_QuickMenu flex-row ai-center rounded bb-color h-fit w-fit m-auto f-1 min-w-0 o-auto no-scroll-bar \",\n        )}\n        style={{ maxWidth: \"fit-content\", margin: \"2px 0\" }}\n        ref={divRef}\n      >\n        {onAddChart && addChartProps && !show && (\n          <AddChartMenu\n            {...addChartProps}\n            tables={tables}\n            childWindows={childWindows}\n            myLinks={myLinks}\n            onAddChart={onAddChart}\n          />\n        )}\n        {hasMinimisedCharts && (\n          <Btn\n            iconPath={mdiChartBoxMultipleOutline}\n            title={t.W_QuickMenu[\"Restore minimised charts\"]}\n            data-command=\"dashboard.window.restoreMinimisedCharts\"\n            color=\"action\"\n            variant=\"icon\"\n            size=\"small\"\n            onClick={() => {\n              minimisedCharts.forEach((w) => {\n                w.$update({ minimised: false });\n              });\n            }}\n          />\n        )}\n        {showLinks &&\n          !window.isMobileDevice &&\n          !!setLinkMenu &&\n          w.type === \"table\" && (\n            <Btn\n              title={t.W_QuickMenu[\"Cross filter tables\"]}\n              data-command=\"Window.W_QuickMenu.addCrossFilteredTable\"\n              size=\"small\"\n              variant=\"icon\"\n              iconPath={mdiSetLeftCenter}\n              style={\n                firstLink && { color: getLinkColorV2(firstLink, 1).colorStr }\n              }\n              onClick={(e) => {\n                setLinkMenu({\n                  w,\n                  anchorEl: e.currentTarget,\n                });\n              }}\n            />\n          )}\n        {table && (!show || show.filter) && w.type === \"table\" && (\n          <Btn\n            title={t.W_QuickMenu[\"Show/Hide filtering\"]}\n            // className={bgColorClass}\n            data-command=\"dashboard.window.toggleFilterBar\"\n            size=\"small\"\n            variant=\"faded\"\n            // iconPath={mdiFilter}\n            iconPath={mdiMagnify}\n            color={\n              (\n                w.filter.some(\n                  (f) =>\n                    !f.disabled ||\n                    f.type === \"not null\" ||\n                    f.type === \"null\" ||\n                    (isJoinedFilter(f) ?\n                      f.filter.value !== undefined\n                    : f.value !== undefined),\n                )\n              ) ?\n                \"action\"\n              : undefined\n            }\n            onClick={() => {\n              w.$update(\n                { options: { showFilters: !w.options.showFilters } },\n                { deepMerge: true },\n              );\n            }}\n          />\n        )}\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/CSVRender.tsx",
    "content": "import React from \"react\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport { getSqlRowsAsCSV } from \"./CopyResultBtn\";\nimport type { W_SQLState } from \"./W_SQL\";\nimport { usePromise } from \"prostgles-client\";\n\ntype P = Pick<W_SQLState, \"cols\" | \"rows\">;\nexport const CSVRender = ({ rows, cols }: P) => {\n  const value = usePromise(async () => {\n    return !cols?.length || !rows ?\n        \"\"\n      : await getSqlRowsAsCSV(\n          rows,\n          cols.map((c) => c.name),\n        );\n  });\n  return <CodeEditor language=\"csv\" value={value ?? \"\"} />;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/CopyResultBtn.tsx",
    "content": "import {\n  mdiAlert,\n  mdiCodeJson,\n  mdiContentCopy,\n  mdiDownload,\n  mdiLanguageTypescript,\n  mdiText,\n} from \"@mdi/js\";\nimport type { SQLHandler, ValidatedColumnInfo } from \"prostgles-types\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { Label } from \"@components/Label\";\nimport { PopupMenuList } from \"@components/PopupMenuList\";\nimport { download } from \"./W_SQL\";\nimport type { Unpromise } from \"./W_SQLMenu\";\nimport { isObject } from \"@common/publishUtils\";\nimport { usePromise } from \"prostgles-client\";\nimport { getPapa } from \"../FileImporter/FileImporter\";\nimport { sliceText } from \"@common/utils\";\nimport { isDefined } from \"../../utils/utils\";\n\nconst getValidPGColumnNames = async (v: string[], sql: SQLHandler) => {\n  return sql(\n    \"SELECT name, format('%I', name) as escaped FROM unnest($1::TEXT[]) as name\",\n    [v],\n    { returnType: \"rows\" },\n  ) as Promise<{ name: string; escaped: string }[]>;\n};\n\ntype Outputs = {\n  val: string;\n  label: string;\n  iconPath: string;\n  fileName: string;\n}[];\nexport const CopyResultBtn = (props: {\n  queryEnded: boolean;\n  sql: SQLHandler;\n  rows: any[][];\n  cols: Pick<ValidatedColumnInfo, \"name\" | \"tsDataType\" | \"udt_name\">[];\n}) => {\n  const { cols, rows: rawValues, sql, queryEnded } = props;\n  const res = usePromise(async () => {\n    try {\n      const rows = rawValues.map((row) => getStringifiedObjects(row, cols));\n      let escapedNames: Unpromise<ReturnType<typeof getValidPGColumnNames>> =\n        [];\n      if (!cols.length || !rows.length) return;\n      try {\n        escapedNames = await getValidPGColumnNames(\n          cols.map((c) => c.name),\n          sql,\n        );\n      } catch (err) {\n        console.error(err, cols);\n      }\n      const _cols = cols.map((c, ci) => {\n        const name: string =\n          escapedNames.find((en) => en.name === c.name)?.escaped ??\n          JSON.stringify(c.name);\n\n        const vals = rows.map((r) => r[ci]);\n        return {\n          ...c,\n          name,\n          nullable: vals.includes(null) ? \" | null\" : \"\",\n          undef: vals.includes(undefined) ? \" | undefined\" : \"\",\n        };\n      });\n\n      const ts = `type Result = { \\n${_cols.map((c) => `  ${c.name}: ${c.tsDataType}${c.nullable}${c.undef};`).join(\"\\n\")} \\n}`;\n      const tsv = [cols.map((c) => JSON.stringify(c.name)), ...rows]\n        .map((v) => v.join(\"\\t\"))\n        .join(\"\\n\");\n\n      const csv = await getSqlRowsAsCSV(\n        rows,\n        cols.map((c) => c.name),\n      );\n      const json = JSON.stringify(\n        rows.map((r) =>\n          r.reduce((a, v, ri) => ({ ...a, [cols[ri]!.name]: v }), {}),\n        ),\n      );\n      const jsonArray =\n        cols.length === 1 ?\n          JSON.stringify(rows.map((row) => Object.values(row)).flat(), null, 2)\n        : undefined;\n      const sqlResult = [\n        `SELECT ${cols.map((c) => `${JSON.stringify(c.name)}::${c.udt_name} as ${JSON.stringify(c.name)}`).join(\"\\n, \")}`,\n        `INTO new_table_name`,\n        `FROM (`,\n        `  VALUES `,\n        rows\n          .map((values) => {\n            const rowStr =\n              \"    (\" +\n              values\n                .map((v) => {\n                  return (\n                    v === null ? \"null\"\n                    : typeof v === \"string\" ?\n                      `'${v.replaceAll(\"'\", \"''\").replaceAll(\"\\n\", \"\\\\n\")}'`\n                    : v\n                  );\n                })\n                .join(\", \") +\n              \")\";\n            return rowStr;\n          })\n          .join(\",\\n\"),\n        `) AS result(${cols.map((c) => JSON.stringify(c.name))}) `,\n      ].join(\"\\n\");\n      const outputs: Outputs = [\n        {\n          val: tsv,\n          label: \"Copy as TSV\",\n          iconPath: mdiText,\n          fileName: \"result.tsv\",\n        },\n        {\n          val: csv,\n          label: \"Copy as CSV\",\n          iconPath: mdiText,\n          fileName: \"result.csv\",\n        },\n        {\n          val: sqlResult,\n          label: \"Copy as SELECT INTO\",\n          iconPath: mdiText,\n          fileName: \"result.sql\",\n        },\n        {\n          val: json,\n          label: \"Copy as JSON\",\n          iconPath: mdiCodeJson,\n          fileName: \"result.json\",\n        },\n        jsonArray ?\n          {\n            val: jsonArray,\n            label: \"Copy as JSON Array\",\n            iconPath: mdiCodeJson,\n            fileName: \"result.json\",\n          }\n        : undefined,\n        {\n          val: ts,\n          label: \"Copy Typescript definition\",\n          iconPath: mdiLanguageTypescript,\n          fileName: \"result.d.ts\",\n        },\n      ].filter(isDefined);\n      return {\n        outputs,\n        error: undefined,\n      };\n    } catch (err) {\n      return {\n        outputs: undefined,\n        error: err,\n      };\n    }\n  }, [cols, rawValues, sql]);\n  const { outputs, error } = res ?? {};\n\n  if (error)\n    return (\n      <Label\n        iconPath={mdiAlert}\n        label=\"\"\n        popupTitle=\"Cannot copy result\"\n        info={<ErrorComponent error={error} />}\n      />\n    );\n\n  return (\n    <PopupMenuList\n      data-command=\"W_SQLBottomBar.copyResults\"\n      button={\n        <Btn\n          title={`Copy result (${rawValues.length} rows)`}\n          size=\"small\"\n          iconPath={mdiContentCopy}\n          style={{\n            visibility: queryEnded && outputs?.length ? \"visible\" : \"hidden\",\n          }}\n        />\n      }\n      listStyle={{\n        flex: 1,\n        display: \"flex\",\n      }}\n      items={(outputs ?? []).map((o) => ({\n        leftIconPath: o.iconPath,\n        label: o.label,\n        title: sliceText(o.val, 100),\n        // iconStyle: { color: \"var(--gray-400)\" },\n        labelStyle: { width: \"100%\", flex: 1 },\n        onPress: (e) => {\n          e.stopPropagation();\n          e.preventDefault();\n          navigator.clipboard.writeText(o.val);\n        },\n        contentRight: (\n          <Btn\n            className=\"show-on-parent-hover\"\n            iconPath={mdiDownload}\n            title=\"Download file\"\n            onClick={() => {\n              download(o.val, o.fileName, \"text\");\n            }}\n          />\n        ),\n      }))}\n    />\n  );\n};\n\nconst getStringifiedObjects = (\n  values: any[],\n  cols: Pick<ValidatedColumnInfo, \"name\" | \"tsDataType\" | \"udt_name\">[],\n) => {\n  return values.map((value, idx) => {\n    const col = cols[idx];\n    if (Array.isArray(value) && col?.udt_name.startsWith(\"_\")) {\n      return `{${value.map((v) => (v === null ? \"null\" : v)).join(\",\")}}`;\n    }\n    return isObject(value) || Array.isArray(value) ?\n        JSON.stringify(value)\n      : value;\n  });\n};\n\nexport const getSqlRowsAsCSV = async (rows: any[][], columnNames: string[]) => {\n  const papa = await getPapa();\n  return papa.unparse(\n    [columnNames, ...rows.map((row) => getStringifiedObjects(row, []))],\n    {\n      quotes: true,\n      header: false,\n      columns: columnNames,\n    },\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/MonacoLanguageRegister.ts",
    "content": "/* eslint-disable no-useless-escape */\nimport { isDefined } from \"../../utils/utils\";\nimport type { LoadedSuggestions } from \"../Dashboard/dashboardUtils\";\nimport { STARTING_KEYWORDS } from \"../SQLEditor/SQLCompletion/CommonMatchImports\";\nimport { getMonaco, LANG } from \"../SQLEditor/W_SQLEditor\";\nimport type { languages } from \"./monacoEditorTypes\";\n\nlet loadedPSQLLanguage = false;\nexport const loadPSQLLanguage = async (\n  loadedSuggestions: LoadedSuggestions | undefined,\n) => {\n  if (loadedPSQLLanguage) {\n    return false;\n  }\n  loadedPSQLLanguage = true;\n  const monaco = await getMonaco();\n  const monacoLanguages = monaco.languages.getLanguages();\n  for await (const lang of monacoLanguages) {\n    if (LANG === lang.id && \"loader\" in lang) {\n      const oldLoader = lang.loader as () => Promise<{\n        language: languages.IMonarchLanguage;\n      }>;\n\n      const langModule = await oldLoader();\n      lang.loader = () => {\n        langModule.language.operators = Array.from(\n          new Set([...operators, ...langModule.language.operators]),\n        );\n\n        /** Remove rule that matches $ to number */\n        //@ts-ignore\n        langModule.language.tokenizer.numbers =\n          langModule.language.tokenizer.numbers?.filter(\n            //@ts-ignore\n            ([ruleRegex], i) => !\"$\".match(ruleRegex),\n          );\n\n        const dataTypes =\n          loadedSuggestions?.suggestions\n            .flatMap(\n              (s) =>\n                s.dataTypeInfo && [\n                  s.dataTypeInfo.name,\n                  s.dataTypeInfo.udt_name,\n                ],\n            )\n            .filter(isDefined) ?? [];\n        // langModule.language.keywords = Array.from(\n        //   new Set([...STARTING_KEYWORDS, ...keywords, ...dataTypes]),\n        // );\n\n        const pgKeywords = loadedSuggestions?.suggestions\n          .filter((s) => {\n            const { keywordInfo, topKwd } = s;\n            if (!keywordInfo) return false;\n            const { catcode, barelabel } = keywordInfo;\n            return topKwd || !barelabel || catcode === \"R\" || catcode === \"C\";\n          })\n          .map((s) => s.name);\n\n        langModule.language.keywords = Array.from(\n          new Set([\n            ...STARTING_KEYWORDS,\n            ...(pgKeywords ?? keywords),\n            ...dataTypes,\n          ]),\n        );\n\n        langModule.language.builtinFunctions =\n          loadedSuggestions?.suggestions\n            .filter(\n              (s) =>\n                s.type === \"function\" &&\n                s.schema === \"pg_catalog\" &&\n                !dataTypes.includes(s.name),\n            )\n            .map((s) => s.name)\n            .filter(isDefined) ?? sqlLanguageDefinition.builtinFunctions;\n\n        // langModule.language.tokenizer.string!.push([/\\$\\$/, 'string']);\n        // langModule.language.tokenizer.strings!.push([/\\$\\$/, 'string']);\n        langModule.language.tokenizer.strings!.push([\n          new RegExp(\"\\\\$\\\\$[^$]*\\\\$\\\\$\"),\n          \"string\",\n        ]);\n        return Promise.resolve(langModule);\n      };\n    }\n  }\n\n  return true;\n};\n\nconst sqlLanguageDefinition = {\n  defaultToken: \"\",\n  tokenPostfix: \".sql\",\n  ignoreCase: true,\n\n  brackets: [\n    { open: \"[\", close: \"]\", token: \"delimiter.square\" },\n    { open: \"(\", close: \")\", token: \"delimiter.parenthesis\" },\n  ],\n\n  operators: [\n    // Logical\n    \"ALL\",\n    \"AND\",\n    \"ANY\",\n    \"BETWEEN\",\n    \"EXISTS\",\n    \"IN\",\n    \"LIKE\",\n    \"NOT\",\n    \"OR\",\n    \"SOME\",\n    // Set\n    \"EXCEPT\",\n    \"INTERSECT\",\n    \"UNION\",\n    // Join\n    \"APPLY\",\n    \"CROSS\",\n    \"FULL\",\n    \"INNER\",\n    \"JOIN\",\n    \"LEFT\",\n    \"OUTER\",\n    \"RIGHT\",\n    // Predicates\n    \"CONTAINS\",\n    \"FREETEXT\",\n    \"IS\",\n    \"NULL\",\n    // Pivoting\n    \"PIVOT\",\n    \"UNPIVOT\",\n    // Merging\n    \"MATCHED\",\n  ],\n  builtinFunctions: [\n    // Aggregate\n    \"AVG\",\n    \"CHECKSUM_AGG\",\n    \"COUNT\",\n    \"COUNT_BIG\",\n    \"GROUPING\",\n    \"GROUPING_ID\",\n    \"MAX\",\n    \"MIN\",\n    \"SUM\",\n    \"STDEV\",\n    \"STDEVP\",\n    \"VAR\",\n    \"VARP\",\n    // Analytic\n    \"CUME_DIST\",\n    \"FIRST_VALUE\",\n    \"LAG\",\n    \"LAST_VALUE\",\n    \"LEAD\",\n    \"PERCENTILE_CONT\",\n    \"PERCENTILE_DISC\",\n    \"PERCENT_RANK\",\n    // Collation\n    \"COLLATE\",\n    \"COLLATIONPROPERTY\",\n    \"TERTIARY_WEIGHTS\",\n    // Azure\n    \"FEDERATION_FILTERING_VALUE\",\n    // Conversion\n    \"CAST\",\n    \"CONVERT\",\n    \"PARSE\",\n    \"TRY_CAST\",\n    \"TRY_CONVERT\",\n    \"TRY_PARSE\",\n    // Cryptographic\n    \"ASYMKEY_ID\",\n    \"ASYMKEYPROPERTY\",\n    \"CERTPROPERTY\",\n    \"CERT_ID\",\n    \"CRYPT_GEN_RANDOM\",\n    \"DECRYPTBYASYMKEY\",\n    \"DECRYPTBYCERT\",\n    \"DECRYPTBYKEY\",\n    \"DECRYPTBYKEYAUTOASYMKEY\",\n    \"DECRYPTBYKEYAUTOCERT\",\n    \"DECRYPTBYPASSPHRASE\",\n    \"ENCRYPTBYASYMKEY\",\n    \"ENCRYPTBYCERT\",\n    \"ENCRYPTBYKEY\",\n    \"ENCRYPTBYPASSPHRASE\",\n    \"HASHBYTES\",\n    \"IS_OBJECTSIGNED\",\n    \"KEY_GUID\",\n    \"KEY_ID\",\n    \"KEY_NAME\",\n    \"SIGNBYASYMKEY\",\n    \"SIGNBYCERT\",\n    \"SYMKEYPROPERTY\",\n    \"VERIFYSIGNEDBYCERT\",\n    \"VERIFYSIGNEDBYASYMKEY\",\n    // Cursor\n    \"CURSOR_STATUS\",\n    // Datatype\n    \"DATALENGTH\",\n    \"IDENT_CURRENT\",\n    \"IDENT_INCR\",\n    \"IDENT_SEED\",\n    \"IDENTITY\",\n    \"SQL_VARIANT_PROPERTY\",\n    // Datetime\n    \"CURRENT_TIMESTAMP\",\n    \"DATEADD\",\n    \"DATEDIFF\",\n    \"DATEFROMPARTS\",\n    \"DATENAME\",\n    \"DATEPART\",\n    \"DATETIME2FROMPARTS\",\n    \"DATETIMEFROMPARTS\",\n    \"DATETIMEOFFSETFROMPARTS\",\n    \"DAY\",\n    \"EOMONTH\",\n    \"GETDATE\",\n    \"GETUTCDATE\",\n    \"ISDATE\",\n    \"MONTH\",\n    \"SMALLDATETIMEFROMPARTS\",\n    \"SWITCHOFFSET\",\n    \"SYSDATETIME\",\n    \"SYSDATETIMEOFFSET\",\n    \"SYSUTCDATETIME\",\n    \"TIMEFROMPARTS\",\n    \"TODATETIMEOFFSET\",\n    \"YEAR\",\n    // Logical\n    \"CHOOSE\",\n    \"COALESCE\",\n    \"IIF\",\n    \"NULLIF\",\n    // Mathematical\n    \"ABS\",\n    \"ACOS\",\n    \"ASIN\",\n    \"ATAN\",\n    \"ATN2\",\n    \"CEILING\",\n    \"COS\",\n    \"COT\",\n    \"DEGREES\",\n    \"EXP\",\n    \"FLOOR\",\n    \"LOG\",\n    \"LOG10\",\n    \"PI\",\n    \"POWER\",\n    \"RADIANS\",\n    \"RAND\",\n    \"ROUND\",\n    \"SIGN\",\n    \"SIN\",\n    \"SQRT\",\n    \"SQUARE\",\n    \"TAN\",\n    // Metadata\n    \"APP_NAME\",\n    \"APPLOCK_MODE\",\n    \"APPLOCK_TEST\",\n    \"ASSEMBLYPROPERTY\",\n    \"COL_LENGTH\",\n    \"COL_NAME\",\n    \"COLUMNPROPERTY\",\n    \"DATABASE_PRINCIPAL_ID\",\n    \"DATABASEPROPERTYEX\",\n    \"DB_ID\",\n    \"DB_NAME\",\n    \"FILE_ID\",\n    \"FILE_IDEX\",\n    \"FILE_NAME\",\n    \"FILEGROUP_ID\",\n    \"FILEGROUP_NAME\",\n    \"FILEGROUPPROPERTY\",\n    \"FILEPROPERTY\",\n    \"FULLTEXTCATALOGPROPERTY\",\n    \"FULLTEXTSERVICEPROPERTY\",\n    \"INDEX_COL\",\n    \"INDEXKEY_PROPERTY\",\n    \"INDEXPROPERTY\",\n    \"OBJECT_DEFINITION\",\n    \"OBJECT_ID\",\n    \"OBJECT_NAME\",\n    \"OBJECT_SCHEMA_NAME\",\n    \"OBJECTPROPERTY\",\n    \"OBJECTPROPERTYEX\",\n    \"ORIGINAL_DB_NAME\",\n    \"PARSENAME\",\n    \"SCHEMA_ID\",\n    \"SCHEMA_NAME\",\n    \"SCOPE_IDENTITY\",\n    \"SERVERPROPERTY\",\n    \"STATS_DATE\",\n    \"TYPE_ID\",\n    \"TYPE_NAME\",\n    \"TYPEPROPERTY\",\n    // Ranking\n    \"DENSE_RANK\",\n    \"NTILE\",\n    \"RANK\",\n    \"ROW_NUMBER\",\n    // Replication\n    \"PUBLISHINGSERVERNAME\",\n    // Rowset\n    \"OPENDATASOURCE\",\n    \"OPENQUERY\",\n    \"OPENROWSET\",\n    \"OPENXML\",\n    // Security\n    \"CERTENCODED\",\n    \"CERTPRIVATEKEY\",\n    \"CURRENT_USER\",\n    \"HAS_DBACCESS\",\n    \"HAS_PERMS_BY_NAME\",\n    \"IS_MEMBER\",\n    \"IS_ROLEMEMBER\",\n    \"IS_SRVROLEMEMBER\",\n    \"LOGINPROPERTY\",\n    \"ORIGINAL_LOGIN\",\n    \"PERMISSIONS\",\n    \"PWDENCRYPT\",\n    \"PWDCOMPARE\",\n    \"SESSION_USER\",\n    \"SESSIONPROPERTY\",\n    // 'SUSER_ID',\n    \"SUSER_NAME\",\n    \"SUSER_SID\",\n    \"SUSER_SNAME\",\n    \"SYSTEM_USER\",\n    \"USER\",\n    // 'USER_ID',\n    // 'USER_NAME',\n    // String\n    \"ASCII\",\n    \"CHAR\",\n    \"CHARINDEX\",\n    \"CONCAT\",\n    \"DIFFERENCE\",\n    \"FORMAT\",\n    \"LEFT\",\n    \"LEN\",\n    \"LOWER\",\n    \"LTRIM\",\n    \"NCHAR\",\n    \"PATINDEX\",\n    \"QUOTENAME\",\n    \"REPLACE\",\n    \"REPLICATE\",\n    \"REVERSE\",\n    \"RIGHT\",\n    \"RTRIM\",\n    \"SOUNDEX\",\n    \"SPACE\",\n    \"STR\",\n    \"STUFF\",\n    \"SUBSTRING\",\n    \"UNICODE\",\n    \"UPPER\",\n    // System\n    \"BINARY_CHECKSUM\",\n    \"CHECKSUM\",\n    \"CONNECTIONPROPERTY\",\n    \"CONTEXT_INFO\",\n    \"CURRENT_REQUEST_ID\",\n    \"ERROR_LINE\",\n    \"ERROR_NUMBER\",\n    \"ERROR_MESSAGE\",\n    \"ERROR_PROCEDURE\",\n    \"ERROR_SEVERITY\",\n    \"ERROR_STATE\",\n    \"FORMATMESSAGE\",\n    \"GETANSINULL\",\n    \"GET_FILESTREAM_TRANSACTION_CONTEXT\",\n    \"HOST_ID\",\n    \"HOST_NAME\",\n    \"ISNULL\",\n    \"ISNUMERIC\",\n    \"MIN_ACTIVE_ROWVERSION\",\n    \"NEWID\",\n    \"NEWSEQUENTIALID\",\n    \"ROWCOUNT_BIG\",\n    \"XACT_STATE\",\n    // TextImage\n    \"TEXTPTR\",\n    \"TEXTVALID\",\n    // Trigger\n    \"COLUMNS_UPDATED\",\n    \"EVENTDATA\",\n    \"TRIGGER_NESTLEVEL\",\n    \"UPDATE\",\n    // ChangeTracking\n    \"CHANGETABLE\",\n    \"CHANGE_TRACKING_CONTEXT\",\n    \"CHANGE_TRACKING_CURRENT_VERSION\",\n    \"CHANGE_TRACKING_IS_COLUMN_IN_MASK\",\n    \"CHANGE_TRACKING_MIN_VALID_VERSION\",\n    // FullTextSearch\n    \"CONTAINSTABLE\",\n    \"FREETEXTTABLE\",\n    // SemanticTextSearch\n    \"SEMANTICKEYPHRASETABLE\",\n    \"SEMANTICSIMILARITYDETAILSTABLE\",\n    \"SEMANTICSIMILARITYTABLE\",\n    // FileStream\n    \"FILETABLEROOTPATH\",\n    \"GETFILENAMESPACEPATH\",\n    \"GETPATHLOCATOR\",\n    \"PATHNAME\",\n    // ServiceBroker\n    \"GET_TRANSMISSION_STATUS\",\n  ],\n  builtinVariables: [\n    // Configuration\n    \"@@DATEFIRST\",\n    \"@@DBTS\",\n    \"@@LANGID\",\n    \"@@LANGUAGE\",\n    \"@@LOCK_TIMEOUT\",\n    \"@@MAX_CONNECTIONS\",\n    \"@@MAX_PRECISION\",\n    \"@@NESTLEVEL\",\n    \"@@OPTIONS\",\n    \"@@REMSERVER\",\n    \"@@SERVERNAME\",\n    \"@@SERVICENAME\",\n    \"@@SPID\",\n    \"@@TEXTSIZE\",\n    \"@@VERSION\",\n    // Cursor\n    \"@@CURSOR_ROWS\",\n    \"@@FETCH_STATUS\",\n    // Datetime\n    \"@@DATEFIRST\",\n    // Metadata\n    \"@@PROCID\",\n    // System\n    \"@@ERROR\",\n    \"@@IDENTITY\",\n    \"@@ROWCOUNT\",\n    \"@@TRANCOUNT\",\n    // Stats\n    \"@@CONNECTIONS\",\n    \"@@CPU_BUSY\",\n    \"@@IDLE\",\n    \"@@IO_BUSY\",\n    \"@@PACKET_ERRORS\",\n    \"@@PACK_RECEIVED\",\n    \"@@PACK_SENT\",\n    \"@@TIMETICKS\",\n    \"@@TOTAL_ERRORS\",\n    \"@@TOTAL_READ\",\n    \"@@TOTAL_WRITE\",\n  ],\n  pseudoColumns: [\"$ACTION\", \"$IDENTITY\", \"$ROWGUID\", \"$PARTITION\"],\n  tokenizer: {\n    root: [\n      { include: \"@comments\" },\n      { include: \"@whitespace\" },\n      { include: \"@pseudoColumns\" },\n      { include: \"@numbers\" },\n      { include: \"@strings\" },\n      { include: \"@complexIdentifiers\" },\n      { include: \"@scopes\" },\n      [/[;,.]/, \"delimiter\"],\n      [/[()]/, \"@brackets\"],\n      [\n        /[\\w@#$]+/,\n        {\n          cases: {\n            \"@operators\": \"operator\",\n            \"@builtinVariables\": \"predefined\",\n            \"@builtinFunctions\": \"predefined\",\n            \"@keywords\": \"keyword\",\n            \"@default\": \"identifier\",\n          },\n        },\n      ],\n      [/[<>=!%&+\\-*/|~^]/, \"operator\"],\n    ] satisfies languages.IMonarchLanguageRule[],\n    whitespace: [[/\\s+/, \"white\"]],\n    comments: [\n      [/--+.*/, \"comment\"],\n      [/\\/\\*/, { token: \"comment.quote\", next: \"@comment\" }],\n    ],\n    comment: [\n      [/[^*/]+/, \"comment\"],\n      // Not supporting nested comments, as nested comments seem to not be standard?\n      // i.e. http://stackoverflow.com/questions/728172/are-there-multiline-comment-delimiters-in-sql-that-are-vendor-agnostic\n      // [/\\/\\*/, { token: 'comment.quote', next: '@push' }],    // nested comment not allowed :-(\n      [/\\*\\//, { token: \"comment.quote\", next: \"@pop\" }],\n      [/./, \"comment\"],\n    ],\n    pseudoColumns: [\n      [\n        /[$][A-Za-z_][\\w@#$]*/,\n        {\n          cases: {\n            \"@pseudoColumns\": \"predefined\",\n            \"@default\": \"identifier\",\n          },\n        },\n      ],\n    ],\n    numbers: [\n      [/0[xX][0-9a-fA-F]*/, \"number\"],\n      [/[$][+-]*\\d*(\\.\\d*)?/, \"number\"],\n      [/((\\d+(\\.\\d*)?)|(\\.\\d+))([eE][\\-+]?\\d+)?/, \"number\"],\n    ],\n    strings: [\n      [/N'/, { token: \"string\", next: \"@string\" }],\n      [/'/, { token: \"string\", next: \"@string\" }],\n    ],\n    string: [\n      [/[^']+/, \"string\"],\n      [/''/, \"string\"],\n      [/'/, { token: \"string\", next: \"@pop\" }],\n    ],\n    complexIdentifiers: [\n      [/\\[/, { token: \"identifier.quote\", next: \"@bracketedIdentifier\" }],\n      [/\"/, { token: \"identifier.quote\", next: \"@quotedIdentifier\" }],\n    ],\n    bracketedIdentifier: [\n      [/[^\\]]+/, \"identifier\"],\n      [/]]/, \"identifier\"],\n      [/]/, { token: \"identifier.quote\", next: \"@pop\" }],\n    ],\n    quotedIdentifier: [\n      [/[^\"]+/, \"identifier\"],\n      [/\"\"/, \"identifier\"],\n      [/\"/, { token: \"identifier.quote\", next: \"@pop\" }],\n    ],\n    scopes: [\n      [/BEGIN\\s+(DISTRIBUTED\\s+)?TRAN(SACTION)?\\b/i, \"keyword\"],\n      [/BEGIN\\s+TRY\\b/i, { token: \"keyword.try\" }],\n      [/END\\s+TRY\\b/i, { token: \"keyword.try\" }],\n      [/BEGIN\\s+CATCH\\b/i, { token: \"keyword.catch\" }],\n      [/END\\s+CATCH\\b/i, { token: \"keyword.catch\" }],\n      [/(BEGIN|CASE)\\b/i, { token: \"keyword.block\" }],\n      [/END\\b/i, { token: \"keyword.block\" }],\n      [/WHEN\\b/i, { token: \"keyword.choice\" }],\n      [/THEN\\b/i, { token: \"keyword.choice\" }],\n    ],\n  },\n};\nconst keywords = [\n  \"insert\",\n  \"disable\",\n  \"delete\",\n  \"enable\",\n  \"all\",\n  \"analyse\",\n  \"analyze\",\n  \"and\",\n  \"any\",\n  \"array\",\n  \"as\",\n  \"asc\",\n  \"asymmetric\",\n  \"both\",\n  \"case\",\n  \"cast\",\n  \"check\",\n  \"collate\",\n  \"column\",\n  \"constraint\",\n  \"create\",\n  \"current_catalog\",\n  \"current_date\",\n  \"current_role\",\n  \"current_time\",\n  \"current_timestamp\",\n  \"current_user\",\n  \"default\",\n  \"deferrable\",\n  \"desc\",\n  \"distinct\",\n  \"do\",\n  \"else\",\n  \"end\",\n  \"except\",\n  \"false\",\n  \"fetch\",\n  \"for\",\n  \"foreign\",\n  \"from\",\n  \"grant\",\n  \"group\",\n  \"having\",\n  \"in\",\n  \"initially\",\n  \"intersect\",\n  \"into\",\n  \"lateral\",\n  \"leading\",\n  \"limit\",\n  \"localtime\",\n  \"localtimestamp\",\n  \"not\",\n  \"null\",\n  \"offset\",\n  \"on\",\n  \"only\",\n  \"or\",\n  \"order\",\n  \"placing\",\n  \"primary\",\n  \"references\",\n  \"returning\",\n  \"select\",\n  \"session_user\",\n  \"some\",\n  \"symmetric\",\n  \"table\",\n  \"then\",\n  \"to\",\n  \"trailing\",\n  \"true\",\n  \"union\",\n  \"unique\",\n  \"user\",\n  \"using\",\n  \"variadic\",\n  \"when\",\n  \"where\",\n  \"window\",\n  \"with\",\n];\n\nconst operators = [\n  \"!~\",\n  \"!~*\",\n  \"!~~\",\n  \"!~~*\",\n  \"&&\",\n  \"&<\",\n  \"&<|\",\n  \"&>\",\n  \"*<\",\n  \"*<=\",\n  \"*<>\",\n  \"*=\",\n  \"*>\",\n  \"*>=\",\n  \"-|-\",\n  \"<\",\n  \"<<\",\n  \"<<=\",\n  \"<<|\",\n  \"<=\",\n  \"<>\",\n  \"<@\",\n  \"<^\",\n  \"=\",\n  \">\",\n  \">=\",\n  \">>\",\n  \">>=\",\n  \">^\",\n  \"?\",\n  \"?#\",\n  \"?&\",\n  \"?-\",\n  \"?-|\",\n  \"?|\",\n  \"?||\",\n  \"@>\",\n  \"@?\",\n  \"@@\",\n  \"@@@\",\n  \"^@\",\n  \"|&>\",\n  \"|>>\",\n  \"~\",\n  \"~*\",\n  \"~<=~\",\n  \"~<~\",\n  \"~=\",\n  \"~>=~\",\n  \"~>~\",\n  \"~~\",\n  \"~~*\",\n  \"&&&\",\n  \"&/&\",\n  \"<<@\",\n  \"@\",\n  \"@>>\",\n  \"~==\",\n  \"~~=\",\n  \"LIKE\",\n  \"NOT LIKE\",\n  \"ILIKE\",\n  \"NOT ILIKE\",\n];\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/SQLHotkeys.tsx",
    "content": "import React from \"react\";\nimport { Hotkey } from \"@components/Hotkey\";\nimport { t } from \"../../i18n/i18nUtils\";\n\nexport const SQLHotkeys = () => {\n  return (\n    <div className=\"flex-col ai-start gap-1 \">\n      <Hotkey\n        label={t.SQLHotkeys[\"Show autocomplete suggestions\"]}\n        keys={[\"ctrl\", \"space\"]}\n      />\n      <Hotkey label={t.SQLHotkeys[\"Execute current statement\"]} keys={[]} />\n      <Hotkey label=\"\" keys={[\"ctrl\", \"enter\"]} />\n      <Hotkey label=\"\" keys={[\"alt\", \"e\"]} />\n      <Hotkey\n        label={t.SQLHotkeys[\"Select current statement\"]}\n        keys={[\"ctrl\", \"b\"]}\n      />\n      <Hotkey label={t.SQLHotkeys[\"Show all suggestions\"]} keys={[\"?\"]} />\n      <Hotkey label={t.SQLHotkeys[\"Show PSQL command queries\"]} keys={[\"\\\\\"]} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/TestSQL.ts",
    "content": "import { tryCatchV2 } from \"prostgles-types\";\nimport { tout } from \"../../pages/ElectronSetup/ElectronSetup\";\nimport {\n  TopHeaderClassName,\n  type WindowSyncItem,\n} from \"../Dashboard/dashboardUtils\";\nimport { createTables } from \"./demoScripts/createTables\";\nimport { mainTestScripts } from \"./demoScripts/mainTestScripts\";\nimport { testBugs } from \"./demoScripts/testBugs\";\nimport { testMiscAndBugs } from \"./demoScripts/testMiscAndBugs\";\nimport { getDemoUtils } from \"./getDemoUtils\";\n\nexport const VIDEO_DEMO_DB_NAME = \"prostgles_video_demo\";\nexport const TestSQL = async (w: WindowSyncItem<\"sql\">) => {\n  const testUtils = getDemoUtils(w);\n\n  // const currDbName = await testUtils.runDbSQL(`SELECT current_database() as db_name`, { }, { returnType: \"value\" });\n  // if(currDbName === VIDEO_DEMO_DB_NAME){\n  //   return videoDemo(testUtils);\n  // }\n\n  document.querySelector(\".\" + TopHeaderClassName)?.remove();\n  alert(\"Ensure cursor is inside the editor so suggestions show as expected\");\n  await tout(1000);\n\n  const { stopWakeLock } = await startWakeLock();\n  await testBugs(testUtils);\n  await testMiscAndBugs(testUtils);\n  await createTables(testUtils);\n  await mainTestScripts(testUtils);\n  stopWakeLock();\n\n  alert(\"Demo finished successfully\");\n};\n\nexport const startWakeLock = async () => {\n  const { data: wakeLock } = await tryCatchV2(async () => {\n    const wakeLock = await navigator.wakeLock.request(\"screen\");\n    return wakeLock;\n  });\n\n  return {\n    stopWakeLock: async () => {\n      try {\n        const res = await wakeLock;\n        res?.release();\n      } catch (e) {\n        console.error(e);\n      }\n    },\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/W_SQL.tsx",
    "content": "import { mdiKeyboard } from \"@mdi/js\";\nimport type { SocketSQLStreamHandlers, SQLResultInfo } from \"prostgles-types\";\n\nimport Loading from \"@components/Loader/Loading\";\nimport type { TableColumn, TableProps } from \"@components/Table/Table\";\nimport React, { useEffect } from \"react\";\nimport type {\n  OnAddChart,\n  Query,\n  WindowData,\n  WindowSyncItem,\n} from \"../Dashboard/dashboardUtils\";\n\nimport type { PopupProps } from \"@components/Popup/Popup\";\nimport Popup from \"@components/Popup/Popup\";\n\nimport type { DeltaOf } from \"../RTComp\";\nimport RTComp from \"../RTComp\";\nimport { getFuncs } from \"../SQLEditor/SQLCompletion/getPGObjects\";\nimport type { MonacoError, SQLEditorRef } from \"../SQLEditor/W_SQLEditor\";\nimport { W_SQLEditor } from \"../SQLEditor/W_SQLEditor\";\n\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport type {\n  SingleSyncHandles,\n  SyncDataItem,\n} from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { DBEventHandles, ValidatedColumnInfo } from \"prostgles-types/lib\";\nimport type { ColumnSortSQL } from \"../W_Table/ColumnMenu/ColumnMenu\";\n\nimport { Icon } from \"@components/Icon/Icon\";\nimport { useIsMounted } from \"prostgles-client\";\nimport { createReactiveState } from \"../../appUtils\";\nimport type { TestSelectors } from \"../../Testing\";\nimport type { CommonWindowProps, DashboardState } from \"../Dashboard/Dashboard\";\nimport type { CodeBlock } from \"../SQLEditor/SQLCompletion/completionUtils/getCodeBlock\";\nimport type { ProstglesQuickMenuProps } from \"../W_QuickMenu\";\nimport { AddChartMenu } from \"../W_Table/TableMenu/AddChartMenu\";\nimport Window from \"../Window\";\nimport { type ChartableSQL, getChartableSQL } from \"./getChartableSQL\";\nimport { runSQL } from \"./runSQL/runSQL\";\nimport { SQLHotkeys } from \"./SQLHotkeys\";\nimport { W_SQLBottomBar } from \"./W_SQLBottomBar/W_SQLBottomBar\";\nimport { ProstglesSQLMenu } from \"./W_SQLMenu\";\nimport { W_SQLResults } from \"./W_SQLResults\";\n\nexport type W_SQLProps = Omit<CommonWindowProps, \"w\"> & {\n  w: WindowSyncItem<\"sql\">;\n  filter?: any;\n  onAddChart?: OnAddChart;\n  titleIcon?: React.ReactNode;\n  activeRowStyle?: React.CSSProperties;\n  childWindow: React.ReactNode | undefined;\n  suggestions?: DashboardState[\"suggestions\"];\n  setLinkMenu: ProstglesQuickMenuProps[\"setLinkMenu\"];\n};\n\nexport const SQL_NOT_ALLOWED =\n  \"Your prostgles account is not allowed to run SQL\";\n\nexport type ProstglesColumn = TableColumn & { computed: boolean } & Pick<\n    ValidatedColumnInfo,\n    \"name\" | \"tsDataType\" | \"label\" | \"udt_name\" | \"filter\"\n  >;\n\nexport type W_SQL_ActiveQuery = {\n  pid: number | undefined;\n  hashedSQL: string;\n  trimmedSql: string;\n  started: Date;\n  stopped?: {\n    date: Date;\n    type: \"terminate\" | \"cancel\";\n  };\n} & (\n  | {\n      state: \"running\";\n    }\n  | {\n      state: \"ended\";\n      commandResult?: string;\n      rowCount: number;\n      /**\n       * For LIMITed select queries will try to get the total row count\n       * to enable pagination\n       */\n      totalRowCount: number | undefined;\n      ended: Date;\n      info: SQLResultInfo | undefined;\n    }\n  | {\n      state: \"error\";\n      ended: Date;\n      error?: MonacoError;\n    }\n);\n\ntype SQLResultCols = Required<W_SQLProps>[\"w\"][\"options\"][\"sqlResultCols\"];\n\nexport type W_SQLState = {\n  table?: TableProps<ColumnSortSQL> & Query;\n  sort: ColumnSortSQL[];\n  loading: boolean;\n  didSetCursorPosition: boolean;\n  currentCodeBlockChartColumns?: ChartableSQL;\n  isSelect: boolean;\n  hideCodeEditor?: boolean;\n  rows?: (string | number)[][];\n  filter: any;\n  pos?: { x: number; y: number };\n  size?: { w: number; h: number };\n  popup?: {\n    positioning: PopupProps[\"positioning\"];\n    anchorEl: Element;\n    content: React.ReactNode;\n    style: React.CSSProperties;\n  };\n  cols?: SQLResultCols;\n  handler?: SocketSQLStreamHandlers;\n  activeQuery: undefined | W_SQL_ActiveQuery;\n  joins: string[];\n  error?: any;\n  w?: SyncDataItem<WindowData>;\n  hideTable?: boolean;\n  sql: string;\n  sqlResult?: boolean;\n  rowPanel?: {\n    type: \"insert\" | \"update\";\n    data: any;\n  };\n  rowDelta?: any;\n  onRowClick: any;\n  filterPopup?: boolean;\n  notifEventSub?: ReturnType<DBEventHandles[\"addListener\"]>;\n  noticeSub?: ReturnType<DBEventHandles[\"addListener\"]>;\n  notices?: {\n    length: number;\n    message: string;\n    name: string;\n    severity: string;\n    code: string;\n    where: string;\n    file: string;\n    line: string;\n    routine: string;\n    received: string;\n  }[];\n  columns?: ValidatedColumnInfo[];\n  /**\n   * Stringified joinFilter that is set after the data has been downloaded.\n   * Used in setting activeRow styles to all rows adequately\n   */\n  joinFilterStr?: string;\n\n  queryEnded?: number;\n  page: number;\n  pageSize: number;\n  loadingSuggestions: boolean;\n};\n\ntype D = {\n  w?: WindowSyncItem<\"sql\">;\n  dataAge?: number;\n  wSync?: SingleSyncHandles;\n};\n\nexport class W_SQL extends RTComp<W_SQLProps, W_SQLState, D> {\n  refHeader?: HTMLDivElement;\n  refResize?: HTMLElement;\n  ref?: HTMLElement & { sqlRef?: SQLEditorRef | undefined };\n\n  state: W_SQLState = {\n    sql: \"\",\n    loading: false,\n    didSetCursorPosition: false,\n    page: 0,\n    pageSize: 100,\n    activeQuery: undefined,\n    isSelect: false,\n    sort: [],\n    joins: [],\n    filter: {},\n    error: \"\",\n    hideTable: true,\n    onRowClick: null,\n    loadingSuggestions: true,\n  };\n  d: D = {\n    w: undefined,\n    dataAge: 0,\n    wSync: undefined,\n  };\n\n  calculatedColWidths = false;\n\n  onMount() {\n    const { w } = this.props;\n\n    if (!this.d.wSync) {\n      const wSync = w.$cloneSync((w, delta) => {\n        this.setData({ w }, { w: delta });\n      });\n\n      this.setData({ wSync });\n    }\n\n    /* Add save hotkey */\n    window.addEventListener(\"keydown\", this.saveFunc, false);\n  }\n\n  saveFunc = (e) => {\n    if (\n      e.key === \"s\" &&\n      e.ctrlKey &&\n      document.activeElement &&\n      this.editorContainer &&\n      this.editorContainer.contains(document.activeElement)\n    ) {\n      e.preventDefault();\n      this.saveQuery();\n    }\n  };\n\n  streamData = createReactiveState(\n    { rows: [] } as { rows: any[] },\n    (newState) => {\n      if (newState.rows.length < this.state.pageSize) {\n        this.setState({ rows: newState.rows });\n      }\n    },\n  );\n\n  async onUnmount() {\n    window.removeEventListener(\"keydown\", this.saveFunc, false);\n\n    this.d.wSync?.$unsync();\n\n    const { notifEventSub, noticeSub, handler } = this.state;\n    notifEventSub?.removeListener();\n    noticeSub?.removeListener();\n    await handler?.stop(true);\n    await this.dataSub?.unsubscribe?.();\n  }\n\n  editorContainer?: HTMLDivElement;\n  saveQuery() {\n    if (this.d.w && this.d.w.sql.trim()) {\n      // window.open('data:text/csv;charset=utf-8,' + w.sql);\n      download(this.d.w.sql, `${this.d.w.name || \"Query\"}.sql`, \"text/sql\");\n    }\n  }\n\n  dataSub?: any;\n  dataSubFilter?: any;\n  dataAge?: number = 0;\n  autoRefresh?: any;\n  onDelta = (\n    dp: DeltaOf<W_SQLProps>,\n    ds: DeltaOf<W_SQLState>,\n    dd: DeltaOf<D>,\n  ) => {\n    const delta = { ...dp, ...ds, ...dd };\n    const { w } = this.d;\n    if (!w) return;\n    if (delta.w?.limit !== undefined) {\n      this.state.handler?.stop();\n      this.setState({ handler: undefined });\n    }\n\n    const shouldReRender =\n      delta.w?.sql_options ||\n      \"hideTable\" in (delta.w?.options ?? {}) ||\n      \"limit\" in (delta.w || {}) ||\n      (delta.w?.sql && !(delta.w.options as any)?.sqlChanged);\n    if (shouldReRender) {\n      this.setState({});\n    }\n  };\n\n  _queryHashAlias?: string;\n  killQuery = (terminate: boolean) => {\n    if (this.state.activeQuery?.state !== \"running\") return;\n    this.setState({\n      activeQuery: {\n        ...this.state.activeQuery,\n        stopped: {\n          date: new Date(),\n          type: terminate ? \"terminate\" : \"cancel\",\n        },\n      },\n    });\n    this.state.handler?.stop(terminate);\n    return true;\n  };\n\n  noticeEventListener = (notice: any) => {\n    const { notices = [] } = this.state;\n    this.setState({\n      notices: [\n        { ...notice, received: new Date().toISOString().replace(\"T\", \" \") },\n        ...notices,\n      ],\n    });\n  };\n\n  notifEventListener = (payload: string) => {\n    const { rows = [] } = this.state;\n    this.setState({\n      rows: [[payload, new Date().toISOString().replace(\"T\", \" \")], ...rows],\n    });\n  };\n  hashedSQL?: string;\n  sort?: ColumnSortSQL[];\n  runSQL = runSQL.bind(this);\n\n  sqlRef?: SQLEditorRef;\n\n  render() {\n    const {\n      loading,\n      joins,\n      popup,\n      error,\n      activeQuery,\n      hideCodeEditor,\n      currentCodeBlockChartColumns,\n      didSetCursorPosition,\n    } = this.state;\n    const { w } = this.d;\n    const {\n      onAddChart,\n      suggestions,\n      tables,\n      setLinkMenu,\n      prgl: { db, dbs, dbsTables, user },\n      myLinks,\n      childWindow,\n      workspace,\n    } = this.props;\n\n    if (loading || !w) return <Loading className=\"m-auto\" />;\n\n    const updateOptions = (\n      newOpts: Partial<WindowData<\"sql\">[\"options\"]>,\n      otherData: Partial<WindowData<\"sql\">> = {},\n    ) => {\n      const options: WindowData[\"options\"] = {\n        ...(this.d.w?.$get()?.options || {}),\n        ...newOpts,\n      };\n      w.$update({ ...otherData, options }, { deepMerge: true });\n    };\n\n    let infoPlaceholder: React.ReactNode = null;\n    if (user && !user.options?.viewedSQLTips && !window.isMobileDevice) {\n      infoPlaceholder = (\n        <div\n          className=\"p-2 flex-col ai-center jc-center gap-1 absolute \"\n          style={{\n            inset: 0,\n            background: \"#00000040\",\n            zIndex: 6, // Ensure it's above the right minimap scrollbar\n          }}\n        >\n          <div className=\"SQLHotkeysWrapper min-s-0 bg-color-0 p-1 rounded max-s-fit flex-col gap-1\">\n            <div color=\"info\" className=\"bg-color-0 o-auto\">\n              <h4 className=\"flex-row ai-center gap-1 font-16 mt-0\">\n                <Icon path={mdiKeyboard} size={1}></Icon> Hotkeys:\n              </h4>\n              <SQLHotkeys />\n            </div>\n            <Btn\n              color=\"action\"\n              variant=\"filled\"\n              onClick={async () => {\n                // const newOptions = { ...user.options, viewedSQLTips: true };\n                await dbs.users.update(\n                  { id: user.id },\n                  { options: { $merge: [{ viewedSQLTips: true }] } },\n                );\n              }}\n            >\n              Ok, don&apos;t show again\n            </Btn>\n          </div>\n        </div>\n      );\n    }\n    const sqlError =\n      activeQuery?.state === \"error\" && !activeQuery.stopped ?\n        activeQuery.error\n      : undefined;\n    const clearActiveQueryError = () => {\n      if (this.state.activeQuery?.state === \"error\") {\n        this.setState({\n          activeQuery: {\n            ...this.state.activeQuery,\n            error: undefined,\n          },\n        });\n      }\n    };\n\n    const content = (\n      <>\n        <div\n          className={\"ProstglesSQL flex-col f-1 min-h-0 min-w-0 relative \"}\n          style={didSetCursorPosition ? {} : { visibility: \"hidden\" }}\n          ref={(r) => {\n            if (r) {\n              this.ref = r;\n              this.ref.sqlRef = this.sqlRef;\n            }\n          }}\n        >\n          {infoPlaceholder}\n          <div\n            ref={(r) => {\n              if (r) {\n                this.editorContainer = r;\n              }\n            }}\n            className={`min-h-0 min-w-0 flex-col relative ${hideCodeEditor ? \"f-0\" : \"f-1\"}`}\n          >\n            {error && <ErrorComponent error={error} className=\"m-2\" />}\n            <W_SQLEditor\n              value={this.d.w?.sql ?? \"\"}\n              style={hideCodeEditor ? { display: \"none\" } : {}}\n              sql={db.sql}\n              suggestions={\n                !suggestions ? undefined : (\n                  {\n                    ...suggestions,\n                    onLoaded: () => {\n                      this.setState({ loadingSuggestions: false });\n                    },\n                  }\n                )\n              }\n              onMount={(sqlRef) => {\n                this.sqlRef = sqlRef;\n                if (this.ref) {\n                  this.ref.sqlRef = this.sqlRef;\n                }\n              }}\n              onDidChangeActiveCodeBlock={async (cb: CodeBlock | undefined) => {\n                if (currentCodeBlockChartColumns?.text === cb?.text) {\n                  return;\n                }\n                const res =\n                  cb &&\n                  (await getChartableSQL(cb, db.sql!, tables).catch(\n                    () => undefined,\n                  ));\n                this.setState({ currentCodeBlockChartColumns: res });\n              }}\n              onUnmount={(_editor, cursorPosition) => {\n                updateOptions({ cursorPosition });\n              }}\n              cursorPosition={this.d.w?.options.cursorPosition}\n              onDidSetCursorPosition={() => {\n                this.setState({ didSetCursorPosition: true });\n              }}\n              onChange={(code, cursorPosition) => {\n                if (!this.d.w) throw new Error(\"this.d.w missing\");\n\n                const newData: Partial<WindowData<\"sql\">> = { sql: code };\n                let opts: WindowData<\"sql\">[\"options\"] = this.d.w.options;\n                if (!opts.sqlChanged) {\n                  opts.sqlChanged = true;\n                }\n                opts = { ...opts, cursorPosition };\n                newData.options = opts;\n                this.d.w.$update(newData, { deepMerge: true });\n                /** Clear error on typing */\n                clearActiveQueryError();\n              }}\n              onRun={async () => {\n                await this.runSQL();\n              }}\n              onStopQuery={this.killQuery}\n              error={sqlError}\n              getFuncDef={\n                !db.sql ? undefined : (\n                  (name, minArgs) => {\n                    return getFuncs({ db: { sql: db.sql! }, name, minArgs });\n                  }\n                )\n              }\n              sqlOptions={{\n                ...w.sql_options,\n              }}\n              activeCodeBlockButtonsNode={\n                !currentCodeBlockChartColumns || !onAddChart ?\n                  null\n                : <AddChartMenu\n                    type=\"sql\"\n                    w={w}\n                    myLinks={myLinks}\n                    childWindows={this.props.childWindows}\n                    onAddChart={onAddChart}\n                    chartableSQL={currentCodeBlockChartColumns}\n                    tables={tables}\n                    size=\"micro\"\n                  />\n              }\n            />\n            {this.d.w && (\n              <W_SQLBottomBar\n                {...this.state}\n                connectionId={this.props.prgl.connectionId}\n                dbsMethods={this.props.prgl.dbsMethods}\n                toggleCodeEditor={() =>\n                  this.setState({ hideCodeEditor: !this.state.hideCodeEditor })\n                }\n                w={this.d.w}\n                onChangeState={(newState) => this.setState(newState)}\n                db={db}\n                dbs={dbs}\n                streamData={this.streamData}\n                killQuery={this.killQuery}\n                noticeEventListener={this.noticeEventListener}\n                runSQL={this.runSQL}\n                notifEventSub={this.state.notifEventSub}\n                clearActiveQueryError={clearActiveQueryError}\n              />\n            )}\n          </div>\n\n          <W_SQLResults\n            {...this.state}\n            w={w}\n            childWindow={childWindow}\n            tables={tables}\n            onPageChange={(newPage) => {\n              this.setState({ page: newPage });\n            }}\n            onPageSizeChange={(pageSize) => {\n              this.setState({ pageSize });\n              if (this.d.w?.limit && pageSize > this.d.w.limit) {\n                w.$update({ limit: pageSize });\n              }\n            }}\n            onResize={(newCols) => {\n              this.setState({ cols: newCols });\n            }}\n            onSort={(sort) => {\n              this.runSQL(sort);\n            }}\n          />\n        </div>\n\n        {popup && (\n          <Popup\n            rootStyle={popup.style}\n            anchorEl={popup.anchorEl}\n            positioning={popup.positioning}\n            clickCatchStyle={{ opacity: 0 }}\n            onClose={() => {\n              this.setState({ popup: undefined });\n            }}\n            contentClassName=\"\"\n          >\n            {popup.content}\n          </Popup>\n        )}\n      </>\n    );\n\n    return (\n      <Window\n        w={w}\n        layoutMode={workspace.layout_mode ?? \"editable\"}\n        quickMenuProps={{\n          dbs,\n          prgl: this.props.prgl,\n          myLinks: this.props.myLinks,\n          onAddChart,\n          tables,\n          setLinkMenu,\n          childWindows: this.props.childWindows,\n          chartableSQL: currentCodeBlockChartColumns,\n        }}\n        getMenu={(w, onClose) => (\n          <ProstglesSQLMenu\n            tables={tables}\n            db={db}\n            dbs={dbs}\n            onAddChart={onAddChart}\n            w={w}\n            dbsTables={dbsTables}\n            joins={joins}\n            onClose={onClose}\n          />\n        )}\n      >\n        {content}\n      </Window>\n    );\n  }\n}\n\n// Function to download data to a file\nexport function download(\n  data,\n  filename: string,\n  type: BlobPropertyBag[\"type\"],\n) {\n  const file = new Blob([data], { type });\n  const navigator = window.navigator as any;\n  if (navigator.msSaveOrOpenBlob) {\n    // IE10+\n    navigator.msSaveOrOpenBlob(file, filename);\n  } else {\n    // Others\n    const a = document.createElement(\"a\"),\n      url = URL.createObjectURL(file);\n    a.href = url;\n    a.download = filename;\n    document.body.appendChild(a);\n    a.click();\n    setTimeout(function () {\n      document.body.removeChild(a);\n      window.URL.revokeObjectURL(url);\n    }, 0);\n  }\n}\n\ntype CounterProps = TestSelectors & {\n  from: Date;\n  className?: string;\n  title?: string;\n};\nexport const Counter = ({\n  from,\n  className,\n  title,\n  ...testingProps\n}: CounterProps) => {\n  const [{ seconds, minutes }, setElapsed] = React.useState({\n    seconds: 0,\n    minutes: 0,\n  });\n  const intervalId = React.useRef<any>(undefined);\n  const getIsMounted = useIsMounted();\n  useEffect(() => {\n    clearInterval(intervalId.current);\n    intervalId.current = setInterval(() => {\n      if (!getIsMounted()) {\n        clearInterval(intervalId.current);\n        return;\n      }\n      const totalSeconds = Math.round((Date.now() - +from) / 1000);\n      const minutes = Math.floor(totalSeconds / 60);\n      const seconds = totalSeconds - minutes * 60;\n      setElapsed({ seconds: seconds, minutes });\n    }, 1000);\n  }, [from, setElapsed, getIsMounted]);\n\n  return (\n    <div title={title} className={\"text-2 \" + className} {...testingProps}>\n      {[minutes, seconds].map((v) => `${v}`.padStart(2, \"0\")).join(\":\")}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/W_SQLBottomBar/W_SQLBottomBar.tsx",
    "content": "import {\n  mdiAlertOutline,\n  mdiCancel,\n  mdiChevronDown,\n  mdiPlay,\n  mdiStopCircleOutline,\n  mdiTable,\n} from \"@mdi/js\";\nimport { isDefined, type DBHandler } from \"prostgles-types\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport type { Prgl } from \"../../../App\";\nimport { dataCommand } from \"../../../Testing\";\nimport { useReactiveState } from \"../../../appUtils\";\nimport Btn from \"@components/Btn\";\nimport ButtonGroup from \"@components/ButtonGroup\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Loading from \"@components/Loader/Loading\";\nimport Popup from \"@components/Popup/Popup\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { DBS, DBSMethods } from \"../../Dashboard/DBS\";\nimport type { WindowSyncItem } from \"../../Dashboard/dashboardUtils\";\nimport { CopyResultBtn } from \"../CopyResultBtn\";\nimport type { W_SQL } from \"../W_SQL\";\nimport type { W_SQLState } from \"../W_SQL\";\nimport { Counter, SQL_NOT_ALLOWED } from \"../W_SQL\";\nimport { W_SQLBottomBarProcStats } from \"./W_SQLBottomBarProcStats\";\nimport { t } from \"../../../i18n/i18nUtils\";\n\n/**\n * @deprecated use from prostgles-types\n */\nexport const includes = <T extends string | undefined, ArrV extends T>(\n  v: T | undefined,\n  arr: ArrV[] | readonly ArrV[],\n): v is ArrV => arr.includes(v as ArrV);\n\nexport type W_SQLBottomBarProps = {\n  killQuery: (terminate: boolean) => void;\n  db: DBHandler;\n  dbs: DBS;\n  dbsMethods: DBSMethods;\n  connectionId: Prgl[\"connectionId\"];\n  runSQL: W_SQL[\"runSQL\"];\n  streamData: W_SQL[\"streamData\"];\n  noticeEventListener: W_SQL[\"noticeEventListener\"];\n  w: WindowSyncItem<\"sql\">;\n  onChangeState: (\n    newState: Pick<W_SQLState, \"noticeSub\" | \"notices\" | \"notifEventSub\">,\n  ) => void;\n  toggleCodeEditor: VoidFunction;\n  clearActiveQueryError: VoidFunction;\n} & Pick<\n  W_SQLState,\n  | \"loadingSuggestions\"\n  | \"cols\"\n  | \"rows\"\n  | \"noticeSub\"\n  | \"activeQuery\"\n  | \"queryEnded\"\n  | \"notifEventSub\"\n  | \"hideCodeEditor\"\n>;\n\nexport const W_SQLBottomBar = (props: W_SQLBottomBarProps) => {\n  const {\n    db,\n    killQuery,\n    runSQL,\n    w,\n    activeQuery,\n    notifEventSub,\n    cols,\n    loadingSuggestions,\n    noticeSub,\n    clearActiveQueryError,\n    noticeEventListener,\n    onChangeState,\n    streamData,\n    hideCodeEditor,\n    toggleCodeEditor,\n  } = props;\n  const myRef = useRef<HTMLDivElement>(null);\n  const refRowCount = myRef.current;\n  const [showRunOptions, setShowRunOptions] = useState<HTMLElement | null>(\n    null,\n  );\n  const [loopMode, setLoopMode] = useState({\n    show: false,\n    seconds: 0,\n    enabled: false,\n  });\n\n  const {\n    state: { rows },\n  } = useReactiveState(streamData);\n\n  useEffect(() => {\n    if (!loopMode.enabled || !loopMode.seconds) return;\n\n    const interval = setInterval(runSQL, loopMode.seconds * 1e3);\n\n    return () => clearInterval(interval);\n  }, [loopMode.enabled, loopMode.seconds, runSQL]);\n\n  const stopQuery = (terminate: boolean) => {\n    killQuery(terminate);\n    setLoopMode({ ...loopMode, enabled: false, show: false, seconds: 0 });\n  };\n\n  const duration =\n    activeQuery?.state === \"running\" ?\n      Date.now() - activeQuery.started.getTime()\n    : activeQuery?.state === \"ended\" ?\n      activeQuery.ended.getTime() - activeQuery.started.getTime()\n    : 0;\n  const queryIsRunning = activeQuery?.state === \"running\";\n  const fetchedRowCount =\n    !activeQuery || activeQuery.state === \"error\" ? 0\n    : activeQuery.state === \"running\" ? rows.length\n    : activeQuery.rowCount;\n  const totalRowCount =\n    activeQuery?.state === \"ended\" && isDefined(activeQuery.totalRowCount) ?\n      activeQuery.totalRowCount\n    : undefined;\n  const limitWasReached =\n    activeQuery?.state === \"ended\" && activeQuery.rowCount === w.limit;\n  return (\n    <div\n      className={\n        \"W_SQLBottomBar relative oy-hidden flex-row text-2 ai-center text-sm o-auto \"\n      }\n      data-command=\"W_SQLBottomBar\"\n      style={{\n        borderTop: \"1px solid #0000000d\",\n        /** Ensure big error messages don't obscure the sql editor */\n        maxHeight: activeQuery?.state === \"error\" ? \"50%\" : undefined,\n      }}\n    >\n      {showRunOptions && (\n        <Popup\n          contentClassName=\"flex-col gap-1 p-p5\"\n          positioning=\"beneath-left\"\n          anchorEl={showRunOptions}\n          clickCatchStyle={{ opacity: 0 }}\n          onClose={() => setShowRunOptions(null)}\n        >\n          <ButtonGroup\n            label={t.W_SQLBottomBar[\"Execution mode\"]}\n            value={w.sql_options.executeOptions ?? \"block\"}\n            options={[\"full\", \"block\", \"smallest-block\"]}\n            onChange={(executeOptions) =>\n              w.$update(\n                { sql_options: { executeOptions } },\n                { deepMerge: true },\n              )\n            }\n          />\n          <SwitchToggle\n            label={t.W_SQLBottomBar[\"Loop query execution\"]}\n            checked={loopMode.enabled}\n            onChange={() => {\n              setLoopMode({ ...loopMode, show: true });\n              setShowRunOptions(null);\n            }}\n          />\n        </Popup>\n      )}\n      {queryIsRunning ?\n        <>\n          <Btn\n            size=\"default\"\n            color=\"action\"\n            iconPath={mdiStopCircleOutline}\n            title={t.W_SQLBottomBar[\"Cancel this query (Esc)\"]}\n            {...dataCommand(\"W_SQLBottomBar.cancelQuery\")}\n            loading={activeQuery.stopped?.type === \"cancel\"}\n            onClick={() => {\n              stopQuery(false);\n            }}\n          >\n            Cancel\n          </Btn>\n          <Btn\n            size=\"default\"\n            title={t.W_SQLBottomBar[\"Terminate this query\"]}\n            {...dataCommand(\"W_SQLBottomBar.terminateQuery\")}\n            color=\"danger\"\n            iconPath={mdiCancel}\n            loading={activeQuery.stopped?.type === \"terminate\"}\n            onClick={() => {\n              stopQuery(true);\n            }}\n          >\n            Terminate\n          </Btn>\n          <Counter\n            data-command=\"W_SQLBottomBar.queryDuration\"\n            title={t.W_SQLBottomBar[\"Query running time\"]}\n            className=\"p-p5 mr-1 noselect\"\n            from={activeQuery.started}\n          />\n          {w.sql_options.showRunningQueryStats && (\n            <W_SQLBottomBarProcStats {...props} />\n          )}\n        </>\n      : <>\n          {notifEventSub ?\n            <Btn\n              title={t.W_SQLBottomBar[\"Stop LISTEN\"]}\n              size=\"default\"\n              color=\"action\"\n              iconPath={mdiStopCircleOutline}\n              {...dataCommand(\"W_SQLBottomBar.stopListen\")}\n              onClick={async () => {\n                await notifEventSub.removeListener();\n                onChangeState({ notifEventSub: undefined });\n              }}\n              fadeIn={true}\n            >\n              {t.W_SQLBottomBar[\"Stop LISTEN\"]}\n            </Btn>\n          : loopMode.show ?\n            <FlexRow>\n              <Btn\n                color={loopMode.enabled ? \"action\" : undefined}\n                iconPath={mdiCancel}\n                data-command=\"W_SQLBottomBar.stopLoopQuery\"\n                onClick={() =>\n                  setLoopMode({ ...loopMode, show: false, enabled: false })\n                }\n              />\n              <label>{t.W_SQLBottomBar[\"repeat every\"]}</label>\n              <input\n                {...{ min: 0, max: 20, step: 0.1 }}\n                type=\"number\"\n                style={{ fontSize: \"12px\" }}\n                value={loopMode.seconds}\n                onChange={(v) =>\n                  setLoopMode({\n                    ...loopMode,\n                    enabled: true,\n                    seconds: +v.target.value,\n                  })\n                }\n              />\n              <label>{t.W_SQLBottomBar.seconds}</label>\n            </FlexRow>\n          : <Btn\n              data-command=\"W_SQLBottomBar.runQuery\"\n              className=\"ml-p25\"\n              color=\"action\"\n              title={t.W_SQLBottomBar[\"Run query (CTRL+E, ALT+E)\"]}\n              disabledInfo={!db.sql ? SQL_NOT_ALLOWED : undefined}\n              size=\"default\"\n              iconPath={mdiPlay}\n              onClick={(e) => {\n                runSQL();\n                hideKeyboard(e.currentTarget);\n              }}\n              onContextMenu={(e) => {\n                e.preventDefault();\n                setShowRunOptions(e.currentTarget);\n              }}\n              fadeIn={true}\n            >\n              {t.W_SQLBottomBar.Run}\n            </Btn>\n          }\n          {activeQuery?.state === \"ended\" && !loopMode.show && (\n            <div\n              className=\"p-p5 mr-1 noselect fade-in\"\n              title={t.W_SQLBottomBar[\"Query running time: \"]({\n                duration: toSecondsString(duration),\n              })}\n            >\n              {notifEventSub ? \"LISTEN...\" : toSecondsString(duration)}\n            </div>\n          )}\n        </>\n      }\n\n      <FlexRow\n        ref={myRef}\n        className=\"flex-row gap-p5 ai-center p-p5 noselect\"\n        style={{ marginRight: \"1em\" }}\n      >\n        {activeQuery && activeQuery.state !== \"error\" && cols && (\n          <>\n            <FlexCol\n              data-command=\"W_SQLBottomBar.rowCount\"\n              className=\"RowCount gap-p25 text-1\"\n            >\n              {fetchedRowCount.toLocaleString()} rows\n              {isDefined(totalRowCount) && totalRowCount > fetchedRowCount && (\n                <div className=\"text-warning\">\n                  {totalRowCount.toLocaleString()} total rows\n                </div>\n              )}\n            </FlexCol>\n            {limitWasReached && (\n              <PopupMenu\n                contentClassName=\"p-1\"\n                positioning=\"above-center\"\n                button={\n                  <Btn iconPath={mdiAlertOutline} size=\"small\" color=\"warn\" />\n                }\n                footerButtons={[\n                  {\n                    label: \"Remove limit\",\n                    color: \"action\",\n                    variant: \"filled\",\n                    onClick: () => w.$update({ limit: null }),\n                  },\n                ]}\n                render={() => (\n                  <InfoRow variant=\"naked\">\n                    Limit was reached: {w.limit} rows fetched out of{\" \"}\n                    {totalRowCount} total rows\n                  </InfoRow>\n                )}\n              />\n            )}\n          </>\n        )}\n        {activeQuery?.state !== \"error\" && (\n          <CopyResultBtn\n            cols={cols ?? []}\n            rows={rows}\n            sql={db.sql!}\n            queryEnded={activeQuery?.state === \"ended\"}\n          />\n        )}\n      </FlexRow>\n\n      {loadingSuggestions && db.sql && (\n        <Loading message=\"Loading suggestions\" />\n      )}\n\n      {w.sql_options.errorMessageDisplay !== \"tooltip\" && (\n        <ErrorComponent\n          noScroll={false}\n          error={\n            activeQuery?.state === \"error\" ? activeQuery.error?.message : \"\"\n          }\n          color={\n            activeQuery?.state === \"error\" && !activeQuery.stopped ?\n              \"info\"\n            : undefined\n          }\n          style={{\n            padding: \"1em\",\n            fontSize: \"16px\",\n            whiteSpace: \"pre\",\n            maxHeight: \"min(100%, 300px)\",\n          }}\n          onClear={clearActiveQueryError}\n          data-command=\"W_SQLBottomBar.sqlError\"\n        />\n      )}\n\n      {!queryIsRunning && (\n        <>\n          <div\n            className={\n              \"ml-auto flex-row ai-center \" +\n              (limitWasReached ? \"text-warning\" : \"\")\n            }\n            style={{ marginRight: \"1em\" }}\n            data-command=\"W_SQLBottomBar.limit\"\n            title={t.W_SQLBottomBar[\"Clear value to show all rows\"]}\n          >\n            <label className=\"mr-p5\">{t.W_SQLBottomBar.Limit}</label>\n            <input\n              id={\"dd\" + w.limit}\n              type=\"number\"\n              style={{ width: \"55px\" }}\n              min={1}\n              max={1000}\n              step={1}\n              className=\"text-0 b b-color bg-color-2 rounded p-p25\"\n              defaultValue={w.limit ?? \"\"}\n              onChange={({ target: { value } }) => {\n                const limit = value.length ? +value : -1;\n                if (!Number.isInteger(limit) || limit < 1) {\n                  w.$update({ limit: null });\n                } else {\n                  w.$update({ limit });\n                }\n              }}\n            />\n          </div>\n          <Btn\n            title={t.W_SQLBottomBar[\"Show/Hide table\"]}\n            data-command=\"W_SQLBottomBar.toggleTable\"\n            iconPath={mdiTable}\n            className=\"mr-1\"\n            onClick={(e) => {\n              const o = w.options;\n              if (\n                o.hideTable &&\n                (activeQuery?.state !== \"ended\" || !activeQuery.rowCount)\n              ) {\n                if (refRowCount) {\n                  refRowCount.classList.remove(\"rubberBand\");\n                  void refRowCount.offsetWidth;\n                  refRowCount.classList.add(\"rubberBand\");\n                }\n              } else {\n                w.$update(\n                  { options: { hideTable: !o.hideTable } },\n                  { deepMerge: true },\n                );\n              }\n            }}\n          />\n          <Btn\n            title={t.W_SQLBottomBar[\"Show/Hide code editor\"]}\n            data-command=\"W_SQLBottomBar.toggleCodeEditor\"\n            iconPath={mdiChevronDown}\n            style={{\n              transform: `rotate(${hideCodeEditor ? 0 : 180}deg)`,\n              transition: \"transform .2s\",\n            }}\n            onClick={toggleCodeEditor}\n          />\n          <Btn\n            title={t.W_SQLBottomBar[\"Show/Hide notices\"]}\n            data-command=\"W_SQLBottomBar.toggleNotices\"\n            iconPath={mdiAlertOutline}\n            color={noticeSub ? \"action\" : undefined}\n            className=\"mr-1\"\n            disabledInfo={!db.sql ? SQL_NOT_ALLOWED : undefined}\n            onClick={async (e) => {\n              if (noticeSub) {\n                await noticeSub.removeListener();\n                onChangeState({\n                  notices: undefined,\n                  noticeSub: undefined,\n                });\n              } else if (db.sql) {\n                const s = await db.sql(\n                  \"\",\n                  {},\n                  { returnType: \"noticeSubscription\" },\n                );\n                const sub = s.addListener(noticeEventListener);\n                onChangeState({ noticeSub: sub });\n              }\n            }}\n          />\n        </>\n      )}\n    </div>\n  );\n};\n\nfunction hideKeyboard(element: HTMLElement) {\n  if (element.nodeName !== \"INPUT\") {\n    const field = document.createElement(\"input\");\n    field.setAttribute(\"type\", \"text\");\n    (element.parentElement || document.body).appendChild(field);\n\n    setTimeout(function () {\n      field.focus();\n      setTimeout(function () {\n        field.setAttribute(\"style\", \"display:none;\");\n        field.remove();\n      }, 50);\n    }, 50);\n    return;\n  }\n\n  element.setAttribute(\"readonly\", \"readonly\"); // Force keyboard to hide on input field.\n  element.setAttribute(\"disabled\", \"true\"); // Force keyboard to hide on textarea field.\n  setTimeout(function () {\n    element.blur(); //actually close the keyboard\n    // Remove readonly attribute after keyboard is hidden.\n    element.removeAttribute(\"readonly\");\n    element.removeAttribute(\"disabled\");\n  }, 100);\n}\nconst toSecondsString = (v: number) => `${(v / 1000).toFixed(3) || 0}s`;\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/W_SQLBottomBar/W_SQLBottomBarProcStats.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { FlexRow } from \"@components/Flex\";\nimport type { W_SQLBottomBarProps } from \"./W_SQLBottomBar\";\nimport { useIsMounted } from \"prostgles-client\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport Chip from \"@components/Chip\";\nimport type { FilterItem } from \"prostgles-types\";\n\nexport const W_SQLBottomBarProcStats = ({\n  dbsMethods,\n  dbs,\n  connectionId,\n  activeQuery,\n}: W_SQLBottomBarProps) => {\n  const { getStatus } = dbsMethods;\n  const getIsMounted = useIsMounted();\n  const [procStats, setProcStats] = useState<DBSSchema[\"stats\"] | undefined>();\n  useEffect(() => {\n    const pid = activeQuery?.pid;\n    if (!getStatus || (activeQuery?.state !== \"running\" && pid)) return;\n    const interval = setInterval(async () => {\n      await getStatus(connectionId);\n      const procInfo = await dbs.stats.findOne({\n        $existsJoined: {\n          \"database_configs.connections\": {\n            id: connectionId,\n          },\n        },\n        pid,\n      } as FilterItem);\n      if (!getIsMounted()) {\n        return clearInterval(interval);\n      }\n      setProcStats(procInfo);\n    }, 1e3);\n\n    return () => clearInterval(interval);\n  }, [activeQuery, getStatus, connectionId, getIsMounted, dbs]);\n\n  if (!procStats) return null;\n  return (\n    <FlexRow title={activeQuery?.pid ? `PID ${activeQuery.pid}` : \"\"}>\n      <Chip label=\"pid\" value={procStats.pid} />\n      <Chip\n        label=\"CPU\"\n        value={`${Number(procStats.cpu || 0).toFixed(1)}% ${procStats.mhz ? `${procStats.mhz}Mhz` : \"\"}`}\n      />\n      <Chip label=\"Mem\" value={procStats.memPretty ?? \"\"} />\n      {procStats.wait_event && (\n        <Chip label=\"Wait Event\" value={procStats.wait_event} />\n      )}\n      {procStats.wait_event_type && (\n        <Chip label=\"Wait Event Type\" value={procStats.wait_event_type} />\n      )}\n      {(procStats.blocked_by?.length ?? 0) > 0 && (\n        <FlexRow>\n          Blocked by:\n          {procStats.blocked_by?.map((pid, i) => (\n            <Chip key={pid} className=\"mt-p25\" color=\"red\">\n              {pid}\n            </Chip>\n          ))}\n        </FlexRow>\n      )}\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/W_SQLMenu.tsx",
    "content": "import {\n  mdiCodeJson,\n  mdiCog,\n  mdiContentSave,\n  mdiDelete,\n  mdiDownload,\n  mdiFileUploadOutline,\n  mdiKeyboard,\n  mdiListBoxOutline,\n  mdiPlay,\n  mdiTable,\n  mdiUpload,\n} from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport type { TabsProps } from \"@components/Tabs\";\nimport Tabs from \"@components/Tabs\";\nimport RTComp from \"../RTComp\";\n\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type {\n  DBSchemaTablesWJoins,\n  OnAddChart,\n  WindowSyncItem,\n} from \"../Dashboard/dashboardUtils\";\n\nimport { getJSONBSchemaAsJSONSchema } from \"prostgles-types\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { SECOND } from \"../Charts\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport type { DBS } from \"../Dashboard/DBS\";\nimport { SQLHotkeys } from \"./SQLHotkeys\";\nimport { TestSQL } from \"./TestSQL\";\nimport { download } from \"./W_SQL\";\n\ntype P = {\n  tableName?: string;\n  db: DBHandlerClient;\n  dbs: DBS;\n  onAddChart?: OnAddChart;\n  w: WindowSyncItem<\"sql\">;\n  joins: string[];\n  dbsTables: CommonWindowProps[\"tables\"];\n  tables: DBSchemaTablesWJoins;\n  onClose: VoidFunction;\n};\n\nconst REFRESH_OPTIONS = [\"Realtime\", \"Custom\", \"None\"] as const;\nexport type Unpromise<T extends Promise<any>> =\n  T extends Promise<infer U> ? U : never;\n\nexport type RefreshOptions = {\n  autoRefreshSeconds?: number;\n  refreshType?: (typeof REFRESH_OPTIONS)[number];\n};\n\ntype S = {\n  indexes?: {\n    indexdef: string;\n    indexname: string;\n    schemaname: string;\n    tablename: string;\n  }[];\n  query?: {\n    hint?: string;\n    label?: string;\n    sql: string;\n  };\n  l1Key?: string;\n  l2Key?: string;\n  running?: boolean;\n  error?: any;\n  initError?: any;\n  hint?: string;\n  infoQuery?: {\n    label: string;\n    query: string;\n  };\n  autoRefreshSeconds?: number;\n  newOptions?: P[\"w\"][\"sql_options\"];\n};\n\ntype D = {\n  w?: P[\"w\"];\n};\n\nexport class ProstglesSQLMenu extends RTComp<P, S, D> {\n  state: S = {\n    // joins: [],\n    l1Key: undefined,\n    l2Key: undefined,\n    query: undefined,\n    running: undefined,\n    error: undefined,\n    initError: undefined,\n    hint: undefined,\n    indexes: undefined,\n    infoQuery: undefined,\n    autoRefreshSeconds: undefined,\n  };\n\n  onUnmount = async () => {\n    if (this.wSub) await this.wSub.$unsync();\n  };\n\n  wSub?: ReturnType<P[\"w\"][\"$cloneSync\"]>;\n  autoRefresh: any;\n  loading = false;\n  onDelta = (dP?: Partial<P>, dS?: Partial<S>, dD?) => {\n    if (dS && \"query\" in dS) {\n      this.setState({ error: undefined });\n    }\n\n    if (\n      dP?.w?.sql_options &&\n      JSON.stringify(this.props.w.sql_options) ===\n        JSON.stringify(this.state.newOptions)\n    ) {\n      this.setState({ newOptions: undefined });\n    }\n  };\n\n  saveQuery = async () => {\n    const w = this.props.w;\n    const sql = w.$get()?.sql || \"\";\n    const fileName = (w.$get()?.name || `Query_${await sha256(sql)}`) + \".sql\";\n\n    download(sql, fileName, \"text/sql\");\n  };\n\n  render() {\n    const { onAddChart, w, dbs, dbsTables, tables, onClose } = this.props;\n\n    const { l1Key, initError, error, newOptions } = this.state;\n\n    if (initError) {\n      return (\n        <div className=\"p-1\">\n          <ErrorComponent error={initError} />\n        </div>\n      );\n    }\n\n    const sqlOptsValue = JSON.stringify(newOptions || w.sql_options, null, 2);\n\n    const table = dbsTables.find((t) => t.name === \"windows\");\n    const sqlOptionsCol = table?.columns.find((c) => c.name === \"sql_options\");\n\n    if (!table) {\n      return <div>dbs.windows table schema not found</div>;\n    }\n\n    const l1Opts: TabsProps[\"items\"] = {\n      General: {\n        label: t.W_SQLMenu.General,\n        leftIconPath: mdiFileUploadOutline,\n        content: (\n          <div className=\"flex-col ai-start gap-1\">\n            <FormField\n              label={t.W_SQLMenu[\"Query name\"]}\n              data-command=\"W_SQLMenu.name\"\n              value={w.name}\n              type=\"text\"\n              onChange={(newVal) => {\n                w.$update(\n                  { name: newVal, options: { sqlWasSaved: true } },\n                  { deepMerge: true },\n                );\n              }}\n            />\n\n            <FormField\n              label={t.W_SQLMenu[\"Result display mode\"]}\n              data-command=\"W_SQLMenu.renderDisplayMode\"\n              fullOptions={[\n                { key: \"table\", label: \"Table\", iconPath: mdiTable },\n                { key: \"csv\", label: \"CSV\", iconPath: mdiListBoxOutline },\n                { key: \"JSON\", label: \"JSON\", iconPath: mdiCodeJson },\n              ]}\n              value={w.sql_options.renderMode ?? \"table\"}\n              onChange={(renderMode) =>\n                w.$update({ sql_options: { renderMode } }, { deepMerge: true })\n              }\n            />\n\n            <Btn\n              title={t.W_SQLMenu[\"Save query as file\"]}\n              data-command=\"W_SQLMenu.saveQuery\"\n              iconPath={mdiDownload}\n              onClick={this.saveQuery}\n              variant=\"faded\"\n            >\n              {t.W_SQLMenu[\"Download query\"]}\n            </Btn>\n\n            <Btn\n              iconPath={mdiUpload}\n              variant=\"faded\"\n              title={t.W_SQLMenu[\"Open query from file\"]}\n              data-command=\"W_SQLMenu.openSQLFile\"\n              onClick={({ currentTarget }) => {\n                currentTarget.querySelector(\"input\")?.click();\n              }}\n            >\n              {t.W_SQLMenu[\"Open SQL file\"]}\n              <input\n                id=\"sql-open\"\n                name=\"sql-open\"\n                title={t.W_SQLMenu[\"Open query from file\"]}\n                type=\"file\"\n                accept=\"text/*, .sql, .txt\"\n                style={{ display: \"none\" }}\n                onChange={(e) => {\n                  if (e.currentTarget.files && e.currentTarget.files[0]) {\n                    const myFile = e.currentTarget.files[0];\n                    getFileText(myFile).then((sql) => {\n                      w.$update({ sql, show_menu: false, closed: true });\n                      w.$update({ closed: false });\n                    });\n                  }\n                }}\n              />\n            </Btn>\n\n            <Btn\n              title={t.W_SQLMenu[\"Delete query\"]}\n              data-command=\"W_SQLMenu.deleteQuery\"\n              color=\"danger\"\n              variant=\"faded\"\n              iconPath={mdiDelete}\n              onClick={() => {\n                w.$update({ closed: true, deleted: true });\n              }}\n            >\n              {t.W_SQLMenu[\"Delete query\"]}\n            </Btn>\n\n            <Btn\n              variant=\"faded\"\n              iconPath={mdiPlay}\n              title=\"Click five times to run test\"\n              onClick={({ currentTarget }) => {\n                const node = currentTarget as {\n                  _lastClickedCount?: number;\n                  _lastClicked?: number;\n                };\n                if (node._lastClickedCount && node._lastClickedCount > 3) {\n                  onClose();\n                  setTimeout(() => {\n                    TestSQL(w);\n                  }, SECOND);\n                  node._lastClicked = 0;\n                  node._lastClickedCount = 0;\n                } else {\n                  const clickedWithinTimeFrame =\n                    !node._lastClicked || Date.now() - node._lastClicked < 500;\n                  node._lastClickedCount =\n                    clickedWithinTimeFrame ?\n                      (node._lastClickedCount ?? 0) + 1\n                    : 0;\n                  node._lastClicked = Date.now();\n                }\n              }}\n            >\n              TEST\n            </Btn>\n          </div>\n        ),\n      },\n\n      \"Editor options\": {\n        label: t.W_SQLMenu[\"Editor options\"],\n        leftIconPath: mdiCog,\n        content: (\n          <div\n            className=\"flex-col ai-start gap-1\"\n            key={JSON.stringify(w.sql_options)}\n          >\n            <div>{t.W_SQLMenu[\"SQL Editor settings\"]}</div>\n            <CodeEditor\n              language={{\n                lang: \"json\",\n                jsonSchemas: [\n                  {\n                    id: \"sql_options\",\n                    schema: getJSONBSchemaAsJSONSchema(\n                      table.name,\n                      \"sql_options\",\n                      sqlOptionsCol?.jsonbSchema ?? {},\n                    ),\n                  },\n                ],\n              }}\n              style={{\n                minHeight: \"200px\",\n                minWidth: \"400px\",\n                flex: 1,\n                resize: \"vertical\",\n                overflow: \"auto\",\n                width: \"100%\",\n              }}\n              value={sqlOptsValue}\n              onChange={(v) => {\n                try {\n                  this.setState({ newOptions: JSON.parse(v) });\n                } catch (err) {}\n              }}\n            />\n            <InfoRow color=\"info\" className=\"ws-pre\">\n              {t.W_SQLMenu.Press} <strong>ctrl</strong> + <strong>space</strong>{\" \"}\n              {t.W_SQLMenu[\"to get a list of possible options\"]}\n            </InfoRow>\n            {!!error && <ErrorComponent error={error} />}\n            <Btn\n              color=\"action\"\n              variant=\"filled\"\n              iconPath={mdiContentSave}\n              disabledInfo={\n                error ? t.W_SQLMenu[\"Cannot save due to error\"]\n                : (\n                  !newOptions ||\n                  JSON.stringify(newOptions) === JSON.stringify(w.sql_options)\n                ) ?\n                  t.W_SQLMenu[\"Nothing to update\"]\n                : undefined\n              }\n              onClickPromise={async () => {\n                const _newOpts = { ...newOptions! };\n                this.setState({ error: undefined });\n                try {\n                  await dbs.windows.update(\n                    { id: w.id },\n                    { sql_options: _newOpts },\n                  );\n                } catch (error) {\n                  this.setState({ error, newOptions: _newOpts });\n                }\n              }}\n            >\n              {t.W_SQLMenu[\"Update options\"]}\n            </Btn>\n          </div>\n        ),\n      },\n      Hotkeys: {\n        label: t.W_SQLMenu.Hotkeys,\n        leftIconPath: mdiKeyboard,\n        content: <SQLHotkeys />,\n      },\n    };\n\n    return (\n      <div\n        className=\"table-menu c--fit flex-row\"\n        style={{ maxHeight: \"100vh\", maxWidth: \"100vw\" }}\n      >\n        <Tabs\n          variant=\"vertical\"\n          contentClass={\n            \" o-auto min-h-0 max-h-100v \" + (l1Key === \"Alter\" ? \" \" : \" p-1\")\n          }\n          items={l1Opts}\n          compactMode={window.isMobileDevice ? \"hide-inactive\" : undefined}\n          // defaultActiveKey={\"General\"}\n        />\n      </div>\n    );\n  }\n}\n\nexport function getFileText(file: File): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n\n    reader.addEventListener(\"load\", function (e) {\n      if (e.target) resolve(e.target.result as string);\n      else reject(\"e.target is null\");\n    });\n\n    reader.readAsBinaryString(file);\n  });\n}\n\nexport async function sha256(message) {\n  // encode as UTF-8\n  const msgBuffer = new TextEncoder().encode(message);\n\n  // hash the message\n  const hashBuffer = await crypto.subtle.digest(\"SHA-256\", msgBuffer);\n\n  // convert ArrayBuffer to Array\n  const hashArray = Array.from(new Uint8Array(hashBuffer));\n\n  // convert bytes to hex string\n  const hashHex = hashArray\n    .map((b) => b.toString(16).padStart(2, \"0\"))\n    .join(\"\");\n  return hashHex;\n}\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/W_SQLResults.tsx",
    "content": "import type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport React, { useMemo, useState } from \"react\";\nimport type { PaginationProps } from \"@components/Table/Pagination\";\nimport { Table } from \"@components/Table/Table\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport type { WindowData } from \"../Dashboard/dashboardUtils\";\nimport type { ColumnSortSQL } from \"../W_Table/ColumnMenu/ColumnMenu\";\nimport { TooManyColumnsWarning } from \"../W_Table/TooManyColumnsWarning\";\nimport { CSVRender } from \"./CSVRender\";\nimport { getSQLResultTableColumns } from \"./getSQLResultTableColumns\";\nimport type { W_SQLProps, W_SQLState } from \"./W_SQL\";\n\nexport type W_SQLResultsProps = Pick<\n  W_SQLState,\n  | \"sqlResult\"\n  | \"rows\"\n  | \"notifEventSub\"\n  | \"notices\"\n  | \"cols\"\n  | \"activeQuery\"\n  | \"sort\"\n  | \"page\"\n  | \"pageSize\"\n  | \"isSelect\"\n> &\n  Pick<W_SQLProps, \"childWindow\" | \"tables\"> & {\n    w: SyncDataItem<Required<WindowData<\"sql\">>, true>;\n    onResize: (newCols: W_SQLState[\"cols\"]) => void;\n    onSort: (newSort: ColumnSortSQL[]) => void;\n    onPageChange: (newPage: number) => void;\n    onPageSizeChange: (newPageSize: W_SQLState[\"pageSize\"]) => void;\n  };\n\nexport const W_SQLResults = (props: W_SQLResultsProps) => {\n  const {\n    sqlResult,\n    notifEventSub,\n    notices,\n    rows = [],\n    cols = [],\n    activeQuery,\n    childWindow,\n    sort,\n    w,\n    tables,\n    page,\n    pageSize,\n    onSort,\n    onResize,\n    isSelect,\n    onPageChange,\n    onPageSizeChange,\n  } = props;\n  const o: WindowData<\"sql\">[\"options\"] = w.options;\n  const { renderMode = \"table\", maxCharsPerCell } = w.sql_options;\n  const {\n    commandResult = undefined,\n    rowCount = undefined,\n    info = undefined,\n  } = activeQuery?.state === \"ended\" ? activeQuery : {};\n  const hideResults =\n    !childWindow &&\n    ((o.hideTable && !notices && !notifEventSub) ||\n      (!rows.length && !sqlResult && activeQuery?.state !== \"running\"));\n\n  const isExplainResult = (info?.command || \"\").toLowerCase() === \"explain\";\n  const [tooManyColumnsWarningWasShown, setTooManyColumnsWarningWasShown] =\n    useState(false);\n\n  const tableColumns = useMemo(() => {\n    return getSQLResultTableColumns({\n      cols,\n      tables,\n      maxCharsPerCell,\n      onResize,\n      rows,\n    });\n  }, [cols, tables, maxCharsPerCell, onResize, rows]);\n\n  const pagination = useMemo(() => {\n    if (!isSelect) return;\n    return {\n      page,\n      pageSize,\n      totalRows: rowCount,\n      onPageChange,\n      onPageSizeChange,\n    } satisfies PaginationProps;\n  }, [isSelect, onPageChange, onPageSizeChange, page, pageSize, rowCount]);\n\n  const paginatedRows =\n    renderMode === \"table\" ?\n      rows.slice(page * pageSize, (page + 1) * pageSize)\n    : rows;\n  return (\n    <div\n      data-command=\"W_SQLResults\"\n      className={\n        \"W_SQLResults flex-col oy-auto relative bt b-color \" +\n        (commandResult ? \" f-0 \" : \" f-1 \") +\n        (hideResults ? \" hidden \" : \"\")\n      }\n    >\n      {notices ?\n        <div className=\"p-1 ws-pre text-1\">\n          {notices\n            .slice(0)\n            .map((n) => JSON.stringify(n, null, 2))\n            .join(\"\\n\")}\n        </div>\n      : commandResult ?\n        <div className=\"p-1 \">{commandResult}</div>\n      : childWindow ?\n        childWindow\n      : renderMode === \"csv\" ?\n        <CSVRender cols={cols} rows={rows} />\n      : renderMode === \"JSON\" ?\n        <CodeEditor\n          language=\"json\"\n          value={JSON.stringify(\n            rows.map((rowValues) =>\n              cols.reduce((a, v, i) => ({ ...a, [v.name]: rowValues[i] }), {}),\n            ),\n            null,\n            2,\n          )}\n        />\n      : <>\n          {!tooManyColumnsWarningWasShown && (\n            <TooManyColumnsWarning\n              w={w}\n              numberOfCols={cols.length}\n              numberOfRows={rows.length}\n              onHide={() => {\n                setTooManyColumnsWarningWasShown(true);\n              }}\n            />\n          )}\n\n          <Table\n            maxCharsPerCell={maxCharsPerCell || 1000}\n            sort={sort}\n            onSort={onSort}\n            enableExperimentalVirtualisation={true}\n            showSubLabel={true}\n            cols={tableColumns}\n            rows={paginatedRows}\n            style={{ flex: 1, boxShadow: \"unset\" }}\n            tableStyle={{\n              borderRadius: \"unset\",\n              border: \"unset\",\n              ...(isExplainResult && {\n                whiteSpace: \"pre\",\n              }),\n            }}\n            pagination={pagination}\n          />\n        </>\n      }\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/customRenderers.tsx",
    "content": "import React from \"react\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { getKeys, isEmpty } from \"prostgles-types\";\nimport type { DivProps } from \"@components/Flex\";\n\nconst SHORT_NAMES = [\n  [\"years\", \"y\"],\n  [\"months\", \"mo\"],\n  [\"weeks\", \"w\"],\n  [\"days\", \"d\"],\n  [\"hours\", \"h\"],\n  [\"minutes\", \"min\"],\n  [\"seconds\", \"s\"],\n  [\"milliseconds\", \"ms\"],\n] as const;\n\nexport type PG_Interval = Partial<\n  Record<(typeof SHORT_NAMES)[number][0], number>\n>;\n\nexport const getPGIntervalAsText = (\n  v: AnyObject = {},\n  shortVersion = false,\n  withAgoText?: boolean,\n  excludeMillis = false,\n) => {\n  let isNegative = false;\n\n  const startIndex = SHORT_NAMES.findIndex(\n    ([name]) => Number.isFinite(v[name]) && v[name] !== 0,\n  );\n  // const endIndex = SHORT_NAMES.slice(0).reverse().find(([name]) => Number.isFinite(v[name]) && v[name] !== 0)\n  const intervalText =\n    SHORT_NAMES.map(([name, shortName], i) => {\n      let val = v[name];\n      if (\n        i >= startIndex &&\n        Number.isFinite(val) &&\n        (!excludeMillis || name !== \"milliseconds\")\n      ) {\n        isNegative = val < 0;\n        if (withAgoText) val = Math.abs(val);\n        return `${val}${shortVersion ? shortName : \" \" + name}`;\n      }\n\n      return undefined;\n    })\n      .filter((v) => v)\n      .join(\" \") || \"0 seconds\";\n\n  if (withAgoText) {\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    return `${isNegative ? \"In \" : \"\"}${intervalText}${!isNegative ? \" ago\" : \"\"}`;\n  }\n\n  return intervalText;\n};\n\ntype StyledIntervalProps = {\n  value: PG_Interval;\n  mode?: \"backups\" | \"pg_stat\" | \"full\" | \"short\";\n} & Pick<DivProps, \"style\" | \"className\">;\nexport const StyledInterval = ({\n  value: v,\n  mode = \"backups\",\n  ...divProps\n}: StyledIntervalProps) => {\n  const colorParts = {\n    red: \"-red-500\",\n    yellow: \"-yellow-500\",\n    green: \"-green-500\",\n    gray: \"-gray-500\",\n  };\n\n  const textParts = getShortText(v);\n  const shortTextParts =\n    mode === \"full\" ? textParts.slice(0)\n    : mode === \"short\" ? textParts.slice(0, 2)\n    : textParts.slice(0, 1);\n  const shortText = shortTextParts.join(\" \");\n\n  const color =\n    mode === \"full\" || mode === \"short\" ? colorParts.gray\n    : mode === \"backups\" ?\n      isEmpty(v) ? colorParts.gray\n      : \"milliseconds\" in v || \"seconds\" in v ? colorParts.green\n      : \"minutes\" in v ? colorParts.yellow\n      : colorParts.red\n    : isEmpty(v) ? colorParts.gray\n    : \"milliseconds\" in v || \"seconds\" in v || \"minutes\" in v || \"hours\" in v ?\n      colorParts.green\n    : \"days\" in v || \"weeks\" in v ? colorParts.yellow\n    : colorParts.red;\n\n  return (\n    <span\n      style={divProps.style}\n      title={JSON.stringify(v).slice(1, -1)}\n      className={`${divProps.className ?? \"\"} text${color}`}\n    >\n      {shortText || \"0s\"}\n      {mode === \"backups\" ? \" ago\" : \"\"}\n    </span>\n  );\n};\n\nconst getShortText = (_v: PG_Interval | null) => {\n  const v = _v ?? {};\n  const res = [\n    ...(isEmpty(v) ? [\"\"] : []),\n    ...(\"years\" in v && v.years ? [`${v.years}y`] : []),\n    ...(\"months\" in v && v.months ? [`${v.months}mo`] : []),\n    ...(\"weeks\" in v && v.weeks ? [`${v.weeks}w`] : []),\n    ...(\"days\" in v && v.days ? [`${v.days}d`] : []),\n    ...(\"hours\" in v && v.hours ? [`${v.hours}h`] : []),\n    ...(\"minutes\" in v && v.minutes ? [`${v.minutes}min`] : []),\n    ...(\"seconds\" in v && v.seconds ? [`${v.seconds}s`] : []),\n    ...(\"milliseconds\" in v && v.milliseconds ? [`${v.milliseconds}ms`] : []),\n  ];\n\n  if (!res.length) {\n    return getKeys(v).map((k) => `${v[k]} ${k}`);\n  }\n\n  return res;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/demoScripts/createTables.ts",
    "content": "import { EXCLUDE_FROM_SCHEMA_WATCH } from \"@common/utils\";\nimport { tout } from \"../../../pages/ElectronSetup/ElectronSetup\";\nimport type { DemoScript, TypeAutoOpts } from \"../getDemoUtils\";\nimport { SQL_TESTING_SCRIPTS, type SqlTestingScripts } from \"./mainTestScripts\";\n\nexport const createTables: DemoScript = async ({\n  fromBeginning,\n  typeAuto,\n  runDbSQL,\n  getEditor,\n  moveCursor,\n  goToNextSnipPos,\n  newLine,\n  goToNextLine,\n  runSQL,\n  typeText,\n  testResult,\n  triggerParamHints,\n}) => {\n  const testEditorValue = (\n    key: keyof SqlTestingScripts | undefined,\n    expectedValue?: string,\n  ) => testResult(expectedValue ?? SQL_TESTING_SCRIPTS[key!]);\n  const createTable = async ({\n    tableName,\n    cols,\n  }: {\n    tableName: string;\n    cols: { text: string; opts?: TypeAutoOpts }[];\n  }) => {\n    const newCol = async (wait = 500) => {\n      await typeText(\",\");\n      newLine();\n      await tout(wait);\n    };\n    await typeAuto(\"cr\");\n    await typeAuto(\" ta\");\n    await typeAuto(tableName, { triggerMode: \"off\", nth: -1 });\n    goToNextSnipPos();\n\n    for (const [idx, col] of cols.entries()) {\n      await typeAuto(col.text, {\n        triggerMode: \"firstChar\",\n        msPerChar: 120,\n        ...col.opts,\n      });\n      if (idx < cols.length - 1) {\n        await newCol();\n        const { e } = getEditor();\n        if (\n          e\n            .getModel()\n            ?.getLineContent(e.getPosition()?.lineNumber as any)\n            .endsWith(\"   \")\n        ) {\n          moveCursor.left();\n        }\n      }\n    }\n\n    goToNextLine();\n    console.log(Date.now(), \"Created table: \", tableName);\n    await runSQL();\n    await tout(1000);\n  };\n\n  /**\n   * Reset schema\n   * Create example data\n   */\n  getEditor().e.setValue(\"\");\n  await runDbSQL(initScript);\n  getEditor().e.setValue(\"\");\n\n  await tout(2000);\n  /**\n   * Create table users\n   * columns are auto-completed\n   */\n  await createTable({\n    tableName: \"users\",\n    cols: [\n      { text: \"idugen\" },\n      { text: \"fir\" },\n      { text: \"last\", opts: { nth: 1 } },\n      { text: \"ema\" },\n      { text: \"cre\" },\n    ],\n  });\n  testEditorValue(\"createTable_users\");\n\n  /**\n   * Create table plans\n   */\n  fromBeginning();\n  await createTable({\n    tableName: \"plans\",\n    cols: [\n      { text: \"id TEXT pri\" },\n      { text: \"nam\" },\n      { text: \"pric\" },\n      { text: \"info js\", opts: { nth: 2 } },\n    ],\n  });\n  testEditorValue(\"createTable_plans\");\n\n  /**\n   * Create table subscriptions\n   * add referenced column\n   */\n  fromBeginning();\n  await createTable({\n    tableName: \"subscriptions\",\n    cols: [{ text: \"crea\" }, { text: \"pla\" }],\n  });\n  testEditorValue(\"createTable_subscriptions\");\n\n  /**\n   * Alter table users\n   * Add referenced column\n   */\n  fromBeginning();\n  await typeAuto(\"alt\", {});\n  await typeAuto(\" ta\", {});\n  await typeAuto(\" su\", {});\n  await typeAuto(\" \\naco\", {});\n  await typeAuto(\" use\", { nth: 2 });\n  await typeAuto(\";\", { nth: -1 });\n  testResult(\n    '\\nALTER TABLE subscriptions \\nADD COLUMN \"user_id\" UUID NOT NULL REFERENCES users;',\n  );\n  await runSQL();\n\n  /**\n   * Select join autocomplete\n   */\n  fromBeginning();\n  await typeAuto(\"sel\");\n  await typeAuto(\" \");\n  await typeAuto(\"\\n\");\n  await typeAuto(\" users u\", { dontAccept: true });\n  await typeAuto(\"\\nlef\");\n  await typeAuto(\" s\");\n  testEditorValue(\"selectJoin\");\n  await runSQL();\n\n  /**\n   * WITH and nested select\n   */\n  fromBeginning();\n  await typeAuto(\"WITH cte1 AS ()\", { nth: -1 });\n  moveCursor.left();\n  await typeAuto(\"\\nse\");\n  await typeAuto(\" \");\n  await typeAuto(\"\\n\"); // FROM\n  await typeAuto(\" ()\", { nth: -1 });\n  moveCursor.left();\n  await typeAuto(\"\\n\"); // SELECT\n  await typeAuto(\" \");\n  await typeAuto(\"\\n\"); // FROM\n  await typeAuto(\" geo\");\n  await typeAuto(\"\\nwh\");\n  await typeAuto(\" coord\");\n  await typeAuto(\" = 1\", { nth: -1 });\n  moveCursor.down();\n  await typeAuto(\" t\", { nth: -1 });\n  moveCursor.down();\n  await newLine();\n  await typeAuto(\"SELECT * fr\");\n  await typeAuto(\" cte1;\", { nth: -1 });\n  await newLine();\n  await typeAuto(\"SELECT st_point(1, 2)\", { nth: -1 });\n  await typeAuto(\"::geog\");\n  testEditorValue(\"nestedSelects\");\n\n  /**\n   * insert cols/vals autocomplete\n   */\n  fromBeginning();\n  await typeAuto(\"ins\");\n  await typeAuto(\" pl\");\n  await typeAuto(\" ()\", { nth: -1 });\n  moveCursor.left();\n  triggerParamHints();\n  await typeAuto(\"'basic', 'basic', 10, '{}'\", { msPerChar: 200, nth: -1 });\n  moveCursor.right();\n  moveCursor.right();\n  await typeAuto(\";\", { nth: -1 });\n  testEditorValue(\"insert\");\n  await runSQL();\n\n  const copyData = async () => {\n    /**\n     * Copy data to table\n     */\n    fromBeginning();\n    await typeAuto(\"cop\");\n    await typeAuto(\" p\");\n    await typeAuto(\" \");\n    newLine();\n    await typeAuto(\"f\");\n    await typeAuto(\" h\");\n    moveCursor.left();\n    await typeAuto(\"/\");\n    moveCursor.right();\n    await typeAuto(\" \");\n    await typeAuto(\"for\");\n    await typeAuto(\" c\");\n    await typeAuto(\", hea\");\n    await typeAuto(\", qu\");\n    await typeAuto(\" \", { nth: 1 });\n    moveCursor.right();\n    moveCursor.right();\n    await typeAuto(\";\", { nth: -1 });\n    // console.log({copy: e.getModel()?.getValue()})\n    testEditorValue(\"copy\");\n    await runSQL();\n  };\n  // Disabled because csv is missing in vm\n  // await copyData();\n};\n\nconst initScript =\n  [\"users\", \"plans\", \"subscriptions\", \"logs\", \"some_table\"]\n    .map((v) => `DROP TABLE IF EXISTS ${v} CASCADE;`)\n    .join(\"\") +\n  `\n/* ${EXCLUDE_FROM_SCHEMA_WATCH}  */\nDROP USER IF EXISTS user1;\nCREATE EXTENSION IF NOT EXISTS pgcrypto;\nCREATE EXTENSION IF NOT EXISTS postgis;\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\nCREATE TABLE logs(id BIGSERIAL, request JSONB, created_at TIMESTAMP NOT NULL DEFAULT NOW());\nINSERT INTO logs(request) VALUES($$ {\n    \"Host\": \"www.example.com\",\n    \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36\",\n    \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\",\n    \"Accept-Language\": \"en-US,en;q=0.5\",\n    \"Accept-Encoding\": \"gzip, deflate, br\",\n    \"Connection\": \"keep-alive\",\n    \"Referer\": \"https://www.google.com/\",\n    \"Cookie\": \"PHPSESSID=1234567890abcdef; _ga=GA1.2.1234567890.1234567890; _gid=GA1.2.1234567890.1234567890\",\n    \"Cache-Control\": \"max-age=0\",\n    \"Upgrade-Insecure-Requests\": \"1\",\n    \"If-Modified-Since\": \"Thu, 21 Oct 2021 12:00:00 GMT\",\n    \"If-None-Match\": \"etag1234567890\"\n  } $$), ($$\n\n    {\n      \"Host\": \"api.example.com\",\n      \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36\",\n      \"Accept\": \"application/json\",\n      \"Accept-Language\": \"en-US,en;q=0.5\",\n      \"Accept-Encoding\": \"gzip, deflate, br\",\n      \"Connection\": \"keep-alive\",\n      \"Authorization\": \"Bearer eyJhbGciOiJJHMINGyomsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMIJgDHMWAGIJKLKI>FtZSI6IkpvaG4gRG9lIiwUHUYGMMjM5MDIyfQ.SflKxwRJOYGRLMJHBGKJf36POk6yJV_adQssw5c\",\n      \"Cache-Control\": \"no-cache\",\n      \"Pragma\": \"no-cache\"\n    }\n    \n  $$);\n`;\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/demoScripts/mainTestScripts.ts",
    "content": "import { fixIndent } from \"../../../demo/scripts/sqlVideoDemo\";\nimport { tout } from \"../../../pages/ElectronSetup/ElectronSetup\";\nimport type { DemoScript } from \"../getDemoUtils\";\n\nexport const mainTestScripts: DemoScript = async ({\n  testResult,\n  fromBeginning,\n  typeAuto,\n  moveCursor,\n  typeText,\n  newLine,\n  runSQL,\n  triggerSuggest,\n  runDbSQL,\n  getEditor,\n  acceptSelectedSuggestion,\n}) => {\n  const testEditorValue = (\n    key: keyof typeof SQL_TESTING_SCRIPTS | undefined,\n    expectedValue?: string,\n  ) => testResult(expectedValue ?? SQL_TESTING_SCRIPTS[key!]);\n\n  /**\n   * Multiple cte\n   */\n  const multipleCtes = fixIndent(`\n    WITH cte1 AS (\n      SELECT *\n      FROM users\n    ), cte2 AS (\n      SELECT *\n      FROM orders\n    )\n    SELECT * \n    FROM`);\n  fromBeginning(false, multipleCtes);\n  await typeAuto(\" c2\");\n  testResult(multipleCtes + \" cte2\");\n\n  fromBeginning(false, multipleCtes);\n  await moveCursor.up(3);\n  await moveCursor.lineEnd();\n  await typeAuto(\"\\nw\");\n  await typeAuto(\" i\");\n  await tout(500);\n  testResult(multipleCtes.replace(\"FROM orders\", \"FROM orders\\n  WHERE id\"));\n\n  /** Cte joins */\n  fromBeginning(false, multipleCtes);\n  await typeAuto(\" c2\");\n  await typeAuto(\" c\", { nth: -1 });\n  await typeAuto(\"\\nj\");\n  await typeAuto(\" c1\");\n  await typeAuto(\" c1\", { nth: -1 });\n  await typeAuto(\"\\no\");\n  await typeAuto(\" ui\");\n  await typeAuto(\" \");\n  await typeAuto(\" c1.i\");\n  testResult(multipleCtes + ` cte2 c\\nJOIN cte1 c1\\nON c.user_id = c1.id`);\n\n  /**\n   * from subquery\n   */\n  const script = fixIndent(`\n    SELECT *\n    FROM (\n      SELECT *\n      FROM files\n    ) f\n    INNER JOIN my_table mt\n      ON mt.files_id = f.id\n    WHERE`);\n  fromBeginning(false, script);\n  await moveCursor.lineEnd();\n  await typeAuto(` f.n`);\n  await typeAuto(` `);\n  await typeAuto(` mt.n`);\n  testResult(`${script} f.name = mt.name`);\n\n  /** Jsonb Path */\n  await runDbSQL(`\n    INSERT INTO plans (id, name, price, info)\n    VALUES('premium', 'premium', 20, '{ \"info\": \"plan info\" }')\n    ON CONFLICT DO NOTHING\n  `);\n  fromBeginning(false, ``);\n  await typeAuto(`s`);\n  await typeAuto(` info`);\n  await moveCursor.lineStart();\n  await moveCursor.lineEnd();\n  await typeAuto(` `);\n  await moveCursor.down();\n  await moveCursor.lineEnd();\n  await newLine();\n  await typeAuto(`w`);\n  await typeAuto(` in`);\n  await typeAuto(` `);\n  newLine();\n  await typeAuto(`a`);\n  await typeAuto(` `);\n  await typeAuto(` `);\n  await typeAuto(` ''`);\n  await moveCursor.left(1);\n  await triggerSuggest();\n  await tout(500);\n  acceptSelectedSuggestion();\n  await testResult(\n    fixIndent(`\n    SELECT info ->>'info'\n    FROM plans\n    WHERE info ->>'info'\n    AND id = 'basic'\n    LIMIT 200\n  `),\n  );\n\n  const jsonB = async () => {\n    /**\n     * JSONB selector autocomplete\n     */\n    fromBeginning();\n    await typeAuto(`SEL`);\n    await typeAuto(` req`);\n    moveCursor.down();\n    // await typeAuto(`\\nF`);\n    // await typeAuto(` lo`);\n    await typeAuto(`\\nW`);\n    await typeAuto(` r`);\n    await typeAuto(\" \", { nth: 3 });\n    await typeAuto(\" is\", { nth: 1 });\n    await tout(1e3);\n    // console.log({jsonb: e.getModel()?.getValue()});\n    testEditorValue(\"jsonb\");\n  };\n\n  const createPolicy = async () => {\n    /**\n     * JSONB selector autocomplete\n     */\n    fromBeginning();\n    await typeAuto(`cr`);\n    await typeAuto(` pol`);\n    await typeAuto(` view_own_data`, { nth: -1, msPerChar: 10 });\n\n    // newLine();\n    await typeAuto(`\\no`);\n    await typeAuto(` use`);\n\n    await typeAuto(\"\\nf\");\n    await typeAuto(\" s\");\n\n    await typeAuto(\"\\nu\");\n    await typeAuto(\" id\");\n    await typeAuto(\"= curuse\");\n\n    moveCursor.lineEnd();\n    typeText(\";\");\n\n    newLine(2);\n    await typeAuto(`cr`);\n    await typeAuto(` use`);\n    await typeAuto(` user1;`, { nth: -1 });\n    await runSQL();\n\n    newLine(2);\n    await typeAuto(`gr`);\n    await typeAuto(` al`, { msPerChar: 100 });\n    await typeAuto(` `);\n    await typeAuto(`\\ntablesi`);\n    await typeAuto(` pu`);\n    await typeAuto(` `);\n    await typeAuto(` use`);\n    await typeAuto(`;`, { nth: -1 });\n    // console.log({createpolicy: e.getModel()?.getValue()})\n    testEditorValue(\"createpolicy\");\n  };\n\n  const schemaInspect = async () => {\n    fromBeginning();\n    typeText(\"?\");\n    await tout(100);\n    triggerSuggest();\n    await tout(100);\n    await typeAuto(\"ta\");\n    await typeAuto(\" use\");\n    triggerSuggest();\n    // console.log({schemainspect: e.getModel()?.getValue()});\n    testEditorValue(\"schemainspect\");\n    // typeAuto(\"?ta\");\n    // typeAuto(\" us\");\n\n    await tout(2100);\n  };\n\n  const disableTrigger = async () => {\n    fromBeginning();\n    await typeAuto(\"alt\");\n    await typeAuto(\" tab\");\n    await typeAuto(\" app\");\n    await typeAuto(\"\\ntrig\", { nth: 1 });\n    await typeAuto(\" \", { nth: -1 });\n    triggerSuggest();\n    await tout(1e3);\n  };\n\n  const realtime = async () => {\n    fromBeginning();\n    await typeAuto(`CREATE TABLE some_table(col_0 INTEGER);`, {\n      nth: -1,\n      msPerChar: 10,\n      triggerMode: \"off\",\n    });\n    await runSQL();\n    newLine(2);\n    await typeAuto(`--A column is created every second...`, {\n      nth: -1,\n      msPerChar: 50,\n      triggerMode: \"off\",\n    });\n    await tout(200);\n\n    await typeAuto(`\\nALTER TABLE some_table \\nALTER COLUMN `, {\n      nth: -1,\n      msPerChar: 10,\n      triggerMode: \"off\",\n    });\n    triggerSuggest();\n\n    let counter = 1;\n    while (counter <= 5) {\n      await runDbSQL(`ALTER TABLE some_table ADD COLUMN col_${counter} TEXT`);\n      counter++;\n      await tout(200);\n    }\n    await tout(3200);\n    if (!window.document.documentElement.innerText.includes(\"col_4\")) {\n      throw \"Realtime not working: col_4 not found\";\n    }\n  };\n  await createPolicy();\n  await schemaInspect();\n  await jsonB();\n  await disableTrigger();\n  await realtime();\n};\n\nexport const SQL_TESTING_SCRIPTS = {\n  createTable_users:\n    'CREATE TABLE users (\\n  id  UUID PRIMARY KEY DEFAULT gen_random_uuid(),\\n  first_name  VARCHAR(150) NOT NULL,\\n  last_name  VARCHAR(150) NOT NULL,\\n  email  VARCHAR(255) NOT NULL UNIQUE \\n   CONSTRAINT \"prevent case and whitespace duplicates\"\\n   CHECK (email = trim(lower(email))),\\n  created_at TIMESTAMP NOT NULL DEFAULT now() \\n);',\n  createTable_plans:\n    \"\\nCREATE TABLE plans (\\n  id TEXT PRIMARY KEY,\\n  name  VARCHAR(150) NOT NULL,\\n  price  DECIMAL(12,2) CHECK(price >= 0),\\n  info JSONB\\n);\",\n  createTable_subscriptions:\n    '\\nCREATE TABLE subscriptions (\\n  created_at TIMESTAMP NOT NULL DEFAULT now(),\\n  \"plan_id\" TEXT NOT NULL REFERENCES plans\\n);',\n  selectJoin:\n    \"\\nSELECT *\\nFROM users u\\nLEFT JOIN subscriptions s\\n  ON s.user_id = u.id\",\n  copy: `COPY plans ( id, name, price, info )\\nFROM '/home/plans.csv' ( FORMAT CSV, HEADER, QUOTE '''' );`,\n  insert:\n    \"\\nINSERT INTO plans (id, name, price, info)\\nVALUES ('basic', 'basic', 10, '{}');\",\n  createpolicy:\n    '\\nCREATE POLICY view_own_data\\nON users\\nFOR SELECT\\nUSING (id = \"current_user\"() );\\n\\nCREATE USER user1;\\n\\nGRANT ALL ON\\nALL TABLES IN SCHEMA public TO user1;',\n  schemainspect: \"\\n?table users\",\n  jsonb:\n    \"\\nSELECT request\\nFROM logs\\nWHERE request ->>'Authorization' IS NULL\\nLIMIT 200\",\n  nestedSelects:\n    \"\\nWITH cte1 AS (\\n  SELECT *\\n  FROM (\\n    SELECT *\\n    FROM geography_columns\\n    WHERE coord_dimension = 1\\n  ) t\\n)\\nSELECT * FROM cte1;\\nSELECT st_point(1, 2)::GEOGRAPHY\",\n};\nexport type SqlTestingScripts = typeof SQL_TESTING_SCRIPTS;\n\n// TODO \"prostgles-server: add implied joins (if a-b-c (where a-b and b-c use the same columns from b) then add/allow a-c)\";\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/demoScripts/testBugs.ts",
    "content": "import { click, waitForElement } from \"../../../demo/demoUtils\";\nimport { fixIndent } from \"../../../demo/scripts/sqlVideoDemo\";\nimport { tout } from \"../../../pages/ElectronSetup/ElectronSetup\";\nimport type { DemoScript } from \"../getDemoUtils\";\nimport { testSqlCharts } from \"./testSqlCharts\";\n\nexport const testBugs: DemoScript = async (args) => {\n  const {\n    typeAuto,\n    fromBeginning,\n    testResult,\n    getEditor,\n    moveCursor,\n    newLine,\n    triggerSuggest,\n    acceptSelectedSuggestion,\n    actions,\n    runDbSQL,\n    runSQL,\n  } = args;\n\n  const testIncompleteQuery = async () => {\n    fromBeginning();\n    await typeAuto(`\\nALTER TABLE my_ ALTER`, { nth: -1 });\n    await typeAuto(` `);\n    await moveCursor.left(13);\n    await typeAuto(`t`);\n    await testResult(\"ALTER TABLE my_table ALTER COLUMN\");\n  };\n  await testIncompleteQuery();\n\n  const nestedSubQueryInWith = fixIndent(`\n    WITH cols AS (\n        SELECT *\n        FROM information_schema.columns c\n      )\n    SELECT\n      t.table_schema,\n      t.table_name,\n      c.*\n    FROM information_schema.tables t\n    LEFT JOIN LATERAL (\n      SELECT\n        t.table_schema,\n        t.table_name,\n        jsonb_agg(to_jsonb(c.*) ORDER BY c.)\n      FROM\n        cols c\n      WHERE t.table_schema = c.table_schema AND t.table_name = c.table_name\n      GROUP BY 1, 2\n    ) c ON TRUE`);\n  fromBeginning(false, nestedSubQueryInWith);\n  await moveCursor.up(5);\n  await moveCursor.lineEnd();\n  await moveCursor.left();\n  await typeAuto(`pos`);\n  testResult(\n    nestedSubQueryInWith.replace(\"ORDER BY c.\", \"ORDER BY c.ordinal_position\"),\n  );\n\n  const alterTable3TknsBug = fixIndent`\n    ALTER TABLE pg_catalog.pg_transform`;\n  fromBeginning(false, alterTable3TknsBug);\n  await typeAuto(` dcs`);\n  await typeAuto(` `);\n  testResult(alterTable3TknsBug + \" DROP CONSTRAINT pg_transform_oid_index\");\n\n  const selectSubSelectBug = fixIndent(`\n    select\n      t.relname as table_name,\n      (SELECT FROM  i.relnamespace),\n    from\n      pg_class t,\n      pg_class i,\n      pg_index ix,\n      pg_attribute a\n    where\n      t.oid = ix.indrelid\n  `);\n  fromBeginning(false, selectSubSelectBug);\n  await moveCursor.pageUp();\n  await moveCursor.down(2);\n  await moveCursor.right(15);\n  await typeAuto(`pna`);\n  await testResult(\n    selectSubSelectBug.replace(\n      \"SELECT FROM  \",\n      \"SELECT FROM pg_catalog.pg_namespace \",\n    ),\n  );\n\n  /** Comma cross join + table alias repeating bug */\n  const crossJoinQuery = fixIndent(`\n    select *\n    from\n      pg_class t,\n      pg_class i,\n      pg_index ix,\n      pg_attribute a\n    where\n      t.oid = ix.indrelid\n      and t.relkind = 'r'\n      and ix.\n  `);\n  fromBeginning(false, crossJoinQuery);\n  await moveCursor.pageDown();\n  await moveCursor.lineEnd();\n  await typeAuto(`uni`);\n  await testResult(crossJoinQuery + \"indisunique\");\n\n  /** hackyFixOptionmatchOnWordStartOnly */\n  fromBeginning(false, crossJoinQuery + \"uni\");\n  await moveCursor.pageDown();\n  await moveCursor.lineEnd();\n  await typeAuto(``);\n  await testResult(crossJoinQuery + \"indisunique\");\n\n  const crossJoinQuery2 = crossJoinQuery.replace(\"and ix.\", \"and \");\n  fromBeginning(false, crossJoinQuery2);\n  await moveCursor.pageDown();\n  await moveCursor.lineEnd();\n  await typeAuto(`uniq`);\n  await testResult(crossJoinQuery2 + \"ix.indisunique\");\n\n  await testSqlCharts(args);\n\n  /** Schema prefix doubling bug */\n  const qPrefDoubling = fixIndent(`\n    DELETE FROM pg_catalog.pg_class pc\n    WHERE pc.rela`);\n  fromBeginning(false, qPrefDoubling);\n  moveCursor.lineEnd();\n  await typeAuto(`c`);\n  testResult(qPrefDoubling + \"cl\");\n\n  /** Public schema prefix */\n  await runDbSQL(\n    `CREATE TABLE IF NOT EXISTS my_p_table (id1 serial PRIMARY KEY, name1 text);`,\n  );\n  await tout(1e3);\n  const publicSchemaPrefix = \"SELECT * FROM public.my_p_table WHERE\";\n  for (const script of [\n    publicSchemaPrefix,\n    publicSchemaPrefix.replace(\"public\", \"PUBLIC\"),\n  ]) {\n    await fromBeginning(false, script);\n    await typeAuto(\" \");\n    await testResult(script + \" id1\");\n  }\n\n  /** Create index public schema prefix */\n  const publicSchemaPrefixIndex = \"CREATE INDEX ON public.my_p_table (  )\";\n  for (const script of [\n    publicSchemaPrefixIndex,\n    publicSchemaPrefixIndex.replace(\"public\", \"PUBLIC\"),\n  ]) {\n    await fromBeginning(false, script);\n    await moveCursor.left(2);\n    await typeAuto(\" \");\n    await testResult(script.replace(\")\", \"id1 )\"));\n  }\n  await runDbSQL(`DROP TABLE  my_p_table;`);\n\n  /** Alter/Drop column */\n  const alterTableQuery = \"ALTER TABLE pg_catalog.pg_class \";\n  await fromBeginning(false, alterTableQuery + \"DROP COLUMN\");\n  await typeAuto(\" nam\");\n  await testResult(alterTableQuery + \"DROP COLUMN relname\");\n  await fromBeginning(false, alterTableQuery + \"ALTER COLUMN\");\n  await typeAuto(\" nam\");\n  await typeAuto(\" drd\");\n  await testResult(alterTableQuery + \"ALTER COLUMN relname DROP DEFAULT\");\n\n  /** Timechart works with codeblocks */\n  await fromBeginning(false, \"SELECT now(), 3; \\n\\nselect 1\");\n  await moveCursor.down(3, 50);\n  await moveCursor.up(3, 50);\n  await tout(1e3);\n  await runSQL();\n  await click(\"AddChartMenu.Timechart\");\n  await tout(2e3);\n  const tchartNode = await waitForElement(\"W_TimeChart\", undefined, {\n    timeout: 2e3,\n  });\n  console.log(tchartNode);\n  const dataItems = (tchartNode as any)._renderedData as any[];\n  if (!dataItems.length) {\n    throw \"Timechart not working\";\n  }\n  await click(\"dashboard.window.closeChart\");\n\n  const lateralJoin = fixIndent(`\n    SELECT *\n    FROM pg_catalog.pg_class c\n    LEFT JOIN LATERAL (\n      SELECT *\n      FROM pg_catalog.pg_proc p\n      WHERE \n    ) tt\n      ON TRUE`);\n  fromBeginning(false, lateralJoin);\n  await moveCursor.up(2);\n  await moveCursor.lineEnd();\n  await typeAuto(`c.`);\n  testResult(lateralJoin.replace(`WHERE `, `WHERE c.oid`));\n\n  const withNestingBug = fixIndent(`\n    WITH cte1 AS (\n      SELECT 1 as he\n      FROM pg_catalog.pg_proc\n    )\n    SELECT *, lag(  ), max( ), first_value()\n    FROM cte1`);\n  const expectedFixed = withNestingBug\n    .replace(`max( )`, `max(  he)`)\n    .replace(`lag(  )`, `lag(   he) OVER()`)\n    .replace(`first_value()`, `first_value( he)`);\n  fromBeginning(false, withNestingBug);\n  await moveCursor.up();\n  await moveCursor.lineStart();\n  await moveCursor.right(16);\n  await typeAuto(` `);\n  await moveCursor.right();\n  await typeAuto(` `);\n  await moveCursor.lineEnd();\n  await moveCursor.left(1);\n  await typeAuto(` `);\n  await moveCursor.left(18);\n  await typeAuto(` `);\n  testResult(expectedFixed);\n\n  const namedValues = fixIndent(`\n    SELECT *\n    FROM (\n    values (1, 1, 'a'), (1, 9, 'a')\n    ) tbl (id, col123, \"name\")\n    ORDER BY`);\n  fromBeginning(false, namedValues);\n  await typeAuto(\" c\");\n  testResult(namedValues + \" tbl.col123\");\n\n  /** Funcs args in CTE */\n  const cteFuncArgQuery = fixIndent(`\n    WITH cte1 AS (\n      SELECT max()\n      FROM pg_catalog.pg_class\n    )`);\n  await fromBeginning(false, cteFuncArgQuery);\n  await moveCursor.up(2);\n  await moveCursor.lineEnd();\n  await moveCursor.left();\n  await typeAuto(` name`);\n  await testResult(cteFuncArgQuery.replace(\"max()\", \"max( relname)\"));\n\n  /** Test explain */\n  await fromBeginning(false, \"EXPLAIN SELECT * FROM\");\n  await typeAuto(\" class\");\n  await testResult(\"EXPLAIN SELECT * FROM pg_catalog.pg_class\");\n\n  await fromBeginning(false, \"EXPLAIN UPDATE\");\n  await typeAuto(\" class\");\n  await typeAuto(\" \");\n  await typeAuto(\" \");\n  await testResult(\"EXPLAIN UPDATE pg_catalog.pg_class SET oid\");\n\n  const sortTextBug = fixIndent(`\n    SELECT *\n    FROM pg_catalog.pg_class\n    ORDER BY`);\n  await fromBeginning(false, sortTextBug);\n  await typeAuto(\" name\");\n  await testResult(sortTextBug + \" relname\");\n\n  await fromBeginning(false, \"\");\n  await typeAuto(\"SELECT unne\");\n  testResult(\"SELECT unnest\");\n\n  const idxQ = \"CREATE INDEX myidx ON pg_catalog.pg_class (  oid )\";\n  await fromBeginning(false, \"\");\n  await typeAuto(\"cr\");\n  await typeAuto(\" in\");\n  await typeAuto(\" myidx\", { triggerMode: \"off\" });\n  await typeAuto(\" \");\n  await typeAuto(\" pgcla\");\n  await typeAuto(\" \", { nth: 1 });\n  await typeAuto(\" \");\n  await testResult(idxQ);\n  await moveCursor.lineEnd();\n  await typeAuto(\"\\n \");\n  await typeAuto(\" nam\");\n  await typeAuto(\"\\n whe\");\n  await typeAuto(\" reln\");\n  await typeAuto(\" \");\n  await testResult(idxQ + \"\\n INCLUDE (relname)\\n  WHERE relname =\");\n\n  const createViewWithOptions = fixIndent(`\n    CREATE OR REPLACE VIEW myview`);\n  await fromBeginning(false, createViewWithOptions);\n  await typeAuto(\" w\");\n  await typeAuto(\" \");\n  await typeAuto(\" \");\n  await typeAuto(\" \");\n  await typeAuto(\" \");\n  await testResult(\n    createViewWithOptions +\n      \" WITH (check_option =cascaded , security_barrier =false)\",\n  );\n\n  const funcsColliding = fixIndent(`\n    SELECT *\n    FROM pg_stat_user_indexes ui\n    WHERE `);\n  await fromBeginning(false, funcsColliding);\n  await typeAuto(` schem`);\n  await testResult(funcsColliding + \" ui.schemaname\");\n\n  const quotedSchemaBug = `\nDROP SCHEMA IF EXISTS \"MySchema\" CASCADE;\nCREATE SCHEMA \"MySchema\";\nCREATE FUNCTION \"MySchema\".\"MyFunction\" ()\n RETURNS VOID AS $$\nBEGIN\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TABLE \"MySchema\".\"MyTable\" (\n  \"MyColumn\" TEXT\n);\n  `;\n  await runDbSQL(quotedSchemaBug);\n  fromBeginning(false, \"\");\n  await tout(2500);\n  await typeAuto(`SELECT MyColu`);\n  await moveCursor.up(2);\n  await moveCursor.lineEnd();\n  await typeAuto(`, MyFunc`);\n  await testResult(\n    fixIndent(`\n    SELECT \"MyColumn\", \"MySchema\".\"MyFunction\"()\n    FROM \"MySchema\".\"MyTable\"\n    LIMIT 200`),\n  );\n\n  const alterQ = fixIndent(`\n      ALTER TABLE \"MySchema\".\"MyTable\"\n      ALTER COLUMN \"MyColu\"`);\n  fromBeginning(false, alterQ);\n  await tout(2500);\n  await moveCursor.lineEnd();\n  await moveCursor.left(1);\n  await triggerSuggest();\n  await tout(500);\n  acceptSelectedSuggestion();\n  await testResult(alterQ.replace(`\"MyColu\"`, `\"MyColumn\"`));\n\n  fromBeginning(false, `DROP SCHEMA`);\n  await typeAuto(` mys`);\n  testResult(`DROP SCHEMA \"MySchema\"`);\n\n  /** Ensure whitespace is kept, replacing quoted identifiers works as expected */\n  fromBeginning(false, `SELECT FROM \"MySchema\".\"MyTable\"`);\n  await moveCursor.lineStart();\n  await moveCursor.right(6);\n  await typeAuto(` `);\n  testResult(`SELECT \"MyColumn\" FROM \"MySchema\".\"MyTable\"`);\n\n  fromBeginning(false, `SELECT \"m\" FROM \"MySchema\".\"MyTable\"`);\n  await moveCursor.lineStart();\n  await moveCursor.right(9);\n  await typeAuto(`y`);\n  testResult(`SELECT \"MyColumn\" FROM \"MySchema\".\"MyTable\"`);\n\n  fromBeginning(false, `SELECT \"m\" FROM \"MySchema\".\"MyTable\"`);\n  await moveCursor.lineStart();\n  await moveCursor.right(9);\n  await typeAuto(`yf`);\n  testResult(`SELECT \"MySchema\".\"MyFunction\"() FROM \"MySchema\".\"MyTable\"`);\n\n  fromBeginning(false, `SELECT mycolum FROM \"MySchema\".\"MyTable\"`);\n  await moveCursor.lineStart();\n  await moveCursor.right(14);\n  await typeAuto(`n`);\n  testResult(`SELECT \"MyColumn\" FROM \"MySchema\".\"MyTable\"`);\n\n  await fromBeginning(false, \" CREATE INDEX myidx ON\");\n  await typeAuto(` myt`);\n  await typeAuto(` `, { nth: 1 });\n  await typeAuto(` `);\n  await moveCursor.lineEnd();\n  await typeAuto(` `);\n  await typeAuto(` `);\n  await testResult(\n    `CREATE INDEX myidx ON \"MySchema\".\"MyTable\" (  \"MyColumn\" ) INCLUDE (\"MyColumn\")`,\n  );\n  await runDbSQL(`DROP SCHEMA IF EXISTS \"MySchema\" CASCADE;`);\n\n  const codeBlockQueries = fixIndent(`\n    SELECT oid\n    FROM pg_catalog.pg_default_acl\n    LIMIT 200;\n\n    SELECT oid\n    FROM pg_catalog.pg_default_acl\n    LIMIT 200;\n    SELECT * FROM pg_catalog.pg_trigger;\n    SELECT * FROM information_schema.tables;\n  `);\n  const codeBlockQueryLines = codeBlockQueries.split(\"\\n\");\n  fromBeginning(false, codeBlockQueries);\n  await tout(500);\n  let cb = await actions.getCodeBlockValue();\n  testResult(codeBlockQueryLines.at(-1)!, cb);\n  await moveCursor.up();\n  cb = await actions.getCodeBlockValue();\n  testResult(codeBlockQueryLines.at(-2)!, cb);\n  await moveCursor.up();\n  cb = await actions.getCodeBlockValue();\n  testResult(codeBlockQueryLines.slice(3, 7).join(\"\\n\"), cb);\n  await moveCursor.up(4);\n  cb = await actions.getCodeBlockValue();\n  testResult(codeBlockQueryLines.slice(3, 7).join(\"\\n\"), cb);\n  await moveCursor.up();\n  cb = await actions.getCodeBlockValue();\n  testResult(codeBlockQueryLines.slice(0, 4).join(\"\\n\"), cb);\n\n  /** Update */\n  const updateQuery = fixIndent(`\n    UPDATE prostgles.app_triggers\n    SET`);\n  fromBeginning(false, updateQuery);\n  await typeAuto(` `);\n  await testResult(\n    fixIndent(`\n    ${updateQuery} app_id\n  `),\n  );\n\n  /** Jsonb Path from cte */\n  const cteQuery = `\n    WITH cte1 AS (\n      SELECT '{ \"a\": { \"b\": { \"c\": 222 } } }'::jsonb as j, 2 as z\n    )`;\n  fromBeginning(\n    false,\n    fixIndent(`\n    ${cteQuery}\n    SELECT\n    FROM cte1\n  `),\n  );\n  await moveCursor.up();\n  await moveCursor.lineEnd();\n  await typeAuto(` `);\n  await typeAuto(` `);\n  await typeAuto(` `);\n  await typeAuto(` `);\n  await moveCursor.down();\n  await newLine();\n  await typeAuto(`w`);\n  await typeAuto(` `);\n  await typeAuto(` `);\n  await typeAuto(` `);\n  await typeAuto(` `);\n  newLine();\n  await typeAuto(`a`);\n  await typeAuto(` z`);\n  await typeAuto(` `);\n  await typeAuto(` ''`);\n  await moveCursor.left();\n  triggerSuggest();\n  await tout(500);\n  acceptSelectedSuggestion();\n  await testResult(\n    fixIndent(`\n    ${cteQuery}\n    SELECT j ->'a' ->'b' ->>'c'\n    FROM cte1\n    WHERE j ->'a' ->'b' ->>'c'\n    AND z = '2'\n  `),\n  );\n\n  /** CTE */\n  const query = `\n  WITH cte1 AS (\n    SELECT * \n    FROM pg_catalog.pg_class\n  )\n  SELECT * \n  FROM cte1 c\n  JOIN information_schema.tables t\n  ON true\n  WHERE`;\n  fromBeginning(false, query);\n  await typeAuto(\" comm\");\n  await typeAuto(\" =\");\n  await typeAuto(\" relper\");\n  await typeAuto(\"\\nOR c.rk\");\n  await typeAuto(\" = t.it\");\n  testResult(\n    query + \" t.commit_action = c.relpersistence\\n  OR c.relkind = t.is_typed\",\n  );\n\n  /** CTE names */\n  const cteScriptCompressed = `WITH cte1 AS (\nSELECT 1 \nFROM (\nSELECT *\nFROM geography_columns\n) t\n)SELECT *`;\n  const cteScriptSpaced = `\n  WITH cte1 AS (\n    SELECT 1 \n    FROM (\n      SELECT *\n      FROM geography_columns\n    ) t\n  )\n  SELECT *`;\n  for (const cteScript of [cteScriptCompressed, cteScriptSpaced]) {\n    fromBeginning(false, cteScript);\n    await typeAuto(\" \");\n    await typeAuto(\" \");\n    await typeAuto(\" w\");\n    await typeAuto(\" \");\n    testResult(cteScript + ` FROM cte1 WHERE \"?column?\"`);\n  }\n\n  const testFunctionArgs = async () => {\n    fromBeginning();\n    await typeAuto(`SELECT quer`);\n    await typeAuto(`, lef`);\n    await typeAuto(`()`, { nth: -1 });\n    moveCursor.left();\n    await typeAuto(``);\n    moveCursor.right();\n    await typeAuto(`,current_sett`);\n    await typeAuto(`()`, { nth: -1 });\n    moveCursor.left();\n    await typeAuto(`allow_in`);\n    const expected = `SELECT query, left(application_name),current_setting('allow_in_place_tablespaces')\nFROM pg_catalog.pg_stat_activity\nLIMIT 200`;\n    testResult(expected);\n\n    fromBeginning();\n    const expect2 = `SELECT * \\nFROM pg_catalog.pg_stat_activity a\\nWH`;\n    await typeAuto(expect2, { msPerChar: 10 });\n    await typeAuto(` lef`);\n    await typeAuto(`()`, { nth: -1 });\n    moveCursor.left();\n    await typeAuto(`a.`);\n    moveCursor.right();\n    await typeAuto(` `);\n    moveCursor.right();\n    await typeAuto(` current_sett`);\n    await typeAuto(`()`, { nth: -1 });\n    moveCursor.left();\n    await typeAuto(`allow_in`);\n    testResult(\n      expect2 +\n        `ERE left(a.application_name) = current_setting('allow_in_place_tablespaces')`,\n    );\n  };\n  await testFunctionArgs();\n\n  /** Actions work */\n  fromBeginning();\n  await typeAuto(`ALTER TABLE`, { nth: -1 });\n  getEditor().e.trigger(\"demo\", \"select2CB\", {});\n  await tout(500);\n  await typeAuto(`sele`);\n  testResult(\"SELECT\");\n\n  /** ALTER TABLE table name with schema */\n  fromBeginning();\n  await typeAuto(`ALTER TABLE prostgles`, { nth: -1 });\n  await typeAuto(`.at`);\n  testResult(\"ALTER TABLE prostgles.app_triggers\");\n\n  /** Documentation not showing */\n  fromBeginning();\n  await typeAuto(`SEL`, { nth: -1, msPerChar: 10 });\n  await tout(1e3);\n  const isOk = document.body.innerText.includes(\"sql-select.html\");\n  if (!isOk) {\n    throw \"Documentation not found\";\n  }\n  const selectScript = `SELECT *\nFROM pg_catalog.pg_tables \nWHERE schemaname = 'public'`;\n\n  /** AND/OR after WHERE */\n  fromBeginning(false, selectScript);\n  await typeAuto(`\\na`);\n  testResult(selectScript + \"\\nAND\");\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/demoScripts/testMiscAndBugs.ts",
    "content": "import { tout } from \"../../../pages/ElectronSetup/ElectronSetup\";\nimport type { DemoScript } from \"../getDemoUtils\";\n\nexport const testMiscAndBugs: DemoScript = async ({\n  typeAuto,\n  fromBeginning,\n  runSQL,\n  sqlAction,\n  runDbSQL,\n  testResult,\n  moveCursor,\n}) => {\n  await runDbSQL(`CREATE EXTENSION IF NOT EXISTS postgis;`);\n\n  fromBeginning();\n  await typeAuto(\"set\");\n  await typeAuto(\" statement\");\n  await typeAuto(\" \");\n  await typeAuto(\" \");\n  await tout(1200);\n  testResult(\"SET statement_timeout TO '0ms'\");\n\n  fromBeginning();\n  await typeAuto(\"va\");\n  await typeAuto(\" \");\n  await typeAuto(\" \");\n  await moveCursor.right();\n  await typeAuto(\" pg_ag\");\n  await tout(1200);\n  testResult(\"VACUUM ( FULL) pg_catalog.pg_aggregate\");\n\n  const notificationText = \"hello from the other side\";\n  const checkText = async () => {\n    await tout(1e3);\n    if (!document.body.innerText.includes(notificationText)) {\n      alert(\"Notification not received\");\n    }\n  };\n  /** Test Notify */\n  fromBeginning();\n  await typeAuto(`LISTEN mychannel`, {\n    nth: -1,\n    msPerChar: 10,\n    triggerMode: \"off\",\n  });\n  await runSQL();\n\n  await runDbSQL(`NOTIFY mychannel, '${notificationText}'`);\n  await checkText();\n  await sqlAction(\"stop-listen\");\n\n  /** test One Packet Bug */\n  fromBeginning();\n  await typeAuto(`SELECT pg_sleep(1), '${notificationText}' as a`, {\n    nth: -1,\n    msPerChar: 10,\n    triggerMode: \"off\",\n  });\n  await runSQL();\n  await checkText();\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/demoScripts/testSqlCharts.ts",
    "content": "import { click, getElement, waitForElement } from \"../../../demo/demoUtils\";\nimport { fixIndent, shouldBeEqual } from \"../../../demo/scripts/sqlVideoDemo\";\nimport { tout } from \"../../../pages/ElectronSetup/ElectronSetup\";\nimport type { DemoScript } from \"../getDemoUtils\";\n\nexport const testSqlCharts: DemoScript = async ({\n  fromBeginning,\n  moveCursor,\n  runSQL,\n}) => {\n  const q0 = fixIndent(`\n    SELECT \n      generate_series(now(), now() + '1 year'::interval, '1day'::interval) as tstamp, \n      random() * 100 as value\n  `);\n  const q1 = fixIndent(`\n    WITH tbl as (\n      ${q0}\n    )\n    SELECT * FROM tbl\n  `);\n  fromBeginning(false, q1);\n\n  await click(\"AddChartMenu.Timechart\");\n\n  const l1 = await waitForElement<HTMLButtonElement>(\n    \"TimeChartLayerOptions.aggFunc\",\n    \"\",\n    { nth: 0 },\n  );\n  shouldBeEqual(\"Avg(\\nvalue\\n),\\ntstamp\", l1.innerText);\n\n  const timechartQueries = `${q1}\\n\\n\\n${q0.replace(\"100 as value\", \"10 as value2\")}`;\n  fromBeginning(false, timechartQueries);\n\n  const addChart = async (chartType: \"Timechart\" | \"Map\") => {\n    moveCursor.pageDown();\n    moveCursor.lineEnd();\n    moveCursor.up(1);\n    moveCursor.down(2);\n    await click(\n      chartType === \"Map\" ? \"AddChartMenu.Map\" : \"AddChartMenu.Timechart\",\n    );\n    await tout(500);\n  };\n  await addChart(\"Timechart\");\n\n  const checkL2 = async () => {\n    const l2 = await waitForElement<HTMLButtonElement>(\n      \"TimeChartLayerOptions.aggFunc\",\n      \"\",\n      { nth: 1 },\n    );\n    shouldBeEqual(\"Avg(\\nvalue2\\n),\\ntstamp\", l2.innerText);\n  };\n  await checkL2();\n\n  const getCloseChartBtn = () => {\n    return getElement<HTMLButtonElement>(\"dashboard.window.closeChart\");\n  };\n  shouldBeEqual(true, !!getCloseChartBtn());\n\n  /** Executing sql will hide charts */\n  const minimiseCharts = async () => {\n    await runSQL();\n    await tout(1e3);\n    shouldBeEqual(false, !!getCloseChartBtn());\n  };\n\n  /** Reopen chart by clicking add layer to chart button */\n  const restoreCharts = async () => {\n    moveCursor.pageDown();\n    // moveCursor.up(1);\n    await click(\"AddChartMenu.Timechart\");\n    await checkL2();\n  };\n  await minimiseCharts();\n  await restoreCharts();\n\n  /** Collapse chart button works */\n  await click(\"dashboard.window.collapseChart\");\n  await tout(500);\n  shouldBeEqual(false, !!getCloseChartBtn());\n\n  /** Restore charts button works */\n  await click(\"dashboard.window.restoreMinimisedCharts\");\n  await tout(500);\n  shouldBeEqual(true, !!getCloseChartBtn());\n\n  /** Map chart works */\n  const qMap0 = fixIndent(`\n    SELECT ST_SetSRID(ST_MakePoint(\n      (random() * 0.01) + -0.1276, \n      (random() * 0.01) +  51.5074\n    ), 4326) AS geom\n    FROM generate_series(1, 100) as pt\n  `);\n  const qMap = fixIndent(`\n    WITH london_center AS (\n     ${qMap0}\n    )\n    SELECT geom\n    FROM london_center\n  `);\n  fromBeginning(false, qMap);\n  await addChart(\"Map\");\n  await waitForElement(\"MapExtentBehavior\");\n\n  fromBeginning(false, qMap0);\n  await addChart(\"Map\");\n  await waitForElement(\"MapExtentBehavior\");\n\n  await click(\"dashboard.window.closeChart\");\n  await click(\"dashboard.window.closeChart\");\n\n  await tout(3e3);\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/getChartableSQL.ts",
    "content": "import type { PG_COLUMN_UDT_DATA_TYPE, SQLHandler } from \"prostgles-types\";\nimport type { CodeBlock } from \"../SQLEditor/SQLCompletion/completionUtils/getCodeBlock\";\nimport {\n  isDateCol,\n  isGeoCol,\n  type ChartColumn,\n  type ColInfo,\n} from \"../W_Table/TableMenu/getChartCols\";\nimport { getTableExpressionReturnType } from \"../SQLEditor/SQLCompletion/completionUtils/getQueryReturnType\";\nimport type { DBSchemaTableWJoins } from \"../Dashboard/dashboardUtils\";\n\nexport type ChartableSQL = {\n  /**\n   * Full with statement IF all cte's are selects\n   */\n  withStatement: string;\n  /**\n   * Select statement used to create the chart\n   * If with is defined then this should be the last select statement\n   */\n  sql: string;\n  columns: ColInfo[];\n  geoCols: ChartColumn[];\n  dateCols: ChartColumn[];\n  barCols: ChartColumn[];\n  text: string;\n};\n\nexport const getChartableSQL = async (\n  {\n    text,\n    tokens,\n    ftoken,\n    blockStartOffset,\n  }: Pick<CodeBlock, \"text\" | \"tokens\" | \"blockStartOffset\" | \"ftoken\">,\n  sqlHandler: SQLHandler,\n  tables: DBSchemaTableWJoins[],\n): Promise<ChartableSQL> => {\n  const emptyResult = {\n    text,\n    withStatement: \"\",\n    sql: \"\",\n    dateCols: [],\n    geoCols: [],\n    barCols: [],\n    columns: [],\n  };\n\n  const sql = cleanSql(text);\n  const { dateCols, geoCols, barCols, columns } = await getChartColsFromSql(\n    sql,\n    sqlHandler,\n    tables,\n  );\n  if (ftoken?.textLC === \"select\") {\n    return {\n      text,\n      withStatement: \"\",\n      sql,\n      dateCols,\n      barCols,\n      geoCols,\n      columns,\n    };\n  }\n\n  /**\n   * Exclude data modyfying statements\n   */\n  if (\n    ftoken?.textLC !== \"with\" ||\n    tokens.some((t) => t.textLC === \"returning\")\n  ) {\n    return emptyResult;\n  }\n\n  const firstSelectIndex = tokens.findIndex(\n    (t) => !t.nestingId && t.textLC === \"select\",\n  );\n  const firstSelectToken = tokens[firstSelectIndex]!;\n  const withStatement = text\n    .slice(0, firstSelectToken.offset - blockStartOffset)\n    .trim();\n  const lastSelectStatement = cleanSql(\n    text.slice(firstSelectToken.offset - blockStartOffset),\n  );\n\n  /** Check if correct */\n  const newCols = await getChartColsFromSql(\n    `\n    ${withStatement}\n    SELECT * \n    FROM (\n      ${lastSelectStatement}\n    ) prostgles_chartable_sql\n  `,\n    sqlHandler,\n    tables,\n  );\n\n  if (\n    newCols.columns.length !== columns.length ||\n    newCols.columns\n      .map((c) => c.name + c.udt_name)\n      .sort()\n      .join() !==\n      columns\n        .map((c) => c.name + c.udt_name)\n        .sort()\n        .join()\n  ) {\n    return emptyResult;\n  }\n\n  return {\n    text,\n    withStatement,\n    sql: lastSelectStatement,\n    dateCols,\n    geoCols,\n    barCols,\n    columns,\n  };\n};\n\nconst cleanSql = (text: string) => {\n  let sql = text.trim();\n  if (sql.endsWith(\";\")) sql = sql.slice(0, -1);\n  return sql;\n};\n\nconst getChartColsFromSql = async (\n  sql: string,\n  sqlHandler: SQLHandler,\n  tables: DBSchemaTableWJoins[],\n) => {\n  const trimmedSql = sql.trim();\n  const { colTypes = [] } = await getTableExpressionReturnType(\n    trimmedSql,\n    sqlHandler,\n    true,\n  );\n  const _allCols: ColInfo[] = colTypes.map((c) => ({\n    ...c,\n    name: c.column_name,\n    is_pkey:\n      Boolean(c.table_oid) &&\n      tables.some(\n        ({ info: { oid }, columns }) =>\n          oid === c.table_oid &&\n          columns.some((col) => col.is_pkey && col.name === c.column_name),\n      ),\n    udt_name: c.udt_name as PG_COLUMN_UDT_DATA_TYPE,\n  }));\n  const allCols: ChartColumn[] = _allCols.map((c) => ({\n    ...c,\n    type: \"normal\",\n    otherColumns: _allCols.filter((c) => !isGeoCol(c) && !isDateCol(c)),\n  }));\n\n  return {\n    sql: trimmedSql,\n    geoCols: allCols.filter((c) => isGeoCol(c)),\n    dateCols: allCols.filter((c) => isDateCol(c)),\n    barCols: allCols,\n    columns: allCols,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/getDemoUtils.ts",
    "content": "import { includes, type SQLHandler } from \"prostgles-types\";\nimport { getCommandElemSelector } from \"../../Testing\";\nimport { tout } from \"../../utils/utils\";\nimport type { WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { triggerCharacters } from \"../SQLEditor/SQLCompletion/monacoSQLSetup/registerSuggestions\";\n\nexport type TypeOpts = {\n  msPerChar?: number;\n  triggerMode?: \"off\" | \"firstChar\";\n  newLinePress?: boolean;\n};\nexport type TypeAutoOpts = {\n  /**\n   * Which suggestion to accept. -1 to not accept any\n   */\n  nth?: number;\n  /**\n   * If true then will not accept any suggestion. Used for demos where we want to cycle through suggestions\n   */\n  dontAccept?: boolean;\n  /**\n   * Wait after accepting and before resolving the promise\n   */\n  wait?: number;\n  /**\n   * Wait after triggering the suggestions\n   */\n  waitAccept?: number;\n  /**\n   * Wait before accepting the selected option\n   */\n  waitBeforeAccept?: number;\n  onEnd?: VoidFunction;\n} & TypeOpts;\n\nexport const runDbSQL: SQLHandler = async (...args: any[]) => {\n  try {\n    return await (window as any).db?.sql(...args);\n  } catch (e) {\n    console.error(e);\n    throw e;\n  }\n};\n\nexport type DemoUtils = ReturnType<typeof getDemoUtils>;\nexport type DemoScript = (utils: DemoUtils) => Promise<void>;\nexport const getDemoUtils = (w: Pick<WindowSyncItem<\"sql\">, \"id\">) => {\n  const getEditors = () => {\n    const editors = document.querySelectorAll<HTMLDivElement>(\n      `[data-box-id=${JSON.stringify(w.id)}] .ProstglesSQL`,\n    );\n    if (!editors.length) throw \"Editor not found\";\n    if (editors.length > 1) throw \"Multiple editors found\";\n    return Array.from(editors);\n  };\n  const getEditor = () => {\n    const editor = getEditors()[0];\n    if (!editor) throw \"Editor not found\";\n    const e = editor.sqlRef!.editor;\n    return { editor, e };\n  };\n\n  const getTriggerFor = (action: string) => {\n    return async (times = 1, delay = 20) => {\n      for (let i = times; i > 0; i--) {\n        getEditor().e.trigger(\"demo\", action, {});\n        await tout(delay);\n      }\n    };\n  };\n  const moveCursor = {\n    left: getTriggerFor(\"cursorLeft\"),\n    down: getTriggerFor(\"cursorDown\"),\n    right: getTriggerFor(\"cursorRight\"),\n    up: getTriggerFor(\"cursorUp\"),\n    lineEnd: getTriggerFor(\"cursorLineEnd\"),\n    lineStart: getTriggerFor(\"cursorLineStart\"),\n    pageUp: getTriggerFor(\"cursorPageUp\"),\n    pageDown: getTriggerFor(\"cursorPageDown\"),\n    setPosition: (line: number, column: number) =>\n      getEditor().e.setPosition({ lineNumber: line, column }),\n  };\n  const triggerSuggest = () => {\n    if (window.getSelection()?.toString()) {\n      return;\n    }\n    getEditor().e.trigger(\"demo\", \"editor.action.triggerSuggest\", {});\n  };\n  const triggerParamHints = () =>\n    getEditor().e.trigger(\"demo\", \"editor.action.triggerParameterHints\", {});\n  const acceptSelectedSuggestion = () =>\n    getEditor().e.trigger(\"demo\", \"acceptSelectedSuggestion\", {});\n\n  const newLine = (n = 1) => {\n    getEditor().e.trigger(\"\", \"editor.action.insertLineAfter\", {});\n    const remaining = n - 1;\n    if (remaining > 0) {\n      newLine(remaining);\n    }\n  };\n  const typeText = (\n    v: string,\n    onEnd?: (triggeredSuggest: boolean) => void,\n    opts?: TypeOpts,\n  ) => {\n    const { msPerChar = 80, triggerMode, newLinePress } = opts ?? {};\n    const chars = v.split(\"\");\n    let triggered = 0;\n    const press = async () => {\n      const char = chars.shift();\n      if (newLinePress && char === \"\\n\") {\n        // disabled cause it affects inline constraint indentation\n        newLine();\n      } else {\n        getEditor().e.trigger(\"keyboard\", \"type\", { text: char });\n        if (\n          (!triggered && !triggerMode && includes(triggerCharacters, char)) ||\n          (!triggered &&\n            triggerMode === \"firstChar\" &&\n            char?.match(/^[a-z0-9]+$/i))\n        ) {\n          triggered++;\n          // triggerSuggest();\n          await tout(500);\n        }\n      }\n\n      if (chars.length) {\n        setTimeout(press, msPerChar);\n      } else {\n        onEnd?.(triggered > 0);\n      }\n    };\n    press();\n  };\n\n  const typeAuto = (text: string, opts?: TypeAutoOpts) => {\n    const {\n      nth = 0,\n      wait = 0,\n      waitAccept = 600,\n      waitBeforeAccept = 0,\n      dontAccept,\n      onEnd,\n      ...typeOpts\n    } = opts ?? {};\n    return new Promise((resolve, reject) => {\n      typeText(\n        text,\n        (triggeredSuggest) => {\n          if (nth > -1 && !triggeredSuggest) {\n            triggerSuggest();\n          }\n          setTimeout(async () => {\n            if (nth > -1) {\n              for (let n = 0; n < nth; n++) {\n                await tout(100);\n                getEditor().e.trigger(\"demo\", \"selectNextSuggestion\", {});\n                await tout(500);\n              }\n              if (!dontAccept) {\n                if (waitBeforeAccept) await tout(waitBeforeAccept);\n                getEditor().e.trigger(\"demo\", \"acceptSelectedSuggestion\", {});\n              }\n            }\n            setTimeout(async () => {\n              if (wait) await tout(wait);\n              await onEnd?.();\n              resolve(1);\n            }, 10);\n          }, waitAccept);\n        },\n        typeOpts,\n      );\n    });\n  };\n\n  const goToNextSnipPos = () => {\n    //@ts-ignore\n    getEditor().e.getContribution(\"snippetController2\")?.next();\n  };\n  const goToNextLine = () => {\n    moveCursor.down();\n  };\n  const sqlAction = async (type: \"kill-query\" | \"stop-listen\" | \"run\") => {\n    await tout(50);\n    const selector =\n      type === \"stop-listen\" ? \"W_SQLBottomBar.stopListen\"\n      : type === \"kill-query\" ? \"W_SQLBottomBar.cancelQuery\"\n      : \"W_SQLBottomBar.runQuery\";\n    const button = getEditor().editor.querySelector<HTMLButtonElement>(\n      getCommandElemSelector(selector),\n    );\n    if (!button) throw type + \" button not found\";\n    button.click();\n    await tout(1300);\n  };\n  const runSQL = async () => sqlAction(\"run\");\n  const fromBeginning = (withNewline = true, text?: string) => {\n    const editorOpts = getEditor();\n    editorOpts.e.setValue(text ?? \"\");\n    if (text) {\n      moveCursor.pageDown();\n    }\n    if (withNewline) {\n      newLine();\n    }\n  };\n\n  const testResult = (expected: string, editorValue?: string): void => {\n    const model = getEditor().e.getModel();\n    const actual =\n      editorValue ?? model?.getValue().replaceAll(model.getEOL(), \"\\n\") ?? \"\";\n    if (!expected) {\n      throw \"empty expected value\";\n    }\n    if (!expected.includes(actual.trim())) {\n      const error = `Script\\n\\n${expected} \\n\\ndoes not match the editor value. Editor value: \\n\\n${actual.trim()}`;\n      console.error(\"Expected: \\n\", expected);\n      console.error(\"Actual: \\n\", actual);\n      confirm(error);\n      throw error;\n    }\n  };\n\n  const selectCodeBlock = () => getEditor().e.trigger(\"demo\", \"select2CB\", {});\n  const getCodeBlockValue = async () => {\n    await selectCodeBlock();\n    await tout(500);\n    const value = window.getSelection()?.toString();\n    return value;\n  };\n  const actions = {\n    selectCodeBlock,\n    getCodeBlockValue,\n  };\n\n  return {\n    runSQL,\n    fromBeginning,\n    typeText,\n    typeAuto,\n    goToNextSnipPos,\n    goToNextLine,\n    sqlAction,\n    runDbSQL,\n    triggerSuggest,\n    acceptSelectedSuggestion,\n    triggerParamHints,\n    newLine,\n    moveCursor,\n    triggerCharacters,\n    testResult,\n    getEditor,\n    getEditors,\n    actions,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/getSQLResultTableColumns.ts",
    "content": "import {\n  _PG_numbers,\n  includes,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport { onRenderColumn } from \"../W_Table/tableUtils/onRenderColumn\";\nimport type { W_SQLResultsProps } from \"./W_SQLResults\";\n\nexport const getSQLResultTableColumns = ({\n  cols = [],\n  tables,\n  onResize,\n  maxCharsPerCell,\n  rows = [],\n}: Pick<W_SQLResultsProps, \"cols\" | \"tables\" | \"onResize\" | \"rows\"> & {\n  maxCharsPerCell: number | undefined;\n}) => {\n  return cols.map((c, i) => {\n    const isNumeric = isNumericColumn(c);\n    return {\n      ...c,\n      key: i,\n      label: c.name,\n      filter: false,\n      /* Align numbers to right for an easier read */\n      headerClassname: isNumeric ? \" jc-end  \" : \" \",\n      className: isNumeric ? \" ta-right \" : \" \",\n      onRender: onRenderColumn({\n        c: { ...c, name: i.toString(), format: undefined },\n        getValues: () => rows.map((r) => r[i]),\n        table: undefined,\n        tables,\n        barchartVals: undefined,\n        maxCellChars: maxCharsPerCell || 1000,\n        maximumFractionDigits: 12,\n      }),\n      onResize: (width) => {\n        const newCols = cols.map((_c) => {\n          if (_c.key === c.key) {\n            _c.width = width;\n          }\n          return _c;\n        });\n        onResize(newCols);\n      },\n    };\n  });\n};\n\nexport const isNumericColumn = ({\n  tsDataType,\n  udt_name,\n}: Pick<ValidatedColumnInfo, \"tsDataType\" | \"udt_name\">): boolean => {\n  return tsDataType === \"number\" || includes(_PG_numbers, udt_name);\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/monacoEditorTypes.ts",
    "content": "export type Monaco = typeof import(\n  /* webpackChunkName: \"monaco-editor-api\" */ /*  webpackPrefetch: 98 */ \"monaco-editor/esm/vs/editor/editor.api\"\n);\nexport type {\n  IRange,\n  languages,\n  editor,\n  Uri,\n  IDisposable,\n  Position,\n  Token,\n  IMarkdownString,\n} from \"monaco-editor/esm/vs/editor/editor.api\";\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/parseExplainResult.ts",
    "content": "import type { W_SQLState } from \"./W_SQL\";\n\nexport const parseExplainResult = ({\n  rows = [],\n  cols = [],\n  activeQuery,\n}: Pick<W_SQLState, \"rows\" | \"cols\" | \"activeQuery\">): Pick<\n  W_SQLState,\n  \"rows\" | \"cols\"\n> => {\n  const isTextPlan = rows.some(\n    ([line]) => typeof line === \"string\" && line.includes(\"(cost=\"),\n  );\n  if (\n    activeQuery?.state === \"ended\" &&\n    activeQuery.info?.command === \"EXPLAIN\" &&\n    isTextPlan\n  ) {\n    const rowsWithInfo: {\n      actionInfo: string;\n      startupCost: number;\n      totalCost: number;\n      rows: number;\n      width: number;\n      plan: string;\n    }[] = [];\n    (rows as [string][]).forEach(([line]) => {\n      const actionInfo =\n        (line.trim().startsWith(\"->\") ?\n          line.split(\"->\")[1]?.split(\"(\")[0]?.trim()\n        : line.split(\"(cost=\")[0]?.trim()) ?? \"\";\n      const startupCost = Number(\n        line.split(\"(cost=\")[1]?.split(\"..\")[0] ??\n          rowsWithInfo.at(-1)?.startupCost ??\n          0,\n      );\n      const totalCost = Number(\n        line.split(\"(cost=\")[1]?.split(\"..\")[1]?.split(\" \")[0] ?? startupCost,\n      );\n      const rowsNum = Number(\n        line.split(\"rows=\")[1]?.split(\" \")[0] ?? startupCost,\n      );\n      const widthNum = Number(\n        line.split(\"width=\")[1]?.split(\")\")[0] ?? startupCost,\n      );\n\n      rowsWithInfo.push({\n        actionInfo,\n        startupCost,\n        totalCost,\n        rows: rowsNum,\n        width: widthNum,\n        plan: line,\n      });\n    });\n\n    const extraColumns = [\n      // \"Action\",\n      \"Startup Cost\",\n      \"Total Cost\",\n      // \"Rows\",\n      // \"Width\",\n    ];\n    return {\n      rows: rowsWithInfo.map((r) => [\n        // r.actionInfo,\n        r.startupCost,\n        r.totalCost,\n        // r.rows,\n        // r.width,\n        r.plan,\n      ]),\n      cols: [\n        ...extraColumns.map(\n          (name, idx) =>\n            ({\n              idx,\n              key: name,\n              name: name,\n              subLabel: \"\",\n              sortable: false,\n              udt_name: name.includes(\"Cost\") ? \"numeric\" : \"text\",\n              tsDataType: name.includes(\"Cost\") ? \"number\" : \"string\",\n            }) satisfies Required<W_SQLState>[\"cols\"][number],\n        ),\n        {\n          ...cols[0]!,\n          idx: extraColumns.length,\n        },\n      ],\n    };\n  }\n\n  return {\n    rows,\n    cols,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/parseSQLError.ts",
    "content": "import { getMonaco } from \"../SQLEditor/W_SQLEditor\";\nimport type { W_SQL } from \"./W_SQL\";\nimport { parseError } from \"./runSQL/runSQL\";\nimport { runSQLErrorHints } from \"./runSQLErrorHints\";\n\nexport const parseSQLError = async function (\n  this: W_SQL,\n  {\n    sql,\n    err: rawErr,\n    trimmedSql,\n  }: { sql: string; trimmedSql: string; err: any },\n) {\n  const { MarkerSeverity } = await getMonaco();\n  const {\n    prgl: { db },\n  } = this.props;\n  const err = parseError(rawErr);\n  let message: string = err?.message;\n  const hint = await runSQLErrorHints(\n    err,\n    this.props.suggestions?.suggestions,\n    db.sql!,\n    trimmedSql,\n  );\n  if (hint) {\n    message = hint;\n  } else {\n    const { hint, detail, where } = err || {};\n    message +=\n      (hint ? \" \\n \" + hint : \"\") +\n      (detail ? \" \\n \" + detail : \"\") +\n      (where ? \" \\n where: \" + where : \"\");\n  }\n\n  /** Ensure error starts from the correct offset due to added query id string */\n  const startOffset = (this.hashedSQL || \"\").indexOf(\n    sql.trimEnd().slice(0, -1),\n  );\n  const rawPosition = +err?.position || 0;\n  const position = Math.max(0, rawPosition - startOffset);\n  const maxLen = 1e5; //sql.length - position;\n  const length = Math.max(\n    maxLen,\n    err?.position ? +err?.length : sql.trimEnd().length,\n  );\n\n  return {\n    code: err?.code || \"\",\n    message,\n    severity: MarkerSeverity.Error,\n    position,\n    length,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/parseSqlResultCols.ts",
    "content": "import type { SQLResult } from \"prostgles-client/dist/prostgles\";\nimport { PALETTE } from \"../Dashboard/PALETTE\";\nimport { getColWidth } from \"../W_Table/tableUtils/getColWidth\";\nimport type { W_SQL } from \"./W_SQL\";\n\nexport const parseSqlResultCols = function (\n  this: W_SQL,\n  {\n    fields,\n    isSelect,\n    rows,\n    sql,\n    trimmedSql,\n  }: {\n    rows: string[][];\n    fields: SQLResult<\"stream\">[\"fields\"];\n    isSelect: boolean;\n    trimmedSql: string;\n    sql: string;\n  },\n) {\n  const w = this.d.w;\n  if (!w) return;\n  const _cols = getFieldsWithActions(fields, isSelect);\n  const keyedRows = rows.map((r) =>\n    r.reduce((a, v, i) => ({ ...a, [i]: v }), {}),\n  );\n  const colsWithWidth =\n    !_cols.length ?\n      []\n    : getColWidth(\n        _cols,\n        keyedRows,\n        \"idx\",\n        this.ref?.getBoundingClientRect().width,\n      );\n  const cols = colsWithWidth;\n  w.$update(\n    { options: { sqlResultCols: cols, lastSQL: isSelect ? trimmedSql : \"\" } },\n    { deepMerge: true },\n  );\n\n  const geoCols = cols.filter((c) => c.udt_name.startsWith(\"geo\"));\n  if (geoCols.length) {\n    this.props.myLinks.forEach((l) => {\n      if (!l.closed && !l.deleted && l.options.type === \"map\") {\n        const newCols = l.options.columns\n          .slice(0)\n          .filter((c) => geoCols.some((nc) => nc.name === c.name));\n        geoCols.forEach((c) => {\n          if (!newCols.some((nc) => nc.name === c.name)) {\n            newCols.push({\n              name: c.name,\n              colorArr: PALETTE.c1.getDeckRGBA(),\n            });\n          }\n        });\n\n        l.$update({\n          options: {\n            ...l.options,\n            columns: newCols,\n            dataSource: {\n              type: \"sql\",\n              sql,\n              withStatement: \"\",\n            },\n          },\n        });\n      }\n    });\n  }\n  return cols;\n};\n\nexport const getFieldsWithActions = (\n  fields: SQLResult<\"stream\">[\"fields\"],\n  isSelect: boolean,\n) =>\n  fields.map((f, idx) => ({\n    ...f,\n    idx,\n    key: f.name,\n    label: f.name,\n    subLabel: f.dataType,\n    sortable: isSelect && ![\"xml\", \"json\"].includes(f.dataType),\n  }));\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/runSQL/getQueryTotalRowCount.ts",
    "content": "import type { SQLHandler } from \"prostgles-types\";\nimport type { W_SQL_ActiveQuery } from \"../W_SQL\";\n\nexport const getQueryTotalRowCount = async (\n  sql: SQLHandler,\n  query: Extract<W_SQL_ActiveQuery, { state: \"ended\" }>,\n  limit: number | string | null,\n  isSelect: boolean,\n) => {\n  if (\n    (query.info?.command !== \"SELECT\" && !isSelect) ||\n    !limit ||\n    query.rowCount < Number(limit)\n  ) {\n    return undefined;\n  }\n  const { rows } = await sql(\n    ` \n      SET LOCAL statement_timeout TO 2000;\n      SELECT count(*) as count \n      FROM (\n        ${query.trimmedSql}\n      ) t; \n    `,\n    {},\n    { returnType: \"default-with-rollback\" },\n  ).catch((e) => {\n    console.error(\"Failed to get total count\", e);\n    return { rows: [] };\n  });\n\n  const count = rows[0]?.count;\n  const countNum = Number(count);\n  if (Number.isFinite(countNum)) {\n    return countNum;\n  }\n  return undefined;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/runSQL/runSQL.ts",
    "content": "import type { SQLResult } from \"prostgles-client/dist/prostgles\";\nimport type { SocketSQLStreamHandlers, SQLResultInfo } from \"prostgles-types\";\nimport type { WindowData } from \"../../Dashboard/dashboardUtils\";\nimport { STARTING_KEYWORDS } from \"../../SQLEditor/SQLCompletion/CommonMatchImports\";\nimport type { ColumnSortSQL } from \"../../W_Table/ColumnMenu/ColumnMenu\";\nimport type { W_SQL, W_SQLState } from \"../W_SQL\";\nimport { SQL_NOT_ALLOWED } from \"../W_SQL\";\nimport { parseExplainResult } from \"../parseExplainResult\";\nimport { parseSQLError } from \"../parseSQLError\";\nimport {\n  getFieldsWithActions,\n  parseSqlResultCols,\n} from \"../parseSqlResultCols\";\nimport { getQueryTotalRowCount } from \"./getQueryTotalRowCount\";\n\nexport async function runSQL(this: W_SQL, sort: ColumnSortSQL[] = []) {\n  const { activeQuery } = this.state;\n  if (activeQuery?.state === \"running\") {\n    alert(\"Must stop active query first\");\n    return;\n  }\n\n  const selected_sql =\n    this.sqlRef?.getSelectedText() ||\n    (await this.sqlRef?.getCurrentCodeBlock())?.text;\n  const sql = selected_sql || this.d.w?.sql || \"\";\n  const { db } = this.props.prgl;\n  const w = this.d.w;\n  if (w && w.selected_sql !== selected_sql) {\n    w.$update({ selected_sql });\n  }\n\n  if (!w || !db.sql) {\n    this.setState({\n      error:\n        !db.sql ? SQL_NOT_ALLOWED : (\n          \"Internal error (w is missing). Try refreshing the page\"\n        ),\n    });\n    return;\n  }\n\n  this.props.childWindows.forEach((cw) => {\n    if (!cw.minimised && (cw.type === \"map\" || cw.type === \"timechart\")) {\n      cw.$update({ minimised: true });\n    }\n  });\n\n  let trimmedSql = sql.trimEnd();\n\n  let isSelect: boolean | undefined;\n\n  const _query = `${sql}`\n    .trim()\n    .toLowerCase()\n    .split(\"\\n\")\n    .filter(\n      (v, i) => i || !(v.trim().startsWith(\"/*\") && v.trim().endsWith(\"*/\")),\n    ) /* Ignore psql top comments */\n    .map((v) => v.split(\"--\")[0])\n    .join(\"\\n\")\n    .trim();\n  const knownCommands = STARTING_KEYWORDS.map((k) => k.toLowerCase());\n  const firstCommand = knownCommands.find(\n    (c) => _query === c || _query.startsWith(c),\n  );\n  if (firstCommand) {\n    isSelect =\n      firstCommand === \"select\" &&\n      !_query.slice(0, -1).includes(\";\") &&\n      !_query.replaceAll(\"\\n\", \" \").includes(\" into \");\n  }\n\n  if (isSelect && trimmedSql.endsWith(\";\"))\n    trimmedSql = trimmedSql.slice(0, -1);\n\n  this.hashedSQL = sql;\n\n  let notifEventSub: W_SQLState[\"notifEventSub\"];\n\n  this.state.notifEventSub?.removeListener();\n\n  try {\n    /* Show table automatically after running a query */\n    const o: WindowData<\"sql\">[\"options\"] = w.options;\n\n    if (o.hideTable) w.$update({ options: { ...o, hideTable: false } });\n\n    const limit = w.limit === null ? null : w.limit || 100;\n\n    let sqlSorted = trimmedSql;\n    if (isSelect && sort.length) {\n      try {\n        /**\n         * Test for errors first to ensure the wrapped LIMIT/ORDER BY does not introduce unrelated errors\n         *  TODO: Better option to check for errors BUT need to Ensure the query ends with a \";\"\n         *    this.hashedSQL = `DO $SYNTAX_CHECK$ BEGIN RETURN; \\n ${_sqlLimited} \\nEND; $SYNTAX_CHECK$;`\n         */\n        this.hashedSQL = `EXPLAIN ${sqlSorted}`;\n        await db.sql(this.hashedSQL);\n        this.hashedSQL = ` SELECT * FROM (\\n ${sqlSorted} \\n ) t LIMIT 0`;\n        const { fields } = await db.sql(\n          this.hashedSQL,\n          {},\n          { returnType: \"default-with-rollback\" },\n        );\n\n        const orderBy =\n          sort\n            .filter((s) => fields[s.key])\n            .map(\n              (s) =>\n                `${(s.key as number) + 1}` +\n                ([1, true].includes(s.asc as any) ? \" ASC \" : \" DESC \"),\n            )\n            .join(\", \") || \" TRUE::BOOLEAN \";\n\n        await db.sql(\n          ` SELECT * FROM (\\n ${sqlSorted} \\n ) t ORDER BY ${orderBy} LIMIT 0`,\n        );\n        sqlSorted = ` SELECT * FROM (\\n ${sqlSorted} \\n ) t ORDER BY ${orderBy}`;\n      } catch (error) {\n        sqlSorted = trimmedSql;\n        w.$update({ sort: [] });\n      }\n    }\n    this._queryHashAlias =\n      `--prostgles-` + hashFnv32a(sqlSorted + Date.now(), true);\n    this.hashedSQL = this._queryHashAlias + \" \\n\" + sqlSorted;\n    let rowCount: number | undefined;\n    let totalRowCount: number | undefined;\n    const hashedSQL = this.hashedSQL;\n    const setRunningQuery = (extra?: {\n      handler: SocketSQLStreamHandlers | undefined;\n    }) => {\n      this.streamData.set({ rows: [] });\n      this.setState({\n        rows: undefined,\n        cols: undefined,\n        sqlResult: false,\n        page: 0,\n        ...extra,\n        sort: sqlSorted !== trimmedSql ? sort : [],\n        activeQuery: {\n          pid: extra?.handler?.pid ?? this.state.handler?.pid ?? -1,\n          state: \"running\",\n          trimmedSql,\n          started: new Date(),\n          hashedSQL,\n        },\n      });\n    };\n\n    if (this.state.handler) {\n      setRunningQuery();\n      await this.state.handler.run(hashedSQL);\n      return;\n    }\n\n    let fields: SQLResult<\"stream\">[\"fields\"] | undefined = undefined;\n    let info: SQLResultInfo | undefined = undefined;\n    let rows: any[] = [];\n    const stream = await db.sql(hashedSQL, undefined, {\n      returnType: \"stream\",\n      persistStreamConnection: true,\n      hasParams: false,\n      streamLimit: limit || undefined,\n    });\n    const handler = await stream.start(async (packet) => {\n      const runningQuery =\n        this.state.activeQuery?.state === \"running\" ?\n          this.state.activeQuery\n        : undefined;\n      if (!runningQuery && packet.type !== \"error\") {\n        if (w.$get()?.closed) {\n          handler.stop();\n        } else {\n          console.error(this.state.activeQuery, sql, packet);\n          alert(\n            \"Something went wrong: No running query found but received data packet\",\n          );\n        }\n        return;\n      }\n      const defaultRunningQuery = {\n        trimmedSql: \"\",\n        state: \"running\",\n        started: new Date(),\n        hashedSQL: \"\",\n      };\n      const { trimmedSql, hashedSQL } = runningQuery ?? defaultRunningQuery;\n      if (packet.type === \"error\") {\n        void this.state.handler?.stop();\n        const sqlError = await parseSQLError.bind(this)({\n          sql: trimmedSql,\n          err: packet.error,\n          trimmedSql,\n        });\n        this.setState({\n          handler: undefined,\n          queryEnded: Date.now(),\n          activeQuery: {\n            pid: handler.pid,\n            ...(runningQuery ?? defaultRunningQuery),\n            state: \"error\",\n            error: sqlError,\n            ended: new Date(),\n          },\n        });\n        w.$update({ options: { sqlResultCols: [] } }, { deepMerge: true });\n      } else {\n        rowCount ??= 0;\n        rowCount += packet.rows.length;\n        if (packet.info) info = packet.info;\n        rows.push(...packet.rows);\n        this.streamData.set({ rows });\n\n        /**\n         * First and last packets contain fields and info.command\n         */\n        if (packet.fields) fields = packet.fields;\n        if (fields || packet.ended) {\n          let cols: typeof this.state.cols = this.state.cols;\n\n          /* For WITH command must wait for response command to work out if it's a SELECT  */\n          isSelect = isSelect || packet.info?.command === \"SELECT\";\n          if (packet.info?.command) {\n            const commandResultIsSelect = packet.info.command === \"SELECT\";\n            if (isSelect !== commandResultIsSelect) {\n              isSelect = commandResultIsSelect;\n              w.$update(\n                { options: { lastSQL: isSelect ? trimmedSql : \"\" } },\n                { deepMerge: true },\n              );\n            }\n          }\n\n          if (fields) {\n            cols = parseSqlResultCols.bind(this)({\n              fields,\n              isSelect,\n              rows: packet.rows,\n              sql,\n              trimmedSql,\n            });\n          }\n\n          this.setState({\n            rows,\n            ...(cols ? { cols } : {}),\n            loading: false,\n            onRowClick: null,\n            sql,\n            isSelect,\n            notifEventSub,\n            sqlResult: true,\n          });\n          if (packet.ended) {\n            if (packet.info?.command === \"LISTEN\") {\n              const sqlRes = await db.sql!(hashedSQL, undefined, {\n                returnType: \"arrayMode\",\n                allowListen: true,\n                hasParams: false,\n              });\n\n              if (\"addListener\" in sqlRes) {\n                const fieldType = {\n                  dataType: \"json\",\n                  udt_name: \"json\",\n                  tsDataType: \"any\",\n                } as const;\n\n                this.setState({\n                  cols: getFieldsWithActions(\n                    [\n                      { ...fieldType, name: \"payload\" },\n                      { ...fieldType, name: \"received\" },\n                    ],\n                    isSelect,\n                  ),\n                  rows: [],\n                  activeQuery: undefined,\n                  notifEventSub: await sqlRes.addListener((ev) => {\n                    console.log(ev);\n                    return this.notifEventListener(ev);\n                  }),\n                });\n                return;\n              }\n            }\n\n            const queriesNotPickedUpBySchemaWatchEventTrigger = [\n              \"create database \",\n              \"drop database \",\n              \"alter database \",\n              \"drop database if exists \",\n              \"create user \",\n              \"drop user \",\n              \"alter user \",\n              \"drop user if exists \",\n              \"create user if exists \",\n              \"alter user if exists \",\n            ];\n            if (\n              queriesNotPickedUpBySchemaWatchEventTrigger.some((query) =>\n                trimmedSql.toLowerCase().replace(/\\s\\s+/g, \" \").includes(query),\n              )\n            ) {\n              this.props.suggestions?.onRenew();\n            }\n\n            let commandResult = \"\";\n            if (!fields?.length && packet.info) {\n              commandResult =\n                `${packet.info.command} command finished successfully! ` +\n                (Number.isFinite(packet.info.rowCount) ?\n                  ` ${packet.info.rowCount || 0} rows affected`\n                : \"\");\n            }\n\n            const activeQuery = {\n              pid: handler.pid,\n              ...(runningQuery ?? defaultRunningQuery),\n              state: \"ended\",\n              ended: new Date(),\n              commandResult,\n              rowCount,\n              totalRowCount,\n              info: packet.info,\n            } satisfies Required<W_SQLState>[\"activeQuery\"];\n            const newColsAndRows = parseExplainResult({\n              rows,\n              cols,\n              activeQuery,\n            });\n\n            if (limit && rowCount && limit > rowCount) {\n              activeQuery.totalRowCount = rowCount;\n            } else {\n              void getQueryTotalRowCount(\n                db.sql!,\n                activeQuery,\n                limit,\n                isSelect,\n              ).then((fetchedTotalRowCount) => {\n                if (\n                  isFinite(fetchedTotalRowCount) &&\n                  activeQuery.hashedSQL === this.state.activeQuery?.hashedSQL &&\n                  this.state.activeQuery.state === \"ended\"\n                ) {\n                  this.setState({\n                    activeQuery: {\n                      ...this.state.activeQuery,\n                      totalRowCount: fetchedTotalRowCount,\n                    },\n                  });\n                }\n              });\n            }\n\n            this.setState({\n              queryEnded: Date.now(),\n              activeQuery,\n              ...newColsAndRows,\n            });\n\n            rowCount = undefined;\n            fields = undefined;\n            info = undefined;\n            rows = [];\n          }\n        }\n      }\n    });\n    setRunningQuery({ handler });\n  } catch (err: any) {\n    const started = this.state.activeQuery?.started || new Date();\n    void this.state.handler?.stop();\n    this.setState({\n      isSelect: false,\n      notifEventSub: undefined,\n      activeQuery: {\n        pid: undefined,\n        started,\n        hashedSQL: this.hashedSQL,\n        trimmedSql,\n        state: \"error\",\n        ended: new Date(),\n        error: await parseSQLError.bind(this)({ sql, err, trimmedSql }),\n      },\n      rows: [],\n      cols: [],\n      handler: undefined,\n      sort: [],\n      queryEnded: Date.now(),\n      sqlResult: false,\n    });\n    w.$update({ options: { sqlResultCols: [] } }, { deepMerge: true });\n  }\n}\n\nexport const parseError = (err: any) => {\n  if (typeof err === \"string\") {\n    return { message: err };\n  }\n\n  return err;\n};\n\nfunction hashFnv32a(str, asString, seed?) {\n  /*jshint bitwise:false */\n  let i,\n    l,\n    hval = seed === undefined ? 0x811c9dc5 : seed;\n\n  for (i = 0, l = str.length; i < l; i++) {\n    hval ^= str.charCodeAt(i);\n    hval +=\n      (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);\n  }\n  if (asString) {\n    // Convert to 8 digit hex string\n    return (\"0000000\" + (hval >>> 0).toString(16)).substr(-8);\n  }\n  return hval >>> 0;\n}\n\nconst isFinite = (v: any): v is number => Number.isFinite(v);\n"
  },
  {
    "path": "client/src/dashboard/W_SQL/runSQLErrorHints.ts",
    "content": "import type { SQLHandler } from \"prostgles-types\";\nimport type { SQLSuggestion } from \"../SQLEditor/W_SQLEditor\";\nimport { isObject } from \"@common/publishUtils\";\nimport { parseError } from \"./runSQL/runSQL\";\n\nexport const runSQLErrorHints = async (\n  rawErr: any,\n  suggestions: SQLSuggestion[] | undefined,\n  sql: SQLHandler,\n  query: string,\n): Promise<string | undefined> => {\n  const err = parseError(rawErr);\n  const message: string =\n    isObject(err) ? err.message?.toLowerCase?.() : undefined;\n  try {\n    const where: string | undefined = err?.message;\n    let hint;\n    if (typeof message !== \"string\") {\n    } else if (\n      message.endsWith(\" is not a function\") &&\n      query.toLowerCase().includes(\"drop function\")\n    ) {\n      const funcName = message.split(\"()\")[0];\n      const [funcInfo] = await sql(\n        `SELECT routine_name, routine_type FROM information_schema.routines WHERE routine_name = \\${funcName}`,\n        { funcName },\n        { returnType: \"rows\" },\n      );\n      if (funcInfo) {\n        hint = `Hint: DROP ${funcInfo.routine_type} ${funcInfo.routine_name}();`;\n      }\n    } else if (message.startsWith(\"error: permission denied for schema \")) {\n      const schemaName = message.split(\"for schema \")[1] ?? \"schema_name\";\n      const currentPGUser =\n        (await sql(`SELECT \"current_user\"()`, {}, { returnType: \"value\" })) ??\n        \"this_user\";\n      hint = `Hint: Must grant usage on schema from a more privileged account: \\nGRANT USAGE ON SCHEMA ${schemaName} TO ${currentPGUser}`;\n    } else if (\n      where?.startsWith(\"PL/pgSQL function \") &&\n      where.endsWith(\"at RAISE\")\n    ) {\n      const funcStartKwd = \" function \";\n      const funcName = where.slice(\n        where.indexOf(funcStartKwd) + funcStartKwd.length,\n        where.indexOf(\"(\"),\n      );\n      const funcS = suggestions?.find(\n        (s) => s.funcInfo && s.funcInfo.escaped_identifier.includes(funcName),\n      );\n      if (funcS?.funcInfo?.definition) {\n        const lineNumKwd = \" line \";\n        const lineNumber = +where.slice(\n          where.lastIndexOf(lineNumKwd) + lineNumKwd.length,\n          where.lastIndexOf(\" at RAISE\"),\n        );\n        if (Number.isInteger(lineNumber)) {\n          return (\n            message +\n            \"\\n\\n\" +\n            where +\n            \":\\n\\n\" +\n            funcS.funcInfo.definition\n              .split(\"\\n\")\n              .map((l, i) => `L${i + 1}     ${l}`)\n              .slice(Math.max(0, lineNumber - 5), lineNumber + 5)\n              .join(\"\\n\")\n          );\n        }\n      }\n    } else if (message === \"error: syntax error at end of input\") {\n      hint = \"Hint: More commands expected at end of query\";\n    } else if (\n      message.includes(`error: cannot drop the currently open database`) ||\n      (message.startsWith(`error: database \"`) &&\n        message.includes(\"is being accessed by other users\") &&\n        query.trim().toLowerCase().startsWith(\"drop database\"))\n    ) {\n      try {\n        const { server_version } = (await sql(\n          \"SHOW server_version;\",\n          {},\n          { returnType: \"row\" },\n        )) as any;\n        if (server_version >= \"13\" || server_version.startsWith(\"13\")) {\n          hint =\n            'Hint: Connect to a different database and run the command with \"WITH (FORCE);\"';\n        } else {\n          let isSameCon = true;\n          try {\n            const { current_database } = (await sql(\n              \"SELECT current_database() as current_database\",\n              {},\n              { returnType: \"row\" },\n            )) as any;\n            isSameCon = query.includes(current_database);\n          } catch (e) {}\n          hint =\n            \"Hint: Must disconnect all users first. \" +\n            (isSameCon ?\n              \"Run the following queries from another connection (this one will be closed). \"\n            : \"\") +\n            \"Replace 'mydb' as required: \\n\\nALTER DATABASE mydb CONNECTION LIMIT 0; \\n SELECT pg_terminate_backend(pid) \\nFROM pg_stat_activity WHERE datname = 'mydb'; \\n DROP DATABASE mydb;\";\n        }\n      } catch (e) {}\n    } else if (\n      message.startsWith(\"error: function\") &&\n      message.endsWith(\"does not exist\") &&\n      err.code === \"42883\" &&\n      suggestions\n    ) {\n      const errFuncDef = message.slice(16, -15);\n      const errFuncName = errFuncDef.split(\"(\")[0];\n      if (errFuncName) {\n        const matchingFuncs = suggestions.filter(\n          (f) => f.type === \"function\" && f.name === errFuncName,\n        );\n        if (matchingFuncs.length) {\n          hint =\n            \"Similar functions: \" +\n            matchingFuncs\n              .map(\n                (f) =>\n                  `${f.escapedIdentifier}(${f.args?.map((a) => a.data_type).join(\", \")})`,\n              )\n              .join(\", \");\n        } else {\n          hint = \"Hint: Check name or ensure the required extension is enabled\";\n        }\n      }\n    } else if (\n      message.includes(\"type\") &&\n      message.match(`geometry|geography`) &&\n      message.includes(\"does not exist\")\n    ) {\n      hint = \"Hint: Might need postgis for this: CREATE EXTENSION postgis;\";\n    } else if (\n      message.includes(\"cannot be dropped because some objects depend on it\")\n    ) {\n      hint = `Hint: Try reassigning objects: \\nREASSIGN OWNED BY ${JSON.stringify(message.split('\"')[1] || \"your_user\")} TO postgres;`;\n    }\n\n    if (hint) {\n      return message + `\\n\\n${hint}`;\n    }\n  } catch (e) {\n    console.error(e);\n  }\n\n  return undefined;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/CardView/CardView.tsx",
    "content": "import type { PaginationProps } from \"@components/Table/Pagination\";\nimport { Pagination } from \"@components/Table/Pagination\";\nimport type { TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type {\n  ChartOptions,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { W_TableProps, W_TableState } from \"../W_Table\";\nimport type { OnClickEditRow } from \"../tableUtils/getEditColumn\";\nimport type { ProstglesTableColumn } from \"../tableUtils/getTableCols\";\nimport { CardViewColumn } from \"./CardViewColumn\";\nimport { CardViewKanban } from \"./CardViewKanban\";\nimport { useCardViewState } from \"./useCardViewState\";\n\nexport type CardViewProps = {\n  props: W_TableProps;\n  state: W_TableState;\n  cardOpts: Extract<ChartOptions<\"table\">[\"viewAs\"], { type: \"card\" }>;\n  w?: WindowSyncItem<\"table\">;\n  tableHandler: Partial<TableHandlerClient<AnyObject, void>>;\n  paginationProps: PaginationProps;\n  style?: React.CSSProperties;\n  className?: string;\n  onEditClickRow: OnClickEditRow;\n  onDataChanged: VoidFunction;\n  cols: ProstglesTableColumn[];\n};\n\nexport type IndexedRow = {\n  data: AnyObject;\n  index: number;\n};\n\nexport const CardView = (_props: CardViewProps) => {\n  const { state, paginationProps, className = \"\", style, cardOpts } = _props;\n  const { rows: _rows = [] } = state;\n\n  const cardViewState = useCardViewState(_props);\n  const {\n    table,\n    groupByColumn,\n    allIndexedRows,\n    draggedRow,\n    setDraggedRow,\n    moveItemsProps,\n  } = cardViewState;\n\n  if (!table) {\n    return <div>Table info missing</div>;\n  }\n\n  return (\n    <div\n      className={\n        \"CardView o-auto min-s-0 flex-col f-1 bg-color-2  \" + className\n      }\n      style={style}\n    >\n      {groupByColumn ?\n        <CardViewKanban\n          key=\"kanban\"\n          {..._props}\n          cardOpts={cardOpts}\n          groupByColumn={groupByColumn}\n          allIndexedRows={allIndexedRows}\n          table={table}\n          draggedRow={draggedRow}\n          setDraggedRow={setDraggedRow}\n          moveItemsProps={moveItemsProps}\n        />\n      : <CardViewColumn\n          key=\"column\"\n          {..._props}\n          table={table}\n          indexedRows={allIndexedRows}\n          draggedRow={draggedRow}\n          setDraggedRow={setDraggedRow}\n          moveItemsProps={moveItemsProps}\n          allIndexedRows={allIndexedRows}\n        />\n      }\n      <Pagination {...paginationProps} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/CardView/CardViewColumn.tsx",
    "content": "import React from \"react\";\nimport type { DBSchemaTableWJoins } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport type { CardViewProps, IndexedRow } from \"./CardView\";\nimport { CardViewRow } from \"./CardViewRow\";\nimport type { CardViewState } from \"./useCardViewState\";\n\nexport type CardViewColumnProps = Pick<\n  CardViewProps,\n  | \"props\"\n  | \"state\"\n  | \"w\"\n  | \"onEditClickRow\"\n  | \"onDataChanged\"\n  | \"cols\"\n  | \"cardOpts\"\n  | \"tableHandler\"\n> &\n  Pick<\n    CardViewState,\n    \"moveItemsProps\" | \"draggedRow\" | \"setDraggedRow\" | \"allIndexedRows\"\n  > & {\n    indexedRows: IndexedRow[];\n    table: DBSchemaTableWJoins;\n  };\n\nexport const CardViewColumn = (_props: CardViewColumnProps) => {\n  const {\n    props,\n    state,\n    w,\n    onEditClickRow,\n    onDataChanged,\n    cols,\n    cardOpts,\n    tableHandler,\n    indexedRows,\n    table,\n    draggedRow,\n    setDraggedRow,\n    moveItemsProps,\n    allIndexedRows,\n  } = _props;\n  const { rows: _rows = [] } = state;\n\n  const { cardRows = 1 } = cardOpts;\n\n  return (\n    <div\n      className={\n        \"CardView_Column\" +\n        (cardRows > 1 ? \" flex-row-wrap \" : \" flex-col \") +\n        \" p-p25 o-auto no-scroll-bar f-1 min-w-0 min-h-0 px-p5 pt-p5\"\n      }\n      style={{\n        placeContent: cardRows > 1 ? \"flex-start\" : undefined,\n      }}\n    >\n      {indexedRows.map((indexedRow, rowIndex) => {\n        return (\n          <CardViewRow\n            key={indexedRow.index}\n            indexedRow={indexedRow}\n            rowIndex={rowIndex}\n            cardOpts={cardOpts}\n            indexedRows={indexedRows}\n            onDataChanged={onDataChanged}\n            onEditClickRow={onEditClickRow}\n            props={props}\n            table={table}\n            tableHandler={tableHandler}\n            cols={cols}\n            draggedRow={draggedRow}\n            setDraggedRow={setDraggedRow}\n            w={w}\n            moveItemsProps={moveItemsProps}\n            allIndexedRows={allIndexedRows}\n          />\n        );\n      })}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/CardView/CardViewKanban.tsx",
    "content": "import ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { ChartOptions } from \"../../Dashboard/dashboardUtils\";\nimport type { CardViewProps, IndexedRow } from \"./CardView\";\nimport { CardViewColumn, type CardViewColumnProps } from \"./CardViewColumn\";\n\nconst MAX_NUMBER_OF_GROUPS = 20;\ntype P = Pick<CardViewProps, \"state\"> &\n  Omit<CardViewColumnProps, \"indexedRows\"> & {\n    cardOpts: Extract<ChartOptions<\"table\">[\"viewAs\"], { type: \"card\" }>;\n    groupByColumn: ValidatedColumnInfo;\n    allIndexedRows: IndexedRow[];\n  };\n\nexport const CardViewKanban = (_props: P) => {\n  const { state, cardOpts, allIndexedRows, table, groupByColumn } = _props;\n  const groupByColumnName = groupByColumn.name;\n  const { rows: _rows = [] } = state;\n\n  /** Kanban. Maintain order */\n\n  const { columnGroups, columnGroupsWithItems } = useMemo(() => {\n    const columnGroups = Array.from(\n      new Set(allIndexedRows.map(({ data }) => data[groupByColumnName])),\n    );\n    const columnGroupsWithItems = columnGroups\n      .slice(0, MAX_NUMBER_OF_GROUPS)\n      .map((groupByValue) => {\n        let groupRows = allIndexedRows.filter(\n          ({ data }) => data[groupByColumnName] === groupByValue,\n        );\n        if (cardOpts.cardOrderBy) {\n          const orderByColumn = cardOpts.cardOrderBy;\n          groupRows = groupRows.sort((a, b) => {\n            const aVal = a.data[orderByColumn];\n            const bVal = b.data[orderByColumn];\n            return aVal - bVal;\n          });\n        }\n\n        return { groupByValue, groupRows };\n      });\n    return { columnGroups, columnGroupsWithItems };\n  }, [allIndexedRows, cardOpts.cardOrderBy, groupByColumnName]);\n  const excesiveGroups = columnGroups.length > MAX_NUMBER_OF_GROUPS;\n\n  return (\n    <div className=\"flex-row f-1 min-s-0 mt-1 o-auto\">\n      {excesiveGroups && (\n        <InfoRow color=\"warning\">\n          Warning: Too many groups ({columnGroups.length}). Only first{\" \"}\n          {MAX_NUMBER_OF_GROUPS} shown to improve performance.\n        </InfoRow>\n      )}\n      {columnGroupsWithItems.map(({ groupByValue, groupRows }) => {\n        return (\n          <FlexCol\n            key={groupByValue}\n            data-key={groupByValue}\n            className=\"gap-0\"\n            data-command=\"CardView.group\"\n          >\n            <div\n              title={groupByColumn.label}\n              className=\"f-0 p-p5 px-1 font-18\"\n              style={{\n                fontWeight: 700,\n              }}\n            >\n              {groupByValue}\n            </div>\n            <div className=\"min-s-0 o-auto f-1\">\n              <CardViewColumn\n                {..._props}\n                table={table}\n                indexedRows={groupRows}\n              />\n            </div>\n          </FlexCol>\n        );\n      })}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/CardView/CardViewRow.tsx",
    "content": "import type { CardLayout } from \"@common/DashboardTypes\";\nimport { matchObj } from \"@common/utils\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport { _PG_date, type AnyObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\nimport { RenderValue } from \"../../SmartForm/SmartFormField/RenderValue\";\nimport { getEditColumn } from \"../tableUtils/getEditColumn\";\nimport type { CardViewProps, IndexedRow } from \"./CardView\";\nimport { DragHeader, DragHeaderHeight } from \"./DragHeader\";\nimport type { CardViewState } from \"./useCardViewState\";\n\nexport type CardViewRowProps = Pick<\n  CardViewProps,\n  | \"props\"\n  | \"cardOpts\"\n  | \"onEditClickRow\"\n  | \"tableHandler\"\n  | \"onDataChanged\"\n  | \"cols\"\n  | \"w\"\n> &\n  Pick<\n    CardViewState,\n    \"moveItemsProps\" | \"draggedRow\" | \"setDraggedRow\" | \"allIndexedRows\"\n  > & {\n    indexedRow: IndexedRow;\n    rowIndex: number;\n    indexedRows: IndexedRow[];\n    table: DBSchemaTableWJoins;\n  };\n\nexport type KanBanDraggedRow = IndexedRow & {\n  height: number;\n  target: IndexedRow | undefined;\n};\n\nexport const CardViewRow = ({\n  props: { activeRow, activeRowColor, joinFilter, onClickRow },\n  indexedRow,\n  cardOpts,\n  onEditClickRow,\n  tableHandler,\n  table,\n  onDataChanged,\n  indexedRows,\n  rowIndex,\n  cols,\n  w,\n  draggedRow,\n  setDraggedRow,\n  moveItemsProps,\n  allIndexedRows,\n}: CardViewRowProps) => {\n  const columns = table.columns;\n\n  const {\n    cardRows = 1,\n    hideCardFieldNames,\n    maxCardWidth = \"100%\",\n    hideEmptyCardCells,\n    maxCardRowHeight,\n    cardCellMinWidth = \"\",\n  } = cardOpts;\n  const marginRight = cardRows > 1 ? `.5em` : \"auto\";\n  const marginLeftRight =\n    cardRows === 1 && maxCardWidth !== \"100%\" ? \"auto\" : \"\";\n  const row = indexedRow.data;\n  const isMoving = draggedRow;\n  const itemMarginTop =\n    isMoving && indexedRow.index === isMoving.index + 1 ?\n      `calc(${isMoving.height}px + 1em)`\n    : marginTop;\n  const isDragTarget = isMoving?.target?.index === indexedRow.index;\n\n  const style = useMemo(() => {\n    const isActive =\n      joinFilter || (activeRow && matchObj(activeRow.row_filter, row));\n    return {\n      gap: padding,\n      background: isDragTarget ? \"var(--bg-li-selected)\" : \"var(--bg-color-0)\",\n      padding,\n      /** Used to ensure top right edit button is visible */\n      paddingRight: \"3em\",\n      /** Used to ensure cell header contextmenu is working */\n      paddingTop: `${DragHeaderHeight}px`,\n      ...(maxCardWidth !== \"100%\" ?\n        {\n          width: maxCardWidth,\n        }\n      : {\n          width: cardRows > 1 ? `calc(${99 / cardRows}% - .5em)` : \"\",\n        }),\n      margin: `${itemMarginTop} ${marginRight} 0 ${marginLeftRight}`,\n      ...(isActive && {\n        boxShadow: `inset 0 0 10px ${activeRowColor}`,\n      }),\n    };\n  }, [\n    activeRow,\n    activeRowColor,\n    cardRows,\n    isDragTarget,\n    itemMarginTop,\n    joinFilter,\n    marginLeftRight,\n    marginRight,\n    maxCardWidth,\n    row,\n  ]);\n\n  const visibleCols = useMemo(\n    () =>\n      cols.filter(\n        (c) =>\n          !c.hidden &&\n          !(\n            hideEmptyCardCells &&\n            [null, undefined, \"\"].includes(`${row[c.name] ?? \"\"}`.trim())\n          ),\n      ),\n    [cols, hideEmptyCardCells, row],\n  );\n\n  return (\n    <FlexRowWrap\n      data-command=\"CardView.row\"\n      key={indexedRow.index}\n      data-row-index={indexedRow.index}\n      className={\n        \"CardView_Item relative card jc-start ai-start f-0 min-w-0 \" +\n        (cardRows > 1 ? \" fit \" : \"\")\n      }\n      style={style}\n      onClick={(e) => {\n        if (window.getSelection()?.toString()) return;\n        onClickRow?.(row, e);\n      }}\n    >\n      {moveItemsProps && (\n        <DragHeader\n          {...moveItemsProps}\n          padding={padding}\n          tableHandler={tableHandler}\n          table={table}\n          allIndexedRows={allIndexedRows}\n          columns={columns}\n          onDataChanged={onDataChanged}\n          onEditClickRow={onEditClickRow}\n          indexedRow={indexedRow}\n          draggedRow={draggedRow}\n          setDraggedRow={setDraggedRow}\n        />\n      )}\n      {!w?.options.hideEditRow && (\n        <div\n          style={{\n            top: \"2px\",\n            right: \"2px\",\n            position: \"absolute\",\n          }}\n        >\n          {getEditColumn({\n            table,\n            columnConfig: w?.columns || undefined,\n            tableHandler,\n            onClickRow: (...args) => {\n              if (draggedRow) return;\n              onEditClickRow(...args);\n            },\n          }).onRender!({\n            value: \"\",\n            renderedVal: \"\",\n            row,\n            prevRow: indexedRows[rowIndex - 1],\n            nextRow: indexedRows[rowIndex + 1],\n            rowIndex: rowIndex,\n          })}\n        </div>\n      )}\n      <CardViewRowContent\n        visibleCols={visibleCols}\n        cardCellMinWidth={cardCellMinWidth}\n        cardRows={cardRows}\n        row={row}\n        hideCardFieldNames={hideCardFieldNames}\n        maxCardRowHeight={maxCardRowHeight}\n        rowIndex={rowIndex}\n        indexedRows={indexedRows}\n        cardLayout={w?.options.cardLayout}\n      />\n    </FlexRowWrap>\n  );\n};\n\nconst CardViewRowContent = ({\n  visibleCols,\n  cardCellMinWidth,\n  cardRows,\n  row,\n  hideCardFieldNames,\n  maxCardRowHeight,\n  rowIndex,\n  indexedRows,\n  cardLayout,\n}: Pick<\n  Required<CardViewRowProps[\"cardOpts\"]>,\n  \"cardCellMinWidth\" | \"cardRows\"\n> & {\n  visibleCols: CardViewRowProps[\"cols\"];\n  row: AnyObject;\n  maxCardRowHeight: number | undefined;\n  hideCardFieldNames: boolean | undefined;\n  cardCellMinWidth: string;\n  rowIndex: number;\n  indexedRows: IndexedRow[];\n  cardLayout: CardLayout | undefined;\n}) => {\n  const columnNodeList = visibleCols.map((c, ci) => {\n    const value = row[c.name] as unknown;\n    return (\n      <div\n        key={ci}\n        title={c.udt_name}\n        className={\"flex-col min-w-0 \" + (cardRows > 1 ? \" h-fit w-fit \" : \"\")}\n        style={{ minWidth: cardCellMinWidth }}\n      >\n        {!hideCardFieldNames && (\n          <div\n            className=\" text-2 pointer noselect\"\n            onContextMenu={\n              c.onContextMenu &&\n              ((e) => c.onContextMenu?.(e, e.currentTarget, c, () => {}))\n            }\n          >\n            {c.name}\n          </div>\n        )}\n        <div\n          className=\" o-auto font-18\"\n          title={\n            (\n              typeof value === \"string\" &&\n              (_PG_date.some((v) => v === c.udt_name) ||\n                c.tsDataType === \"number\")\n            ) ?\n              value\n            : \"\"\n          }\n          style={{\n            lineHeight: 1.33,\n            ...(c.getCellStyle?.(row, value, value) || {}),\n            maxHeight: `${maxCardRowHeight || 800}px`,\n          }}\n        >\n          {c.onRender?.({\n            row: row,\n            value,\n            renderedVal: value,\n            rowIndex: rowIndex,\n            nextRow: indexedRows[rowIndex + 1],\n            prevRow: indexedRows[rowIndex - 1],\n          }) ?? <RenderValue column={c} value={value} />}\n        </div>\n      </div>\n    );\n  });\n\n  if (cardLayout) {\n    const columnNodes: Record<string, React.ReactNode> = {};\n    visibleCols.forEach((c, i) => {\n      columnNodes[c.name] = columnNodeList[i];\n    });\n    return (\n      <CardLayoutRenderer\n        cardLayout={cardLayout}\n        columnNodes={columnNodes}\n        item={cardLayout}\n      />\n    );\n  }\n\n  return <>{columnNodeList}</>;\n};\n\nconst CardLayoutRenderer = ({\n  cardLayout,\n  columnNodes,\n  item,\n}: {\n  cardLayout: CardLayout;\n  columnNodes: Record<string, React.ReactNode>;\n  item: CardLayout[\"children\"][number];\n}) => {\n  if (item.type === \"node\") {\n    const node = columnNodes[item.columnName];\n    if (!node) return <>Column node missing for {item.columnName}</>;\n    return node;\n  }\n\n  return (\n    <div style={item.style} data-node-type={item.type || \"container\"}>\n      {item.children.map((childItem, index) => (\n        <CardLayoutRenderer\n          key={index}\n          cardLayout={cardLayout}\n          columnNodes={columnNodes}\n          item={childItem}\n        />\n      ))}\n    </div>\n  );\n};\n\nconst marginTop = \".5em\";\nconst padding = !window.isMobileDevice ? \"1em\" : \".5em\";\n"
  },
  {
    "path": "client/src/dashboard/W_Table/CardView/DragHeader.tsx",
    "content": "import { Pan } from \"@components/Pan\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport React from \"react\";\nimport type { CardViewProps, IndexedRow } from \"./CardView\";\nimport type { CardViewRowProps } from \"./CardViewRow\";\nimport { useDragHeader } from \"./useDragHeader\";\n\nexport type DragHeaderProps = Pick<\n  CardViewProps,\n  \"onDataChanged\" | \"onEditClickRow\"\n> &\n  Pick<\n    CardViewRowProps,\n    \"indexedRow\" | \"table\" | \"tableHandler\" | \"draggedRow\" | \"setDraggedRow\"\n  > & {\n    padding: string;\n    allIndexedRows: IndexedRow[];\n    groupByColumn: ValidatedColumnInfo;\n    orderByColumn: ValidatedColumnInfo | undefined;\n    columns: ValidatedColumnInfo[];\n  };\n\nexport const DragHeader = (props: DragHeaderProps) => {\n  const { onPan, onPanEnd, onPanStart } = useDragHeader(props);\n  return (\n    <Pan\n      data-command=\"CardView.DragHeader\"\n      className=\" \"\n      style={style}\n      onPanStart={onPanStart}\n      onPan={onPan}\n      onPanEnd={onPanEnd}\n    />\n  );\n};\n\nexport const DragHeaderHeight = 30;\n\nconst style: React.CSSProperties = {\n  position: \"absolute\",\n  left: 0,\n  top: 0,\n  right: \"3em\", // Space for edit button\n  height: `${DragHeaderHeight}px`,\n  cursor: \"move\",\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/CardView/useCardViewState.ts",
    "content": "import { useMemo, useState } from \"react\";\nimport type { CardViewProps, IndexedRow } from \"./CardView\";\nimport type { KanBanDraggedRow } from \"./CardViewRow\";\n\nexport const useCardViewState = (_props: CardViewProps) => {\n  const { props, state, w, cardOpts } = _props;\n  const { rows: _rows = [] } = state;\n  const [draggedRow, setDraggedRow] = useState<KanBanDraggedRow | undefined>();\n  const { allIndexedRows, table } = useMemo(() => {\n    const allIndexedRows: IndexedRow[] = _rows.map((data, index) => ({\n      data,\n      index,\n    }));\n    const { tables } = props;\n    const table = tables.find((t) => t.name === w?.table_name);\n    return {\n      allIndexedRows,\n      table,\n    };\n  }, [_rows, props, w?.table_name]);\n\n  const { cardGroupBy } = cardOpts;\n\n  const { columns = [] } = table ?? {};\n  const groupByColumn = columns.find(\n    (c) => c.name === cardOpts.cardGroupBy && c.update,\n  );\n  const moveItemsProps = useMemo(\n    () =>\n      groupByColumn ?\n        {\n          groupByColumn,\n          orderByColumn: columns.find((c) => c.name === cardOpts.cardOrderBy),\n        }\n      : undefined,\n    [cardOpts.cardOrderBy, columns, groupByColumn],\n  );\n\n  return {\n    allIndexedRows,\n    table,\n    draggedRow,\n    setDraggedRow,\n    groupByColumn,\n    moveItemsProps,\n    cardGroupBy,\n  };\n};\n\nexport type CardViewState = ReturnType<typeof useCardViewState>;\n"
  },
  {
    "path": "client/src/dashboard/W_Table/CardView/useDragHeader.ts",
    "content": "import { getSmartGroupFilter } from \"@common/filterUtils\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { isEmpty } from \"../../../utils/utils\";\nimport { getDistanceBetweenBoxes } from \"../../SilverGrid/SilverGridChild\";\nimport { getRowFilter } from \"../tableUtils/getRowFilter\";\nimport type { KanBanDraggedRow } from \"./CardViewRow\";\nimport type { DragHeaderProps } from \"./DragHeader\";\n\nexport const useDragHeader = (props: DragHeaderProps) => {\n  const {\n    indexedRow,\n    allIndexedRows,\n    groupByColumn,\n    orderByColumn,\n    table,\n    columns,\n    tableHandler,\n    onDataChanged,\n    onEditClickRow,\n    draggedRow,\n    setDraggedRow,\n  } = props;\n\n  const draggedRowRef = useRef<\n    {\n      top: number;\n      left: number;\n    } & KanBanDraggedRow\n  >();\n  useEffect(() => {\n    if (draggedRow?.index !== draggedRowRef.current?.index) {\n      draggedRowRef.current = undefined;\n    }\n  }, [draggedRow]);\n\n  const getNodes = useCallback(\n    (node: HTMLDivElement) => {\n      const rowNode = node.parentElement!;\n      const rootView = node.closest(\".CardView\");\n      const siblings = Array.from<HTMLDivElement>(\n        rootView?.querySelectorAll(`[data-row-index]`) ?? [],\n      )\n        .map((n) => {\n          const rowIndex = parseInt(n.dataset.rowIndex!);\n          const isValid = Number.isInteger(rowIndex);\n          const isNotSelf = isValid && rowIndex !== indexedRow.index;\n          return {\n            n,\n            rowIndex,\n            isValid,\n            isNotSelf,\n          };\n        })\n        .filter((d) => d.isNotSelf);\n      const rSource = node.getBoundingClientRect();\n      const targetSibling = siblings\n        .map((s) => {\n          const rTarget = s.n.getBoundingClientRect();\n          return { ...s, d: getDistanceBetweenBoxes(rSource, rTarget) };\n        })\n        .sort((a, b) => a.d - b.d)[0];\n\n      return {\n        rowNode,\n        groupNode: rowNode.parentElement,\n        siblings: siblings.map((s) => s.n),\n        targetSibling: targetSibling?.isNotSelf ? targetSibling : undefined,\n        rootView,\n      };\n    },\n    [indexedRow.index],\n  );\n\n  const onPanStart = useCallback(\n    ({ node }, e) => {\n      e.preventDefault();\n      e.stopPropagation();\n      const handlerNode = node.getBoundingClientRect();\n      const rowNode = node.parentElement;\n      const groupNode = rowNode?.parentElement?.getBoundingClientRect();\n      if (!rowNode || !groupNode) return;\n\n      draggedRowRef.current = {\n        top: handlerNode.top - groupNode.top,\n        left: handlerNode.left - groupNode.left,\n        ...indexedRow,\n        height: rowNode.getBoundingClientRect().height,\n        target: undefined,\n      };\n      setDraggedRow({\n        ...indexedRow,\n        height: rowNode.getBoundingClientRect().height,\n        target: undefined,\n      });\n    },\n    [indexedRow, draggedRowRef, setDraggedRow],\n  );\n  const onPan = useCallback(\n    ({ xDiff, yDiff, x, y, node }, e) => {\n      const draggedRow = draggedRowRef.current;\n      if (!draggedRow) {\n        return;\n      }\n      const { targetSibling, rowNode, groupNode } = getNodes(node);\n\n      rowNode.style.position = \"absolute\";\n      rowNode.style.zIndex = \"22\";\n      rowNode.style.transform = `translate(${draggedRow.left + xDiff}px, ${draggedRow.top + yDiff - (groupNode?.parentElement?.scrollTop ?? 0)}px)`;\n      e.preventDefault();\n      e.stopPropagation();\n\n      const targetIndex =\n        targetSibling ? parseInt(targetSibling.n.dataset.rowIndex!) : undefined;\n      const targetIRow = allIndexedRows.find((d) => d.index === targetIndex);\n      // console.log(targetIRow);\n      if (draggedRow.target?.index !== targetIndex) {\n        draggedRowRef.current = {\n          ...draggedRow,\n          target: targetIRow,\n        };\n        setDraggedRow({\n          ...draggedRow,\n          target: targetIRow,\n        });\n      }\n    },\n    [allIndexedRows, getNodes, setDraggedRow],\n  );\n\n  const onPanEnd = useCallback(\n    ({ node }, e) => {\n      const draggedRow = draggedRowRef.current;\n      if (!draggedRow) return;\n      e.preventDefault();\n      e.stopPropagation();\n      const { rowNode, siblings } = getNodes(node);\n\n      (async () => {\n        const nodes = {\n          didMove: false,\n          dragged: rowNode,\n          beneathDragged: siblings[draggedRow.index + 1],\n          target: siblings[draggedRow.target?.index ?? -1],\n        };\n        if (\n          draggedRow.target &&\n          draggedRow.index !== draggedRow.target.index - 1\n        ) {\n          const { error, filter } = await getRowFilter(\n            draggedRow.data,\n            table,\n            columns,\n            tableHandler,\n          );\n          if (error) {\n            alert(error);\n          } else if (filter) {\n            const noSiblingData = {\n              nextRow: undefined,\n              nextRowFilter: undefined,\n              prevRow: undefined,\n              prevRowFilter: undefined,\n            };\n            const sourceGroupValue = draggedRow.data[groupByColumn.name];\n            const targetGroupValue = draggedRow.target.data[groupByColumn.name];\n            const newGroupFilter = {\n              [groupByColumn.name]: targetGroupValue,\n            };\n            let newOrderValue = {};\n            if (orderByColumn) {\n              const targetOrderValue =\n                draggedRow.target.data[orderByColumn.name];\n              newOrderValue = { [orderByColumn.name]: targetOrderValue };\n              const increment = 0.000001;\n              try {\n                const targetSiblingFilter = await getRowFilter(\n                  draggedRow.target.data,\n                  table,\n                  columns,\n                  tableHandler,\n                );\n                if (targetSiblingFilter.error) throw targetSiblingFilter.error;\n                const finalTargetFilter = getSmartGroupFilter(\n                  targetSiblingFilter.filter,\n                );\n                /** Move target order slightly below its current position which will be taken by the dragged item */\n                const prevTargetItem = await tableHandler.findOne?.(\n                  {\n                    $and: [\n                      finalTargetFilter,\n                      { [orderByColumn.name]: { \">\": targetOrderValue } },\n                    ],\n                  },\n                  { orderBy: { [orderByColumn.name]: false } },\n                );\n\n                let newTargetOrderValue = Number(targetOrderValue) + increment;\n                if (prevTargetItem) {\n                  newTargetOrderValue =\n                    (Number(prevTargetItem[orderByColumn.name]) +\n                      Number(targetOrderValue)) /\n                    2;\n                }\n                await tableHandler.update?.(\n                  finalTargetFilter,\n                  { [orderByColumn.name]: newTargetOrderValue },\n                  { returning: \"*\" },\n                );\n              } catch (err) {\n                onEditClickRow(\n                  filter,\n                  noSiblingData,\n                  indexedRow.index,\n                  newGroupFilter,\n                );\n              }\n            }\n            const newGroupValue =\n              targetGroupValue === sourceGroupValue ?\n                {}\n              : { [groupByColumn.name]: targetGroupValue };\n            if (!isEmpty(newGroupValue) || !isEmpty(newOrderValue)) {\n              try {\n                const finalFilter = getSmartGroupFilter(filter);\n                await tableHandler.update!(\n                  finalFilter,\n                  {\n                    ...newGroupValue,\n                    ...newOrderValue,\n                  },\n                  { returning: \"*\" },\n                );\n\n                onDataChanged();\n                nodes.didMove = true;\n              } catch (err) {\n                onEditClickRow(\n                  filter,\n                  noSiblingData,\n                  indexedRow.index,\n                  newGroupFilter,\n                );\n              }\n            }\n          }\n        }\n\n        setTimeout(() => {\n          rowNode.style.opacity = \"\";\n          rowNode.style.position = \"\";\n          rowNode.style.zIndex = \"\";\n          rowNode.style.transform = ``;\n          rowNode.style.transition = \"\";\n        }, 22);\n        draggedRowRef.current = undefined;\n        setDraggedRow(undefined);\n      })();\n\n      return false;\n    },\n    [\n      getNodes,\n      table,\n      columns,\n      tableHandler,\n      groupByColumn.name,\n      orderByColumn,\n      onEditClickRow,\n      indexedRow.index,\n      onDataChanged,\n      setDraggedRow,\n    ],\n  );\n\n  return {\n    onPanStart,\n    onPan,\n    onPanEnd,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AddColumnMenu.tsx",
    "content": "import Popup, { POPUP_CLASSES } from \"@components/Popup/Popup\";\nimport type { FullOption } from \"@components/Select/Select\";\nimport { Select } from \"@components/Select/Select\";\nimport {\n  mdiFunction,\n  mdiLink,\n  mdiTableColumnPlusAfter,\n  mdiTableEdit,\n} from \"@mdi/js\";\nimport { type DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useMemo, useState } from \"react\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport type {\n  DBSchemaTablesWJoins,\n  LoadedSuggestions,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport { CreateFileColumn } from \"../../FileTableControls/CreateFileColumn\";\nimport { updateWCols } from \"../tableUtils/tableUtils\";\nimport { QuickAddComputedColumn } from \"./AddComputedColumn/QuickAddComputedColumn\";\nimport { CreateColumn } from \"./AlterColumn/CreateColumn\";\nimport { LinkedColumn } from \"./LinkedColumn/LinkedColumn\";\nimport type { NestedColumnOpts } from \"./getNestedColumnTable\";\n\nconst options = [\n  {\n    key: \"Computed\",\n    label: t.AddColumnMenu[\"Add Computed Field\"],\n    subLabel: t.AddColumnMenu[\"Show a computed column\"],\n    iconPath: mdiFunction,\n  },\n  {\n    key: \"Referenced\",\n    label: t.AddColumnMenu[\"Add Linked Data\"],\n    subLabel: t.AddColumnMenu[\"Show data from a related table\"],\n    iconPath: mdiLink,\n  },\n  {\n    key: \"Create\",\n    label: t.AddColumnMenu[\"Create New Column\"],\n    subLabel: t.AddColumnMenu[\"Create a new column in this table\"],\n    disabledInfo: undefined,\n    iconPath: mdiTableEdit,\n  },\n  {\n    key: \"CreateFileColumn\",\n    label: t.AddColumnMenu[\"Create New File Column\"],\n    subLabel: t.AddColumnMenu[\"Create a new file column in this table\"],\n    disabledInfo: undefined,\n    iconPath: mdiTableEdit,\n  },\n] as const satisfies readonly FullOption[];\n\nexport type AddColumnMenuProps = {\n  w: WindowSyncItem<\"table\">;\n  tables: DBSchemaTablesWJoins;\n  db: DBHandlerClient;\n  suggestions: LoadedSuggestions | undefined;\n  variant?: \"detailed\";\n  nestedColumnOpts: NestedColumnOpts | undefined;\n};\n\nexport const AddColumnMenu = ({\n  w,\n  tables,\n  db,\n  variant,\n  nestedColumnOpts,\n  suggestions,\n}: AddColumnMenuProps) => {\n  const table = tables.find((t) => t.name === w.table_name);\n  const [colType, setColType] = useState<\n    (typeof options)[number][\"key\"] | void\n  >();\n  const [anchorEl, setAnchorEl] = useState<HTMLElement | void>();\n\n  /**\n   * Root query aggregation AND nested joins not allowed\n   */\n  const wCols = w.columns;\n  const dissallow = useMemo(() => {\n    return (\n      (\n        wCols?.some(\n          (c) =>\n            c.computedConfig?.funcDef.isAggregate ||\n            c.computedConfig?.funcDef.key.startsWith(\"$count\"),\n        )\n      ) ?\n        \"Referenced\"\n      : wCols?.some((c) => c.nested) ? \"aggs\"\n      : undefined\n    );\n  }, [wCols]);\n\n  if (!table) {\n    return <>Table {w.table_name} not found</>;\n  }\n\n  const cannotCreateColumns =\n    !db.sql ? t.AddColumnMenu[\"Not enough privileges\"]\n    : table.info.isView ?\n      t.AddColumnMenu[\"This is a view. Cannot create columns, must recreate\"]\n    : undefined;\n  const onClose = () => setColType();\n\n  return (\n    <>\n      <Select\n        data-command=\"AddColumnMenu\"\n        onOpen={setAnchorEl}\n        btnProps={{\n          children: variant ? t.AddColumnMenu[\"New Field\"] : \"\",\n          variant: variant ? \"faded\" : undefined,\n          color: variant ? \"action\" : undefined,\n          iconPath: mdiTableColumnPlusAfter,\n          size: variant ? undefined : \"small\",\n          title: t.AddColumnMenu[\"Add column\"],\n        }}\n        fullOptions={options.map((o) => ({\n          ...o,\n          disabledInfo:\n            !table.joinsV2.length && o.key === \"Referenced\" ?\n              t.AddColumnMenu[\"No foreign keys to/from this table\"]\n            : nestedColumnOpts && o.key === \"Referenced\" ?\n              t.AddColumnMenu[\"Not allowed for nested columns\"]\n            : o.key === \"Create\" ? cannotCreateColumns\n            : o.key === dissallow ?\n              t.AddColumnMenu[\n                \"Aggregates and/or Count not allowed with linked \"\n              ]\n            : o.key === \"CreateFileColumn\" && table.info.isFileTable ?\n              \"Cannot add file column to a file table\"\n            : undefined,\n        }))}\n        onChange={(type) => setColType(type)}\n      />\n      {!colType || !anchorEl ?\n        null\n      : colType === \"CreateFileColumn\" ?\n        <CreateFileColumn\n          db={db}\n          tables={tables}\n          fileTable={tables[0]?.info.fileTableName}\n          tableName={table.name}\n          onClose={() => setColType(undefined)}\n        />\n      : <Popup\n          title={\n            colType === \"Computed\" ? t.AddColumnMenu[`Add Computed Field`]\n            : colType === \"Create\" ?\n              t.AddColumnMenu[`Create new column`]\n            : t.AddColumnMenu[\"Add Referenced/Linked Fields\"]\n          }\n          positioning=\"beneath-left\"\n          anchorEl={anchorEl}\n          onClose={onClose}\n          autoFocusFirst={{ selector: `.${POPUP_CLASSES.content} input` }}\n          clickCatchStyle={{ opacity: 0.5 }}\n          contentClassName=\"p-1\"\n        >\n          {colType === \"Computed\" ?\n            <QuickAddComputedColumn\n              existingColumn={undefined}\n              tableName={table.name}\n              onAddColumn={(computedColumn) => {\n                if (!computedColumn) {\n                  setAnchorEl(undefined);\n                  return;\n                }\n                updateWCols(\n                  w,\n                  [computedColumn, ...(w.columns ?? [])],\n                  undefined,\n                );\n                setAnchorEl(undefined);\n              }}\n            />\n          : colType === \"Create\" ?\n            <CreateColumn\n              db={db}\n              field=\"\"\n              table={table}\n              tables={tables}\n              suggestions={suggestions}\n              onClose={onClose}\n            />\n          : <LinkedColumn column={undefined} onClose={onClose} w={w} />}\n        </Popup>\n      }\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AddComputedColumn/AddComputedColMenu.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { Label } from \"@components/Label\";\nimport type { FooterButton } from \"@components/Popup/FooterButtons\";\nimport { FooterButtons } from \"@components/Popup/FooterButtons\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiChevronDown, mdiPlus } from \"@mdi/js\";\nimport type { TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { _PG_date, pickKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport type { Prgl } from \"../../../../App\";\nimport { isEmpty } from \"../../../../utils/utils\";\nimport type {\n  DBSchemaTablesWJoins,\n  WindowSyncItem,\n} from \"../../../Dashboard/dashboardUtils\";\nimport RTComp from \"../../../RTComp\";\nimport type { ColumnConfigWInfo } from \"../../W_Table\";\nimport { getTableSelect } from \"../../tableUtils/getTableSelect\";\nimport { updateWCols } from \"../../tableUtils/tableUtils\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport { FunctionSelector } from \"../FunctionSelector/FunctionSelector\";\nimport {\n  CountAllFunc,\n  getColumnsAcceptedByFunction,\n  type FuncDef,\n} from \"../FunctionSelector/functions\";\nimport { NEW_COL_POSITIONS } from \"../LinkedColumn/LinkedColumnFooter\";\nimport {\n  getNestedColumnTable,\n  type NestedColumnOpts,\n} from \"../getNestedColumnTable\";\nimport { getColumnListItem } from \"../ColumnSelect/getColumnListItem\";\n\nconst ColTypes = [\"Function\", \"Aggregate Function\"] as const;\n\ntype AddComputedColMenuP = Pick<Prgl, \"db\"> & {\n  tableHandler?: Partial<TableHandlerClient>;\n  anchorEl?: Element;\n  onClose: VoidFunction;\n  w: WindowSyncItem<\"table\">;\n  tables: DBSchemaTablesWJoins;\n  nestedColumnOpts: NestedColumnOpts | undefined;\n  selectedColumn?: string;\n  variant?: \"no-popup\";\n};\n\ntype AddComputedColMenuS = {\n  colType?: (typeof ColTypes)[number];\n  column?: string;\n\n  funcDef?: FuncDef;\n\n  name?: string;\n\n  args?: Required<ColumnConfig>[\"computedConfig\"][\"args\"];\n\n  template_string_hint?: string;\n  template_string_error?: any;\n  addTo: (typeof NEW_COL_POSITIONS)[number][\"key\"];\n};\n\n/**\n * @deprecated Use QuickAddComputedColumn instead\n */\nexport class AddComputedColMenu extends RTComp<\n  AddComputedColMenuP,\n  AddComputedColMenuS\n> {\n  state: AddComputedColMenuS = {\n    args: {},\n    addTo: \"start\",\n  };\n\n  onDelta(deltaP?: Partial<AddComputedColMenuP>): void {\n    if (deltaP?.selectedColumn && !this.state.column) {\n      this.setState({ column: deltaP.selectedColumn });\n    }\n  }\n\n  onAdd = (newCol: ColumnConfig, addTo: AddComputedColMenuS[\"addTo\"]) => {\n    const tableOrError = this.table;\n    if (tableOrError.error !== undefined) {\n      console.error(tableOrError.error);\n      return;\n    }\n    const { columns, nestedColumn } = tableOrError;\n    const { w, nestedColumnOpts } = this.props;\n    if (!nestedColumn) {\n      const newColumns = columns.map((c) => ({ ...c }));\n      if (addTo === \"start\") newColumns.unshift(newCol);\n      else newColumns.push(newCol);\n\n      updateWCols(w, newColumns);\n    } else {\n      if (nestedColumnOpts?.type === \"new\") {\n        const { config } = nestedColumnOpts;\n        const updatedNestedColumn: ColumnConfigWInfo = {\n          ...config,\n          nested: {\n            ...config.nested!,\n            columns: [...config.nested!.columns, newCol],\n          },\n        };\n        nestedColumnOpts.onChange(updatedNestedColumn);\n      } else {\n        updateWCols(\n          w,\n          [...nestedColumn.nested!.columns, newCol],\n          nestedColumn.name,\n        );\n      }\n    }\n  };\n\n  get table() {\n    const { w, tables, nestedColumnOpts } = this.props;\n    return getNestedColumnTable(nestedColumnOpts, w, tables);\n  }\n\n  render() {\n    const { onClose, tableHandler, w, tables, db, variant } = this.props;\n    const {\n      column,\n      funcDef,\n      args,\n      template_string_hint,\n      template_string_error,\n      addTo,\n    } = this.state;\n\n    const name =\n      this.state.name ||\n      (funcDef?.label ?\n        `${funcDef.label.toUpperCase()}(${column || \"\"}${args?.$duration?.otherColumn ? ` TO ${args.$duration.otherColumn}` : \"\"})`\n      : \"\") ||\n      \"col_name\";\n    const { table, error } = this.table;\n\n    if (!table) return error;\n\n    const { columns } = table;\n\n    const allowedColumnsForFunction =\n      (funcDef && getColumnsAcceptedByFunction(funcDef, columns)) ?? columns;\n\n    const isAggNocol =\n      funcDef && !funcDef.tsDataTypeCol && !funcDef.udtDataTypeCol;\n    const canAdd =\n      funcDef?.key === \"$template_string\" ? !template_string_error\n      : funcDef?.key === \"$duration\" ? !!args?.$duration?.otherColumn\n      : Boolean(funcDef && (column || isAggNocol));\n\n    const hasJoinCols = w.columns?.some((c) => c.nested);\n    const content = (\n      <>\n        <FlexCol\n          className=\"AddComputedColMenu gap-2 f-1 min-h-0 ai-start\"\n          data-command=\"AddComputedColMenu\"\n          style={{ maxHeight: \"600px\" }}\n        >\n          {!column && !hasJoinCols && (\n            <FlexRowWrap>\n              <FlexCol className=\"gap-p25\">\n                <Btn\n                  variant=\"faded\"\n                  color=\"action\"\n                  data-command=\"AddComputedColMenu.countOfAllRows\"\n                  onClick={() => {\n                    this.setState({\n                      funcDef: CountAllFunc,\n                    });\n                  }}\n                >\n                  Add count of all rows\n                </Btn>\n                <div\n                  className=\"text-0p75 p-p25 ta-left font-14\"\n                  style={{ maxWidth: \"250px\" }}\n                >\n                  Will show total row counts grouped by the selected columns\n                </div>\n              </FlexCol>\n              {!funcDef && <div>OR</div>}\n            </FlexRowWrap>\n          )}\n          {funcDef?.key === CountAllFunc.key ?\n            null\n          : column ?\n            <div className=\"flex-col  f-0 min-h-fit  \">\n              <label className=\"noselect f-0 text-1p5 ta-left  mb-p5 \">\n                Column\n              </label>\n              <Btn\n                variant=\"faded\"\n                color=\"action\"\n                iconPosition=\"right\"\n                iconPath={mdiChevronDown}\n                onClick={() => {\n                  this.setState({ column: undefined });\n                }}\n              >\n                {column}\n              </Btn>\n            </div>\n          : <SearchList\n              id=\"cols-elect\"\n              className=\"  f-1\"\n              label={\n                funcDef ? `Columns for ${funcDef.label}` : `Choose a column`\n              }\n              items={allowedColumnsForFunction.map((c) => ({\n                ...getColumnListItem(c),\n                onPress: () => {\n                  this.setState({ column: c.name });\n                },\n              }))}\n            />\n          }\n\n          {!column ?\n            null\n          : funcDef ?\n            <div className=\"flex-col\">\n              <label className=\"noselect f-0 text-1p5 ta-left  mb-p5 \">\n                Function\n              </label>\n              <Btn\n                variant=\"faded\"\n                color=\"action\"\n                iconPosition=\"right\"\n                iconPath={mdiChevronDown}\n                onClick={() => {\n                  this.setState({ funcDef: undefined });\n                }}\n              >\n                {funcDef.label}\n              </Btn>\n            </div>\n          : <FlexCol className=\"gap-p25 \">\n              <Label label=\"Function\" variant=\"normal\" />\n              <FunctionSelector\n                column={column}\n                wColumns={w.columns ?? undefined}\n                currentNestedColumnName={\n                  this.props.nestedColumnOpts?.config.name\n                }\n                tableColumns={table.columns}\n                onSelect={(newFuncDef) => {\n                  this.setState({\n                    funcDef: newFuncDef,\n                    args: {},\n                  });\n                }}\n              />\n            </FlexCol>\n          }\n        </FlexCol>\n\n        {funcDef?.key === \"$template_string\" && (\n          <>\n            <FormField\n              className=\"mt-1\"\n              value={args?.$template_string ?? \"\"}\n              label=\"Template string\"\n              hint={\n                template_string_hint ??\n                \"Use column names. E.g.: Dear {FirstName} {LastName}\"\n              }\n              error={template_string_error}\n              onChange={async ($template_string) => {\n                let template_string_hint: string | undefined = undefined;\n                if (tableHandler && tableHandler.find) {\n                  try {\n                    template_string_hint = (await tableHandler.find(\n                      {},\n                      {\n                        returnType: \"value\",\n                        limit: 1,\n                        select: {\n                          val: { $template_string: [$template_string] },\n                        },\n                      },\n                    )) as any as string;\n                    this.setState({\n                      args: { $template_string },\n                      template_string_error: undefined,\n                      template_string_hint,\n                    });\n                  } catch (template_string_error) {\n                    this.setState({ template_string_error });\n                  }\n                } else {\n                  this.setState({\n                    args: { $template_string },\n                    template_string_error: undefined,\n                  });\n                }\n              }}\n            />\n            <div className=\"flex-row-wrap\">\n              {columns.map((c, i) => (\n                <div key={i} className=\"p-p25\">{`{${c.name}}`}</div>\n              ))}\n            </div>\n          </>\n        )}\n\n        {funcDef?.key === \"$duration\" &&\n          (args?.$duration?.otherColumn ?\n            <FlexCol className=\"gap-p5 mt-1\">\n              <label className=\"noselect f-0 text-1p5 ta-left\">\n                Compare to\n              </label>\n              <Btn\n                variant=\"filled\"\n                style={{\n                  backgroundColor: \"rgb(0, 183, 255)\",\n                }}\n                onClick={() => {\n                  this.setState({ args: {} });\n                }}\n              >\n                {args.$duration.otherColumn}\n              </Btn>\n            </FlexCol>\n          : <SearchList\n              id=\"duration_othercolumn\"\n              label=\"Compare to column\"\n              items={columns\n                .filter(\n                  (c) =>\n                    c.name !== column && _PG_date.some((v) => v === c.udt_name),\n                )\n                .map((c) => ({\n                  key: c.name,\n                  label: c.label,\n                  onPress: () => {\n                    this.setState({\n                      args: {\n                        $duration: { otherColumn: c.name },\n                      },\n                    });\n                  },\n                }))}\n            />)}\n        {canAdd && (\n          <>\n            <FormField\n              label=\"Name\"\n              type=\"text\"\n              className=\"mt-1\"\n              data-command=\"AddComputedColMenu.name\"\n              value={name}\n              onChange={(name) => {\n                this.setState({ name });\n              }}\n            />\n            <Select\n              label={\"Add to\"}\n              value={addTo}\n              data-command=\"AddComputedColMenu.addTo\"\n              fullOptions={NEW_COL_POSITIONS}\n              onChange={(addTo) => this.setState({ addTo })}\n            />\n          </>\n        )}\n      </>\n    );\n    const footerButtons: FooterButton[] = [\n      { onClickClose: true, label: \"Cancel\", variant: \"outline\" },\n      {\n        label: \"Add\",\n        variant: \"filled\",\n        color: \"action\",\n        iconPath: mdiPlus,\n        className: \"ml-auto\",\n        \"data-command\": \"AddComputedColMenu.addBtn\",\n        disabledInfo: canAdd ? undefined : \"Some function inputs are missing\",\n        onClickPromise: async () => {\n          if (!name) {\n            alert(\"Provide a column name\");\n          } else if (columns.find((c) => c.name === name)) {\n            alert(\"Column name already in use: \" + name);\n          } else {\n            if (!funcDef) {\n              alert(\"Something went wrong. No function definition found\");\n              return;\n            }\n            const columnInfo =\n              !column ? undefined : columns.find((c) => c.name === column);\n            const { outType } = funcDef;\n            const outInfo = outType === \"sameAsInput\" ? columnInfo : outType;\n            if (!outInfo) {\n              throw new Error(\n                \"Cannot determine output data type for the computed column\",\n              );\n            }\n            const newComputedCol: ColumnConfig = {\n              name: name,\n              show: true,\n              width: 130,\n              computedConfig: {\n                funcDef,\n                column,\n                ...pickKeys(outInfo, [\"tsDataType\", \"udt_name\"]),\n                args: isEmpty(args) ? undefined : args,\n              },\n            };\n            const { select } = await getTableSelect(\n              { table_name: w.table_name, columns: [newComputedCol] },\n              tables,\n              db,\n              {},\n              true,\n            );\n            await tableHandler?.find?.({}, { select, limit: 0 });\n            this.onAdd(newComputedCol, addTo);\n            onClose();\n          }\n        },\n      },\n    ];\n    if (variant === \"no-popup\") {\n      const cancelButton: FooterButton | undefined =\n        this.state.funcDef ?\n          {\n            onClick: () => {\n              this.setState({ funcDef: undefined });\n            },\n            label: \"Cancel\",\n            variant: \"outline\",\n          }\n        : undefined;\n      return (\n        <FlexCol className=\"f-1\">\n          {content}\n          <FooterButtons\n            style={{ borderTop: \"unset\" }}\n            className=\"mt-auto mb-0\"\n            footerButtons={[cancelButton, footerButtons[1]]}\n          />\n        </FlexCol>\n      );\n    }\n    return (\n      <Popup\n        title=\"Add computed column\"\n        showFullscreenToggle={{}}\n        positioning=\"top-center\"\n        persistInitialSize={true}\n        clickCatchStyle={{ opacity: 1 }}\n        contentClassName=\"gap-2 p-1\"\n        rootChildClassname=\"f-1\"\n        footerButtons={footerButtons}\n        onClose={onClose}\n      >\n        {content}\n      </Popup>\n    );\n  }\n}\n\n/*\n\nconst basicFunctions = [\n  { key: \"$D\" },\n  { key: \"$dy\" },\n  { key: \"$Dy\" },\n  { key: \"$DD\" },\n  { key: \"$ID\" },\n  { key: \"$MM\" },\n  { key: \"$yy\" },\n  { key: \"$yr\" },\n  { key: \"$day\" },\n  { key: \"$Day\" },\n  { key: \"$dow\" },\n  { key: \"$mon\" },\n  { key: \"$Mon\" },\n  { key: \"$age\" },\n  { key: \"$md5\" },\n  { key: \"$left\" },\n  { key: \"$date\" },\n  { key: \"$time\" },\n  { key: \"$year\" },\n  { key: \"$yyyy\" },\n  { key: \"$trim\" },\n  { key: \"$ceil\" },\n  { key: \"$sign\" },\n  { key: \"$right\" },\n  { key: \"$DayNo\" },\n  { key: \"$dowUS\" },\n  { key: \"$month\" },\n  { key: \"$Month\" },\n  { key: \"$upper\" },\n  { key: \"$lower\" },\n  { key: \"$round\" },\n  { key: \"$floor\" },\n  { key: \"$count\" },\n  { key: \"$time12\" },\n  { key: \"$timeAM\" },\n  { key: \"$length\" },\n  { key: \"$MonthNo\" },\n  { key: \"$reverse\" },\n  { key: \"$initcap\" },\n  { key: \"$datetime\" },\n  { key: \"$timedate\" },\n  { key: \"$ST_AsText\" },\n  { key: \"$string_agg\" },\n  { key: \"$ST_AsGeoJSON\" },\n  { key: \"$date_trunc_day\" },\n  { key: \"$date_trunc_hour\" },\n  { key: \"$date_trunc_week\" },\n  { key: \"$date_trunc_year\" },\n  { key: \"$date_trunc_month\" },\n  { key: \"$date_trunc_8hour\" },\n  { key: \"$date_trunc_4hour\" },\n  { key: \"$date_trunc_2hour\" },\n  { key: \"$date_trunc_second\" },\n  { key: \"$date_trunc_minute\" },\n  { key: \"$date_trunc_decade\" },\n  { key: \"$date_trunc_6month\" },\n  { key: \"$date_trunc_4month\" },\n  { key: \"$date_trunc_2month\" },\n  { key: \"$date_trunc_quarter\" },\n  { key: \"$date_trunc_century\" },\n  { key: \"$date_trunc_6minute\" },\n  { key: \"$date_trunc_5minute\" },\n  { key: \"$date_trunc_4minute\" },\n  { key: \"$date_trunc_3minute\" },\n  { key: \"$date_trunc_2minute\" },\n  { key: \"$date_trunc_8second\" },\n  { key: \"$date_trunc_6second\" },\n  { key: \"$date_trunc_5second\" },\n  { key: \"$date_trunc_4second\" },\n  { key: \"$date_trunc_3second\" },\n  { key: \"$date_trunc_2second\" },\n  { key: \"$date_trunc_30minute\" },\n  { key: \"$date_trunc_15minute\" },\n  { key: \"$date_trunc_30second\" },\n  { key: \"$date_trunc_15second\" },\n  { key: \"$date_trunc_10second\" },\n  { key: \"$date_trunc_millennium\" },\n  { key: \"$date_trunc_microseconds\" },\n  { key: \"$date_trunc_milliseconds\" },\n] as const;\n\n\n | $datetime  :[column_name] -> get timestamp formated as YYYY-MM-DD HH24:MI\n | $timedate  :[column_name] -> get timestamp formated as HH24:MI YYYY-MM-DD\n | $D  :[column_name] -> get timestamp formated as D\n | $dy  :[column_name] -> get timestamp formated as dy\n | $Dy  :[column_name] -> get timestamp formated as Dy\n | $DD  :[column_name] -> get timestamp formated as DD\n | $ID  :[column_name] -> get timestamp formated as ID\n | $MM  :[column_name] -> get timestamp formated as MM\n | $MonthNo  :[column_name] -> get timestamp formated as MM\n | $yy  :[column_name] -> get timestamp formated as yy\n | $yr  :[column_name] -> get timestamp formated as yy\n | $day  :[column_name] -> get timestamp formated as day\n | $Day  :[column_name] -> get timestamp formated as Day\n | $dow  :[column_name] -> get timestamp formated as ID\n | $mon  :[column_name] -> get timestamp formated as mon\n | $Mon  :[column_name] -> get timestamp formated as Mon\n | $DayNo  :[column_name] -> get timestamp formated as DD\n | $dowUS  :[column_name] -> get timestamp formated as D\n | $month  :[column_name] -> get timestamp formated as month\n | $Month  :[column_name] -> get timestamp formated as Month\n | $date  :[column_name] -> get timestamp formated as YYYY-MM-DD\n | $time  :[column_name] -> get timestamp formated as HH24:MI\n | $year  :[column_name] -> get timestamp formated as yyyy\n | $yyyy  :[column_name] -> get timestamp formated as yyyy\n | $time12  :[column_name] -> get timestamp formated as HH:MI\n | $timeAM  :[column_name] -> get timestamp formated as HH:MI AM\n | $to_char  :[column_name, format<string>] -> format dates and strings. Eg: [current_timestamp, 'HH12:MI:SS']\n | $age\n\n\n\n\n | $left  :[column_name, number] -> substring\n | $trim\n | $ceil\n | $sign\n | $upper\n | $lower\n | $round\n | $floor\n | $count\n | $length\n | $reverse\n | $initcap\n | $json_agg\n | $countAll agg :[]  COUNT of all rows\n | $md5_multi  :[...column_names] -> md5 hash of the column content\n | $date_part  :[unit<string>, column_name] ->  extract date unit as float8.  E.g. ['hour', col]\n | $array_agg\n | $diff_perc\n | $date_trunc  :[unit<string>, column_name] ->  round down timestamp to closest unit value.  E.g. ['hour', col]\n | $string_agg\n\n | $ST_AsGeoJSON  :[column_name] -> json GeoJSON output of a geometry column\n | $md5_multi_agg  :[...column_names] -> md5 hash of the string aggregation of column content\n | $date_trunc_day  :[column_name] -> round down timestamp to closest  day\n | $term_highlight  :[column_names<string[] | \"*\">, search_term<string>, opts?<{ edgeTruncate?: number; noFields?: boolean }>] -> get case-insensitive text match highlight\n | $date_trunc_hour  :[column_name] -> round down timestamp to closest  hour\n | $date_trunc_week  :[column_name] -> round down timestamp to closest  week\n | $date_trunc_year  :[column_name] -> round down timestamp to closest  year\n | $sha256_multi_agg  :[...column_names] -> sha256 hash of the string aggregation of column content\n | $sha512_multi_agg  :[...column_names] -> sha512 hash of the string aggregation of column content\n | $date_trunc_month  :[column_name] -> round down timestamp to closest  month\n | $date_trunc_8hour  :[column_name] -> round down timestamp to closest 8 hour\n | $date_trunc_4hour  :[column_name] -> round down timestamp to closest 4 hour\n | $date_trunc_2hour  :[column_name] -> round down timestamp to closest 2 hour\n | $date_trunc_second  :[column_name] -> round down timestamp to closest  second\n | $date_trunc_minute  :[column_name] -> round down timestamp to closest  minute\n | $date_trunc_decade  :[column_name] -> round down timestamp to closest  decade\n | $date_trunc_6month  :[column_name] -> round down timestamp to closest 6 month\n | $date_trunc_4month  :[column_name] -> round down timestamp to closest 4 month\n | $date_trunc_2month  :[column_name] -> round down timestamp to closest 2 month\n | $ts_headline_simple  :[column_name <string>, search_term: <string | { to_tsquery: string } > ] -> sha512 hash of the of column content\n | $date_trunc_quarter  :[column_name] -> round down timestamp to closest  quarter\n | $date_trunc_century  :[column_name] -> round down timestamp to closest  century\n | $date_trunc_6minute  :[column_name] -> round down timestamp to closest 6 minute\n | $date_trunc_5minute  :[column_name] -> round down timestamp to closest 5 minute\n | $date_trunc_4minute  :[column_name] -> round down timestamp to closest 4 minute\n | $date_trunc_3minute  :[column_name] -> round down timestamp to closest 3 minute\n | $date_trunc_2minute  :[column_name] -> round down timestamp to closest 2 minute\n | $date_trunc_8second  :[column_name] -> round down timestamp to closest 8 second\n | $date_trunc_6second  :[column_name] -> round down timestamp to closest 6 second\n | $date_trunc_5second  :[column_name] -> round down timestamp to closest 5 second\n | $date_trunc_4second  :[column_name] -> round down timestamp to closest 4 second\n | $date_trunc_3second  :[column_name] -> round down timestamp to closest 3 second\n | $date_trunc_2second  :[column_name] -> round down timestamp to closest 2 second\n | $date_trunc_30minute  :[column_name] -> round down timestamp to closest 30 minute\n | $date_trunc_15minute  :[column_name] -> round down timestamp to closest 15 minute\n | $date_trunc_30second  :[column_name] -> round down timestamp to closest 30 second\n | $date_trunc_15second  :[column_name] -> round down timestamp to closest 15 second\n | $date_trunc_10second  :[column_name] -> round down timestamp to closest 10 second\n | $date_trunc_millennium  :[column_name] -> round down timestamp to closest  millennium\n | $date_trunc_microseconds  :[column_name] -> round down timestamp to closest  microseconds\n | $date_trunc_milliseconds  :[column_name] -> round down timestamp to closest  milliseconds\n\n | $ts_headline_english  :[column_name <string>, search_term: <string | { to_tsquery: string } > ] -> sha512 hash of the of column content\n\n\n */\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AddComputedColumn/FunctionColumnList.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { mdiCircleHalf, mdiSetLeftCenter } from \"@mdi/js\";\nimport type { ValidatedColumnInfo } from \"prostgles-types/lib\";\nimport React from \"react\";\nimport { getColumnListItem } from \"../ColumnSelect/getColumnListItem\";\nimport type { AddComputedColumnState } from \"./useAddComputedColumn\";\n\ntype P = Pick<\n  AddComputedColumnState,\n  \"allowedColumns\" | \"setIncludeJoins\" | \"includeJoins\"\n> & {\n  column: ValidatedColumnInfo | undefined;\n  onChange: (column: ValidatedColumnInfo | undefined) => void;\n};\n\nexport const FunctionColumnList = ({\n  column,\n  allowedColumns,\n  onChange,\n  includeJoins,\n  setIncludeJoins,\n}: P) => {\n  if (!allowedColumns) return;\n\n  return (\n    <>\n      {column ?\n        <Btn\n          style={{ minWidth: \"50px\" }}\n          label={{\n            label: \"Column\",\n            variant: \"normal\",\n            className: \"mb-p25\",\n          }}\n          variant=\"faded\"\n          color=\"action\"\n          onClick={() => {\n            onChange(undefined);\n          }}\n        >\n          {column.label || column.name}\n        </Btn>\n      : <SearchList\n          leftContent={\n            <Btn\n              title={\"Include joins: \" + (includeJoins ? \"On\" : \"Off\")}\n              iconPath={mdiSetLeftCenter}\n              color={includeJoins ? \"action\" : undefined}\n              onClick={() => setIncludeJoins(!includeJoins)}\n            />\n          }\n          style={{ maxHeight: \"500px\" }}\n          id=\"cols-select\"\n          label=\"Applicable columns\"\n          placeholder=\"Search columns\"\n          className=\"f-1\"\n          inputProps={{\n            autoFocus: true,\n            \"data-command\": \"FunctionColumnList.SearchInput\",\n          }}\n          items={allowedColumns.map((c) => ({\n            ...getColumnListItem(c),\n            key: c.label,\n            parentLabels: c.join?.labels.map(({ label }) => label) ?? [],\n            // label: c.label || c.name,\n            onPress: () => {\n              onChange(c);\n            },\n          }))}\n        />\n      }\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AddComputedColumn/QuickAddComputedColumn.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { mdiFunction, mdiSigma } from \"@mdi/js\";\nimport React from \"react\";\nimport { t } from \"src/i18n/i18nUtils\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\nimport type { ColumnConfigWInfo } from \"../../W_Table\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport { FunctionExtraArguments } from \"../FunctionSelector/FunctionExtraArguments\";\nimport { FunctionSelector } from \"../FunctionSelector/FunctionSelector\";\nimport { FunctionColumnList } from \"./FunctionColumnList\";\nimport { useAddComputedColumnState } from \"./useAddComputedColumn\";\n\nexport type QuickAddComputedColumnProps = {\n  tableName: string;\n  existingColumn: ColumnConfigWInfo | undefined;\n  onAddColumn: (newColumn: ColumnConfig | undefined) => void;\n};\n\nexport const QuickAddComputedColumn = ({\n  tableName,\n  onAddColumn,\n  existingColumn,\n}: QuickAddComputedColumnProps) => {\n  const state = useAddComputedColumnState({\n    tableName,\n    onAddColumn,\n    existingColumn,\n  });\n  const { db } = usePrgl();\n  const {\n    table,\n    allowedColumns,\n    column,\n    funcDef,\n    name,\n    setColumn,\n    setFuncDef,\n    setName,\n    args,\n    setArgs,\n  } = state;\n\n  if (!table) return <>Table not found {tableName}</>;\n\n  return (\n    <FlexCol data-command=\"QuickAddComputedColumn\" className=\"min-h-0 gap-2\">\n      <p className=\"m-0 ta-left\">\n        A computed column is a column that is calculated based on other data\n        from the table.\n        <br></br>\n        It will not be stored in the database but will be calculated on the fly\n        when queried.\n      </p>\n\n      {funcDef ?\n        <Btn\n          style={{ minWidth: \"50px\" }}\n          label={{ label: \"Function\", variant: \"normal\", className: \"mb-p25\" }}\n          variant=\"faded\"\n          color=\"action\"\n          iconPath={funcDef.isAggregate ? mdiSigma : mdiFunction}\n          onClick={() => {\n            setFuncDef(undefined);\n          }}\n        >\n          {funcDef.label}\n        </Btn>\n      : <FunctionSelector\n          column={undefined}\n          wColumns={undefined}\n          tableColumns={table.columns}\n          onSelect={(funcDef) => {\n            setFuncDef(funcDef);\n          }}\n        />\n      }\n\n      {funcDef?.requiresArg && (\n        <FunctionExtraArguments\n          argName={funcDef.requiresArg}\n          args={args}\n          onChange={setArgs}\n          columnName={undefined}\n          db={db}\n          table={table}\n        />\n      )}\n\n      {allowedColumns && funcDef && (\n        <FunctionColumnList\n          allowedColumns={allowedColumns}\n          column={column}\n          onChange={setColumn}\n          setIncludeJoins={state.setIncludeJoins}\n          includeJoins={state.includeJoins}\n        />\n      )}\n\n      {!state.onAddDisabledInfo && (\n        <FormField\n          label=\"Computed column name\"\n          value={name}\n          inputProps={{\n            autoFocus: !funcDef?.requiresArg,\n            \"data-command\": \"QuickAddComputedColumn.name\",\n          }}\n          onChange={setName}\n        />\n      )}\n\n      <FlexRow className=\"mt-1\">\n        <Btn onClick={() => onAddColumn(undefined)}>{t.common.Cancel}</Btn>\n        <Btn\n          variant=\"filled\"\n          color=\"action\"\n          data-command=\"QuickAddComputedColumn.Add\"\n          disabledInfo={state.onAddDisabledInfo}\n          onClick={state.onAdd}\n        >\n          {existingColumn ?\n            t.common.Update\n          : t.AddColumnMenu[\"Add Computed Field\"]}\n        </Btn>\n      </FlexRow>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AddComputedColumn/useAddComputedColumn.ts",
    "content": "import { isDefined, pickKeys } from \"prostgles-types\";\nimport type { ValidatedColumnInfo } from \"prostgles-types/lib\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport {\n  FUCTION_DEFINITIONS,\n  getColumnsAcceptedByFunction,\n  type FuncDef,\n} from \"../FunctionSelector/functions\";\nimport { getAllJoins } from \"../JoinPathSelectorV2\";\nimport type { QuickAddComputedColumnProps } from \"./QuickAddComputedColumn\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\n\nexport const useAddComputedColumnState = ({\n  tableName,\n  onAddColumn,\n  existingColumn,\n}: Pick<\n  QuickAddComputedColumnProps,\n  \"tableName\" | \"onAddColumn\" | \"existingColumn\"\n>) => {\n  const { tables } = usePrgl();\n  const table = useMemo(\n    () => tables.find((t) => t.name === tableName),\n    [tables, tableName],\n  );\n  const [includeJoins, setIncludeJoins] = useState(true);\n\n  const existingColumnInfo = useMemo(() => {\n    if (!existingColumn?.computedConfig || !table) return undefined;\n    const config = existingColumn.computedConfig;\n    return {\n      args: config.args,\n      funcDef:\n        FUCTION_DEFINITIONS.find((f) => f.key === config.funcDef.key) ||\n        undefined,\n      column: table.columns.find((c) => c.name === config.column),\n    };\n  }, [existingColumn, table]);\n\n  const [funcDef, setFuncDef] = useState<FuncDef | undefined>(\n    existingColumnInfo?.funcDef,\n  );\n  const [args, setArgs] = useState<\n    Required<ColumnConfig>[\"computedConfig\"][\"args\"]\n  >(existingColumnInfo?.args);\n\n  const allowedColumns: undefined | ValidatedColumnInfoWithJoin[] =\n    useMemo(() => {\n      if (!funcDef || !table) return undefined;\n      const tableColumns = getColumnsAcceptedByFunction(funcDef, table.columns);\n      if (!includeJoins || !tableColumns) {\n        return tableColumns;\n      }\n\n      const { allJoins } = getAllJoins({ tableName, tables, value: undefined });\n\n      const joinedColumns = allJoins\n        .map((join) => {\n          const { table: joinTable } = join;\n          const joinColumns = getColumnsAcceptedByFunction(\n            funcDef,\n            joinTable.columns,\n          );\n          if (!joinColumns?.length) return;\n\n          return joinColumns.map((col) => ({\n            ...col,\n            join,\n            label: `${join.label}.${col.label || col.name}`,\n          }));\n        })\n        .filter(isDefined)\n        .flat();\n\n      return [...tableColumns, ...joinedColumns];\n    }, [funcDef, includeJoins, table, tableName, tables]);\n\n  const [column, setColumn] = useState<ValidatedColumnInfoWithJoin | undefined>(\n    existingColumnInfo?.column,\n  );\n  const [name, setName] = useState(existingColumn?.name || \"\");\n\n  useEffect(() => {\n    if (!funcDef) {\n      return;\n    }\n    const name =\n      column ?\n        `${funcDef.label}( ${[column.join?.table.label, column.name].filter(isDefined).join(\".\")} )`\n      : funcDef.label;\n    setName(name);\n  }, [funcDef, column]);\n\n  const addColumn = useCallback(\n    (column: ValidatedColumnInfoWithJoin | undefined, funcDef: FuncDef) => {\n      const { outType } = funcDef;\n      const outInfo = outType === \"sameAsInput\" ? column : outType;\n      if (!outInfo) {\n        throw new Error(\n          \"Cannot determine output data type for the computed column\",\n        );\n      }\n\n      if (column?.join) {\n        onAddColumn({\n          name,\n          nested: {\n            columns: [\n              {\n                name,\n                show: true,\n                computedConfig: {\n                  ...pickKeys(outInfo, [\"tsDataType\", \"udt_name\"]),\n                  funcDef,\n                  column: column.name,\n                },\n              },\n              ...column.join.table.columns.map((c) => ({\n                name: c.name,\n                show: false,\n              })),\n            ],\n            path: column.join.path,\n          },\n          show: true,\n        });\n      } else {\n        onAddColumn({\n          name,\n          show: true,\n          computedConfig: {\n            ...pickKeys(outInfo, [\"tsDataType\", \"udt_name\"]),\n            funcDef,\n            args,\n            column: column?.name,\n          },\n        });\n      }\n    },\n    [onAddColumn, name, args],\n  );\n\n  const [onAddDisabledInfo, onAdd] =\n    !funcDef ? [\"Must select a function\", undefined]\n    : funcDef.requiresArg && !args?.[funcDef.requiresArg] ?\n      [`Must provide argument: ${funcDef.requiresArg}`, undefined]\n    : allowedColumns && !column ? [\"Must select a column\", undefined]\n    : [undefined, () => addColumn(column, funcDef)];\n\n  return {\n    table,\n    includeJoins,\n    setIncludeJoins,\n    name,\n    setName,\n    column,\n    setColumn,\n    allowedColumns,\n    setFuncDef,\n    funcDef,\n    onAddDisabledInfo,\n    onAdd,\n    args,\n    setArgs,\n  };\n};\n\nexport type ValidatedColumnInfoWithJoin = ValidatedColumnInfo & {\n  join?: ReturnType<typeof getAllJoins>[\"allJoins\"][0];\n};\n\nexport type AddComputedColumnState = ReturnType<\n  typeof useAddComputedColumnState\n>;\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AlterColumn/AlterColumn.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { IconPalette } from \"@components/IconPalette/IconPalette\";\nimport type { DBSchemaTable } from \"prostgles-types\";\nimport { asName, getKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport type { Prgl } from \"../../../../App\";\nimport { getStringFormat } from \"../../../../utils/utils\";\nimport type { CommonWindowProps } from \"../../../Dashboard/Dashboard\";\nimport { debounce } from \"../../../Map/DeckGLWrapped\";\nimport type { DeltaOf } from \"../../../RTComp\";\nimport RTComp from \"../../../RTComp\";\nimport { SQLSmartEditor } from \"../../../SQLEditor/SQLSmartEditor\";\nimport { AlterColumnFileOptions } from \"./AlterColumnFileOptions\";\nimport {\n  type ColumnConstraint,\n  getColumnConstraints,\n} from \"./alterColumnUtilts\";\nimport { ColumnEditor } from \"./ColumnEditor\";\nimport { getAlterFkeyQuery } from \"./ReferenceEditor\";\n\nexport type AlterColumnProps = Pick<CommonWindowProps, \"suggestions\"> & {\n  prgl: Prgl;\n  table: DBSchemaTable;\n  field: string;\n  onClose: VoidFunction;\n};\n\ntype S = {\n  query: string;\n  queryAge: number;\n  error?: string;\n  hint?: string;\n  editableQuery: boolean;\n  running: boolean;\n  onSuccess: () => void;\n  field: string;\n  edited?: {\n    name?: string;\n    isPkey?: boolean;\n    dataType?: string;\n    notNull?: boolean;\n    defaultValue?: string;\n    references?: {\n      ftable: string;\n      onDelete?: string;\n      onUpdate?: string;\n    };\n  };\n  showMore?: boolean;\n  constraints?: ColumnConstraint[];\n  showCreateQuery?: boolean;\n  fKeysLoaded?: boolean;\n\n  defaultValue?: any;\n  notNull?: boolean;\n  dataType?: string;\n  newName?: string;\n};\n\nexport class AlterColumn extends RTComp<AlterColumnProps, S> {\n  state: S = {\n    query: \"\",\n    queryAge: 0,\n    error: \"\",\n    hint: \"\",\n    field: \"\",\n    editableQuery: true,\n    running: false,\n    onSuccess: () => {},\n  };\n\n  setConstraints = debounce(async () => {\n    const {\n      table,\n      prgl: { db },\n      field,\n    } = this.props;\n    if (!db.sql || !field) return;\n    const constraints = await getColumnConstraints(table.name, field, db.sql);\n    this.setState({ constraints });\n  }, 100);\n\n  onDeltaCombined(delta: DeltaOf<AlterColumnProps> & DeltaOf<S>) {\n    const deltaKeys = getKeys(delta ?? {});\n    if (deltaKeys.includes(\"queryAge\") || !this.state.constraints) {\n      this.setConstraints();\n    }\n  }\n\n  onNewDataType = async (\n    args: Pick<Required<S>[\"edited\"], \"dataType\" | \"notNull\" | \"defaultValue\">,\n  ) => {\n    const {\n      table,\n      prgl: { db },\n    } = this.props;\n\n    const field = JSON.stringify(this.state.field || this.props.field);\n\n    const col = table.columns.find((c) => c.name === field);\n\n    const tableName = table.name;\n\n    const {\n      dataType: newType,\n      defaultValue,\n      notNull,\n    } = { ...this.state, ...args };\n\n    const usingCol = ` NULLIF(${field}::TEXT, '') `;\n    let using = `USING  ${usingCol}::${newType}`;\n\n    const alterColQuery =\n      `ALTER TABLE ${tableName} \\n` + `ALTER COLUMN ${field} \\n`;\n\n    let query = \"\";\n\n    if (\n      typeof newType === \"string\" &&\n      newType !== col?.data_type.toUpperCase()\n    ) {\n      if (newType.startsWith(\"TIMESTAMP\") || newType === \"DATE\") {\n        using = `USING to_timestamp(${usingCol}, 'YYYY-MM-DD HH:MI:SS')::${newType}`;\n\n        const value = await db.sql!(\n          `SELECT ${field} FROM ${tableName} WHERE  ${field} IS NOT NULL LIMIT 1`,\n          {},\n          { returnType: \"value\" },\n        );\n\n        if (value) {\n          if (isNumeric(value)) {\n            using = `USING to_timestamp(cast(${usingCol} as BIGINT))::${newType}`;\n          } else {\n            const format = getStringFormat(value);\n            const formatStr = format\n              .map((f) =>\n                f.type === \"n\" ? \"N\".repeat(f.len) : `${f.val}`.repeat(f.len),\n              )\n              .join(\"\");\n            const knownFormats = [\n              { format: \"NN NN NNNN\", pg_format: \"DD MM YYYY HH24:MI:SS\" },\n              {\n                format: \"NN NN NNNN NN:NN:NN\",\n                pg_format: \"DD MM YYYY HH24:MI:SS\",\n              },\n              {\n                format: \"NN NN NNNN NN:NN:NN.NNNZ\",\n                pg_format: \"DD MM YYYY HH24:MI:SS.MSZ\",\n              },\n              {\n                format: \"NNNN-NN-NN NN:NN:NN\",\n                pg_format: \"YYYY-MM-DD HH24:MI:SS\",\n              },\n              {\n                format: \"NNNN-NN-NNTNN:NN:NN\",\n                pg_format: \"YYYY-MM-DDTHH24:MI:SS\",\n              },\n              {\n                format: \"NNNN-NN-NN NN:NN:NN.NNNZ\",\n                pg_format: \"YYYY-MM-DD HH24:MI:SS.MSZ\",\n              },\n              {\n                format: \"NNNN-NN-NNTNN:NN:NN.NNNZ\",\n                pg_format: \"YYYY-MM-DDTHH24:MI:SS.MSZ\",\n              },\n            ];\n            const pgFormat = knownFormats.find((kf) => formatStr === kf.format);\n            const mask = (pgFormat ?? knownFormats[0]!).pg_format;\n            using = `USING to_timestamp(${usingCol}, '${mask}')::${newType}`;\n          }\n        }\n\n        if (newType === \"TIMESTAMPZ\") using += `at time zone 'utc'`;\n      } else if (newType.startsWith(\"GEO\")) {\n        using =\n          `USING st_transform( \\n` +\n          ` st_setsrid( \\n` +\n          `   st_geometryfromtext( \\n` +\n          `        'POINT' || replace(${usingCol}, ',', ' ')  \\n` +\n          `   ), \\n` +\n          `   27700 \\n` +\n          ` ), \\n` +\n          ` 4326 \\n` +\n          `)`;\n      }\n\n      query = alterColQuery + `TYPE ${newType} \\n` + using + \";\";\n    }\n\n    if (typeof notNull === \"boolean\" && !notNull !== col?.is_nullable) {\n      query +=\n        `\\n\\n` + alterColQuery + (notNull ? \"SET NOT NULL;\" : \"DROP NOT NULL;\");\n    }\n\n    if (defaultValue !== undefined) {\n      query +=\n        `\\n\\n` +\n        alterColQuery +\n        `SET DEFAULT ${col?.tsDataType === \"string\" ? `'${defaultValue}'` : defaultValue};`;\n    }\n\n    this.setState({\n      edited: { dataType: newType },\n      editableQuery: true,\n      query,\n      ...args,\n      error: undefined,\n    });\n  };\n\n  render() {\n    const { table, prgl } = this.props;\n    const { db, tables } = prgl;\n\n    const isCreate = !this.props.field;\n    const field = this.state.field || this.props.field;\n\n    const col = table.columns.find((c) => c.name === field);\n\n    const { edited, query = \"\", constraints, showCreateQuery } = this.state;\n\n    const tableName = table.name;\n\n    const tName = tableName;\n    const cName = JSON.stringify(field);\n    const alter = `ALTER TABLE ${tName} \\n`;\n    const alterColQuery = `${alter}${!isCreate ? \"ALTER\" : \"ADD\"} COLUMN ${cName}\\n`;\n\n    const resetEdit = () => {\n      this.setState({ edited: undefined, query: \"\" });\n    };\n\n    const dropConstraint = (conName: string, endQuery = \"\") => {\n      const pkCon = constraints?.find((c) => c.constraint_name === conName);\n      if (pkCon) {\n        this.setState({\n          query: `${alter}DROP CONSTRAINT ${asName(pkCon.constraint_name)};\\n\\n${endQuery}`,\n        });\n      }\n    };\n\n    if (!col)\n      return <ErrorComponent error={`Column ${this.props.field} not found`} />;\n    const pkeyCons = constraints?.find(\n      (c) => c.constraint_type === \"PRIMARY KEY\",\n    );\n    const fkeyCons =\n      constraints?.filter((c) => c.constraint_type === \"FOREIGN KEY\") ?? [];\n\n    return (\n      <FlexCol className=\"f-1 flex-col p-1 o-auto\">\n        <FlexCol className=\"gap-2\">\n          <ColumnEditor\n            isAlter={true}\n            tableName={tableName}\n            suggestions={this.props.suggestions}\n            tables={tables}\n            name={col.name}\n            notNull={!col.is_nullable}\n            defaultValue={col.column_default}\n            isPkey={col.is_pkey}\n            references={fkeyCons.map((c) => ({\n              fCol: c.foreign_column_name!,\n              ftable: c.foreign_table_name!,\n              onDelete: c.delete_rule?.toUpperCase(),\n              onUpdate: c.update_rule?.toUpperCase(),\n            }))}\n            onAddReference={(c, r) => {\n              this.setState({\n                query: getAlterFkeyQuery({ ...r, col: field, tableName }),\n              });\n            }}\n            dataType={edited?.dataType ?? col.udt_name.toUpperCase()}\n            onEditReference={(r, i) => {\n              this.setState({\n                query: `${alter}DROP CONSTRAINT ${asName(fkeyCons[i]!.constraint_name)};\\n\\n${r ? getAlterFkeyQuery({ ...r, col: field, tableName }) : \"\"}`,\n              });\n            }}\n            onChange={(k, val) => {\n              if (k === \"name\") {\n                const { name: newName } = val;\n                this.setState({\n                  newName,\n                  edited: { name: newName },\n                  editableQuery: false,\n                  query: `ALTER TABLE ${tName} \\nRENAME COLUMN ${JSON.stringify(field)} \\nTO ${JSON.stringify(newName)} `,\n                });\n              } else if (k === \"dataType\") {\n                this.onNewDataType({ dataType: val.dataType });\n              } else if (k === \"isPkey\") {\n                if (!pkeyCons) {\n                  this.setState({\n                    edited: { isPkey: val.isPkey },\n                    query: `${alter}ADD PRIMARY KEY (${cName}) \\n`,\n                  });\n                } else {\n                  dropConstraint(pkeyCons.constraint_name);\n                }\n              } else if (k === \"notNull\") {\n                const { notNull } = val;\n                if (\n                  typeof notNull === \"boolean\" &&\n                  !notNull !== col.is_nullable\n                ) {\n                  this.setState({\n                    notNull,\n                    edited: { notNull },\n                    query:\n                      alterColQuery +\n                      (notNull ? \"SET NOT NULL;\" : \"DROP NOT NULL;\"),\n                  });\n                } else {\n                  resetEdit();\n                }\n              } else if (k === \"defaultValue\") {\n                const { defaultValue } = val;\n                if (defaultValue !== col.column_default) {\n                  const q =\n                    defaultValue === undefined ? \"DROP DEFAULT;\" : (\n                      `SET DEFAULT ${col.tsDataType === \"string\" ? `'${defaultValue}'` : defaultValue};`\n                    );\n                  this.setState({\n                    defaultValue,\n                    edited: { defaultValue },\n                    query: alterColQuery + q,\n                  });\n                } else {\n                  resetEdit();\n                }\n              }\n            }}\n          />\n          {table.info.fileTableName && (\n            <AlterColumnFileOptions\n              columnName={col.name}\n              tableName={tableName}\n              {...prgl}\n            />\n          )}\n          <IconPalette\n            label={{ label: \"Icon\" }}\n            iconName={\n              prgl.connection.table_options?.[tableName]?.columns?.[col.name]\n                ?.icon\n            }\n            onChange={async (iconName) => {\n              await prgl.dbs.connections.update(\n                {\n                  id: prgl.connection.id,\n                },\n                {\n                  table_options: {\n                    $merge: [\n                      {\n                        [tableName]: {\n                          columns: {\n                            [col.name]: {\n                              icon: iconName,\n                            },\n                          },\n                        },\n                      },\n                    ],\n                  },\n                },\n              );\n            }}\n          />\n          <FlexRow>\n            <Btn onClick={this.props.onClose} variant=\"outline\">\n              Close\n            </Btn>\n            <Btn\n              color=\"danger\"\n              variant=\"faded\"\n              onClick={() => {\n                this.setState({\n                  query: `${alter}DROP COLUMN ${cName}\\n\"delete this line to confirm\"`,\n                });\n              }}\n            >\n              DROP ...\n            </Btn>\n          </FlexRow>\n        </FlexCol>\n\n        {query && (!isCreate || showCreateQuery) && (\n          <SQLSmartEditor\n            query={query}\n            sql={db.sql!}\n            title={isCreate ? \"Create column query\" : \"Alter column query\"}\n            suggestions={this.props.suggestions}\n            onCancel={() => {\n              this.setState({ query: \"\" });\n            }}\n            onSuccess={() => {\n              this.props.onClose();\n              this.setState({\n                query: \"\",\n                queryAge: Date.now(),\n                edited: undefined,\n              });\n            }}\n          />\n        )}\n      </FlexCol>\n    );\n  }\n}\n\nexport const isNumeric = (str: any) => {\n  str = str + \"\";\n  if (typeof str != \"string\") return false; // we only process strings!\n  return (\n    !isNaN(str as unknown as number) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...\n    !isNaN(parseFloat(str))\n  ); // ...and ensure strings of whitespace fail\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AlterColumn/AlterColumnFileOptions.tsx",
    "content": "import { mdiFileCogOutline } from \"@mdi/js\";\nimport { useIsMounted } from \"prostgles-client\";\nimport React, { useState } from \"react\";\nimport { type Prgl } from \"../../../../App\";\nimport Btn from \"@components/Btn\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { FileColumnConfigEditor } from \"../../../FileTableControls/FileColumnConfigEditor\";\nimport { useFileTableConfigControls } from \"../../../FileTableControls/useFileTableConfigControls\";\n\ntype P = Pick<Prgl, \"db\" | \"tables\" | \"dbsMethods\" | \"dbs\" | \"connectionId\"> & {\n  tableName: string;\n  columnName: string;\n};\n\nexport const AlterColumnFileOptions = ({\n  columnName,\n  tableName,\n  tables,\n  db,\n  dbs,\n  dbsMethods,\n  connectionId,\n}: P) => {\n  const table = tables.find((t) => t.name === tableName);\n  const column = table?.columns.find((c) => c.name === columnName);\n  const {\n    connection,\n    database_config,\n    refsConfig,\n    setRefsConfig,\n    updateRefsConfig,\n    canUpdateRefColumns: canUpdate,\n  } = useFileTableConfigControls({ connectionId, db, dbs, dbsMethods });\n  const getIsMounted = useIsMounted();\n  const [error, setError] = useState<any>();\n  if (!column?.file || !connectionId || !connection || !database_config)\n    return null;\n\n  return (\n    <PopupMenu\n      button={\n        <Btn iconPath={mdiFileCogOutline} color=\"action\" variant=\"faded\">\n          Allowed files...\n        </Btn>\n      }\n      render={(pClose) => (\n        <FileColumnConfigEditor\n          columnName={columnName}\n          tableName={tableName}\n          refsConfig={refsConfig}\n          onChange={setRefsConfig}\n          onSetError={setError}\n        />\n      )}\n      footerButtons={(pClose) => [\n        {\n          label: \"Cancel\",\n          onClick: (e) => {\n            setRefsConfig(undefined);\n            pClose?.(e);\n          },\n        },\n        {\n          label: \"Save\",\n          color: \"action\",\n          variant: \"filled\",\n          disabledInfo:\n            error ? \"Must fix error\"\n            : !canUpdate ? \"No changes\"\n            : undefined,\n          onClickPromise: async (e) => {\n            await updateRefsConfig();\n            if (!getIsMounted()) return;\n            pClose?.(e);\n          },\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AlterColumn/ColumnEditor.tsx",
    "content": "import {\n  _PG_bool,\n  _PG_date,\n  _PG_geometric,\n  _PG_interval,\n  _PG_json,\n  _PG_numbers,\n  _PG_postgis,\n  _PG_strings,\n} from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { Select } from \"@components/Select/Select\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { CommonWindowProps } from \"../../../Dashboard/Dashboard\";\nimport type { DBSchemaTablesWJoins } from \"../../../Dashboard/dashboardUtils\";\nimport type { ColumnReference } from \"./ReferenceEditor\";\nimport { AddColumnReference, References } from \"./ReferenceEditor\";\nimport type { PG_DataType } from \"../../../SQLEditor/SQLCompletion/getPGObjects\";\n\nexport type ColumnOptions = {\n  name?: string;\n  isPkey?: boolean;\n  dataType?: string;\n  notNull?: boolean;\n  defaultValue?: string;\n  references?: ColumnReference[];\n};\n\ntype P = Pick<CommonWindowProps, \"suggestions\"> &\n  ColumnOptions & {\n    tables: DBSchemaTablesWJoins;\n    tableName: string;\n    onChange: (key: keyof ColumnOptions, change: ColumnOptions) => void;\n    onAddReference: (\n      c: ColumnOptions,\n      r: Required<ColumnOptions>[\"references\"][number],\n    ) => void;\n    onEditReference: (c: ColumnReference | undefined, index: number) => void;\n    isAlter: boolean;\n  };\n\nexport const ColumnEditorTestSelectors = {\n  columnName: \"\",\n} as const;\n\nexport const ColumnEditor = ({\n  onChange,\n  onAddReference,\n  tables,\n  onEditReference,\n  isAlter,\n  suggestions,\n  tableName,\n  ...colOpts\n}: P) => {\n  const {\n    dataType,\n    defaultValue,\n    isPkey,\n    name,\n    notNull,\n    references = [],\n  } = colOpts;\n\n  const DATA_TYPES = useMemo(() => {\n    type Item = { key: string; label: string; subLabel?: string };\n    const pgDataTypes: PG_DataType[] | undefined = suggestions?.suggestions\n      /** Must exclude arrays */\n      .filter((s) => s.dataTypeInfo && s.name.startsWith(\"_\"))\n      .map((dt) => dt.dataTypeInfo!)\n      .sort((a, b) => a.priority.localeCompare(b.priority));\n\n    const _dataTypes: Item[] =\n      pgDataTypes?.map(\n        (di) =>\n          ({\n            key:\n              [\"serial\", \"bigserial\"].includes(di.name.toLowerCase()) ?\n                di.name.toLowerCase()\n              : di.udt_name,\n            label: di.name,\n            subLabel: di.desc,\n          }) as Item,\n      ) ??\n      dataTypes\n        .concat([\"SERIAL\", \"BIGSERIAL\"].map((key) => ({ key })))\n        .map((dt) => ({ label: dt.key, ...dt }));\n\n    return _dataTypes.concat(\n      _dataTypes.map((dt) => ({\n        key: `_${dt.key}`,\n        label: `${dt.label}[]`,\n        subLabel: dt.subLabel,\n      })),\n    );\n  }, [suggestions]);\n\n  return (\n    <FlexCol className=\"ColumnEditor gap-2\">\n      <FlexRowWrap className=\"ai-end\">\n        <FormFieldDebounced\n          label=\"Column name\"\n          type=\"text\"\n          data-command=\"ColumnEditor.name\"\n          value={name}\n          onChange={(newName) => {\n            onChange(\"name\", { name: newName });\n          }}\n        />\n        {!name && (\n          <>\n            <div className=\"px-p25 py-1\">OR</div>\n            <AddColumnReference\n              existingReferences={references}\n              dataType={dataType}\n              tableName={tableName}\n              columnName={name}\n              tables={tables}\n              onAdd={(refCol, newRef) => {\n                onAddReference(\n                  {\n                    name: `${refCol.table.name}_id`,\n                    dataType: refCol.column.udt_name.toUpperCase(),\n                    references: [newRef],\n                  },\n                  newRef,\n                );\n              }}\n            />\n          </>\n        )}\n        <FlexRow className={!name ? \"hidden\" : \"\"}>\n          <Select\n            label=\"Data type\"\n            data-command=\"ColumnEditor.dataType\"\n            value={dataType}\n            fullOptions={DATA_TYPES}\n            btnProps={{ color: \"action\" }}\n            onChange={(dataType) => onChange(\"dataType\", { dataType })}\n          />\n          {!!dataType && (\n            <>\n              <SwitchToggle\n                variant=\"col\"\n                label={{\n                  label: \"Primary key\",\n                  info: `A primary key constraint indicates that a column, or group of columns, can be used as a unique identifier for rows in the table. This requires that the values be both unique and not null`,\n                }}\n                checked={!!isPkey}\n                onChange={(isPkey) => {\n                  onChange(\"isPkey\", { isPkey });\n                }}\n              />\n              <SwitchToggle\n                variant=\"col\"\n                label={{\n                  label: \"Not null\",\n                  info: \"If true then NULL values will not be accepted\",\n                }}\n                disabledInfo={\n                  isPkey ? \"Primary key constraint overrides this\" : undefined\n                }\n                checked={!!notNull}\n                onChange={(notNull) => {\n                  onChange(\"notNull\", { notNull });\n                }}\n              />\n            </>\n          )}\n        </FlexRow>\n      </FlexRowWrap>\n      <FlexCol className={!dataType ? \"hidden\" : \"\"}>\n        <FormFieldDebounced\n          id={\"defval\"}\n          label={{\n            label: \"Default value\",\n            info: \"Will be added to all new records if no other value is specified\",\n          }}\n          type=\"text\"\n          autoComplete=\"off\"\n          value={defaultValue}\n          optional={true}\n          onChange={(defaultValue) => {\n            onChange(\"defaultValue\", { defaultValue });\n          }}\n        />\n        {((colOpts.references?.length ?? 0) > 0 || isAlter) && (\n          <References\n            {...colOpts}\n            tableName={tableName}\n            tables={tables}\n            onAdd={(newRef) => {\n              onAddReference(colOpts, newRef);\n            }}\n            onChange={(r, i) => {\n              onEditReference(r, i);\n            }}\n          />\n        )}\n      </FlexCol>\n    </FlexCol>\n  );\n};\n\nconst dataTypeRename = {\n  INT2: \"SMALLINT\",\n  INT4: \"INTEGER\",\n  INT: \"INTEGER\",\n  INT8: \"BIGINT\",\n  FLOAT4: \"REAL\",\n  BOOL: \"BOOLEAN\",\n  SERIAL8: \"BIGSERIAL\",\n};\nexport const dataTypes = [\n  ...[\n    ..._PG_strings,\n    ..._PG_numbers,\n    ..._PG_json,\n    ..._PG_bool,\n    ..._PG_date,\n    ..._PG_interval,\n    ..._PG_postgis,\n    ..._PG_geometric,\n  ].map((key) => ({\n    key: dataTypeRename[key.toUpperCase()] ?? key.toUpperCase(),\n  })),\n] as const;\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AlterColumn/CreateColumn.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { DBSchemaTable } from \"prostgles-types\";\nimport { isDefined } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { t } from \"../../../../i18n/i18nUtils\";\nimport type { CommonWindowProps } from \"../../../Dashboard/Dashboard\";\nimport type { DBSchemaTablesWJoins } from \"../../../Dashboard/dashboardUtils\";\nimport { colIs } from \"../../../SmartForm/SmartFormField/fieldUtils\";\nimport { SQLSmartEditor } from \"../../../SQLEditor/SQLSmartEditor\";\nimport type { ColumnOptions } from \"./ColumnEditor\";\nimport { ColumnEditor } from \"./ColumnEditor\";\nimport { getAlterFkeyQuery } from \"./ReferenceEditor\";\n\nexport type CreateColumnProps = Pick<CommonWindowProps, \"suggestions\"> & {\n  table: DBSchemaTable;\n  field: string | undefined;\n  db: DBHandlerClient;\n  tables: DBSchemaTablesWJoins;\n  onClose: VoidFunction;\n};\n\nexport const CreateColumn = ({\n  db,\n  onClose,\n  suggestions,\n  table,\n  tables,\n}: CreateColumnProps) => {\n  const [col, setCol] = useState<ColumnOptions>({ name: \"\" });\n  const [query, setQuery] = useState(\"\");\n\n  return (\n    <FlexCol data-command=\"CreateColumn\" className=\"CreateColumn gap-2\">\n      {!query ?\n        <ColumnEditor\n          {...col}\n          tableName={table.name}\n          suggestions={suggestions}\n          isAlter={false}\n          tables={tables}\n          onChange={(k, v) => {\n            setCol({ ...col, ...v });\n          }}\n          onAddReference={(newCol, reference) => {\n            setCol({\n              ...newCol,\n              references: [...(col.references ?? []), reference],\n            });\n          }}\n          onEditReference={(r, i) => {\n            setCol({\n              ...col,\n              references: col\n                .references!.map((_r, _i) => {\n                  if (i === _i) {\n                    return r;\n                  }\n\n                  return _r;\n                })\n                .filter(isDefined),\n            });\n          }}\n        />\n      : <SQLSmartEditor\n          asPopup={false}\n          query={query}\n          sql={db.sql!}\n          title={t.CreateColumn[\"Create column query\"]}\n          suggestions={suggestions}\n          onCancel={() => {\n            setQuery(\"\");\n          }}\n          onSuccess={() => {\n            setQuery(\"\");\n            setCol({});\n            onClose();\n          }}\n        />\n      }\n      <FlexRow className={query ? \"hidden\" : \"\"}>\n        <Btn onClick={onClose} variant=\"outline\">\n          {t.common.Cancel}\n        </Btn>\n        <Btn\n          title={t.CreateColumn[\"Show create column query\"]}\n          data-command=\"CreateColumn.next\"\n          className=\"ml-auto\"\n          disabledInfo={\n            !col.name ? t.CreateColumn[\"New column name missing\"]\n            : !col.dataType ?\n              t.CreateColumn[\"Data type missing\"]\n            : undefined\n          }\n          variant=\"filled\"\n          color=\"action\"\n          onClick={() => {\n            const query = [\n              `ALTER TABLE ${table.name}`,\n              `ADD COLUMN ${getAddColumnDefinitionQuery(col, table.name)};`,\n            ].join(\"\\n\");\n            setQuery(query);\n          }}\n        >\n          {t.common.Next}\n        </Btn>\n      </FlexRow>\n    </FlexCol>\n  );\n};\nexport const getColumnDefinitionQuery = ({\n  dataType,\n  defaultValue,\n  isPkey,\n  name,\n  notNull,\n}: ColumnOptions) => {\n  const defVal =\n    colIs({ udt_name: dataType as any }, [\"_PG_numbers\", \"_PG_bool\"]) ?\n      defaultValue\n    : `'${defaultValue}'`;\n  return `${JSON.stringify(name)} ${dataType ?? \"\"}${isPkey ? \" PRIMARY KEY \" : \"\"}${notNull ? \" NOT NULL \" : \"\"}${defaultValue ? ` DEFAULT ${defVal}` : \"\"}`;\n};\n\nexport const getAddColumnDefinitionQuery = (\n  col: ColumnOptions,\n  tableName: string,\n) => {\n  return [\n    `${getColumnDefinitionQuery(col)};`,\n    ...(col.references ?? []).map(\n      (r) =>\n        getAlterFkeyQuery({\n          ...r,\n          col: col.name!,\n          tableName,\n        }) + \"\\n\",\n    ),\n  ].join(\"\\n\");\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AlterColumn/ReferenceEditor.tsx",
    "content": "import { mdiDelete, mdiLinkPlus } from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Label } from \"@components/Label\";\nimport { Select } from \"@components/Select/Select\";\nimport { REFERENCES_COL_OPTS } from \"../../../SQLEditor/SQLCompletion/TableKWDs\";\nimport type { ColumnOptions } from \"./ColumnEditor\";\nimport { isDefined } from \"../../../../utils/utils\";\nimport type { DBSchemaTablesWJoins } from \"../../../Dashboard/dashboardUtils\";\n\nconst FKEY_DOCS =\n  \"Constraint to ensure that every value from this column has a coresponding record in another table. The column in the other table must have unique values\";\n\nexport type ColumnReference = {\n  ftable: string;\n  fCol: string;\n  onDelete?: string;\n  onUpdate?: string;\n};\ntype ReferenceEditorProps = ColumnReference & {\n  notNullErr: \"u\" | \"d\" | undefined;\n  onChange: (newRef: ColumnReference | undefined) => void;\n};\n\nexport const onDeleteOptions = REFERENCES_COL_OPTS.filter((d) =>\n  d.kwd.startsWith(\"ON DELETE\"),\n).map((d) => ({ key: d.kwd.split(\" \").slice(2).join(\" \"), subLabel: d.docs }));\nexport const onUpdateOptions = REFERENCES_COL_OPTS.filter((d) =>\n  d.kwd.startsWith(\"ON UPDATE\"),\n).map((d) => ({ key: d.kwd.split(\" \").slice(2).join(\" \"), subLabel: d.docs }));\n\nexport const ReferenceEditor = ({\n  onChange,\n  notNullErr,\n  ...colOpts\n}: ReferenceEditorProps) => {\n  const { fCol, ftable, onDelete, onUpdate } = colOpts;\n  return (\n    <FlexRowWrap className=\"rounded b b-color p-p5\">\n      <Chip variant=\"header\" label={\"Foreign table\"} value={ftable} />\n      <Chip variant=\"header\" label={\"Foreign column\"} value={fCol} />\n      <Select\n        label=\"ON DELETE\"\n        className={\n          notNullErr === \"d\" ? \"b-2 b-danger p-p25 rounded\" : undefined\n        }\n        value={onDelete || \"NO ACTION\"}\n        fullOptions={onDeleteOptions}\n        onChange={(onDelete) => {\n          onChange({ ...colOpts, onDelete });\n        }}\n      />\n      <Select\n        label=\"ON UPDATE\"\n        className={\n          notNullErr === \"u\" ? \"b-2 b-danger p-p25 rounded\" : undefined\n        }\n        value={onUpdate || \"NO ACTION\"}\n        fullOptions={onUpdateOptions}\n        onChange={(onUpdate) => {\n          onChange({ ...colOpts, onUpdate });\n        }}\n      />\n      <Btn\n        iconPath={mdiDelete}\n        className=\"mb-auto\"\n        onClick={() => {\n          onChange(undefined);\n        }}\n      />\n    </FlexRowWrap>\n  );\n};\n\ntype P = ColumnOptions & {\n  onChange: (newRef: ColumnReference | undefined, index: number) => void;\n  onAdd: (newRef: ColumnReference) => void;\n  tables: DBSchemaTablesWJoins;\n  tableName: string;\n};\nexport const References = ({\n  onChange,\n  tables,\n  onAdd,\n  tableName,\n  ...opts\n}: P) => {\n  const references = opts.references?.map((r) => {\n    return {\n      ...r,\n      notNullErr:\n        opts.notNull ?\n          r.onDelete?.includes(\"SET NULL\") ? (\"d\" as const)\n          : r.onUpdate?.includes(\"SET NULL\") ? (\"u\" as const)\n          : undefined\n        : undefined,\n    };\n  });\n\n  return (\n    <FlexCol className=\"References gap-p25\">\n      <Label label=\"References\" variant=\"normal\" info={FKEY_DOCS} />\n      {references?.some((c) => c.notNullErr) && (\n        <InfoRow color=\"danger\">\n          Some foreign keys contain a SET NULL option and this column is not\n          nullable. This will lead to error\n        </InfoRow>\n      )}\n      {references?.map((r, index) => (\n        <ReferenceEditor\n          key={index}\n          {...r}\n          onChange={(newRef) => onChange(newRef, index)}\n        />\n      ))}\n      <AddColumnReference\n        tableName={tableName}\n        columnName={opts.name}\n        dataType={opts.dataType}\n        existingReferences={opts.references ?? []}\n        tables={tables}\n        variant=\"without-label\"\n        onAdd={(rcol, ref) => {\n          onAdd(ref);\n        }}\n      />\n    </FlexCol>\n  );\n};\n\ntype ReferencedColumn = {\n  table: DBSchemaTablesWJoins[number];\n  column: DBSchemaTablesWJoins[number][\"columns\"][number];\n};\ntype AddColumnReferenceProps = {\n  variant?: \"without-label\";\n  tables: DBSchemaTablesWJoins;\n  existingReferences: ColumnReference[];\n  tableName: string;\n  columnName: string | undefined;\n  dataType: string | undefined;\n  onAdd: (referencedColumn: ReferencedColumn, newRef: ColumnReference) => void;\n};\nexport const AddColumnReference = ({\n  tables,\n  variant,\n  onAdd,\n  tableName,\n  columnName,\n  dataType,\n  existingReferences,\n}: AddColumnReferenceProps) => {\n  const referenceableColumns = useMemo(\n    () =>\n      tables.flatMap((table) => {\n        return table.columns\n          .filter(\n            (c) =>\n              c.is_pkey && !(table.name === tableName && columnName === c.name),\n          )\n          .map((column) => ({\n            key: `${table.name}.${column.name}`,\n            label: `${table.name} (${column.name})`,\n            subLabel: column.udt_name,\n            disabledInfo:\n              (\n                existingReferences.some(\n                  (r) => r.ftable === table.name && r.fCol === column.name,\n                )\n              ) ?\n                \"Already referenced by this column\"\n              : (\n                dataType &&\n                column.udt_name.toUpperCase() !== dataType.toUpperCase()\n              ) ?\n                \"Column is not of same data type\"\n              : undefined,\n            table,\n            column,\n          }));\n      }),\n    [tables, existingReferences, dataType, tableName, columnName],\n  );\n\n  return (\n    <Select\n      className=\"AddColumnReference mt-1\"\n      label={\n        variant === \"without-label\" ? undefined : (\n          {\n            label: \"References\",\n            info: FKEY_DOCS,\n          }\n        )\n      }\n      btnProps={{\n        children: \"Add reference\",\n        iconPath: mdiLinkPlus,\n        color: \"action\",\n      }}\n      data-command=\"AddColumnReference\"\n      fullOptions={referenceableColumns}\n      onChange={(cKey) => {\n        const rCol = referenceableColumns.find((c) => c.key === cKey);\n        if (rCol) {\n          onAdd(rCol, {\n            ftable: rCol.table.name,\n            fCol: rCol.column.name,\n          });\n        }\n      }}\n    />\n  );\n};\n\nexport const getReferencesQuery = ({\n  ftable,\n  fCol,\n  onDelete = \"\",\n  onUpdate = \"\",\n}: ColumnReference) => {\n  return [\n    `REFERENCES ${JSON.stringify(ftable)} (${JSON.stringify(fCol)})`,\n    onDelete ? `ON DELETE ${onDelete}` : undefined,\n    onUpdate ? `ON UPDATE ${onUpdate}` : undefined,\n  ];\n};\n\nexport const getAlterFkeyQuery = (\n  arg: ColumnReference & { col: string; tableName: string },\n) => {\n  const { col, tableName } = arg;\n  return [\n    `ALTER TABLE ${JSON.stringify(tableName)}`,\n    `ADD FOREIGN KEY (${JSON.stringify(col)})`,\n    ...getReferencesQuery(arg),\n  ]\n    .filter(isDefined)\n    .join(\"\\n\");\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/AlterColumn/alterColumnUtilts.ts",
    "content": "import type { SQLHandler } from \"prostgles-types\";\n\nexport type ColumnConstraint = {\n  constraint_name: string;\n  table_name: string;\n  column_name: string;\n  data_type: string;\n  constraint_type: \"PRIMARY KEY\" | \"FOREIGN KEY\" | \"CHECK\" | \"UNIQUE\";\n  delete_rule: string | null;\n  update_rule: string | null;\n  foreign_table_schema: string | null;\n  foreign_table_name: string | null;\n  foreign_column_name: string | null;\n};\n\nexport const getColumnConstraints = (\n  tableName: string,\n  columnName: string,\n  sql: SQLHandler,\n): Promise<ColumnConstraint[]> => {\n  return sql(\n    `\n    SELECT DISTINCT \n      trim(constraint_type) as constraint_type, tc.constraint_name,\n      tc.table_schema, \n      tc.table_name, \n      kcu.column_name, \n      c.data_type,\n      rc.delete_rule,\n      rc.update_rule,\n      ccu.table_schema AS foreign_table_schema,\n      ccu.table_name AS foreign_table_name,\n      ccu.column_name AS foreign_column_name \n    FROM \n      information_schema.table_constraints AS tc \n      JOIN information_schema.key_column_usage AS kcu\n        ON tc.constraint_name = kcu.constraint_name\n        AND tc.table_schema = kcu.table_schema\n      JOIN information_schema.constraint_column_usage AS ccu\n        ON ccu.constraint_name = tc.constraint_name\n        AND ccu.table_schema = tc.table_schema\n      JOIN information_schema.columns AS c \n        ON c.table_schema = tc.table_schema\n        AND tc.table_name = c.table_name AND kcu.column_name = c.column_name\n      LEFT JOIN information_schema.referential_constraints rc \n        ON rc.constraint_name = tc.constraint_name \n        AND tc.table_schema = rc.constraint_schema\n      WHERE tc.table_name = $1 AND c.column_name = $2\n  `,\n    [tableName, columnName],\n    { returnType: \"rows\" },\n  ) as any;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColorPicker.tsx",
    "content": "import { mdiPalette } from \"@mdi/js\";\nimport React from \"react\";\nimport { isObject } from \"@common/publishUtils\";\nimport type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow, FlexRowWrap, classOverride } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { type LabelProps } from \"@components/Label\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { Command } from \"../../../Testing\";\nimport { getRandomElement } from \"./ColumnStyleControls/ColumnStyleControls\";\nimport { rgba2hex } from \"./rgba2hex\";\nimport { asHex, asRGB, type RGBA } from \"src/utils/colorUtils\";\n\ntype S = {\n  anchorEl: Element | null;\n};\n\nexport class ColorPicker extends React.Component<\n  {\n    style?: React.CSSProperties;\n    className?: string;\n    value: string;\n    onChange: (color: string, rgb: RGBA, rgb255Alpha: RGBA) => void;\n    btnProps?: BtnProps;\n    label?: string | LabelProps;\n    required?: boolean;\n    title?: string;\n    variant?: \"legend\";\n    \"data-command\"?: Command;\n  },\n  S\n> {\n  state: S = {\n    anchorEl: null,\n  };\n\n  asString = ([r, g, b, a = 1]: RGBA) => {\n    return `rgba(${[r, g, b, a]})`;\n  };\n\n  color?: string;\n  lastChanged = Date.now();\n  willChange: any;\n  onChange = (c: string) => {\n    this.color = c;\n\n    const { onChange } = this.props;\n    const thresh = 500;\n    const now = Date.now();\n\n    if (this.willChange || now - this.lastChanged < thresh) {\n      clearTimeout(this.willChange);\n      this.willChange = setTimeout(() => {\n        this.willChange = null;\n        this.onChange(this.color ?? \"\");\n      }, thresh);\n    } else {\n      onChange(asHex(this.color), asRGB(this.color), asRGB(this.color, \"255\"));\n      this.lastChanged = now;\n    }\n  };\n\n  render() {\n    const { anchorEl } = this.state;\n    const {\n      value,\n      style = {},\n      className = \"\",\n      onChange,\n      label,\n      variant,\n      btnProps,\n    } = this.props;\n\n    const rgba = asRGB(value);\n    const opacity = rgba[3] <= 1 ? rgba[3] : rgba[3] / 255;\n\n    const labelNode =\n      label ?\n        isObject(label) ? null\n        : <div className=\" noselect f-d1\">{label}</div>\n      : null;\n    const colorNode = (\n      <ColorCircle\n        {...btnProps}\n        label={isObject(label) ? label : undefined}\n        color={value}\n        onClick={(e) => {\n          this.setState({ anchorEl: e.currentTarget });\n        }}\n      />\n    );\n\n    return (\n      <FlexRow\n        data-command={this.props[\"data-command\"]}\n        className={classOverride(\"gap-p5 ai-center \", className)}\n        style={style}\n      >\n        {variant === \"legend\" ?\n          <>\n            {colorNode}\n            {labelNode}\n          </>\n        : <>\n            {labelNode}\n            {colorNode}\n          </>\n        }\n        {anchorEl && (\n          <Popup\n            title={\"Layer color\"}\n            anchorEl={anchorEl}\n            positioning=\"beneath-left\"\n            onClose={() => this.setState({ anchorEl: null })}\n            contentClassName=\"p-1 flex-col gap-1\"\n          >\n            <FlexRowWrap>\n              {COLOR_PALETTE.map((c, ci) => (\n                <Btn\n                  key={ci}\n                  data-key={c}\n                  className={\"pointer shadow\"}\n                  style={{\n                    width: \"24px\",\n                    height: \"24px\",\n                    backgroundColor: c,\n                    borderRadius: \"1000%\",\n                  }}\n                  onClick={(e) => {\n                    onChange(asHex(c), asRGB(c), asRGB(c, \"255\"));\n                    this.setState({ anchorEl: null });\n                  }}\n                />\n              ))}\n            </FlexRowWrap>\n            <FlexRow>\n              <FormField\n                label=\"Other\"\n                type=\"color\"\n                value={asHex(`rgb(${rgba.slice(0, -1).join(\",\")})`)}\n                style={{ opacity }}\n                onChange={(c) => this.onChange(c)}\n              />\n              <FormFieldDebounced\n                label=\"Opacity\"\n                type=\"number\"\n                value={rgba[3] || 1}\n                maxWidth={50}\n                inputProps={{\n                  step: 0.1,\n                  min: 0.1,\n                  max: 1,\n                }}\n                onChange={(opacity: number) => {\n                  const [r, g, b] = rgba;\n                  const colorStr = this.asString([r, g, b, opacity]);\n                  const hex = rgba2hex(colorStr);\n                  this.onChange(hex);\n                }}\n              />\n            </FlexRow>\n            {!this.props.required && (\n              <Btn\n                className=\"mt-1\"\n                onClick={() => {\n                  this.onChange(\"\");\n                }}\n              >\n                None\n              </Btn>\n            )}\n          </Popup>\n        )}\n      </FlexRow>\n    );\n  }\n}\n\nexport const ColorCircle = ({\n  color,\n  onClick,\n  label,\n  size,\n}: Pick<BtnProps, \"onClick\" | \"label\" | \"size\"> & { color: string }) => {\n  return (\n    <Btn\n      label={label}\n      size={size}\n      className={\"shadow b b-color f-0\"}\n      style={{ backgroundColor: color }}\n      onClick={onClick}\n      iconProps={{\n        path: mdiPalette,\n        style: {\n          /** button has bg which stacks when opacity < 1  */\n          opacity: 0,\n        },\n      }}\n    />\n  );\n};\n\nexport const COLORS = {\n  \"dark blue\": \"#174CFA\",\n  blue: \"#0AA1FA\",\n  cyan: \"#00D5FF\",\n  green: \"#089981\",\n  orange: \"#F79800\",\n  red: \"#f23645\",\n  purple: \"#CB11F0\",\n  indigo: \"#7430F0\",\n  gray: \"#cecece\",\n} as const;\n\nexport const COLOR_PALETTE = Object.values(COLORS);\n\nexport const COLOR_PALETTE_RGB = COLOR_PALETTE.map(\n  (c) => asRGB(c).slice(0, 3) as [number, number, number],\n);\n\nexport const getPaletteRGBColor = (layerIndex: number) => {\n  return (\n    COLOR_PALETTE_RGB[layerIndex] ?? getRandomElement(COLOR_PALETTE_RGB).elem\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnDisplayFormat/ChipStylePalette.tsx",
    "content": "import React from \"react\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport { StyledCell } from \"../../tableUtils/StyledTableColumn\";\n\ntype ChipStylePaletteProps = {\n  onChange: (chipStyle: {\n    color: string;\n    borderColor: string | undefined;\n    textColor: string;\n  }) => void;\n};\n\nexport const chipColors = [\n  { color: \"#E91E63\", borderColor: undefined, textColor: \"#ffffff\" }, // Red\n  { color: \"#9C27B0\", borderColor: undefined, textColor: \"#ffffff\" }, // Purple\n  { color: \"#673AB7\", borderColor: undefined, textColor: \"#ffffff\" }, // Deep Purple\n  { color: \"#2196F3\", borderColor: undefined, textColor: \"#ffffff\" }, // Blue\n  { color: \"#00BCD4\", borderColor: undefined, textColor: \"#ffffff\" }, // Cyan\n  { color: \"rgb(57 185 121)\", borderColor: undefined, textColor: \"#ffffff\" }, // Green\n  { color: \"rgb(189 171 0)\", borderColor: undefined, textColor: \"#ffffff\" }, // Yellow\n  { color: \"rgb(156 156 156)\", borderColor: undefined, textColor: \"#ffffff\" }, // Gray\n\n  // { color: '#2196F3', borderColor: undefined, textColor: undefined }, // Blue\n  // { color: '#03A9F4', borderColor: undefined, textColor: undefined }, // Light Blue\n  // { color: '#00BCD4', borderColor: undefined, textColor: undefined }, // Cyan\n];\n\nexport const chipColorsFadedBorder = [\n  {\n    color: \"#ffd0cd\",\n    borderColor: \"rgb(216 71 71)\",\n    textColor: \"#940000\",\n    textColorDarkMode: \"#ff004a\",\n  }, // Red\n  {\n    color: \"#f6beff\",\n    borderColor: \"rgb(172 64 211)\",\n    textColor: \"#490063\",\n    textColorDarkMode: \"#a95cc5\",\n  }, // Pink\n  {\n    color: \"#e5e9ff\",\n    borderColor: \"rgb(108 130 231)\",\n    textColor: \"#002fff\",\n    textColorDarkMode: \"#4f6ffb\",\n  }, // Purple\n  {\n    color: \"#c9e7ff7d\",\n    borderColor: \"rgb(120 189 243)\",\n    textColor: \"#0075d2\",\n    textColorDarkMode: \"#2386d5\",\n  }, // Blue\n\n  {\n    color: \"#00bcd42e\",\n    borderColor: \"rgb(93 186 198)\",\n    textColor: \"#009aad\",\n    textColorDarkMode: \"#0cbacf\",\n  }, // Indigo\n  {\n    color: \"#01d4002e\",\n    borderColor: \"#01d4008a\",\n    textColor: \"#00ad44\",\n    textColorDarkMode: \"#0ad75b\",\n  }, // Green\n  {\n    color: \"#d4b7002e\",\n    borderColor: \"rgb(227 217 41)\",\n    textColor: \"#716400\",\n    textColorDarkMode: \"#c1ad10\",\n  }, // Yellow\n\n  {\n    color: \"#b4b4b42e\",\n    borderColor: \"rgb(169 169 169)\",\n    textColor: \"#4b4b4b\",\n    textColorDarkMode: \"#838181\",\n  }, // Gray\n];\n\nexport const CHIP_COLOR_NAMES = {\n  red: chipColorsFadedBorder[0],\n  pink: chipColorsFadedBorder[1],\n  purple: chipColorsFadedBorder[2],\n  blue: chipColorsFadedBorder[3],\n  indigo: chipColorsFadedBorder[4],\n  green: chipColorsFadedBorder[5],\n  yellow: chipColorsFadedBorder[6],\n  gray: chipColorsFadedBorder[7],\n};\n\nconst chipColorsFaded = chipColorsFadedBorder.map((c) => ({\n  ...c,\n  borderColor: undefined,\n}));\n\nexport const ChipStylePalette = ({ onChange }: ChipStylePaletteProps) => {\n  return (\n    <FlexRowWrap className=\"ChipStylePalette flex-col flex-row gap-1 o-auto mt-1 pt-1 bt b-color noselect pointer\">\n      {[chipColors, chipColorsFadedBorder, chipColorsFaded].map(\n        (colors, ci) => (\n          <div key={ci} className=\"flex-row gap-1 o-auto \">\n            {colors.map(({ color, textColor, borderColor }) => (\n              <div\n                key={color}\n                onClick={() => {\n                  onChange({ color, textColor, borderColor });\n                }}\n              >\n                <StyledCell\n                  style={{\n                    chipColor: color,\n                    textColor: textColor, // ?? cs.textColor ?? style.textColor ?? \"white\",\n                    borderColor: borderColor ?? \"transparent\",\n                  }}\n                  renderedVal={\"Lorem\"}\n                />\n              </div>\n            ))}\n          </div>\n        ),\n      )}\n    </FlexRowWrap>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnDisplayFormat/ColumnDisplayFormat.tsx",
    "content": "import type { DeepWriteable } from \"@common/utils\";\nimport { JSONBSchema } from \"@components/JSONBSchema/JSONBSchema\";\nimport { includes, type DBSchemaTable } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { Prgl } from \"src/App\";\nimport type { DBSchemaTablesWJoins } from \"../../../Dashboard/dashboardUtils\";\nimport type { ColumnConfigWInfo } from \"../../W_Table\";\nimport type { ColumnFormat } from \"./columnFormatUtils\";\nimport { ColumnFormatSchema, getFormatOptions } from \"./columnFormatUtils\";\n\ntype P = {\n  db: Prgl[\"db\"];\n  column: ColumnConfigWInfo;\n  table: DBSchemaTable;\n  tables: DBSchemaTablesWJoins;\n  onChange: (newFormat: ColumnFormat) => void;\n};\n\nexport const ColumnDisplayFormat = ({\n  column,\n  table,\n  tables,\n  onChange,\n  db,\n}: P) => {\n  const schema = useMemo(() => {\n    const schemaWithoutAllowedValues = {\n      ...ColumnFormatSchema,\n    } as DeepWriteable<typeof ColumnFormatSchema>;\n    const allowedRenderers = getFormatOptions(\n      column.info ?? column.computedConfig,\n    );\n    const textCols = table.columns\n      .filter((c) => c.tsDataType === \"string\")\n      .map((c) => c.name);\n    schemaWithoutAllowedValues.oneOfType = schemaWithoutAllowedValues.oneOfType\n      .map((t) => {\n        if (\"params\" in t) {\n          if (t.type.enum[0] === \"Currency\") {\n            // @ts-ignore\n            t.params.oneOfType[1].currencyCodeField.allowedValues = textCols;\n            // @ts-ignore\n            t.params.oneOfType[0].currencyCode.allowedValues =\n              getCurrencyCodes().map((c) => ({\n                label: c.symbol !== c.code ? `${c.code} (${c.symbol})` : c.code,\n                value: c.code,\n                subLabel: c.country,\n              }));\n          } else if (t.type.enum[0] === \"Media\") {\n            //@ts-ignore\n            t.params.oneOfType[2]!.contentTypeColumnName.allowedValues =\n              textCols;\n          }\n        }\n\n        return t;\n      })\n      .filter((t) =>\n        allowedRenderers.find((df) => includes(t.type.enum, df.type)),\n      ) as typeof schemaWithoutAllowedValues.oneOfType;\n    return schemaWithoutAllowedValues;\n  }, [column, table.columns]);\n\n  return (\n    <JSONBSchema\n      schema={schema}\n      db={db}\n      tables={tables}\n      value={column.format}\n      onChange={onChange}\n    />\n  );\n};\n\nconst getCurrencyCodes = () => {\n  // Get all ISO currency codes\n  const currencies = Intl.supportedValuesOf(\"currency\");\n\n  const data: {\n    code: string;\n    country: string;\n    symbol: string;\n  }[] = [];\n\n  for (const code of currencies) {\n    try {\n      const region = new Intl.DisplayNames([\"en\"], { type: \"region\" }).of(\n        code.slice(0, 2).toUpperCase(),\n      );\n      const symbol = (0)\n        .toLocaleString(\"en\", { style: \"currency\", currency: code })\n        .replace(/\\d|[.,\\s]/g, \"\")\n        .trim();\n\n      data.push({ code, country: region || \"Unknown\", symbol });\n    } catch (e) {\n      console.warn(\"Could not get currency for code\", code, e);\n    }\n  }\n\n  return data;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnDisplayFormat/ConditionalCellIconStyleControls.tsx",
    "content": "import { mdiClose, mdiPlus } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { IconPalette } from \"@components/IconPalette/IconPalette\";\nimport { SmartSearch } from \"../../../SmartFilter/SmartSearch/SmartSearch\";\nimport type {\n  ConditionalStyleIcons,\n  StyleColumnProps,\n} from \"../ColumnStyleControls/ColumnStyleControls\";\nimport FormField from \"@components/FormField/FormField\";\n\ntype P = StyleColumnProps & {\n  style: ConditionalStyleIcons;\n};\n\nexport const ConditionalCellIconStyleControls = ({\n  column,\n  style,\n  tableName,\n  tables,\n  db,\n  onUpdate,\n}: P) => {\n  const updateStyle = (partialStyle: Partial<typeof style>) => {\n    onUpdate({ style: { ...style, ...partialStyle } });\n  };\n  const updateCondStyle = (\n    newStyle: ConditionalStyleIcons[\"valueToIconMap\"],\n    overWrite = false,\n  ) => {\n    updateStyle({\n      ...style,\n      valueToIconMap:\n        overWrite ? newStyle : (\n          {\n            ...style.valueToIconMap,\n            ...newStyle,\n          }\n        ),\n    });\n  };\n\n  return (\n    <FlexCol className=\"ConditionalCellStyleControls\">\n      <FormField\n        label={\"Icon size (px)\"}\n        value={style.size ?? 24}\n        inputProps={{\n          type: \"number\",\n          min: 1,\n          step: 1,\n          max: 200,\n        }}\n        onChange={(newIconSize) => {\n          updateStyle({\n            size: newIconSize,\n          });\n        }}\n      />\n      {Object.entries(style.valueToIconMap).map(([value, iconName]) => {\n        const remove = () => {\n          const { [value]: _, ...rest } = style.valueToIconMap;\n          updateCondStyle(rest, true);\n        };\n        return (\n          <div\n            key={value}\n            className=\"flex-col gap-1 p-p5 ai-start card  \"\n            style={{ padding: \"1em\", alignItems: \"stretch\" }}\n          >\n            <div className=\"flex-row gap-0 ai-center\">\n              <SmartSearch\n                className=\" \"\n                key={value.toString()}\n                db={db}\n                tableName={tableName}\n                variant=\"search-no-shadow\"\n                tables={tables}\n                defaultValue={(value || \"\").toString()}\n                column={column.name}\n                onPressEnter={(term) => {\n                  updateCondStyle({ [term]: iconName });\n                }}\n                onChange={(val) => {\n                  if (!val) {\n                    return;\n                  }\n                  const { columnValue, term } = val;\n                  const newValue = columnValue ?? term;\n                  if (!newValue) return;\n                  updateCondStyle({ [newValue.toString()]: \"\" });\n                }}\n              />\n              <Btn title=\"Remove\" iconPath={mdiClose} onClick={remove} />\n            </div>\n            <IconPalette\n              iconName={iconName}\n              onChange={(newIconName) => {\n                if (!newIconName) {\n                  remove();\n                } else {\n                  updateCondStyle({ [value]: newIconName });\n                }\n              }}\n            />\n          </div>\n        );\n      })}\n      <Btn\n        title=\"Add condition style\"\n        className=\"w-fit h-fit mt-1\"\n        color=\"action\"\n        variant=\"faded\"\n        iconPath={mdiPlus}\n        onClick={async () => {\n          const firstNonNullValueRow = await db[tableName]?.findOne?.(\n            { [column.name]: { $ne: null } },\n            { select: { [column.name]: 1 } },\n          );\n          const firstNonNullValue = firstNonNullValueRow?.[column.name];\n          if (firstNonNullValue !== undefined) {\n            updateCondStyle({\n              [firstNonNullValue]: \"\",\n            });\n          } else {\n            alert(\"No data to create a condition style for this column\");\n          }\n        }}\n      >\n        Add condition\n      </Btn>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnDisplayFormat/ConditionalCellStyleControls.tsx",
    "content": "import { mdiClose, mdiPlus } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport { SmartSearch } from \"../../../SmartFilter/SmartSearch/SmartSearch\";\nimport { StyledCell } from \"../../tableUtils/StyledTableColumn\";\nimport { ColorPicker } from \"../ColorPicker\";\nimport type {\n  ConditionalStyle,\n  StyleColumnProps,\n} from \"../ColumnStyleControls/ColumnStyleControls\";\nimport { ChipStylePalette } from \"./ChipStylePalette\";\nimport { isDefined } from \"../../../../utils/utils\";\n\nexport const CONDITION_OPERATORS = [\n  \"=\",\n  \"<=\",\n  \"<\",\n  \">\",\n  \">=\",\n  \"!=\",\n  \"in\",\n  \"not in\",\n  \"contains\",\n  \"not null\",\n  \"null\",\n] as const;\n\ntype P = StyleColumnProps & {\n  style: ConditionalStyle;\n};\n\nexport const ConditionalCellStyleControls = ({\n  column,\n  style,\n  tableName,\n  tables,\n  db,\n  onUpdate,\n}: P) => {\n  const conditions = style.conditions;\n\n  const updateStyle = (partialStyle: Partial<typeof style>) => {\n    onUpdate({ style: { ...style, ...partialStyle } });\n  };\n  const updateCondStyle = (\n    newStyle: Partial<ConditionalStyle[\"conditions\"][number]> | null,\n    idx: number | undefined,\n  ) => {\n    let newConditions = conditions.map((c) => ({ ...c }));\n    if (!newStyle) {\n      newConditions = newConditions.filter((_c, _i) => _i !== idx);\n    } else if (idx === undefined) {\n      newConditions.push({ chipColor: \"red\", operator: \"=\", condition: \"\" });\n    } else {\n      newConditions = newConditions.map((cs, i) => {\n        if (i === idx) return { ...cs, ...newStyle };\n        return cs;\n      }) as typeof newConditions;\n    }\n    updateStyle({ conditions: newConditions });\n  };\n\n  return (\n    <FlexCol className=\"ConditionalCellStyleControls\">\n      {conditions.map((cs, condIdx) => (\n        <div\n          key={condIdx}\n          className=\"flex-col gap-1 p-p5 ai-start card  \"\n          style={{ padding: \"1em\", alignItems: \"stretch\" }}\n        >\n          <FlexRowWrap className=\"gap-p5\">\n            <Btn color=\"action\" variant=\"faded\" className=\"max-w-full\">\n              {column.name}\n            </Btn>\n            <Select\n              className=\"ml-p25\"\n              value={cs.operator}\n              variant=\"div\"\n              options={CONDITION_OPERATORS}\n              onChange={(operator) => updateCondStyle({ operator }, condIdx)}\n            />\n            {cs.operator !== \"not null\" && cs.operator !== \"null\" && (\n              <SmartSearch\n                className=\" \"\n                key={(cs.condition ?? \"\").toString()}\n                db={db}\n                tableName={tableName}\n                style={{\n                  padding: \".5em\",\n                }}\n                variant=\"search-no-shadow\"\n                tables={tables}\n                defaultValue={(cs.condition ?? \"\").toString()}\n                column={column.name}\n                onPressEnter={(term) => {\n                  updateCondStyle({ condition: term }, condIdx);\n                }}\n                onChange={(val) => {\n                  if (!val) {\n                    return;\n                  }\n                  const { columnValue, term } = val;\n                  const condition = columnValue ?? term;\n                  if (!isDefined(condition)) return;\n                  updateCondStyle({ condition }, condIdx);\n                }}\n              />\n            )}\n            <Btn\n              title=\"Remove style\"\n              iconPath={mdiClose}\n              onClick={() => {\n                updateCondStyle(null, condIdx);\n              }}\n            />\n          </FlexRowWrap>\n\n          <PopupMenu\n            button={\n              <div className=\"flex-row ai-center gap-1 pointer noselect\">\n                <div>Chip style</div>\n                <StyledCell\n                  style={{\n                    chipColor: cs.chipColor,\n                    textColor: cs.textColor ?? style.defaultStyle?.textColor,\n                    borderColor: cs.borderColor,\n                  }}\n                  renderedVal={(cs.condition ?? \"Result\").toString()}\n                />\n              </div>\n            }\n            positioning=\"center\"\n            clickCatchStyle={{ opacity: 0.5 }}\n            footerButtons={[\n              {\n                label: \"Done\",\n                variant: \"filled\",\n                color: \"action\",\n                onClickClose: true,\n              },\n            ]}\n            render={() => (\n              <FlexCol>\n                <FlexRow className=\"jc-start ai-center  gap-1 o-auto f-1\">\n                  <ColorPicker\n                    label=\"Text:\"\n                    value={\n                      cs.textColor ?? style.defaultStyle?.textColor ?? \"black\"\n                    }\n                    onChange={(textColor) => {\n                      updateStyle({\n                        defaultStyle: { ...style.defaultStyle, textColor },\n                      });\n                      updateCondStyle({ textColor }, condIdx);\n                    }}\n                  />\n                  <ColorPicker\n                    label=\"Chip\"\n                    value={cs.chipColor ?? \"transparent\"}\n                    onChange={(chipColor) => {\n                      updateCondStyle({ chipColor }, condIdx);\n                    }}\n                  />\n                  <ColorPicker\n                    label=\"Border:\"\n                    className=\"mr-auto\"\n                    value={cs.borderColor ?? \"transparent\"}\n                    onChange={(borderColor) => {\n                      updateCondStyle({ borderColor }, condIdx);\n                    }}\n                  />\n\n                  <FlexRow className=\"ai-center gap-1\">\n                    <div>Chip style</div>\n                    <StyledCell\n                      style={{\n                        chipColor: cs.chipColor,\n                        textColor:\n                          cs.textColor ?? style.defaultStyle?.textColor,\n                        borderColor: cs.borderColor,\n                      }}\n                      renderedVal={\"Result\"}\n                    />\n                  </FlexRow>\n                </FlexRow>\n\n                <ChipStylePalette\n                  onChange={(chipStyle) => {\n                    updateCondStyle(\n                      { ...chipStyle, chipColor: chipStyle.color },\n                      condIdx,\n                    );\n                  }}\n                />\n              </FlexCol>\n            )}\n          />\n        </div>\n      ))}\n      <Btn\n        title=\"Add condition style\"\n        className=\"w-fit h-fit mt-1\"\n        color=\"action\"\n        variant=\"faded\"\n        iconPath={mdiPlus}\n        onClick={() => {\n          updateCondStyle({}, undefined);\n        }}\n      >\n        Add condition\n      </Btn>\n      <FlexRow className=\"flex-row p-p5 ai-center\">\n        <ColorPicker\n          label=\"Default chip color:\"\n          className=\"mr-p5\"\n          value={style.defaultStyle?.chipColor ?? \"transparent\"}\n          onChange={(chipColor) => {\n            updateStyle({ defaultStyle: { ...style.defaultStyle, chipColor } });\n          }}\n        />\n      </FlexRow>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnDisplayFormat/NestedColumnRender.tsx",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport { omitKeys } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport {\n  TimeChart,\n  type TimeChartLayer,\n} from \"../../../Charts/TimeChart/TimeChart\";\nimport type { DBSchemaTablesWJoins } from \"../../../Dashboard/dashboardUtils\";\nimport { RenderValue } from \"../../../SmartForm/SmartFormField/RenderValue\";\nimport { getYLabelFunc } from \"../../../W_TimeChart/fetchData/getTimeChartData\";\nimport { getColWInfo } from \"../../tableUtils/getColWInfo\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\n\nconst NESTED_LIMIT = 10;\n\nexport type NestedTimeChartMeta = {\n  fullExtent: [Date, Date];\n  // binSize: number;\n};\ntype P = {\n  c: ColumnConfig;\n  value: (AnyObject | undefined)[] | null;\n  row: AnyObject;\n  nestedTimeChartMeta: NestedTimeChartMeta | undefined;\n  tables: DBSchemaTablesWJoins;\n};\nexport const NestedColumnRender = ({\n  value,\n  c,\n  row,\n  nestedTimeChartMeta,\n  tables,\n}: P): JSX.Element => {\n  const table = tables.find((t) => t.name === c.nested?.path.at(-1)?.table);\n  const isMedia = table?.info.isFileTable;\n  const nestedColumns =\n    c.nested && table ? getColWInfo(table, c.nested.columns) : undefined;\n  const layers: TimeChartLayer[] = useMemo(\n    () =>\n      !c.nested?.chart || !nestedTimeChartMeta ?\n        []\n      : [\n          {\n            label: `${Object.entries(omitKeys(row, [c.name])).map(([key, val]) => `${key}: ${JSON.stringify(val)}`)}`,\n            getYLabel: getYLabelFunc(\"\"),\n            color: \"rgb(0, 183, 255)\",\n            cols: [],\n            data: value as any,\n            variant:\n              c.nested.chart.renderStyle === \"smooth-line\" ?\n                \"smooth\"\n              : undefined,\n            ...nestedTimeChartMeta,\n          },\n        ],\n    [c.name, c.nested?.chart, nestedTimeChartMeta, row, value],\n  );\n  if (!nestedColumns) {\n    return <>Unexpected issue: No nested columns</>;\n  }\n  if (value?.length && c.nested?.chart && nestedTimeChartMeta) {\n    return (\n      <TimeChart\n        binSize={undefined}\n        showXAxis={false}\n        yAxisVariant=\"compact\"\n        className=\"bg-transparent\"\n        padding={{\n          top: 10,\n          bottom: 10,\n        }}\n        zoomPanDisabled={true}\n        renderStyle={\n          c.nested.chart.renderStyle === \"smooth-line\" ?\n            undefined\n          : c.nested.chart.renderStyle\n        }\n        layers={layers}\n      />\n    );\n  }\n  const shownNestedColumns = nestedColumns.filter((c) => c.show);\n  const render = ({ key, value }: { key: string; value: any }) => {\n    const columnWInfo = nestedColumns.find((c) => c.name === key);\n    const datType = columnWInfo?.info ?? columnWInfo?.computedConfig;\n    const renderedValue =\n      columnWInfo ?\n        <RenderValue\n          column={datType}\n          value={value}\n          getValues={() => valueList.map((v) => v?.[key])}\n        />\n      : JSON.stringify(value);\n    return renderedValue;\n  };\n  const valueList = value ?? [];\n  const [firstValue, ...rest] = valueList;\n  const isSingleValue = shownNestedColumns.length === 1;\n  if (isSingleValue && !isMedia && firstValue && !rest.length) {\n    const [key, value] = Object.entries(firstValue)[0]!;\n    return <>{render({ key, value })}</>;\n  }\n  const content = valueList.slice(0, NESTED_LIMIT).map((nestedObj, idx) => {\n    if (!nestedObj) return null;\n\n    if (isMedia) {\n      return (\n        <MediaViewer\n          style={{ height: \"100%\" }}\n          key={nestedObj.url}\n          url={nestedObj.url}\n        />\n      );\n    }\n\n    const objectEntries = Object.entries(nestedObj);\n\n    return (\n      <div key={idx} className=\"flex-row-wrap gap-p5 ws-pre mb-p5\">\n        {objectEntries.map(([key, value]) => {\n          const displayModeClass = {\n            column: \"flex-col\",\n            row: \"flex-row gap-p25\",\n            \"no-headers\": \"flex-row-wrap gap-p25\",\n          };\n          const { displayMode = \"column\" } = c.nested!;\n          return (\n            <div key={key} className={`${displayModeClass[displayMode]} gap-0`}>\n              {displayMode !== \"no-headers\" && (\n                <div className=\"text-2 font-12\">{key}</div>\n              )}\n              <div>{render({ key, value })}</div>\n            </div>\n          );\n        })}\n      </div>\n    );\n  });\n\n  if (isMedia) {\n    return <FlexRowWrap className=\"max-h-full\">{content}</FlexRowWrap>;\n  }\n\n  return <>{content}</>;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnDisplayFormat/columnFormatUtils.tsx",
    "content": "import {\n  includes,\n  type AnyObject,\n  type DBSchemaTable,\n  type JSONB,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport React from \"react\";\nimport sanitizeHtml from \"sanitize-html\";\nimport { getAge } from \"@common/utils\";\nimport { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport { QRCodeImage } from \"@components/QRCodeImage\";\nimport { RenderValue } from \"../../../SmartForm/SmartFormField/RenderValue\";\nimport { StyledInterval } from \"../../../W_SQL/customRenderers\";\nimport type { RenderedColumn } from \"../../tableUtils/onRenderColumn\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport type { TableWindowInsertModel } from \"@common/DashboardTypes\";\nimport { ContentTypes } from \"@components/MediaViewer/RenderMedia\";\n\nconst CurrencySchema = {\n  type: {\n    title: \"Format\",\n    enum: [\"Currency\"],\n    description: \"With currency symbol\",\n  },\n  params: {\n    oneOfType: [\n      {\n        mode: { enum: [\"Fixed\"], title: \"Type\" },\n        metricPrefix: {\n          type: \"boolean\",\n          title: \"Use Metric Prefix\",\n          optional: true,\n        },\n        currencyCode: {\n          type: \"string\",\n          title: \"Currency code\",\n          description: \"EUR, GBP, USD, etc...\",\n        },\n      },\n      {\n        mode: { enum: [\"From column\"], title: \"Type\" },\n        metricPrefix: {\n          type: \"boolean\",\n          title: \"Use Metric Prefix\",\n          optional: true,\n        },\n        currencyCodeField: {\n          type: \"string\",\n          title: \"Currency Field\",\n          description:\n            \"Column containint the currency code (EUR, GBP, USD, etc...)\",\n        },\n      },\n    ],\n  },\n} as const satisfies JSONB.ObjectType[\"type\"];\n\nconst MediaSchema = {\n  type: {\n    enum: [\"Media\"],\n    title: \"Format\",\n    description: \"Display media (video/image/audio) from URL\",\n  },\n  params: {\n    optional: true,\n    oneOfType: [\n      {\n        type: {\n          enum: [\"Auto\"],\n          description: \"Auto detect from URL and headers (default)\",\n        },\n      },\n      {\n        type: {\n          enum: [\"Fixed\"],\n          description: \"Fixed\",\n        },\n        fixedContentType: {\n          type: \"string\",\n          title: \"Fixed content type\",\n          allowedValues: ContentTypes,\n        },\n      },\n      {\n        type: {\n          enum: [\"From column\"],\n          description: \"From column\",\n        },\n        contentTypeColumnName: {\n          title: \"MIME column\",\n          type: \"string\",\n          description:\n            \"Column that contains valid extesion values (img, mp4, mp3, ...)\",\n        },\n      },\n      {\n        type: {\n          enum: [\"From URL Extension\"],\n          description: \"From URL Extension (e.g. .png, .mp4)\",\n        },\n      },\n    ],\n  },\n} as const satisfies JSONB.JSONBSchema[\"type\"];\n\nconst tryParseNumber = (v) => {\n  if (typeof v === \"string\" && v.length && Number.isFinite(+v)) {\n    return +v;\n  }\n  return v;\n};\n\nexport const ColumnFormatSchema = {\n  title: \"Display format\",\n  description: \"Control how data is displayed\",\n  oneOfType: [\n    {\n      type: {\n        enum: [\"NONE\"],\n        title: \"Format\",\n        description: \"Display data as is. Default\",\n      },\n    },\n    {\n      type: {\n        enum: [\"URL\"],\n        title: \"Format\",\n        description: \"Clickable URL\",\n      },\n    },\n    {\n      type: {\n        enum: [\"Email\"],\n        title: \"Format\",\n        description: \"Email link\",\n      },\n    },\n    {\n      type: {\n        enum: [\"Tel\"],\n        title: \"Format\",\n        description: \"Telephone number link\",\n      },\n    },\n    {\n      type: {\n        enum: [\"QR Code\"],\n        title: \"Format\",\n        description: \"Display a URL as an image\",\n      },\n    },\n    CurrencySchema,\n    {\n      type: {\n        enum: [\"Metric Prefix\"],\n        title: \"Format\",\n        description: \"Display large numbers with metric prefixes (e.g. 1.2K)\",\n      },\n    },\n    {\n      type: {\n        enum: [\"UNIX Timestamp\"],\n        title: \"Format\",\n        description: \"Display unix timestamp as datetime\",\n      },\n    },\n    {\n      type: {\n        enum: [\"Age\"],\n        title: \"Format\",\n        description: \"Display time difference between now and the value\",\n      },\n      params: {\n        optional: true,\n        type: {\n          variant: {\n            title: \"Variant\",\n            description: \"Short shows top two biggest units\",\n            enum: [\"short\", \"full\"],\n          },\n        },\n      },\n    },\n    {\n      type: {\n        enum: [\"HTML\"],\n        title: \"Format\",\n        description: \"Display string as sanitised HTML\",\n      },\n      params: {\n        type: {\n          noSanitize: {\n            type: \"boolean\",\n            title: \"Do not sanitize HTML\",\n            description: \"Leave unchecked if you understand the risks\",\n            optional: true,\n          },\n          allowedHTMLTags: {\n            title: \"Allowed HTML Tags\",\n            type: \"string[]\",\n            allowedValues: [\n              { key: \"img\", label: \"Image\" },\n              { key: \"video\", label: \"Video\" },\n              { key: \"audio\", label: \"Audio\" },\n              { key: \"svg\", label: \"SVG\" },\n              { key: \"path\", label: \"Path (SVG)\" },\n            ].map((v) => v.key),\n            description: \"List of allowed HTML tags. E.g.: div, p, html\",\n            optional: true,\n          },\n        },\n      },\n    },\n    MediaSchema,\n  ],\n} as const satisfies JSONB.JSONBSchema;\n\nexport type ColumnFormat = JSONB.GetSchemaType<typeof ColumnFormatSchema>;\n\nconst _ensureAITypesAreInSync = {} as Exclude<\n  ColumnFormat,\n  { type: \"NONE\" | \"UNIX Timestamp\" }\n> satisfies NonNullable<TableWindowInsertModel[\"columns\"]>[number][\"format\"];\n_ensureAITypesAreInSync;\n\ntype ColumnRenderer = {\n  type: ColumnFormat[\"type\"];\n  tsDataType: ValidatedColumnInfo[\"tsDataType\"][] | undefined;\n  match?: (table: DBSchemaTable, column: ColumnConfig) => boolean | undefined;\n  render: (\n    value: any,\n    row: AnyObject,\n    c: RenderedColumn,\n    format: ColumnFormat,\n    maxCellChars: number,\n  ) => React.ReactNode;\n};\n\ntype FormattedColRender<F extends ColumnFormat> = Pick<\n  ColumnRenderer,\n  \"match\"\n> & {\n  type: F[\"type\"];\n  tsDataType: ValidatedColumnInfo[\"tsDataType\"][] | undefined;\n  render: (\n    value: any,\n    row: AnyObject,\n    c: RenderedColumn,\n    format: F,\n    maxCellChars: number,\n  ) => any;\n};\n\nconst removeQuotes = (value: string | number | null) => {\n  const v = (value ?? \"\").toString();\n  return (\n      [\"'\", '\"'].includes(v.at(0) ?? \"\") && [\"'\", '\"'].includes(v.at(-1) ?? \"\")\n    ) ?\n      v.slice(1, -1)\n    : v;\n};\n\nconst HREFRender: FormattedColRender<any>[\"render\"] = (v, r, c) => (\n  <a\n    href={\n      (c.format?.type === \"Email\" ? \"mailto:\"\n      : c.format?.type === \"Tel\" ? \"tel:\"\n      : \"\") + removeQuotes(v)\n    }\n    target=\"_blank\"\n    rel=\"noreferrer\"\n  >\n    {removeQuotes(v)}\n  </a>\n);\n\nconst metricPrefixOptions = {\n  notation: \"compact\",\n  compactDisplay: \"short\",\n} as const;\n\nexport const DISPLAY_FORMATS = [\n  {\n    type: \"NONE\",\n    tsDataType: undefined,\n    render: (v) => {\n      return v;\n    },\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"NONE\" }>>,\n  {\n    type: \"Email\",\n    tsDataType: [\"string\"],\n    render: HREFRender,\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"Email\" }>>,\n  {\n    type: \"Tel\",\n    tsDataType: undefined,\n    render: HREFRender,\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"Tel\" }>>,\n  {\n    type: \"Metric Prefix\",\n    tsDataType: [\"number\", \"string\"],\n    render: (rawValue: any) => {\n      const v = tryParseNumber(rawValue);\n      if (!Number.isFinite(v)) return rawValue;\n      const formatter = new Intl.NumberFormat(undefined, metricPrefixOptions);\n      return formatter.format(v); // 1.2K, 1.2M, 1.2B, etc\n    },\n  },\n  {\n    type: \"Media\",\n    tsDataType: [\"string\"],\n    render: (v, row, c, f) => {\n      const mediaFormat = f;\n      const params = mediaFormat.params;\n      return (\n        <MediaViewer\n          url={v}\n          content_type={\n            params?.type === \"Fixed\" ? params.fixedContentType\n            : params?.type === \"From column\" && params.contentTypeColumnName ?\n              row[params.contentTypeColumnName]\n            : undefined\n          }\n          // onPrevOrNext={!allowMediaSkip ? undefined : (increment) => {\n          //   console.error(\"MUST HAVE A PARENT VIEWER MANAGING NEXT AND PREV URLS\")\n          //   const requestedRow = data?.[rowIndex + increment];\n          //   return { url: requestedRow?.[c.name] };\n          // }}\n        />\n      );\n    },\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"Media\" }>>,\n  {\n    type: \"URL\",\n    tsDataType: [\"string\"],\n    match: (t, c) =>\n      t.info.isFileTable && [\"cloud_url\", \"signed_url\"].includes(c.name),\n    render: HREFRender,\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"URL\" }>>,\n  {\n    type: \"QR Code\",\n    tsDataType: [\"string\"],\n    render: (v, r, c, p, maxCellChars) => {\n      // Using \"90px\" because default max row height is 100px\n      return v?.toString().trim().length ?\n          <QRCodeImage url={v} size={90} variant=\"table-cell\" />\n        : <RenderValue column={c} value={v} maxLength={maxCellChars} />;\n    },\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"QR Code\" }>>,\n  {\n    type: \"HTML\",\n    tsDataType: [\"string\"],\n    render: (v, c, rc, p) => {\n      return (\n        <div\n          className=\"relative min-w-0 min-h-0\"\n          dangerouslySetInnerHTML={{\n            __html:\n              c.noSanitize ?\n                (v ?? \"\")\n              : sanitizeHtml(v, {\n                  allowedTags: sanitizeHtml.defaults.allowedTags.concat(\n                    p.params.allowedHTMLTags ?? [],\n                  ),\n                  parser: { lowerCaseAttributeNames: false },\n                }),\n          }}\n        />\n      );\n    },\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"HTML\" }>>,\n  {\n    type: \"UNIX Timestamp\",\n    tsDataType: [\"number\", \"string\"],\n    render: (v) => {\n      if (v && (+v).toString() == v) {\n        try {\n          return (\n            <RenderValue\n              column={{ tsDataType: \"string\", udt_name: \"timestamp\" }}\n              value={new Date().toISOString()}\n            />\n          );\n        } catch (e) {\n          console.error(\"Failed to render field as a unix timestamp\");\n        }\n      }\n\n      return v;\n    },\n  } satisfies FormattedColRender<\n    Extract<ColumnFormat, { type: \"UNIX Timestamp\" }>\n  >,\n  {\n    type: \"Age\",\n    tsDataType: [\"string\", \"number\"],\n    render: (v, c, rc, p) => {\n      if (v) {\n        const age = getAge(+new Date(v), Date.now(), true);\n        return (\n          <StyledInterval value={age} mode={p.params?.variant ?? \"short\"} />\n        );\n      }\n\n      return v;\n    },\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"Age\" }>>,\n  {\n    type: \"Currency\",\n    tsDataType: [\"number\", \"string\"],\n    render: (rawValue: any, row, c, { params }) => {\n      const v = tryParseNumber(rawValue);\n\n      try {\n        const currencyCode =\n          params.mode === \"Fixed\" ?\n            params.currencyCode\n          : row[params.currencyCodeField];\n        if (!Number.isFinite(v) || !currencyCode) {\n          return v;\n        }\n        const formatter = new Intl.NumberFormat(undefined, {\n          style: \"currency\",\n          currency: currencyCode,\n          ...(params.metricPrefix ? metricPrefixOptions : undefined),\n        });\n        return formatter.format(v); // $2,500.00\n      } catch (error) {\n        console.warn(\"Failed to render as currency:\", { params, error });\n      }\n\n      return v;\n    },\n  } satisfies FormattedColRender<Extract<ColumnFormat, { type: \"Currency\" }>>,\n] as ColumnRenderer[];\n\nexport function getFormatOptions(\n  colInfo?: Partial<Pick<ValidatedColumnInfo, \"udt_name\" | \"tsDataType\">>,\n): ColumnRenderer[] {\n  if (!colInfo) return [];\n\n  return DISPLAY_FORMATS.filter(\n    (r) => !r.tsDataType || includes(r.tsDataType, colInfo.tsDataType),\n  );\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnList.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { mdiDelete, mdiFunction, mdiLink, mdiPencil } from \"@mdi/js\";\nimport type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport { omitKeys } from \"prostgles-types\";\nimport React, { useMemo, useState } from \"react\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\nimport type {\n  DBSchemaTablesWJoins,\n  LoadedSuggestions,\n  WindowData,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\nimport { AlterColumn } from \"./AlterColumn/AlterColumn\";\nimport type { ColumnConfig } from \"./ColumnMenu\";\nimport { getColumnListItem } from \"./ColumnSelect/getColumnListItem\";\nimport { LinkedColumn } from \"./LinkedColumn/LinkedColumn\";\nimport { SummariseColumn } from \"./SummariseColumns\";\n\ntype P = {\n  columns: ColumnConfigWInfo[];\n  onChange: (newCols: ColumnConfigWInfo[]) => void;\n  w: SyncDataItem<Required<WindowData<\"table\">>, true>;\n  table: DBSchemaTablesWJoins[number];\n  suggestions: LoadedSuggestions | undefined;\n  onClose: VoidFunction;\n  showToggle?: boolean;\n};\n\nexport const ColumnList = ({\n  columns: columnsWithoutInfo,\n  table,\n  onChange,\n  showToggle = true,\n  w,\n  onClose,\n}: P) => {\n  const prgl = usePrgl();\n  const { db } = prgl;\n  const tableColumns = table.columns;\n  const columns: ColumnConfigWInfo[] = useMemo(\n    () =>\n      columnsWithoutInfo.map((c) => {\n        const col = tableColumns.find((tc) => tc.name === c.name);\n        return { ...c, info: col };\n      }),\n    [columnsWithoutInfo, tableColumns],\n  );\n\n  /** Ensure columns do not change order when toggling */\n  const [order, setOrder] = useState<Record<string, number>>(\n    Object.fromEntries(\n      columns\n        .sort((a, b) => +Boolean(b.show) - +Boolean(a.show))\n        .map((c, i) => [c.name, i]),\n    ),\n  );\n\n  return (\n    <SearchList\n      id=\"cols\"\n      onReorder={async (nc) => {\n        setOrder(Object.fromEntries(nc.map((d, i) => [d.key, i])));\n        await onChange(\n          nc.map((n) => ({ ...(n.data as ColumnConfig), show: n.checked })),\n        );\n      }}\n      limit={200}\n      className=\"f-1 p-1\"\n      onMultiToggle={\n        !showToggle ? undefined : (\n          (items) => {\n            const nc = columns.slice(0).map((_c) => ({\n              ..._c,\n              show: items.find((d) => d.key === _c.name)?.checked ?? _c.show,\n            }));\n            onChange(nc);\n          }\n        )\n      }\n      placeholder={`Search ${columns.length} columns`}\n      items={columns\n        .toSorted(\n          (a, b) => (order[a.name] ?? Infinity) - (order[b.name] ?? Infinity),\n        )\n        .map((c) => {\n          const computedRemove =\n            c.format ? \"Remove formatting\"\n            : c.computedConfig?.isColumn ? \"Remove Function\"\n            : c.computedConfig || c.nested ? \"Remove computed field\"\n            : undefined;\n          return {\n            ...getColumnListItem({ ...c.info, name: c.name }, c),\n            ...(showToggle ? { checked: c.show } : {}),\n            data: c,\n            rowClassname: \"trigger-hover\",\n            contentRight:\n              !db.sql && !c.computedConfig ?\n                null\n              : <FlexRow className=\"mr-p5\" onClick={(e) => e.stopPropagation()}>\n                  {db.sql && !c.computedConfig && !c.nested && (\n                    <PopupMenu\n                      positioning=\"center\"\n                      title={`Alter ${c.name}`}\n                      clickCatchStyle={{ opacity: 1 }}\n                      data-command=\"W_TableMenu_ColumnList.alter\"\n                      button={\n                        <Btn\n                          iconPath={mdiPencil}\n                          title=\"Alter column\"\n                          color=\"action\"\n                          className=\"show-on-trigger-hover\"\n                        />\n                      }\n                      onClickClose={false}\n                      contentClassName=\"p-1\"\n                    >\n                      <AlterColumn\n                        table={table}\n                        onClose={onClose}\n                        prgl={prgl}\n                        suggestions={undefined}\n                        field={c.name}\n                      />\n                    </PopupMenu>\n                  )}\n                  {c.nested && (\n                    <PopupMenu\n                      title=\"Edit Linked Field\"\n                      data-command=\"W_TableMenu_ColumnList.linkedColumnOptions\"\n                      button={\n                        <Btn\n                          color=\"action\"\n                          iconPath={mdiLink}\n                          title=\"Edit Linked Field\"\n                        />\n                      }\n                      render={(pClose) => (\n                        <LinkedColumn w={w} column={c} onClose={pClose} />\n                      )}\n                    />\n                  )}\n                  {!c.computedConfig && !c.nested && c.info && (\n                    <SummariseColumn\n                      column={c}\n                      columns={columns}\n                      tableColumns={tableColumns}\n                      onChange={onChange}\n                    />\n                  )}\n                  {!!computedRemove && (\n                    <Btn\n                      data-command=\"W_TableMenu_ColumnList.removeComputedColumn\"\n                      className=\"mr-1\"\n                      color=\"danger\"\n                      title={computedRemove}\n                      children={\n                        computedRemove === \"Remove Function\" ?\n                          c.computedConfig?.funcDef.label\n                        : undefined\n                      }\n                      iconPath={\n                        computedRemove === \"Remove Function\" ? mdiFunction : (\n                          mdiDelete\n                        )\n                      }\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        let newCols;\n                        if (\n                          computedRemove === \"Remove formatting\" ||\n                          computedRemove === \"Remove Function\"\n                        ) {\n                          newCols = columns.map((_c) => {\n                            if (_c.name === c.name) {\n                              const res = omitKeys(\n                                {\n                                  ..._c,\n                                  computed: false,\n                                },\n                                computedRemove === \"Remove formatting\" ?\n                                  [\"format\"]\n                                : [\"computedConfig\"],\n                              );\n\n                              return res;\n                            }\n                            return _c;\n                          });\n                        } else {\n                          newCols = columns.filter((_c) => _c.name !== c.name);\n                        }\n\n                        onChange(newCols);\n                      }}\n                    />\n                  )}\n                </FlexRow>,\n            onPress: () => {\n              const nc = columns\n                .slice(0)\n                .map((_c) => ({ ..._c }))\n                .map((_c) => {\n                  if (_c.name === c.name) {\n                    _c.show = !c.show;\n                  }\n                  return _c;\n                });\n              onChange(nc);\n            },\n          };\n        })}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnMenu.tsx",
    "content": "import type { TabItems } from \"@components/Tabs\";\nimport Tabs from \"@components/Tabs\";\nimport {\n  mdiChartBar,\n  mdiEye,\n  mdiEyeOff,\n  mdiEyeRemove,\n  mdiFilter,\n  mdiFormatColorFill,\n  mdiFormatText,\n  mdiFunction,\n  mdiLinkPlus,\n  mdiSort,\n  mdiTableColumnPlusAfter,\n  mdiTools,\n  mdiViewColumnOutline,\n} from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useEffect, useState } from \"react\";\n\nimport type {\n  TIMECHART_STAT_TYPES,\n  TimechartRenderStyle,\n} from \"../../W_TimeChart/W_TimeChartMenu\";\nimport { AlterColumn } from \"./AlterColumn/AlterColumn\";\nimport type {\n  BarchartStyle,\n  ConditionalStyle,\n  ConditionalStyleIcons,\n  FixedStyle,\n  ScaleStyle,\n} from \"./ColumnStyleControls/ColumnStyleControls\";\nimport { ColumnStyleControls } from \"./ColumnStyleControls/ColumnStyleControls\";\n\nimport type { DetailedFilter } from \"@common/filterUtils\";\nimport Popup from \"@components/Popup/Popup\";\nimport {\n  includes,\n  pickKeys,\n  type ParsedJoinPath,\n  type ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport { useReactiveState } from \"../../../appUtils\";\nimport type { DBS } from \"../../Dashboard/DBS\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type { WindowSyncItem } from \"../../Dashboard/dashboardUtils\";\nimport { useEffectAsync } from \"../../DashboardMenu/DashboardMenuSettings\";\nimport { getAndFixWColumnsConfig } from \"../TableMenu/getAndFixWColumnsConfig\";\nimport type W_Table from \"../W_Table\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\nimport { getFullColumnConfig } from \"../tableUtils/getFullColumnConfig\";\nimport { updateWCols } from \"../tableUtils/tableUtils\";\nimport { AddComputedColMenu } from \"./AddComputedColumn/AddComputedColMenu\";\nimport { QuickAddComputedColumn } from \"./AddComputedColumn/QuickAddComputedColumn\";\nimport { ColumnDisplayFormat } from \"./ColumnDisplayFormat/ColumnDisplayFormat\";\nimport type { ColumnFormat } from \"./ColumnDisplayFormat/columnFormatUtils\";\nimport { getFormatOptions } from \"./ColumnDisplayFormat/columnFormatUtils\";\nimport { ColumnQuickStats } from \"./ColumnQuickStats/ColumnQuickStats\";\nimport { ColumnSortMenu } from \"./ColumnSortMenu\";\nimport { ColumnsMenu } from \"./ColumnsMenu\";\nimport { FunctionSelector } from \"./FunctionSelector/FunctionSelector\";\nimport type { FuncDef } from \"./FunctionSelector/functions\";\nimport type { NESTED_COLUMN_DISPLAY_MODES } from \"./LinkedColumn/LinkedColumn\";\nimport { LinkedColumn } from \"./LinkedColumn/LinkedColumn\";\nimport { useIsMounted } from \"prostgles-client\";\n\nexport type ColumnConfig = {\n  idx?: number;\n  name: string;\n  show?: boolean;\n  nested?: {\n    path: ParsedJoinPath[];\n    columns: Omit<ColumnConfig, \"nested\">[];\n    joinType?: \"inner\" | \"left\";\n    displayMode?: (typeof NESTED_COLUMN_DISPLAY_MODES)[number][\"key\"];\n    limit?: number;\n    sort?: ColumnSort;\n    detailedFilter?: DetailedFilter[];\n    detailedHaving?: DetailedFilter[];\n    chart?: {\n      type: \"time\";\n      dateCol: string;\n      renderStyle: TimechartRenderStyle | \"smooth-line\";\n      yAxis:\n        | {\n            isCountAll: false;\n            colName: string;\n            funcName: (typeof TIMECHART_STAT_TYPES)[number][\"func\"];\n          }\n        | {\n            isCountAll: true;\n          };\n    };\n  };\n  style?:\n    | { type: \"None\" }\n    | ConditionalStyle\n    | ConditionalStyleIcons\n    | FixedStyle\n    | ScaleStyle\n    | BarchartStyle;\n  format?: ColumnFormat;\n\n  /** If present then this is a computed column */\n  computedConfig?: Pick<ValidatedColumnInfo, \"tsDataType\" | \"udt_name\"> & {\n    /**\n     * If true then this (name === computedConfig.column) represents an actual column and should not be removed\n     */\n    isColumn?: boolean;\n\n    /** Out type removed to prevent confusion */\n    funcDef: Omit<FuncDef, \"outType\">;\n\n    /**\n     * In case of functions that don't need cols column will be undefined\n     */\n    column: string | undefined;\n    args?: {\n      $duration?: { otherColumn: string };\n      $string_agg?: { separator: string };\n      $template_string?: string;\n    };\n  };\n  width?: number;\n};\n\ntype P = Pick<CommonWindowProps, \"suggestions\" | \"tables\" | \"prgl\"> & {\n  db: DBHandlerClient;\n  dbs: DBS;\n  w: WindowSyncItem<\"table\">;\n  columnMenuState: W_Table[\"columnMenuState\"];\n};\n\nexport type ColumnSortSQL = {\n  key: string | number;\n  asc?: boolean | null;\n  nulls?: \"first\" | \"last\" | null;\n  nullEmpty?: boolean;\n};\nexport type ColumnSort = Omit<ColumnSortSQL, \"key\"> & {\n  key: string;\n};\n\nexport const ColumnMenu = (props: P) => {\n  const { db, tables, prgl } = props;\n  const [w, setW] = useState<WindowSyncItem<\"table\">>(props.w);\n  const tableName = w.table_name;\n  const [column, setColumn] = useState<ColumnConfigWInfo>();\n  const [activeKey, setActiveKey] = useState<string>();\n  const { state, setState } = useReactiveState(props.columnMenuState);\n  const colName = state?.column;\n  const getIsMounted = useIsMounted();\n\n  useEffect(() => {\n    const wSub = props.w.$cloneSync((wdata) => {\n      if (!getIsMounted()) return;\n      setW(wdata);\n    });\n    return wSub.$unsync;\n  }, [setW, getIsMounted, props.w]);\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useEffectAsync(async () => {\n    if (!w.columns || !Array.isArray(w.columns)) {\n      updateWCols(w, await getAndFixWColumnsConfig(tables, w));\n    } else if (colName) {\n      const column = getFullColumnConfig(tables, w).find(\n        (c) => c.name === colName,\n      );\n      if (!column) {\n        console.warn(`Column (${colName}) was not found, delete?!`);\n      } else {\n        setColumn(column);\n      }\n    }\n  }, [w, colName, tables]);\n\n  const onUpdate = (nc: Partial<ColumnConfig>) => {\n    if (!column) return;\n    const newCols = w.columns?.map((c, i) => {\n      if (c.name === column.name) {\n        return { ...c, ...nc };\n      }\n      return c;\n    });\n    updateWCols(w, newCols);\n  };\n\n  if (!column || !state) return null;\n\n  const onClose = () => {\n    setState(undefined);\n    setActiveKey(undefined);\n  };\n\n  const table = tables.find((t) => t.name === w.table_name);\n  const validatedColumn = table?.columns.find((c) => c.name === colName);\n\n  const isComputed = Boolean(\n    column.computedConfig ||\n    column.nested?.columns.some((c) => c.computedConfig),\n  );\n  const computedType =\n    column.nested ? \"nested\" : (\n      column.computedConfig &&\n      (column.computedConfig.isColumn ? \"column\" : \"added\")\n    );\n  const hasSort = w.sort?.some((s) =>\n    !column.nested ?\n      s.key === column.name\n    : column.nested.columns.some((nc) => `${column.name}.${nc.name}`),\n  );\n  const items = {\n    Sort: {\n      leftIconPath: mdiSort,\n      disabledText:\n        !validatedColumn?.orderBy && !column.nested ?\n          \"Not permitted\"\n        : undefined,\n      style: hasSort ? { color: \"var(--active)\" } : {},\n      content: <ColumnSortMenu column={column} w={w} tables={tables} />,\n    },\n    Style: {\n      leftIconPath: mdiFormatColorFill,\n      hide: !!column.nested,\n      disabledText:\n        column.format?.type === \"Media\" ?\n          \"Cannot style a media format column\"\n        : undefined,\n      style:\n        column.style?.type && column.style.type !== \"None\" ?\n          { color: \"var(--active)\" }\n        : {},\n      content: (\n        <ColumnStyleControls\n          db={db}\n          tableName={tableName}\n          tables={tables}\n          column={column}\n          onUpdate={onUpdate}\n          tsDataType={\n            column.info?.tsDataType ||\n            column.computedConfig?.tsDataType ||\n            \"any\"\n          }\n          udt_name={\n            column.info?.udt_name || column.computedConfig?.udt_name || \"text\"\n          }\n        />\n      ),\n    },\n    \"Display format\": {\n      style:\n        column.format && column.format.type !== \"NONE\" ?\n          { color: \"var(--active)\" }\n        : {},\n      leftIconPath: mdiFormatText,\n      hide: !!column.nested,\n      disabledText:\n        getFormatOptions(column.info || column.computedConfig).length <= 1 ?\n          \"Only text columns can have custom formats at the moment\"\n        : undefined,\n      content: (\n        <ColumnDisplayFormat\n          db={db}\n          column={column}\n          tables={props.tables}\n          table={props.tables.find((t) => t.name === tableName)!}\n          onChange={(format) => {\n            onUpdate({ format });\n          }}\n        />\n      ),\n    },\n    Filter: {\n      leftIconPath: mdiFilter,\n      hide: !!column.nested,\n      disabledText:\n        !validatedColumn?.filter ? \"Not permitted\"\n        : isComputed ? \"Cannot filter a computed column\"\n        : undefined,\n      style:\n        w.filter.some((f) => \"fieldName\" in f && f.fieldName === column.name) ?\n          { color: \"var(--active)\" }\n        : {},\n    },\n    \"Quick Stats\": {\n      leftIconPath: mdiChartBar,\n      hide: !!column.nested,\n      disabledText:\n        isComputed ? \"Cannot add quick stats on a computed column\"\n        : (\n          validatedColumn?.udt_name.startsWith(\"geo\") ||\n          includes([\"json\", \"xml\"], validatedColumn?.udt_name)\n        ) ?\n          `Not supported for ${validatedColumn.udt_name} columns`\n        : undefined,\n      content: table && w.columns && validatedColumn && (\n        <ColumnQuickStats column={validatedColumn} db={db} w={w} />\n      ),\n    },\n    Columns: {\n      leftIconPath: mdiViewColumnOutline,\n      content: (\n        <ColumnsMenu\n          w={w}\n          db={db}\n          tables={tables}\n          onClose={onClose}\n          suggestions={props.suggestions}\n          nestedColumnOpts={undefined}\n        />\n      ),\n    },\n    \"Add Computed Column\": {\n      hide: !!column.nested || isComputed,\n      disabledText: isComputed ? \"Not allowed on computed columns\" : undefined,\n      leftIconPath: mdiTableColumnPlusAfter,\n      content: (\n        <AddComputedColMenu\n          variant=\"no-popup\"\n          nestedColumnOpts={\n            column.nested ? { type: \"existing\", config: column } : undefined\n          }\n          onClose={onClose}\n          tables={tables}\n          w={w}\n          db={db}\n          selectedColumn={column.name}\n          tableHandler={db[tableName]}\n        />\n      ),\n    },\n    \"Edit Computed Column\": {\n      hide: !isComputed,\n      leftIconPath: mdiTableColumnPlusAfter,\n      style: { color: \"var(--active)\" },\n      content: (\n        <QuickAddComputedColumn\n          existingColumn={column}\n          onAddColumn={(newCol) => {\n            updateWCols(\n              w,\n              (w.columns ?? []).map((c) => {\n                if (c.name === column.name) {\n                  return {\n                    ...c,\n                    ...newCol,\n                  };\n                }\n                return c;\n              }),\n            );\n            onClose();\n          }}\n          tableName={tableName}\n        />\n      ),\n    },\n    \"Apply function\": {\n      style: column.computedConfig ? { color: \"var(--active)\" } : {},\n      leftIconPath: mdiFunction,\n      hide:\n        !!column.nested ||\n        (column.computedConfig && !column.computedConfig.isColumn),\n      content: table && w.columns && validatedColumn && (\n        // <SummariseColumn\n        //   column={column}\n        //   columns={w.columns}\n        //   onChange={cols => updateWCols(w, cols)}\n        //   tableColumns={table.columns}\n        // />\n        <FunctionSelector\n          selectedFunction={column.computedConfig?.funcDef.key}\n          column={validatedColumn.name}\n          tableColumns={table.columns}\n          wColumns={w.columns}\n          onSelect={(funcDef) =>\n            onUpdate({\n              computedConfig: funcDef && {\n                ...pickKeys(validatedColumn, [\"tsDataType\", \"udt_name\"]),\n                funcDef,\n                isColumn: column.computedConfig?.isColumn ?? true,\n                column: validatedColumn.name,\n              },\n            })\n          }\n        />\n      ),\n    },\n    \"Add Linked Data\": {\n      style: column.nested ? { color: \"var(--active)\" } : {},\n      leftIconPath: mdiLinkPlus,\n      disabledText:\n        !table?.joinsV2.length ?\n          \"No foreign keys to/from this table\"\n        : undefined,\n      label: `${column.nested ? \"Edit\" : \"Add\"} Linked Columns`,\n      content: <LinkedColumn w={w} column={column} onClose={onClose} />,\n    },\n    Alter: {\n      leftIconPath: mdiTools,\n      disabledText:\n        !db.sql ? \"Not enough privileges\"\n        : computedType === \"added\" ? \"Cannot alter a computed column\"\n        : undefined,\n      hide: isComputed,\n      content: !!table && (\n        <AlterColumn\n          table={table}\n          field={column.name}\n          prgl={prgl}\n          suggestions={props.suggestions}\n          onClose={onClose}\n        />\n      ),\n    },\n    Hide: {\n      leftIconPath: mdiEyeOff,\n      hide: (w.columns?.filter((c) => c.show).length ?? 0) < 2,\n    },\n    Remove: {\n      label: \"Remove computed column\",\n      leftIconPath: mdiEyeRemove,\n      style: { color: \"var(--text-warning)\" },\n      hide: !computedType || computedType === \"column\",\n    },\n    \"Hide Others\": {\n      leftIconPath: mdiEyeOff,\n      hide: (w.columns?.filter((c) => c.show).length ?? 0) < 2,\n    },\n    \"Unhide all\": {\n      leftIconPath: mdiEye,\n      hide: !w.columns?.some((c) => !c.show),\n    },\n  } as const satisfies TabItems;\n\n  const content = (\n    <div className=\"min-h-0 flex-col\">\n      <Tabs\n        compactMode={window.isMobileDevice ? \"hide-inactive\" : undefined}\n        variant=\"vertical\"\n        listClassName=\"o-auto\"\n        contentClass={\n          \" min-w-300 flex-col ml-p25 o-auto \" +\n          (activeKey === \"Alter\" ? \" o-auto \" : \" p-1 \")\n        }\n        activeKey={activeKey as any}\n        items={items}\n        menuStyle={{ borderRadius: 0 }}\n        onChange={async (v) => {\n          if (v === \"Add Computed Column\") {\n            // onClose();\n          } else if (v === \"Filter\") {\n            const nf: DetailedFilter = await getDefaultFilter(column);\n            w.$update({ filter: [nf, ...w.filter] });\n            onClose();\n          } else if (v === \"Remove\") {\n            const columns = (w.columns ?? []) //(await TableMenu.getWCols(db[tableName] as any, w, false))\n              .filter((cc) => column.name !== cc.name);\n            updateWCols(w, columns);\n            onClose();\n          } else if (v === \"Hide\") {\n            const columns = (w.columns ?? []) //(await TableMenu.getWCols(db[tableName] as any, w, false))\n              .map((cc) => ({\n                ...cc,\n                show: column.name === cc.name ? false : cc.show,\n              }));\n            updateWCols(w, columns);\n            onClose();\n          } else if (v === \"Hide Others\") {\n            const columns = (w.columns ?? []) //(await TableMenu.getWCols(db[tableName] as any, w, false))\n              .map((cc) => ({\n                ...cc,\n                show: column.name === cc.name,\n              }));\n            updateWCols(w, columns);\n            onClose();\n          } else if (v === \"Unhide all\") {\n            const columns = (w.columns ?? []) //(await TableMenu.getWCols(db[tableName] as any, w, false))\n              .map((cc) => ({\n                ...cc,\n                show: true,\n              }));\n\n            w.$update({ columns });\n            onClose();\n          }\n\n          setActiveKey(v);\n        }}\n      />\n    </div>\n  );\n\n  return (\n    <Popup\n      anchorXY={{ x: state.clientX, y: state.clientY }}\n      clickCatchStyle={{ opacity: 0.5 }}\n      key=\"columnMenu\"\n      rootStyle={{ padding: 0 }}\n      contentClassName={\"p-0 o-none\"}\n      showFullscreenToggle={{}}\n      title={colName}\n      positioning=\"beneath-left\"\n      onClose={onClose}\n      fixedTopLeft={false}\n    >\n      {content}\n    </Popup>\n  );\n};\n\n/** undefined value means filter is disabled (gray col name text) */\nconst getDefaultFilter = (col: ColumnConfigWInfo): DetailedFilter => {\n  const isNumeric = [\"number\", \"Date\"].includes(\n    col.info?.tsDataType || (col.computedConfig?.funcDef.tsDataTypeCol as any),\n  );\n  const nf: DetailedFilter = {\n    fieldName: col.name,\n    type:\n      col.info?.is_pkey || col.info?.references?.length ? \"=\"\n      : isNumeric ? \"$between\"\n      : \"$in\",\n  };\n\n  return nf;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnQuickStats/ColumnQuickStats.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { mdiFilter, mdiSortAscending, mdiSortDescending } from \"@mdi/js\";\nimport { type DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport { type ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport type { WindowData } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport { useColumnStats } from \"./useColumnQuickStats\";\n\nexport type ColumnQuickStatsProps = {\n  column: ValidatedColumnInfo;\n  db: DBHandlerClient;\n  w: SyncDataItem<Required<WindowData<\"table\">>, true>;\n};\nexport const ColumnQuickStats = (props: ColumnQuickStatsProps) => {\n  const [sortAscending, setSortAscending] = useState(false);\n  const stats = useColumnStats(props, sortAscending);\n\n  if (!stats) return <Loading />;\n  if (stats.type === \"error\") return <ErrorComponent error={stats} />;\n\n  const maxCount =\n    stats.distribution ?\n      Math.max(...stats.distribution.map((d) => d.count))\n    : 0;\n\n  return (\n    <FlexCol\n      className=\"ColumnQuickStats gap-1 ta-start\"\n      data-command=\"ColumnQuickStats\"\n      style={{ minWidth: \"250px\" }}\n    >\n      <StatRow label=\"Distinct\" value={stats.distinctCount.toLocaleString()} />\n\n      {stats.minMax && (\n        <>\n          <StatRow label=\"Min\" value={stats.minMax.min?.toString() ?? \"N/A\"} />\n          <StatRow label=\"Max\" value={stats.minMax.max?.toString() ?? \"N/A\"} />\n        </>\n      )}\n\n      {stats.distribution && stats.distribution.length > 0 && (\n        <FlexCol\n          className=\"gap-p5 mt-p5\"\n          style={{\n            borderTop: \"1px solid var(--border-color, #e0e0e0)\",\n          }}\n        >\n          <FlexRow\n            className=\"ta-start\"\n            style={{\n              opacity: 0.7,\n            }}\n          >\n            <div>\n              {stats.canSortDistribution ? \"Top Values\" : \"Distribution\"}\n            </div>\n            {stats.canSortDistribution && (\n              <Btn\n                className=\"ml-auto\"\n                iconPath={sortAscending ? mdiSortAscending : mdiSortDescending}\n                onClick={() => setSortAscending(!sortAscending)}\n              />\n            )}\n          </FlexRow>\n          <ScrollFade className=\"no-scroll-bar o-auto flex-col gap-p5\">\n            {stats.distribution.map((d) => (\n              <DistributionBar\n                key={d.label}\n                label={d.label}\n                count={d.count}\n                onClickAddFilter={d.onClick}\n                max={maxCount}\n              />\n            ))}\n          </ScrollFade>\n        </FlexCol>\n      )}\n    </FlexCol>\n  );\n};\n\nconst StatRow = ({\n  label,\n  value,\n}: {\n  label: string;\n  value: React.ReactNode;\n}) => (\n  <div className=\"flex-row gap-p5 ai-center\">\n    <span style={{ opacity: 0.7, minWidth: \"80px\" }}>{label}:</span>\n    <strong>{value}</strong>\n  </div>\n);\n\nconst DistributionBar = ({\n  label,\n  count,\n  max,\n  onClickAddFilter,\n}: {\n  label: string;\n  count: number;\n  max: number;\n  onClickAddFilter?: () => void;\n}) => {\n  const percentage = max > 0 ? (count / max) * 100 : 0;\n  return (\n    <div className=\"flex-col gap-p5 trigger-hover\">\n      <div className=\"flex-row gap-p5 ai-center jc-between\">\n        <FlexRow>\n          <strong>{label}</strong>\n          {onClickAddFilter && (\n            <Btn\n              iconPath={mdiFilter}\n              title=\"Add filter\"\n              color=\"action\"\n              size=\"micro\"\n              data-key={label}\n              data-command=\"ColumnQuickStats.addFilter\"\n              className=\"show-on-trigger-hover\"\n              onClick={onClickAddFilter}\n            />\n          )}\n        </FlexRow>\n        <span style={{ opacity: 0.6 }} title=\"Count\">\n          {count.toLocaleString()}\n        </span>\n      </div>\n      <div\n        style={{\n          height: \"4px\",\n          background: \"var(--bg-color-2, #e0e0e0)\",\n          borderRadius: \"2px\",\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            width: `${percentage}%`,\n            height: \"100%\",\n            background: \"var(--action-color, #2196F3)\",\n            transition: \"width 0.3s ease\",\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnQuickStats/useColumnQuickStats.ts",
    "content": "import { getSmartGroupFilter } from \"@common/filterUtils\";\nimport { usePromise } from \"prostgles-client\";\nimport {\n  _PG_numbers,\n  includes,\n  isDefined,\n  type AnyObject,\n} from \"prostgles-types\";\nimport type { ColumnQuickStatsProps } from \"./ColumnQuickStats\";\n\nexport const useColumnStats = (\n  { column, db, w }: ColumnQuickStatsProps,\n  sortAscending: boolean,\n) => {\n  const { name, udt_name } = column;\n  const {\n    table_name: tableName,\n    filter,\n    options: { filterOperand = \"AND\" },\n  } = w;\n\n  const res = usePromise(async () => {\n    const tableFindHandler = db[tableName]?.find;\n    const tableFindOneHandler = db[tableName]?.findOne;\n    const tableCountHandler = db[tableName]?.count;\n\n    const finalFilter =\n      filter.length ?\n        getSmartGroupFilter(\n          filter,\n          {},\n          filterOperand === \"OR\" ? \"or\" : undefined,\n        )\n      : undefined;\n    if (!tableFindHandler || !tableFindOneHandler || !tableCountHandler) {\n      return {\n        type: \"error\" as const,\n        error: `Table ${tableName} not found in DB handler`,\n      };\n    }\n\n    const distinctCount = await tableCountHandler(finalFilter, {\n      select: [name],\n      groupBy: true,\n    });\n    const minMax = await tableFindOneHandler(finalFilter, {\n      select: { min: { $min: [name] }, max: { $max: [name] } },\n    });\n\n    let distribution:\n      | {\n          label: string;\n          count: number;\n          onClick?: () => void;\n        }[]\n      | undefined = undefined;\n    let canSortDistribution = false;\n\n    if (minMax && minMax.min !== minMax.max) {\n      if (udt_name === \"date\" || udt_name.startsWith(\"timestamp\")) {\n        const intervals: { label: string; filters: AnyObject[] }[] = [];\n        const minDate = new Date(minMax.min);\n        const maxDate = new Date(minMax.max);\n        const intervalCount = Math.min(5, distinctCount);\n        const totalMs = maxDate.getTime() - minDate.getTime();\n        const intervalMs = totalMs / intervalCount;\n\n        for (let i = 0; i < intervalCount; i++) {\n          const start = new Date(minDate.getTime() + i * intervalMs);\n          const end = new Date(minDate.getTime() + (i + 1) * intervalMs);\n          intervals.push({\n            label: `${start.toISOString().split(\"T\")[0]} - ${end.toISOString().split(\"T\")[0]}`,\n            filters: [\n              {\n                [name]: {\n                  $gte: start.toISOString(),\n                },\n                [name]: {\n                  $lt: end.toISOString(),\n                },\n              },\n            ],\n          });\n        }\n\n        distribution ??= [];\n        for (const interval of intervals) {\n          const count = await tableCountHandler({\n            $and: [finalFilter, ...interval.filters].filter(isDefined),\n          });\n          distribution.push({ label: interval.label, count });\n        }\n      } else if (includes(_PG_numbers, udt_name)) {\n        const intervals: { label: string; filters: AnyObject[] }[] = [];\n        const minNum = Number(minMax.min);\n        const maxNum = Number(minMax.max);\n        const totalRange = maxNum - minNum;\n        const intervalCount = Math.min(5, distinctCount);\n        const intervalRange = totalRange / intervalCount;\n\n        for (let i = 0; i < intervalCount; i++) {\n          const start = minNum + i * intervalRange;\n          const end = minNum + (i + 1) * intervalRange;\n          intervals.push({\n            label: `${start.toFixed(2)} - ${end.toFixed(2)}`,\n            filters: [\n              {\n                [name]: {\n                  $gte: start,\n                },\n                [name]: {\n                  $lt: end,\n                },\n              },\n            ],\n          });\n        }\n\n        distribution ??= [];\n        for (const interval of intervals) {\n          const count = await tableCountHandler({\n            $and: [finalFilter, ...interval.filters].filter(isDefined),\n          });\n          distribution.push({ label: interval.label, count });\n        }\n      } else if (udt_name.startsWith(\"geo\")) {\n        // No min max\n      } else {\n        canSortDistribution = true;\n\n        const topValues = await tableFindHandler(finalFilter, {\n          select: {\n            label: { $column: [name] },\n            count: { $countAll: [] },\n          },\n          orderBy: [{ key: \"count\", asc: sortAscending, nulls: \"last\" }],\n          limit: Math.min(distinctCount, 15),\n        });\n        distribution = topValues.map((v) => ({\n          label: v.label?.toString() ?? \"NULL\",\n          count: v.count,\n          onClick: () => {\n            w.$update({\n              filter: [\n                ...w.filter,\n                {\n                  fieldName: name,\n                  type: \"$in\",\n                  value: [v.label],\n                  minimised: true,\n                },\n              ],\n            });\n          },\n        }));\n      }\n    }\n\n    return {\n      type: \"success\" as const,\n      distinctCount,\n      minMax,\n      distribution,\n      canSortDistribution,\n    };\n  }, [db, tableName, filter, filterOperand, name, udt_name, sortAscending, w]);\n\n  return res;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnSelect/ColumnSelect.tsx",
    "content": "import { mdiClose } from \"@mdi/js\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport {\n  _PG_bool,\n  _PG_date,\n  _PG_geometric,\n  _PG_interval,\n  _PG_json,\n  _PG_numbers,\n  _PG_postgis,\n  _PG_strings,\n} from \"prostgles-types\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport type { TestSelectors } from \"../../../../Testing\";\nimport { getColumnListItem } from \"./getColumnListItem\";\ntype P = TestSelectors & {\n  columns: (ValidatedColumnInfo & { disabledInfo?: string })[];\n  onChange: (columnName?: string) => void;\n  label?: string;\n  value?: string;\n};\n\nexport const ColumnSelect = ({\n  columns,\n  onChange,\n  label,\n  value,\n  ...testSelectors\n}: P) => {\n  const items = columns.map((c) => ({\n    ...getColumnListItem(c),\n    data: c,\n  }));\n\n  return (\n    <div className=\"flex-row ai-end\">\n      <FormField\n        {...testSelectors}\n        label={label}\n        value={value}\n        fullOptions={items}\n        onChange={onChange}\n      />\n      <Btn\n        iconPath={mdiClose}\n        onClick={() => {\n          onChange();\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnSelect/getColumnListItem.tsx",
    "content": "import React from \"react\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport type { SearchListItem } from \"@components/SearchList/SearchList\";\nimport { type ValidatedColumnInfo } from \"prostgles-types\";\nimport { getColumnDataColor } from \"src/dashboard/SmartForm/SmartFormField/RenderValue\";\nimport type { ColumnConfigWInfo } from \"../../W_Table\";\nimport {\n  colIs,\n  tsDataTypeFromUdtName,\n} from \"src/dashboard/SmartForm/SmartFormField/fieldUtils\";\nimport {\n  mdiCalendar,\n  mdiCodeBrackets,\n  mdiCodeJson,\n  mdiFileQuestion,\n  mdiFormatText,\n  mdiFunctionVariant,\n  mdiKey,\n  mdiKeyLink,\n  mdiLink,\n  mdiMapMarker,\n  mdiNumeric,\n  mdiShapeOutline,\n  mdiTimetable,\n  mdiToggleSwitchOutline,\n} from \"@mdi/js\";\n\nexport const getColumnListItem = (\n  c: Pick<ValidatedColumnInfo, \"name\"> &\n    Partial<\n      Pick<\n        ValidatedColumnInfo,\n        \"udt_name\" | \"tsDataType\" | \"references\" | \"is_pkey\"\n      >\n    > & { disabledInfo?: string },\n  columnWInfo?: ColumnConfigWInfo,\n): Pick<SearchListItem, \"data\" | \"title\"> & {\n  key: string;\n  label: string;\n  subLabel?: string;\n  contentLeft: React.ReactNode;\n  disabledInfo?: string;\n} => {\n  const subLabel =\n    columnWInfo?.nested ?\n      columnWInfo.nested.columns.map((c) => c.name).join(\", \")\n    : columnWInfo ?\n      `${columnWInfo.info?.udt_name ?? columnWInfo.computedConfig?.udt_name}      ${columnWInfo.info?.is_nullable ? \"nullable\" : \"\"}`\n    : c.udt_name;\n  return {\n    key: c.name,\n    label:\n      c.name +\n      (!c.references ? \"\" : (\n        `    (${c.references.map((r) => r.ftable).join(\", \")})`\n      )),\n    subLabel,\n    data: c,\n    disabledInfo: c.disabledInfo,\n    title: columnWInfo?.nested ? \"referenced data\" : c.udt_name || \"computed\",\n    contentLeft: (\n      <Icon\n        className=\"text-2\"\n        style={{ color: getColumnDataColor(c, \"var(--text-2)\") }}\n        path={getColumnIconPath(c, columnWInfo)}\n      />\n    ),\n  };\n};\n\nexport const getColumnIconPath = (\n  c: Partial<\n    Pick<\n      ValidatedColumnInfo,\n      \"udt_name\" | \"tsDataType\" | \"references\" | \"is_pkey\"\n    >\n  >,\n  columnWInfo?: ColumnConfigWInfo,\n) => {\n  const tsDataType = c.tsDataType ?? tsDataTypeFromUdtName(c.udt_name ?? \"\");\n  return (\n    c.is_pkey ? mdiKey\n    : c.references ? mdiKeyLink\n    : c.udt_name === \"date\" ? mdiCalendar\n    : c.udt_name?.startsWith(\"timestamp\") ? mdiTimetable\n    : columnWInfo?.computedConfig ? mdiFunctionVariant\n    : colIs(c, \"_PG_numbers\") ? mdiNumeric\n    : colIs(c, \"_PG_postgis\") ? mdiMapMarker\n    : colIs(c, \"_PG_geometric\") ? mdiShapeOutline\n    : colIs(c, \"_PG_date\") ? mdiCalendar\n    : tsDataType === \"any\" || c.udt_name?.startsWith(\"json\") ? mdiCodeJson\n    : tsDataType === \"string\" ? mdiFormatText\n    : tsDataType === \"boolean\" ? mdiToggleSwitchOutline\n    : tsDataType.endsWith(\"[]\") ? mdiCodeBrackets\n    : columnWInfo?.nested ? mdiLink\n    : mdiFileQuestion\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnSortMenu.tsx",
    "content": "import React from \"react\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport type { ColumnSort } from \"./ColumnMenu\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\nimport type {\n  DBSchemaTablesWJoins,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport FormField from \"@components/FormField/FormField\";\n\nconst SORT_NULLS_OPTIONS = [\n  { key: \"first\", label: \"First\" },\n  { key: \"last\", label: \"Last\" },\n  { key: null, label: \"None\" },\n] as const;\n\nconst SORT_OPTIONS = [\n  { key: true, label: \"Ascending\" },\n  { key: false, label: \"Descending\" },\n  { key: null, label: \"None\" },\n] as const;\n\nexport type ColumnSortMenuProps = {\n  column: ColumnConfigWInfo;\n  w: WindowSyncItem<\"table\">;\n  tables: DBSchemaTablesWJoins;\n};\n\nexport const getDefaultSort = (columnName: string): ColumnSort => {\n  return {\n    key: columnName,\n    asc: true,\n    nulls: \"last\",\n  };\n};\n\nexport const ColumnSortMenu = ({ column, w }: ColumnSortMenuProps) => {\n  /**\n   * w.$get is used because newest w is not propagated from Table.tsx\n   */\n  const existingSort = (w.$get()?.sort || []).find((s) =>\n    column.nested ?\n      `${s.key}`.startsWith(`${column.name}.`)\n    : s.key === column.name,\n  );\n  const sort: ColumnSort = existingSort || {\n    ...getDefaultSort(column.name),\n    asc: null,\n  };\n  const colIsText =\n    column.info?.tsDataType === \"string\" ||\n    column.computedConfig?.tsDataType === \"string\";\n  const updateSort = (newColSort: ColumnSort) => {\n    let matched = false;\n    let newSort = (w.sort || []).map((s) => {\n      if (s.key === existingSort?.key) {\n        matched = true;\n        return { ...s, ...newColSort };\n      }\n      return { ...s };\n    });\n    if (newColSort.asc === null) {\n      newSort = newSort.filter((s) => s.key !== newColSort.key);\n    } else if (!matched as any) {\n      newSort.push(newColSort);\n    }\n    w.$update({ sort: newSort });\n  };\n\n  const nested = column.nested && {\n    ...column.nested,\n    table: column.nested.path.at(-1)!.table,\n  };\n  const nestedCols =\n    nested?.chart ?\n      [\n        { name: \"date\", show: true },\n        { name: \"value\", show: true },\n      ]\n    : nested?.columns;\n  return (\n    <FlexCol>\n      {nestedCols?.length && (\n        <Select\n          label={\"Nested column\"}\n          fullOptions={nestedCols.map((nc) => ({\n            key: `${column.name}.${nc.name}`,\n            disabledInfo:\n              nc.show ? undefined : \"Must unhide column before sorting\",\n          }))}\n          value={existingSort?.key}\n          onChange={(key, e) => {\n            const newSort =\n              existingSort ?\n                w.sort!.map((s) =>\n                  s.key === existingSort.key ? { ...existingSort, key } : s,\n                )\n              : [{ key, asc: true }];\n            w.$update({ sort: newSort });\n          }}\n        />\n      )}\n      <Select\n        label=\"Sort by\"\n        value={\n          sort.asc === true ? \"Ascending\"\n          : sort.asc === false ?\n            \"Descending\"\n          : null\n        }\n        variant=\"div\"\n        emptyLabel=\"None\"\n        fullOptions={SORT_OPTIONS}\n        optional={typeof sort.asc === \"boolean\" ? true : false}\n        onChange={(asc) => {\n          updateSort({ ...sort, asc: asc ?? null });\n        }}\n      />\n      {typeof sort.asc !== \"boolean\" ? null : (\n        <>\n          <Select\n            label=\"Nulls\"\n            value={sort.nulls || null}\n            variant=\"div\"\n            fullOptions={SORT_NULLS_OPTIONS}\n            onChange={(nulls) => {\n              updateSort({ ...sort, nulls });\n            }}\n          />\n          {colIsText && (\n            <FormField\n              type=\"checkbox\"\n              label=\"Null empty text\"\n              className=\"mt-1\"\n              value={sort.nullEmpty}\n              onChange={(e) => {\n                updateSort({ ...sort, nullEmpty: e });\n              }}\n            />\n          )}\n        </>\n      )}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnStyleControls/ColumnStyleControls.tsx",
    "content": "import { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { MINI_BARCHART_COLOR } from \"@components/ProgressBar\";\nimport { Select } from \"@components/Select/Select\";\nimport { _PG_numbers, includes } from \"prostgles-types\";\nimport type { ValidatedColumnInfo } from \"prostgles-types/lib\";\nimport React from \"react\";\nimport { type Prgl } from \"../../../../App\";\nimport { appTheme, useReactiveState } from \"../../../../appUtils\";\nimport { ColorPicker } from \"../ColorPicker\";\nimport { ChipStylePalette } from \"../ColumnDisplayFormat/ChipStylePalette\";\nimport { ConditionalCellIconStyleControls } from \"../ColumnDisplayFormat/ConditionalCellIconStyleControls\";\nimport type { CONDITION_OPERATORS } from \"../ColumnDisplayFormat/ConditionalCellStyleControls\";\nimport { ConditionalCellStyleControls } from \"../ColumnDisplayFormat/ConditionalCellStyleControls\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport { getValueColors } from \"./getValueColors\";\n\nexport type ColumnValue = string | number | Date | null | undefined | boolean;\n\ntype BasicConditionFilter = {\n  operator: Exclude<(typeof CONDITION_OPERATORS)[number], \"in\" | \"not in\">;\n  condition: ColumnValue;\n};\n\ntype ConditionFilter =\n  | BasicConditionFilter\n  | {\n      operator: \"in\" | \"not in\";\n      condition: BasicConditionFilter[\"condition\"][];\n    };\n\nexport type ChipStyle = {\n  textColor?: string;\n  chipColor?: string;\n  cellColor?: string;\n  borderColor?: string;\n};\n\nexport type ConditionalStyle = {\n  type: \"Conditional\";\n  conditions: (ConditionFilter & ChipStyle)[];\n  defaultStyle?: ChipStyle;\n};\nexport type ConditionalStyleIcons = {\n  type: \"Icons\";\n  size?: number;\n  valueToIconMap: Record<string, string>;\n};\nexport type FixedStyle = {\n  type: \"Fixed\";\n} & ChipStyle;\n\nexport type ScaleStyle = {\n  type: \"Scale\";\n  textColor: string;\n  minColor: string;\n  maxColor: string;\n};\nexport type BarchartStyle = {\n  type: \"Barchart\";\n  barColor: string;\n  textColor: string;\n};\n\nexport type StyleColumnProps = Pick<Prgl, \"db\" | \"tables\"> & {\n  column: ColumnConfig;\n  onUpdate: (newCol: Pick<ColumnConfig, \"style\">) => void;\n  tsDataType: ValidatedColumnInfo[\"tsDataType\"];\n  udt_name: ValidatedColumnInfo[\"udt_name\"];\n  tableName: string;\n};\n\nexport const ColumnStyleControls = (props: StyleColumnProps) => {\n  const { column, onUpdate, tsDataType, udt_name, tableName, db } = props;\n\n  const STYLE_MODES: Array<Required<ColumnConfig>[\"style\"][\"type\"]> = [\n    \"None\",\n    \"Fixed\",\n    \"Conditional\",\n    \"Icons\",\n  ];\n  const { style = { type: \"None\" as const } } = column;\n\n  if (\n    [\"number\", \"Date\"].includes(tsDataType) ||\n    includes(_PG_numbers, udt_name)\n  ) {\n    STYLE_MODES.push(\"Scale\");\n    STYLE_MODES.push(\"Barchart\");\n  }\n\n  const style_type = style.type;\n  const setStyle = (newStyle: ColumnConfig[\"style\"]) => {\n    /* If different style type then full overwrite. Otherwise update */\n    if (newStyle?.type && newStyle.type !== style.type) {\n      let _newStyle = { ...newStyle };\n      if (newStyle.type === \"Barchart\") {\n        _newStyle = {\n          barColor: \"rgba(0,246,96,1)\",\n          textColor: \"black\",\n          ..._newStyle,\n        } as BarchartStyle;\n      }\n      onUpdate({ style: _newStyle });\n    } else {\n      onUpdate({ style: { ...style, ...newStyle } });\n    }\n  };\n\n  const updateStylePart = (\n    newStyle: Partial<Required<ColumnConfig>[\"style\"]>,\n  ) => {\n    setStyle({ ...style, ...newStyle } as typeof style);\n  };\n  const { state: theme } = useReactiveState(appTheme);\n  return (\n    <FlexCol className=\"ColumnStyleControls flex-col gap-1\">\n      <Select\n        label=\"Style mode\"\n        value={style_type}\n        variant=\"div\"\n        options={STYLE_MODES}\n        onChange={(type) => {\n          if (type === \"Conditional\") {\n            void getValueColors(\n              {\n                type: \"table\",\n                db,\n                tableName,\n                columnName: column.name,\n                theme: theme,\n              },\n              setStyle,\n            );\n          } else {\n            setStyle(\n              type === \"Scale\" ?\n                {\n                  type,\n                  minColor: \"#8fccf0\",\n                  maxColor: \"#0AA1FA\",\n                  textColor: \"#1c1c1c\",\n                }\n              : type === \"Barchart\" ?\n                { type, barColor: MINI_BARCHART_COLOR, textColor: \"#646464\" }\n              : type === \"Fixed\" ? { type }\n              : type === \"Icons\" ? { type, valueToIconMap: {} }\n              : { type },\n            );\n          }\n        }}\n      />\n\n      {style.type === \"Fixed\" ?\n        <>\n          <FlexRowWrap>\n            <ColorPicker\n              label=\"Text\"\n              className=\"m-p5\"\n              value={style.textColor || \"black\"}\n              onChange={(textColor) => {\n                updateStylePart({ textColor });\n              }}\n            />\n            <ColorPicker\n              label=\"Chip\"\n              className=\"m-p5\"\n              value={style.chipColor || \"red\"}\n              onChange={(chipColor) => {\n                updateStylePart({ chipColor });\n              }}\n            />\n            <ColorPicker\n              label=\"Cell\"\n              className=\"m-p5\"\n              value={style.cellColor || \"white\"}\n              onChange={(cellColor) => {\n                updateStylePart({ cellColor });\n              }}\n            />\n            <ColorPicker\n              label=\"Border\"\n              className=\"m-p5\"\n              value={style.borderColor || \"transparent\"}\n              onChange={(borderColor) => {\n                updateStylePart({ borderColor });\n              }}\n            />\n          </FlexRowWrap>\n          <ChipStylePalette\n            onChange={({ borderColor, color, textColor }) =>\n              updateStylePart({ borderColor, textColor, chipColor: color })\n            }\n          />\n        </>\n      : style.type === \"Conditional\" ?\n        <ConditionalCellStyleControls {...props} style={style} />\n      : style.type === \"Icons\" ?\n        <ConditionalCellIconStyleControls {...props} style={style} />\n      : style.type === \"Scale\" ?\n        <FlexRowWrap>\n          <ColorPicker\n            className=\"mr-p5\"\n            label=\"Min color\"\n            value={style.minColor}\n            onChange={(minColor) => {\n              updateStylePart({ minColor });\n            }}\n          />\n          <ColorPicker\n            className=\"mr-p5\"\n            label=\"Max color\"\n            value={style.maxColor}\n            onChange={(maxColor) => {\n              updateStylePart({ maxColor });\n            }}\n          />\n          <ColorPicker\n            className=\"mr-p5\"\n            label=\"Text\"\n            value={style.textColor}\n            onChange={(textColor) => {\n              updateStylePart({ textColor });\n            }}\n          />\n        </FlexRowWrap>\n      : style.type === \"Barchart\" ?\n        <FlexRowWrap>\n          <ColorPicker\n            label=\"Bar\"\n            className=\"m-p5\"\n            value={style.barColor}\n            onChange={(barColor) => {\n              updateStylePart({ barColor });\n            }}\n          />\n          <ColorPicker\n            label=\"Text\"\n            className=\"m-p5\"\n            value={style.textColor}\n            onChange={(textColor) => {\n              updateStylePart({ textColor });\n            }}\n          />\n        </FlexRowWrap>\n      : null}\n    </FlexCol>\n  );\n};\n\nexport const getRandomElement = <Arr,>(\n  items: Arr[],\n): { elem: Arr; index: number } => {\n  const randomIndex = Math.floor(Math.random() * items.length);\n  return { elem: items[randomIndex]!, index: randomIndex };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnStyleControls/getValueColors.ts",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport type { Theme } from \"src/App\";\nimport { chipColorsFadedBorder } from \"../ColumnDisplayFormat/ChipStylePalette\";\nimport { getRandomElement, type ConditionalStyle } from \"./ColumnStyleControls\";\n\ntype DefaultConditionalStyleArgs =\n  | {\n      type: \"table\";\n      db: DBHandlerClient;\n      tableName: string;\n      columnName: string;\n      filter?: AnyObject;\n      theme: Theme;\n    }\n  | {\n      type: \"sql\";\n      db: DBHandlerClient;\n      query: string;\n      columnName: string;\n      theme: Theme;\n    };\nexport const DefaultConditionalStyleLimit = 5;\nexport const getValueColors = async (\n  args: DefaultConditionalStyleArgs,\n  setStyle: (newStyle: ConditionalStyle) => void,\n) => {\n  const { theme } = args;\n  const values = await fetchColumnValues(args);\n  if (!values) return;\n  const prevSyleIndexes = new Set<number>();\n  setStyle({\n    type: \"Conditional\",\n    conditions: values.map((v) => {\n      const nonPickedStyles =\n        prevSyleIndexes.size === chipColorsFadedBorder.length ?\n          chipColorsFadedBorder\n        : chipColorsFadedBorder.filter((_, i) => !prevSyleIndexes.has(i));\n      const { elem: style, index } = getRandomElement(nonPickedStyles);\n      prevSyleIndexes.add(index);\n      return {\n        condition: v,\n        operator: \"=\",\n        ...style,\n        textColor: theme === \"dark\" ? style.textColorDarkMode : style.textColor,\n        chipColor: style.color,\n      };\n    }),\n  });\n};\n\nexport const fetchColumnValues = async (args: DefaultConditionalStyleArgs) => {\n  const { db } = args;\n  if (args.type === \"table\") {\n    const { columnName, db, tableName, filter = {} } = args;\n    const tableHandler = db[tableName];\n    if (!tableHandler?.find) return undefined;\n    const rows = await tableHandler.find(filter, {\n      select: { [columnName]: 1 },\n      limit: DefaultConditionalStyleLimit,\n      groupBy: true,\n    });\n    const values = rows.map((v) => v[columnName]) as string[];\n    return values;\n  }\n\n  const values = await db.sql!(\n    `SELECT DISTINCT \\${columnName:name} \n      FROM (\n        ${args.query}\n      ) t \n      LIMIT ${DefaultConditionalStyleLimit}`,\n    { columnName: args.columnName },\n    { returnType: \"values\" },\n  );\n  return values as string[];\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/ColumnsMenu.tsx",
    "content": "import Popup from \"@components/Popup/Popup\";\n\nimport type {\n  SingleSyncHandles,\n  SyncDataItem,\n} from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React from \"react\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type {\n  WindowData,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport RTComp from \"../../RTComp\";\nimport { SQLSmartEditor } from \"../../SQLEditor/SQLSmartEditor\";\nimport { getFullColumnConfig } from \"../tableUtils/getFullColumnConfig\";\nimport { updateWCols } from \"../tableUtils/tableUtils\";\nimport { AddColumnMenu } from \"./AddColumnMenu\";\nimport { AddComputedColMenu } from \"./AddComputedColumn/AddComputedColMenu\";\nimport { ColumnList } from \"./ColumnList\";\nimport type { ColumnConfig } from \"./ColumnMenu\";\nimport { LinkedColumn } from \"./LinkedColumn/LinkedColumn\";\nimport type { NestedColumnOpts } from \"./getNestedColumnTable\";\n\ntype P = {\n  db: DBHandlerClient;\n  w: WindowSyncItem<\"table\">;\n  nestedColumnOpts: NestedColumnOpts | undefined;\n  tables: CommonWindowProps[\"tables\"];\n  suggestions: CommonWindowProps[\"suggestions\"];\n  onClose: () => any;\n  showAddCompute?: { colName?: string };\n};\ntype S = {\n  query?: {\n    sql: string;\n    hint?: string;\n  };\n  addColMenu?: Element;\n  addRefColMenu?: boolean;\n  w?: SyncDataItem<Required<WindowData<\"table\">>, true>;\n};\n\nexport class ColumnsMenu extends RTComp<P, S> {\n  state: S = {\n    addColMenu: undefined,\n    query: undefined,\n  };\n\n  wSub?: SingleSyncHandles<Required<WindowData<\"table\">>, true>;\n\n  onDelta = async () => {\n    const w = this.props.w;\n\n    if (!this.wSub) {\n      this.wSub = await w.$cloneSync((w) => {\n        this.setState({ w });\n      });\n    }\n  };\n\n  get tableName() {\n    const { nestedColumnOpts, w } = this.props;\n    if (nestedColumnOpts) {\n      // const nestedCol = w.columns?.find(c => c.name === nestedColumnName)\n      // return nestedCol?.nested?.path.at(-1)?.table;\n      return nestedColumnOpts.config.nested?.path.at(-1)?.table;\n    }\n\n    return w.table_name;\n  }\n\n  render() {\n    const { w, addColMenu, query } = this.state;\n    const { db, tables, nestedColumnOpts, onClose, showAddCompute } =\n      this.props;\n    if (!w) return null;\n\n    let table = tables.find((t) => t.name === this.tableName);\n    let cols = getFullColumnConfig(tables, w);\n    const nestedColumnName = nestedColumnOpts?.config.name;\n    const onUpdateCols = (columns: ColumnConfig[]) => {\n      if (nestedColumnOpts?.type === \"new\") {\n        throw \"Not implemented\";\n      }\n      return updateWCols(w, columns, nestedColumnName);\n    };\n\n    const nestedColumn =\n      nestedColumnName ?\n        w.columns?.find((c) => c.name === nestedColumnName)\n      : undefined;\n    if (nestedColumn) {\n      if (!nestedColumn.nested) {\n        return <div>Invalid nested column config</div>;\n      }\n      const nestedTableName = nestedColumn.nested.path.at(-1)!.table;\n      table = tables.find((t) => t.name === nestedTableName);\n      cols = getFullColumnConfig(tables, {\n        table_name: nestedTableName,\n        columns: nestedColumn.nested.columns,\n      });\n    }\n\n    if (!table || !this.tableName) {\n      return <div>{this.tableName} table not found</div>;\n    }\n\n    let popup: React.ReactNode = null;\n    if (addColMenu || showAddCompute) {\n      popup = (\n        <AddComputedColMenu\n          db={db}\n          tableHandler={db[this.tableName] as any}\n          selectedColumn={showAddCompute?.colName}\n          w={w}\n          anchorEl={addColMenu}\n          tables={tables}\n          nestedColumnOpts={nestedColumnOpts}\n          onClose={() => {\n            this.setState({ addColMenu: undefined });\n            onClose();\n          }}\n        />\n      );\n    } else if (this.state.addRefColMenu) {\n      popup = (\n        <Popup title=\"Add Linked Data\">\n          <LinkedColumn column={undefined} w={w} onClose={onClose} />\n        </Popup>\n      );\n    }\n\n    if (query && this.props.suggestions) {\n      return (\n        <div className=\"flex-col f-1 min-h-0 p-1\">\n          <SQLSmartEditor\n            title=\"Create new column\"\n            sql={db.sql!}\n            query={query.sql}\n            hint={query.hint}\n            suggestions={this.props.suggestions}\n            onCancel={() => this.setState({ query: undefined })}\n            onSuccess={onClose}\n          />\n        </div>\n      );\n    }\n\n    return (\n      <div className=\"flex-col f-1 min-h-0\">\n        {popup}\n        <ColumnList\n          columns={cols}\n          w={w}\n          onClose={onClose}\n          suggestions={this.props.suggestions}\n          table={table}\n          onChange={onUpdateCols}\n        />\n        <div className=\"flex-col p-1\">\n          <AddColumnMenu\n            variant=\"detailed\"\n            w={w}\n            db={db}\n            suggestions={this.props.suggestions}\n            tables={tables}\n            nestedColumnOpts={nestedColumnOpts}\n          />\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/FunctionSelector/FunctionExtraArguments.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { usePromise } from \"prostgles-client\";\nimport { _PG_date } from \"prostgles-types\";\nimport React, { useEffect } from \"react\";\nimport type { Prgl } from \"src/App\";\nimport type { DBSchemaTableWJoins } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\n\nexport type FuncArgs = NonNullable<\n  Required<ColumnConfig>[\"computedConfig\"][\"args\"]\n>;\n\ntype P = {\n  argName: keyof FuncArgs;\n  args: FuncArgs | undefined;\n  onChange: (newArgs: FuncArgs) => void;\n  db: Prgl[\"db\"];\n  columnName: string | undefined;\n  table: DBSchemaTableWJoins;\n};\nexport const FunctionExtraArguments = ({\n  argName,\n  args,\n  db,\n  table,\n  onChange,\n  columnName,\n}: P) => {\n  const tableHandler = db[table.name];\n\n  useEffect(() => {\n    if (argName === \"$string_agg\" && !args?.$string_agg) {\n      onChange({\n        $string_agg: { separator: \", \" },\n      });\n    }\n  }, [argName, args, tableHandler, onChange]);\n\n  const { $template_string } = args ?? {};\n  const templateStringInfo = usePromise(async () => {\n    let template_string_error: any = undefined;\n    let template_string_hint: string | undefined = undefined;\n    if (!$template_string) {\n      // No value yet\n    } else if (!$template_string.includes(\"{\")) {\n      template_string_error =\n        \"Template string must include at least one column in curly braces, e.g. {FirstName}\";\n    } else if (tableHandler && tableHandler.find) {\n      try {\n        template_string_hint = (await tableHandler.find(\n          {},\n          {\n            returnType: \"value\",\n            limit: 1,\n            select: {\n              val: { $template_string: [$template_string] },\n            },\n          },\n        )) as unknown as string;\n      } catch (error) {\n        template_string_error = error;\n      }\n    }\n    return { template_string_hint, template_string_error };\n  }, [$template_string, tableHandler]);\n\n  if (argName === \"$template_string\") {\n    return (\n      <>\n        <FormFieldDebounced\n          value={args?.$template_string ?? \"\"}\n          label=\"Template string\"\n          hint={\n            templateStringInfo?.template_string_hint ??\n            \"Use column names. E.g.: Dear {FirstName} {LastName}\"\n          }\n          inputProps={{ autoFocus: true }}\n          error={templateStringInfo?.template_string_error}\n          onChange={($template_string: string) => {\n            onChange({ $template_string });\n          }}\n        />\n        <div className=\"flex-row-wrap\">\n          {table.columns.map((c, i) => (\n            <div key={i} className=\"p-p25\">{`{${c.name}}`}</div>\n          ))}\n        </div>\n      </>\n    );\n  }\n\n  if (argName === \"$duration\") {\n    return args?.$duration?.otherColumn ?\n        <FlexCol className=\"gap-p5 mt-1\">\n          <label className=\"noselect f-0 text-1p5 ta-left\">Compare to</label>\n          <Btn\n            variant=\"filled\"\n            style={{\n              backgroundColor: \"rgb(0, 183, 255)\",\n            }}\n            onClick={() => {\n              onChange({});\n            }}\n          >\n            {args.$duration.otherColumn}\n          </Btn>\n        </FlexCol>\n      : <SearchList\n          id=\"duration_othercolumn\"\n          label=\"Compare to column\"\n          items={table.columns\n            .filter(\n              (c) =>\n                c.name !== columnName && _PG_date.some((v) => v === c.udt_name),\n            )\n            .map((c) => ({\n              key: c.name,\n              label: c.label,\n              onPress: () => {\n                onChange({\n                  $duration: { otherColumn: c.name },\n                });\n              },\n            }))}\n        />;\n  }\n\n  return (\n    <FormField\n      label=\"Separator\"\n      hint=\"Defaults to ', '\"\n      value={args?.$string_agg?.separator}\n      onChange={(separator) => onChange({ $string_agg: { separator } })}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/FunctionSelector/FunctionSelector.tsx",
    "content": "import { Select } from \"@components/Select/Select\";\nimport { mdiFunction, mdiSigma } from \"@mdi/js\";\nimport type { ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport {\n  funcAcceptsColumn,\n  getAggFuncs,\n  getFuncs,\n  type FuncDef,\n} from \"./functions\";\n\ntype P = {\n  selectedFunction?: string;\n  column: string | undefined;\n  /**\n   * If defined then this is not a nested column\n   */\n  wColumns: ColumnConfig[] | undefined;\n  tableColumns: ValidatedColumnInfo[];\n  onSelect: (func: FuncDef | undefined) => void;\n  className?: string;\n  currentNestedColumnName?: string;\n};\n\nexport const FunctionSelector = ({\n  column,\n  tableColumns,\n  onSelect,\n  selectedFunction,\n  wColumns: parentColumns,\n  className,\n  currentNestedColumnName,\n}: P) => {\n  const cannotUseAggs = useMemo(\n    () =>\n      parentColumns?.some(\n        (c) => c.show && c.nested && c.name !== currentNestedColumnName,\n      ),\n    [parentColumns, currentNestedColumnName],\n  );\n\n  const funcs = useMemo(() => {\n    const columnInfo = tableColumns.find((c) => c.name === column);\n    const funcs = [...getAggFuncs(), ...getFuncs()]\n      .map((f) => ({\n        ...f,\n        isAllowedForColumn: funcAcceptsColumn(\n          f,\n          columnInfo ? [columnInfo] : tableColumns,\n        ),\n      }))\n      .filter((f) => f.isAllowedForColumn);\n    return funcs;\n  }, [column, tableColumns]);\n\n  return (\n    <>\n      <Select\n        id=\"func_selector\"\n        data-command=\"FunctionSelector\"\n        className={className}\n        style={{ maxHeight: \"500px\" }}\n        value={selectedFunction}\n        label={\"Applied function\"}\n        placeholder=\"Search functions...\"\n        showSelectedSublabel={true}\n        optional={true}\n        onChange={(key) => {\n          const def = funcs.find((f) => f.key === key);\n          onSelect(def);\n        }}\n        noSearchLimit={5}\n        variant={selectedFunction ? undefined : \"search-list-only\"}\n        fullOptions={funcs.map((def) => {\n          return {\n            ...def,\n            iconPath: def.isAggregate ? mdiSigma : mdiFunction,\n            disabledInfo:\n              cannotUseAggs && def.isAggregate ?\n                \"Cannot use aggregations with nested column\"\n              : def.isAllowedForColumn ? undefined\n              : \"Not suitable for the selected column data type\",\n          };\n        })}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/FunctionSelector/functions.ts",
    "content": "import type { ValidatedColumnInfo } from \"prostgles-types\";\nimport { _PG_date, _PG_interval, _PG_numbers } from \"prostgles-types\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\n\nconst infoTypes = {\n  string: { udt_name: \"text\", tsDataType: \"string\" },\n  number: { udt_name: \"numeric\", tsDataType: \"number\" },\n  bigint: { udt_name: \"int8\", tsDataType: \"string\" },\n  date: { udt_name: \"date\", tsDataType: \"string\" },\n  interval: { udt_name: \"interval\", tsDataType: \"any\" },\n  geo: { udt_name: \"geography\", tsDataType: \"any\" },\n} as const;\n\nexport type FuncDef = {\n  key: string;\n  label: string;\n  subLabel: string;\n  tsDataTypeCol?: \"any\" | ValidatedColumnInfo[\"tsDataType\"][];\n  udtDataTypeCol?: \"any\" | ValidatedColumnInfo[\"udt_name\"][];\n  outType: \"sameAsInput\" | Pick<ValidatedColumnInfo, \"tsDataType\" | \"udt_name\">;\n  isAggregate?: boolean;\n  requiresArg?: keyof NonNullable<\n    Required<ColumnConfig>[\"computedConfig\"][\"args\"]\n  >;\n};\n\nexport const getFuncs = (): FuncDef[] => {\n  const GeometryFuncs: {\n    key: string;\n    label: string;\n    subLabel: string;\n    outType: Pick<ValidatedColumnInfo, \"tsDataType\" | \"udt_name\">;\n  }[] = [\n    {\n      key: \"$ST_Length\",\n      label: \"ST_Length\",\n      subLabel:\n        \"returns the 2D Cartesian length of the geometry if it is a LineString, MultiLineString, ST_Curve, ST_MultiCurve.\",\n      outType: { udt_name: \"numeric\", tsDataType: \"number\" },\n    },\n    {\n      key: \"$ST_AsText\",\n      label: \"ST_AsText\",\n      subLabel:\n        \"Returns the OGC Well-Known Text (WKT) representation of the geometry/geography\",\n      outType: { udt_name: \"text\", tsDataType: \"string\" },\n    },\n    {\n      key: \"$ST_AsGeoJSON\",\n      label: \"ST_AsGeoJSON\",\n      subLabel:\n        \"Returns a geometry as a GeoJSON 'geometry', or a row as a GeoJSON 'feature'\",\n      outType: { udt_name: \"json\", tsDataType: \"string\" },\n    },\n    {\n      key: \"$ST_SnapToGrid\",\n      label: \"ST_SnapToGrid\",\n      subLabel:\n        \"Snap all points of the input geometry to the grid defined by its origin and cell size. Remove consecutive points falling on the same cell\",\n      outType: { udt_name: \"geometry\", tsDataType: \"any\" },\n    },\n  ];\n\n  const DateFuncs = [\n    {\n      key: \"$datetime\",\n      label: \"Date and time\",\n      subLabel: \"Timestamp formated as 'YYYY-MM-DD HH24:MI'\",\n    },\n    {\n      key: \"$timedate\",\n      label: \"Time and date\",\n      subLabel: \"Timestamp formated as 'HH24:MI YYYY-MM-DD'\",\n    },\n    // { key: \"$DayNo\",    label: \"\", subLabel: \"Timestamp formated as 'DD\" },\n    // { key: \"$dowUS\",    label: \"\", subLabel: \"Timestamp formated as 'D\" },\n    {\n      key: \"$D\",\n      label: \"Day of week number\",\n      subLabel:\n        \"Timestamp formated as 'D'. Day of the week, Sunday (1) to Saturday (7)\",\n    },\n    {\n      key: \"$dy\",\n      label: \"Day name short\",\n      subLabel:\n        \"Timestamp formated as 'dy'. Abbreviated lower case day name (3 chars in English, localized lengths vary)\",\n    },\n    {\n      key: \"$Dy\",\n      label: \"Day Name short\",\n      subLabel:\n        \"Timestamp formated as 'Dy'. Abbreviated capitalized day name (3 chars in English, localized lengths vary)\",\n    },\n    {\n      key: \"$DD\",\n      label: \"Day of month\",\n      subLabel: \"Timestamp formated as 'DD'. Day of month (01-31)\",\n    },\n    {\n      key: \"$ID\",\n      label: \"ISO 8601 day of the week\",\n      subLabel: \"Timestamp formated as 'ID'. Monday (1) to Sunday (7)\",\n    },\n    {\n      key: \"$MM\",\n      label: \"Month number\",\n      subLabel: \"Timestamp formated as 'MM'. Month number (01-12)\",\n    },\n    // { key: \"$MonthNo\",  label: \"Month number\", subLabel: \"Timestamp formated as 'MM. Month number (01-12)\" },\n    {\n      key: \"$yy\",\n      label: \"Year short\",\n      subLabel: \"Timestamp formated as 'YY'. Last 2 digits of year\",\n    },\n    // { key: \"$yr\",       label: \"\", subLabel: \"Timestamp formated as 'yy\" },\n    {\n      key: \"$day\",\n      label: \"Day name\",\n      subLabel: \"Timestamp formated as 'day'. Full lower case day name\",\n    },\n    {\n      key: \"$Day\",\n      label: \"Day Name\",\n      subLabel: \"Timestamp formated as 'Day'. Full capitalized day name\",\n    },\n    // { key: \"$dow\",      label: \"\", subLabel: \"Timestamp formated as 'ID\" },\n    {\n      key: \"$mon\",\n      label: \"Month name short\",\n      subLabel:\n        \"Timestamp formated as 'mon'. Abbreviated lower case month name (3 chars in English, localized lengths vary)\",\n    },\n    {\n      key: \"$Mon\",\n      label: \"Month Name short\",\n      subLabel:\n        \"Timestamp formated as 'Mon'. Abbreviated capitalized month name (3 chars in English, localized lengths vary)\",\n    },\n    {\n      key: \"$month\",\n      label: \"Month name\",\n      subLabel: \"Timestamp formated as 'month'. Full lower case month name \",\n    },\n    {\n      key: \"$Month\",\n      label: \"Month Name\",\n      subLabel: \"Timestamp formated as 'Month'. Full capitalized month name \",\n    },\n    {\n      key: \"$date\",\n      label: \"Date\",\n      subLabel: \"Timestamp formated as 'YYYY-MM-DD'\",\n    },\n    {\n      key: \"$time\",\n      label: \"Time\",\n      subLabel: \"Timestamp formated as 'HH24:MI'. 24 Hour time format\",\n    },\n    // { key: \"$year\",     label: \"\", subLabel: \"Timestamp formated as 'yyyy\" },\n    {\n      key: \"$yyyy\",\n      label: \"Year full\",\n      subLabel: \"Timestamp formated as 'yyyy'. Year (4 or more digits)\",\n    },\n    {\n      key: \"$time12\",\n      label: \"Time 12H\",\n      subLabel: \"Timestamp formated as 'HH:MI'. 12 Hour time format\",\n    },\n    {\n      key: \"$timeAM\",\n      label: \"Time AM/PM\",\n      subLabel:\n        \"Timestamp formated as 'HH:MI AM'. 24 Hour time format with AM/PM meridiem indicators\",\n    },\n    // { label: \"$to_char\", subLabel: \"format dates and strings. Eg: [current_timestamp, 'HH12:MI:SS']\" },\n    {\n      key: \"$age\",\n      label: \"Age at start of day\",\n      subLabel: \"Age of timestamp compared to start of today\",\n    },\n    {\n      key: \"$ageNow\",\n      label: \"Age\",\n      subLabel: \"Age of timestamp compared to now\",\n    },\n    {\n      key: \"$duration\",\n      label: \"Duration\",\n      subLabel:\n        \"Duration between another date field. Expressed as an interval of years, months, days, minutes\",\n    },\n  ] as const;\n\n  const StringFuncs = [\n    // { label: \"$left\", subLabel: \"[column_name, number] -> substring\"  },\n    // { label: \"$right\", subLabel: \"[column_name, number] -> substring\"   },\n    {\n      key: \"$trim\",\n      label: \"TRIM\",\n      subLabel:\n        \"Remove spaces or set of characters from the leading or trailing or both side from a string\",\n    },\n    {\n      key: \"$upper\",\n      label: \"Uppercase\",\n      subLabel: \"Convert all characters to uppercase\",\n    },\n    {\n      key: \"$lower\",\n      label: \"Lowercase\",\n      subLabel: \"Convert all characters to lowercase\",\n    },\n    {\n      key: \"$length\",\n      label: \"Length\",\n      subLabel: \"Number of characters in the string\",\n    },\n    {\n      key: \"$reverse\",\n      label: \"Reverse\",\n      subLabel: \"Arrange a string in reverse order\",\n    },\n    {\n      key: \"$initcap\",\n      label: \"Capitalise Initials\",\n      subLabel:\n        \"Convert the first letter of each word to upper case and the remaining to lower case\",\n    },\n    {\n      key: \"$unnest_words\",\n      label: \"Split into words\",\n      subLabel: \"Split text from a column at spaces\",\n    },\n  ];\n\n  const NumberFuncs = [\n    {\n      key: \"$ceil\",\n      label: \"Ceil\",\n      subLabel:\n        \"Smallest integer which is greater than, or equal to, the specified numeric expression\",\n    },\n    {\n      key: \"$floor\",\n      label: \"Floor\",\n      subLabel:\n        \"Smallest integer which is lower than, or equal to, the specified numeric expression\",\n    },\n    {\n      key: \"$sign\",\n      label: \"Sign\",\n      subLabel:\n        \"Returns -1 0 or 1 depending of the column value. Returns null if null is provided\",\n    },\n    { key: \"$round\", label: \"Round\", subLabel: \"Round to nearest integer\" },\n    // { key: \"$sum\", label: \"Sum\",  subLabel: \"Sum of all values\" },\n    // { key: \"$avg\", label: \"Average\",  subLabel: \"Average of all values\" },\n  ];\n\n  const AnyFuncs = [\n    {\n      key: \"$template_string\",\n      label: \"Template string\",\n      subLabel:\n        \"Create a string based on column values. E.g.: Dear {FirstName} {LastName}, ...\",\n      requiresArg: \"$template_string\",\n    } satisfies Pick<FuncDef, \"key\" | \"label\" | \"subLabel\" | \"requiresArg\">,\n  ];\n\n  const res: FuncDef[] = [\n    ...GeometryFuncs.map((f) => ({\n      ...f,\n      udtDataTypeCol: [\"geography\", \"geometry\"] as any,\n    })),\n    ...DateFuncs.map(\n      (f) =>\n        ({\n          ...f,\n          outType:\n            f.key.startsWith(\"$age\") || f.key === \"$duration\" ?\n              infoTypes.interval\n            : infoTypes.string,\n          requiresArg: undefined,\n          udtDataTypeCol: [\"int8\", ..._PG_date] as any,\n        }) satisfies Pick<\n          FuncDef,\n          | \"key\"\n          | \"label\"\n          | \"subLabel\"\n          | \"requiresArg\"\n          | \"outType\"\n          | \"udtDataTypeCol\"\n        >,\n    ),\n    ...StringFuncs.map((f) => ({\n      ...f,\n      outType: infoTypes.string,\n      tsDataTypeCol: [\"string\"] as any,\n    })),\n    ...NumberFuncs.map((f) => ({\n      ...f,\n      outType: infoTypes.number,\n      tsDataTypeCol: [\"number\"] as any,\n    })),\n    ...AnyFuncs.map((f) => ({\n      ...f,\n      outType: infoTypes.string,\n    })),\n  ].map((f) => ({\n    ...f,\n  }));\n\n  return res;\n};\n\nconst numericOrDate: FuncDef[\"udtDataTypeCol\"] = [\n  ..._PG_date,\n  ..._PG_interval,\n  ..._PG_numbers,\n];\nconst numeric: FuncDef[\"udtDataTypeCol\"] = [..._PG_interval, ..._PG_numbers];\n\nexport const CountAllFunc: FuncDef & { name: string } = {\n  key: \"$countAll\",\n  name: \"COUNT ALL\",\n  label: \"Count of all rows\",\n  subLabel: \"\",\n  tsDataTypeCol: undefined,\n  outType: infoTypes.bigint,\n  isAggregate: true,\n};\n\nexport const aggFunctions = [\n  CountAllFunc,\n  {\n    key: \"$count\",\n    name: \"COUNT\",\n    label: \"Count of rows for which the column value is not null\",\n    tsDataTypeCol: \"any\",\n    outType: infoTypes.bigint,\n  },\n\n  {\n    key: \"$max\",\n    name: \"MAX\",\n    label: \"Maximum value\",\n    udtDataTypeCol: numericOrDate,\n    outType: \"sameAsInput\",\n  },\n  {\n    key: \"$min\",\n    name: \"MIN\",\n    label: \"Minimum value\",\n    udtDataTypeCol: numericOrDate,\n    outType: \"sameAsInput\",\n  },\n  {\n    key: \"$avg\",\n    name: \"AVERAGE\",\n    label: \"Average value\",\n    udtDataTypeCol: numeric,\n    outType: infoTypes.number,\n  },\n  {\n    key: \"$sum\",\n    name: \"SUM\",\n    label: \"Sum of all non-null input values\",\n    udtDataTypeCol: numeric,\n    outType: infoTypes.number,\n  },\n  {\n    key: \"$string_agg\",\n    name: \"AGGREGATE STRING\",\n    label:\n      \"Non-null string/text values concatenated into a string, separated by delimiter\",\n    tsDataTypeCol: [\"string\"] as [\"string\"],\n    requiresArg: \"$string_agg\",\n    outType: infoTypes.string,\n  },\n] as const satisfies readonly (Omit<FuncDef, \"subLabel\"> & { name: string })[];\n\nexport const getAggFuncs = (): FuncDef[] => {\n  return aggFunctions.map(\n    (f) =>\n      ({\n        ...f,\n        isAggregate: true,\n        label: f.name,\n        subLabel: f.label,\n      }) satisfies FuncDef,\n  );\n};\n\nexport const funcAcceptsColumn = (\n  f: FuncDef,\n  targetCols: ValidatedColumnInfo[],\n) => {\n  return (\n    (!f.tsDataTypeCol && !f.udtDataTypeCol) ||\n    f.tsDataTypeCol === \"any\" ||\n    f.udtDataTypeCol === \"any\" ||\n    (Array.isArray(f.udtDataTypeCol) &&\n      f.udtDataTypeCol.some((udt) =>\n        targetCols.some((c) => c.udt_name === udt),\n      )) ||\n    (Array.isArray(f.tsDataTypeCol) &&\n      f.tsDataTypeCol.some((tsd) =>\n        targetCols.some((c) => c.tsDataType === tsd),\n      ))\n  );\n};\n\nexport const getColumnsAcceptedByFunction = (\n  {\n    tsDataTypeCol,\n    udtDataTypeCol,\n  }: Pick<FuncDef, \"tsDataTypeCol\" | \"udtDataTypeCol\">,\n  columns: ValidatedColumnInfo[],\n) => {\n  if (tsDataTypeCol || udtDataTypeCol) {\n    return columns.filter((c) => {\n      if (tsDataTypeCol === \"any\" || udtDataTypeCol === \"any\") {\n        return true;\n      } else if (tsDataTypeCol) {\n        return tsDataTypeCol.includes(c.tsDataType);\n      } else if (udtDataTypeCol) {\n        return udtDataTypeCol.includes(c.udt_name);\n      }\n\n      return false;\n    });\n  }\n  return undefined;\n};\n\nexport const FUCTION_DEFINITIONS = [...getAggFuncs(), ...getFuncs()];\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/JoinPathSelectorV2.tsx",
    "content": "import type { BtnProps } from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { getSearchRanking } from \"@components/SearchList/searchMatchUtils/getSearchRanking\";\nimport type { FullOption } from \"@components/Select/Select\";\nimport { Select } from \"@components/Select/Select\";\nimport type { ParsedJoinPath } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { isDefined } from \"../../../utils/utils\";\nimport CodeExample from \"../../CodeExample\";\nimport type { DBSchemaTablesWJoins } from \"../../Dashboard/dashboardUtils\";\nimport type { TargetPath } from \"../tableUtils/getJoinPaths\";\nimport { getJoinPathStr, getJoinPaths } from \"../tableUtils/getJoinPaths\";\ntype P = {\n  tables: DBSchemaTablesWJoins;\n  tableName: string;\n  btnProps?: BtnProps<void>;\n  onChange: (\n    targetPath: TargetPath,\n    multiJoin:\n      | {\n          value: [string, string][][];\n          chosen: [string, string][];\n        }\n      | undefined,\n  ) => void;\n  value: ParsedJoinPath[] | undefined;\n  getFullOption?: (\n    path: ParsedJoinPath[],\n  ) => Pick<FullOption<string>, \"disabledInfo\"> | undefined;\n  variant?: \"expanded\";\n};\n\nexport const getJoinPathLabel = (\n  j: TargetPath,\n  { tableName, tables }: Pick<P, \"tableName\" | \"tables\">,\n) => {\n  const labels = j.path.map((p, pIdx) => {\n    const prevPath = j.path[pIdx - 1];\n    const hasMultipleFkeyConstraints =\n      !prevPath ?\n        hasMultiFkeys(tables, tableName, p.table)\n      : hasMultiFkeys(tables, p.table, prevPath.table);\n    let label = p.table;\n    if (hasMultipleFkeyConstraints) {\n      label = `(${Object.entries(p.on[0]!)\n        .map(([l, r]) => `${l} = ${r}`)\n        .join(\" AND \")}) ${p.table}`;\n    }\n    return {\n      label,\n      multiJoin: hasMultipleFkeyConstraints && {\n        value: hasMultipleFkeyConstraints,\n        chosen: Object.entries(p.on[0]!),\n      },\n    };\n  });\n\n  return {\n    labels,\n    label: labels.map((d) => d.label).join(\" > \"),\n  };\n};\n\nexport const getAllJoins = ({\n  tableName,\n  tables,\n  value,\n}: Pick<P, \"tableName\" | \"tables\" | \"value\">) => {\n  const allJoins = getJoinPaths(tableName, tables);\n  const valueStr = value && getJoinPathStr(value);\n  const targetPathIdx = allJoins.findIndex((j) => j.pathStr === valueStr);\n  const targetPath = allJoins[targetPathIdx];\n  const allJoinsWithLabels = allJoins.map((j) => {\n    const { label, labels } = getJoinPathLabel(j, { tableName, tables });\n\n    return {\n      ...j,\n      label,\n      labels,\n    };\n  });\n  return {\n    allJoins: allJoinsWithLabels,\n    targetPath,\n    targetPathIdx: targetPath && targetPathIdx,\n  };\n};\n\nexport const JoinPathSelectorV2 = (props: P) => {\n  const {\n    tables,\n    tableName,\n    value,\n    onChange,\n    variant,\n    getFullOption,\n    btnProps,\n  } = props;\n\n  const { allJoins, targetPathIdx } = useMemo(\n    () => getAllJoins({ tableName, tables, value }),\n    [tableName, tables, value],\n  );\n\n  const nestedColumnQuery = useMemo(() => {\n    if (!value) return;\n    const asName = (v) => JSON.stringify(v);\n    const rootTable = asName(tableName);\n    return [\n      `SELECT ${rootTable}.*, ${asName(value.at(-1)!.table)}.*`,\n      `FROM ${rootTable}`,\n      ...value.map(\n        (path, index) =>\n          `JOIN ${asName(path.table)} \\n  ON ${path.on\n            .map((on) =>\n              Object.entries(on).map(([k, v]) => {\n                const prevTable =\n                  !index ? rootTable : asName(value[index - 1]?.table);\n                return `${prevTable}.${k} = ${asName(path.table)}.${v}`;\n              }),\n            )\n            .join(\" AND \")}`,\n      ),\n    ].join(\"\\n\");\n  }, [value, tableName]);\n\n  const fullOptions = useMemo(\n    () =>\n      allJoins.map((j) => {\n        return {\n          ...getFullOption?.(j.path),\n          key: j.label,\n          lastJoinLabel: j.labels.at(-1),\n          ranking: (searchTerm) =>\n            getSearchRanking(\n              searchTerm,\n              j.labels.map((l) => l.label),\n            ),\n          subLabel: j.table.columns.map((c) => c.name).join(\", \"),\n        };\n      }),\n    [allJoins, getFullOption],\n  );\n\n  const targetValue =\n    isDefined(targetPathIdx) ? fullOptions[targetPathIdx] : undefined;\n\n  const infoNode =\n    !nestedColumnQuery ? undefined : (\n      <FlexCol>\n        <div>Join path details</div>\n        <CodeExample\n          language=\"sql\"\n          value={nestedColumnQuery}\n          style={{\n            minWidth: \"400px\",\n            minHeight: \"250px\",\n          }}\n        />\n      </FlexCol>\n    );\n\n  return (\n    <Select\n      label={\n        btnProps ? undefined : (\n          {\n            label: \"Target table\",\n            info: infoNode,\n          }\n        )\n      }\n      btnProps={btnProps}\n      value={targetValue?.key}\n      data-command=\"JoinPathSelectorV2\"\n      fullOptions={fullOptions}\n      variant={variant ? \"search-list-only\" : undefined}\n      onChange={(key) => {\n        const idx = fullOptions.findIndex((d) => d.key === key);\n        const targetPath = allJoins[idx];\n        const fullOpt = fullOptions[idx];\n        if (!targetPath || !fullOpt) {\n          console.error(\"Path not found\");\n          return;\n        }\n        onChange(targetPath, fullOpt.lastJoinLabel?.multiJoin);\n      }}\n    />\n  );\n};\n\nconst hasMultiFkeys = (\n  tables: DBSchemaTablesWJoins,\n  t1: string,\n  t2: string,\n) => {\n  const table = tables.find((t) => t.name === t1);\n  if (table) {\n    const join = table.joinsV2.find(\n      (j) => j.tableName === t2 && j.on.length > 1,\n    );\n    return join?.on;\n  }\n\n  return undefined;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/LinkedColumn/LinkedColumn.tsx",
    "content": "import { ExpandSection } from \"@components/ExpandSection\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiDotsHorizontal } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { t } from \"../../../../i18n/i18nUtils\";\nimport type {\n  DBSchemaTablesWJoins,\n  WindowSyncItem,\n} from \"../../../Dashboard/dashboardUtils\";\nimport { SmartFilterBar } from \"../../../SmartFilterBar/SmartFilterBar\";\nimport type { ColumnConfigWInfo } from \"../../W_Table\";\nimport { getColWInfo } from \"../../tableUtils/getColWInfo\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport { JoinPathSelectorV2, getAllJoins } from \"../JoinPathSelectorV2\";\nimport { LinkedColumnFooter } from \"./LinkedColumnFooter\";\nimport { LinkedColumnSelect } from \"./LinkedColumnSelect\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\n\nexport type LinkedColumnProps = {\n  w: WindowSyncItem<\"table\">;\n  column: ColumnConfigWInfo | undefined;\n  onClose: VoidFunction | undefined;\n};\n\nconst JOIN_TYPES = [\n  {\n    key: \"inner\",\n    label: \"Inner join\",\n    subLabel: \"Will discard parent rows without a matching record\",\n  },\n  {\n    key: \"left\",\n    label: \"Left join\",\n    subLabel: \"Will keep parent rows without a matching record\",\n  },\n] as const;\n\nexport const NESTED_COLUMN_DISPLAY_MODES = [\n  { key: \"row\", label: t.LinkedColumn[\"Row\"] },\n  { key: \"column\", label: t.LinkedColumn[\"Column\"] },\n  { key: \"no-headers\", label: t.LinkedColumn[\"No headers\"] },\n] as const;\n\nexport const LinkedColumn = (props: LinkedColumnProps) => {\n  const { w } = props;\n  const { tables, db } = usePrgl();\n  const getCol = (name: string) => w.columns?.find((c) => c.name === name);\n\n  const [localColumn, setLocalColumn] = useState<ColumnConfigWInfo>();\n  const currentColumn = localColumn ?? props.column;\n  const table = useMemo(() => {\n    const currentTargetPath =\n      currentColumn?.nested &&\n      getAllJoins({\n        tableName: w.table_name,\n        tables,\n        value: currentColumn.nested.path,\n      }).targetPath;\n    return currentTargetPath?.table;\n  }, [currentColumn, tables, w.table_name]);\n  const newColumnNameError =\n    !props.column && currentColumn && getCol(currentColumn.name) ?\n      t.LinkedColumn[\"Column name already used. Change to another\"]\n    : undefined;\n\n  const updateColumn = useCallback(\n    (newCol: Partial<ColumnConfig>) => {\n      if (!currentColumn) throw \"Cannot update a column that does not exist\";\n      setLocalColumn({ ...currentColumn, ...newCol });\n    },\n    [currentColumn],\n  );\n\n  const updateNested = (newNested: Partial<ColumnConfig[\"nested\"]>) => {\n    if (!currentColumn) throw \"Cannot update a column that does not exist\";\n    return updateColumn({ nested: { ...currentColumn.nested!, ...newNested } });\n  };\n\n  const nestedColumns = currentColumn?.nested?.columns;\n  const disabledInfo =\n    newColumnNameError ??\n    (!nestedColumns?.filter((c) => c.show).length ?\n      t.LinkedColumn[\"Must select columns\"]\n    : !props.column?.nested && !currentColumn ?\n      t.LinkedColumn[\"Must select a table\"]\n    : undefined);\n\n  useEffect(() => {\n    if (!localColumn) return;\n    const shownCols = localColumn.nested?.columns.filter((c) => c.show) ?? [];\n    const width = shownCols.length > 2 || table?.info.isFileTable ? 250 : 150;\n    if (localColumn.width !== width) {\n      setLocalColumn({ ...localColumn, width });\n    }\n  }, [localColumn, table]);\n\n  return (\n    <FlexCol data-command=\"LinkedColumn\" className=\"LinkedColumn gap-2\">\n      <InfoRow color=\"info\" variant=\"naked\" className=\" \" iconPath=\"\">\n        {\n          t.LinkedColumn[\n            \"Join to and show data from tables that are related through a\"\n          ]\n        }\n        <a\n          className=\"ml-p25\"\n          href=\"https://www.postgresql.org/docs/current/tutorial-fk.html\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n        >\n          FOREIGN KEY\n        </a>\n      </InfoRow>\n      {currentColumn && (\n        <FormFieldDebounced\n          id=\"nested-col-name\"\n          label={t.LinkedColumn[\"Column label\"]}\n          value={currentColumn.name}\n          error={newColumnNameError}\n          onChange={(newColName) => {\n            updateColumn({ name: newColName });\n          }}\n        />\n      )}\n      <FlexRowWrap className=\"ai-end gap-p25\">\n        <JoinPathSelectorV2\n          tableName={w.table_name}\n          tables={tables}\n          value={currentColumn?.nested?.path}\n          onChange={(targetPath, multiJoin) => {\n            let colName = targetPath.table.name;\n            if (multiJoin) {\n              const distinctLeftCols = Array.from(\n                new Set(multiJoin.value.flatMap((d) => d.map((_d) => _d[0]))),\n              );\n              const distinctRightCols = Array.from(\n                new Set(multiJoin.value.flatMap((d) => d.map((_d) => _d[1]))),\n              );\n              /** If one of the groups has two distinct cols */\n              if (distinctLeftCols.length * distinctRightCols.length === 2) {\n                const chosenDifferentColname =\n                  distinctLeftCols.length > 1 ?\n                    multiJoin.chosen[0]![0]\n                  : multiJoin.chosen[0]![1];\n                colName = `${chosenDifferentColname}_${targetPath.table.name}`;\n              }\n            }\n            const newColName = getCol(colName) ? `${colName} (1)` : colName;\n\n            const { table } = targetPath;\n            /**\n             * Show first 5 cols to improve performance\n             * If fileTable show all columns to ensure the images/media preview works\n             */\n            const nestedColumns = getColWInfo(table, null).map((c, i) => ({\n              ...c,\n              show: !!table.info.isFileTable || i < 5,\n            }));\n            const newCol: ColumnConfig = {\n              name: newColName,\n              show: true,\n              width: 250,\n              nested: {\n                columns: nestedColumns,\n                path: targetPath.path,\n                joinType: \"left\",\n                limit: 20,\n              },\n            };\n            setLocalColumn(newCol);\n          }}\n        />\n      </FlexRowWrap>\n      <LinkedColumnSelect\n        {...props}\n        updateNested={updateNested}\n        updateColumn={updateColumn}\n        table={table}\n        currentColumn={currentColumn}\n      />\n      {currentColumn && (\n        <>\n          <ExpandSection\n            iconPath={mdiDotsHorizontal}\n            label={t.LinkedColumn[\"More options\"]}\n          >\n            {currentColumn.nested && (\n              <FlexRowWrap className=\"ai-end\">\n                <Select\n                  label={t.LinkedColumn[\"Layout\"]}\n                  data-command=\"LinkedColumn.layoutType\"\n                  fullOptions={NESTED_COLUMN_DISPLAY_MODES}\n                  disabledInfo={\n                    currentColumn.nested.chart ?\n                      t.LinkedColumn[\"Must disable chart first\"]\n                    : undefined\n                  }\n                  value={currentColumn.nested.displayMode}\n                  onChange={(displayMode) => {\n                    updateNested({ displayMode });\n                  }}\n                />\n              </FlexRowWrap>\n            )}\n            <FlexRowWrap>\n              <Select\n                label={t.LinkedColumn[\"Join type\"]}\n                value={currentColumn.nested?.joinType}\n                fullOptions={JOIN_TYPES}\n                data-command=\"LinkedColumn.joinType\"\n                onChange={(joinType) => {\n                  updateNested({ joinType });\n                }}\n              />\n              {currentColumn.nested && (\n                <FormFieldDebounced\n                  id=\"nested-col-limit\"\n                  label={t.W_SQLBottomBar.Limit}\n                  optional={true}\n                  value={currentColumn.nested.limit}\n                  type=\"number\"\n                  inputProps={{\n                    min: 0,\n                    step: 1,\n                    max: 30,\n                  }}\n                  onChange={(limit) => {\n                    updateNested({\n                      limit:\n                        limit && Number.isFinite(+limit) ? +limit : undefined,\n                    });\n                  }}\n                />\n              )}\n            </FlexRowWrap>\n\n            {table && currentColumn.nested && (\n              <>\n                <SmartFilterBar\n                  innerClassname=\"mt-1 px-0\"\n                  filter={currentColumn.nested.detailedFilter}\n                  having={currentColumn.nested.detailedHaving}\n                  table_name={table.name}\n                  db={db}\n                  tables={tables}\n                  columns={currentColumn.nested.columns}\n                  rowCount={-1}\n                  methods={{}}\n                  showInsertUpdateDelete={{\n                    showupdate: false,\n                    showdelete: false,\n                    showInsert: false,\n                  }}\n                  sort={currentColumn.nested.sort}\n                  onSortChange={(sort) => updateNested({ sort })}\n                  onChange={(detailedFilter) =>\n                    updateNested({ detailedFilter })\n                  }\n                  onHavingChange={(detailedHaving) =>\n                    updateNested({ detailedHaving })\n                  }\n                />\n              </>\n            )}\n          </ExpandSection>\n        </>\n      )}\n\n      <LinkedColumnFooter\n        {...props}\n        localColumn={localColumn}\n        disabledInfo={disabledInfo}\n      />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/LinkedColumn/LinkedColumnFooter.tsx",
    "content": "import React, { useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport type { ColumnConfigWInfo } from \"../../W_Table\";\nimport { updateWCols } from \"../../tableUtils/tableUtils\";\nimport type { LinkedColumnProps } from \"./LinkedColumn\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiCheck } from \"@mdi/js\";\nimport { t } from \"../../../../i18n/i18nUtils\";\n\nexport const NEW_COL_POSITIONS = [\n  { key: \"start\", label: \"Start of table\" },\n  { key: \"end\", label: \"End of table\" },\n] as const;\n\ntype P = LinkedColumnProps & {\n  localColumn: ColumnConfigWInfo | undefined;\n  disabledInfo: string | undefined;\n};\nexport const LinkedColumnFooter = ({\n  onClose,\n  column,\n  w,\n  localColumn,\n  disabledInfo,\n}: P) => {\n  const [addTo, setPosition] =\n    useState<(typeof NEW_COL_POSITIONS)[number][\"key\"]>(\"start\");\n  return (\n    <>\n      {column?.nested && (\n        <FlexRow className=\"mt-2 ai-end\">\n          <Select\n            label={\"Add to\"}\n            value={addTo}\n            fullOptions={NEW_COL_POSITIONS}\n            onChange={setPosition}\n          />\n        </FlexRow>\n      )}\n      <FlexRow className=\"mt-2\">\n        {onClose && (\n          <Btn onClick={onClose} variant=\"outline\">\n            {t.common[\"Cancel\"]}\n          </Btn>\n        )}\n        {column?.nested && (\n          <Btn\n            color=\"danger\"\n            onClick={() => {\n              updateWCols(\n                w,\n                w.$get()?.columns?.filter((c) => c.name !== column.name),\n              );\n              onClose?.();\n            }}\n          >\n            {t.common.Remove}\n          </Btn>\n        )}\n\n        {localColumn && (\n          <Btn\n            color=\"action\"\n            variant=\"filled\"\n            className=\"ml-auto\"\n            disabledInfo={disabledInfo}\n            data-command=\"LinkedColumn.Add\"\n            iconPath={mdiCheck}\n            onClickMessage={(e, setM) => {\n              setM({ loading: 1 });\n              if (!w.columns) throw \"not possible\";\n              const newColumns =\n                !column ?\n                  addTo === \"start\" ?\n                    [localColumn, ...w.columns]\n                  : [...w.columns, localColumn]\n                : w.columns.map((c) =>\n                    c.name === column.name ? localColumn : c,\n                  );\n              updateWCols(w, newColumns);\n              setM({ ok: t.LinkedColumn[\"Added!\"] });\n              onClose?.();\n            }}\n          >\n            {!column ? t.common.Add : t.common.Update} Linked Column\n          </Btn>\n        )}\n      </FlexRow>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/LinkedColumn/LinkedColumnSelect.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { mdiPlus, mdiSigma } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport type { DBSchemaTablesWJoins } from \"../../../Dashboard/dashboardUtils\";\nimport type { ColumnConfigWInfo } from \"../../W_Table\";\nimport { getColWInfo } from \"../../tableUtils/getColWInfo\";\nimport { getMinimalColumnInfo } from \"../../tableUtils/tableUtils\";\nimport { AddComputedColMenu } from \"../AddComputedColumn/AddComputedColMenu\";\nimport { QuickAddComputedColumn } from \"../AddComputedColumn/QuickAddComputedColumn\";\nimport { ColumnList } from \"../ColumnList\";\nimport type { ColumnConfig } from \"../ColumnMenu\";\nimport { NestedTimechartControls } from \"../NestedTimechartControls\";\nimport type { LinkedColumnProps } from \"./LinkedColumn\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\n\ntype P = LinkedColumnProps & {\n  updateNested: (newNested: Partial<ColumnConfig[\"nested\"]>) => void;\n  table: DBSchemaTablesWJoins[number] | undefined;\n  currentColumn: ColumnConfigWInfo | undefined;\n  updateColumn: (newCol: Partial<ColumnConfig>) => void;\n};\nexport const LinkedColumnSelect = ({\n  w,\n  table,\n  currentColumn,\n  column,\n  updateNested,\n  updateColumn,\n}: P) => {\n  const { tables, db } = usePrgl();\n  const nestedColumns = currentColumn?.nested?.columns;\n  const updateNestedColumns = (newCols: ColumnConfigWInfo[]) => {\n    if (!table) throw \"not ok\";\n    updateNested({\n      columns: getMinimalColumnInfo(getColWInfo(table, newCols)),\n    });\n  };\n  const [showAddComputedCol, setShowAddComputedCol] = useState(false);\n\n  return (\n    <FlexRowWrap className=\"ai-end\">\n      {nestedColumns && table && (\n        <PopupMenu\n          data-command=\"LinkedColumn.ColumnListMenu\"\n          title=\"Select columns\"\n          contentClassName=\"\"\n          clickCatchStyle={{ opacity: 0.1 }}\n          positioning=\"beneath-left\"\n          button={\n            <FlexCol className=\"gap-p25\">\n              <Label label=\"Columns\" variant=\"normal\"></Label>\n              <Btn\n                variant=\"faded\"\n                color={!currentColumn.nested?.chart ? \"action\" : undefined}\n                data-command=\"LinkedColumn.ColumnList.toggle\"\n                disabledInfo={\n                  currentColumn.nested?.chart ?\n                    \"Must disable time chart first\"\n                  : undefined\n                }\n              >\n                {nestedColumns.filter((c) => c.show).length} selected\n              </Btn>\n            </FlexCol>\n          }\n          render={(pClose) => {\n            return (\n              <FlexCol>\n                <ColumnList\n                  columns={nestedColumns}\n                  table={table}\n                  onClose={pClose}\n                  suggestions={undefined}\n                  w={w}\n                  onChange={updateNestedColumns}\n                />\n\n                <FlexRow className=\"p-1\">\n                  <Btn\n                    variant=\"faded\"\n                    iconPath={mdiPlus}\n                    color=\"action\"\n                    onClick={() => setShowAddComputedCol(true)}\n                  >\n                    Add computed column\n                  </Btn>\n                  {showAddComputedCol && (\n                    <AddComputedColMenu\n                      db={db}\n                      nestedColumnOpts={\n                        !column ?\n                          {\n                            type: \"new\",\n                            config: currentColumn,\n                            onChange: updateColumn,\n                          }\n                        : {\n                            type: \"existing\",\n                            config: currentColumn,\n                          }\n                      }\n                      tables={tables}\n                      w={w}\n                      onClose={() => setShowAddComputedCol(false)}\n                    />\n                  )}\n                </FlexRow>\n              </FlexCol>\n            );\n          }}\n        />\n      )}\n      {table && (\n        <>\n          <NestedTimechartControls\n            tableName={table.name}\n            chart={currentColumn?.nested?.chart}\n            onChange={(chart) => {\n              updateNested({ chart, limit: chart ? 200 : 20 });\n            }}\n          />\n          <div className=\"py-p75\">OR</div>\n\n          <PopupMenu\n            contentClassName=\"p-1 flex-col gap-1\"\n            title=\"Add computed column\"\n            positioning=\"beneath-left\"\n            data-command=\"QuickAddComputedColumn\"\n            button={\n              <Btn\n                variant=\"faded\"\n                iconPath={mdiSigma}\n                data-command=\"QuickAddComputedColumn\"\n              >\n                Row count/Aggregate\n              </Btn>\n            }\n            render={(popupClose) => (\n              <QuickAddComputedColumn\n                tableName={table.name}\n                existingColumn={undefined}\n                onAddColumn={(newCol) => {\n                  if (!newCol) {\n                    popupClose();\n                    return;\n                  }\n                  const oldHiddenCols = (nestedColumns ?? []).map((c) => ({\n                    ...c,\n                    show: false,\n                  }));\n                  const newCols = [newCol, ...oldHiddenCols];\n                  updateNested({ displayMode: \"no-headers\", columns: newCols });\n                }}\n              />\n            )}\n          />\n        </>\n      )}\n    </FlexRowWrap>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/NestedTimechartControls.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { mdiChartTimelineVariant } from \"@mdi/js\";\nimport { _PG_numbers } from \"prostgles-types\";\nimport React from \"react\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\nimport {\n  TIMECHART_STAT_TYPES,\n  TimechartRenderStyles,\n} from \"../../W_TimeChart/W_TimeChartMenu\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\n\nexport const SORTABLE_CHART_COLUMNS = [\"date\", \"value\"];\n\nexport type ColTimeChart = Required<ColumnConfigWInfo>[\"nested\"][\"chart\"];\ntype P = {\n  tableName: string | undefined;\n  chart: ColTimeChart;\n  onChange: (newCol: ColTimeChart) => void;\n};\nexport const NestedTimechartControls = ({ tableName, chart, onChange }: P) => {\n  const { tables } = usePrgl();\n  if (!tableName) return null;\n\n  const table = tables.find((t) => t.name === tableName);\n  if (!table) return null;\n\n  const dateCols = table.columns.filter(\n    (c) => c.udt_name.startsWith(\"timestamp\") || c.udt_name === \"date\",\n  );\n  const numericCols = table.columns.filter((c) =>\n    _PG_numbers.includes(c.udt_name as any),\n  );\n  const timeChartOpts =\n    !dateCols.length ? undefined : (\n      {\n        dateCols,\n        numericCols,\n      }\n    );\n\n  if (!timeChartOpts) return null;\n\n  return (\n    <>\n      <div className=\"py-p75\">OR</div>\n      <PopupMenu\n        button={\n          <Btn\n            color={chart ? \"action\" : undefined}\n            variant=\"faded\"\n            iconPath={mdiChartTimelineVariant}\n          >\n            Time chart {chart ? \": Enabled\" : \"\"}\n          </Btn>\n        }\n        clickCatchStyle={{ opacity: 0 }}\n        contentClassName=\"p-p5\"\n        positioning=\"beneath-left\"\n        render={(pClose) => (\n          <FlexCol>\n            <SwitchToggle\n              label={\"Enable\"}\n              checked={!!chart}\n              onChange={(checked) => {\n                const dateCol = timeChartOpts.dateCols[0]!.name;\n                onChange(\n                  !checked ? undefined : (\n                    {\n                      type: \"time\",\n                      dateCol,\n                      renderStyle: \"smooth-line\",\n                      yAxis: {\n                        isCountAll: true,\n                      },\n                    }\n                  ),\n                );\n              }}\n            />\n            {chart && numericCols.length > 0 && (\n              <>\n                <Select\n                  label={\"Aggregation type\"}\n                  fullOptions={TIMECHART_STAT_TYPES.map(\n                    ({ func: key, label }) => ({ key, label }),\n                  )}\n                  value={\n                    chart.yAxis.isCountAll === true ?\n                      \"$countAll\"\n                    : chart.yAxis.funcName\n                  }\n                  onChange={(funcName) => {\n                    onChange({\n                      ...chart,\n                      yAxis:\n                        funcName === \"$countAll\" ?\n                          { isCountAll: true }\n                        : {\n                            colName: numericCols[0]!.name,\n                            ...chart.yAxis,\n                            funcName,\n                            isCountAll: false,\n                          },\n                    });\n                  }}\n                />\n                {chart.yAxis.isCountAll !== true && (\n                  <Select\n                    label={\"Aggregated column\"}\n                    fullOptions={numericCols.map((c) => ({\n                      key: c.name,\n                      subLabel: c.udt_name,\n                    }))}\n                    value={chart.yAxis.colName}\n                    onChange={(colName) => {\n                      onChange({\n                        ...chart,\n                        yAxis: {\n                          ...(chart.yAxis as any),\n                          isCountAll: false,\n                          colName,\n                        },\n                      });\n                    }}\n                  />\n                )}\n\n                <Select\n                  label=\"Chart style\"\n                  value={chart.renderStyle}\n                  fullOptions={TimechartRenderStyles.concat([\n                    { key: \"smooth-line\", label: \"Smooth line\" } as any,\n                  ])}\n                  onChange={(renderStyle) => {\n                    onChange({\n                      ...chart,\n                      renderStyle,\n                    });\n                  }}\n                />\n              </>\n            )}\n          </FlexCol>\n        )}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/SummariseColumns.tsx",
    "content": "import { mdiFunction } from \"@mdi/js\";\nimport { pickKeys, type ValidatedColumnInfo } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\nimport { FunctionSelector } from \"./FunctionSelector/FunctionSelector\";\n\ntype SummariseColumnProps = {\n  tableColumns: ValidatedColumnInfo[];\n  column: ColumnConfigWInfo;\n  onChange: (newCols: ColumnConfigWInfo[]) => void;\n  columns: ColumnConfigWInfo[];\n};\n\nexport const SummariseColumn = ({\n  column,\n  columns,\n  onChange,\n  tableColumns,\n}: SummariseColumnProps) => {\n  const topFuncs =\n    (\n      column.info?.udt_name.startsWith(\"timestamp\") ||\n      column.info?.udt_name.startsWith(\"date\")\n    ) ?\n      [\"\"]\n    : [];\n  const otherColumnsShown = columns.some(\n    (c) => c.name !== column.name && c.show,\n  );\n  const currFuncDef = column.computedConfig?.funcDef;\n  const [funcDef, setFuncDef] = useState(currFuncDef);\n  const [hideOthers, setHideOthers] = useState(false);\n\n  const [showPopup, setShowPopup] = useState<HTMLButtonElement>();\n  return (\n    <>\n      <Btn\n        title=\"Apply function/summarise column\"\n        color=\"action\"\n        className={showPopup || funcDef ? \"\" : \"show-on-trigger-hover\"}\n        iconPath={mdiFunction}\n        data-command=\"SummariseColumn.toggle\"\n        onClick={(e) => setShowPopup(e.currentTarget)}\n      >\n        {funcDef?.label ??\n          column.computedConfig?.funcDef.label ??\n          `${topFuncs.join(\", \")}...`}\n      </Btn>\n      {showPopup && (\n        <Popup\n          anchorEl={showPopup}\n          positioning=\"beneath-left\"\n          onClose={() => setShowPopup(undefined)}\n          clickCatchStyle={{ opacity: 0 }}\n          contentClassName=\"p-0\"\n          footerButtons={\n            !funcDef ? undefined : (\n              [\n                { label: \"Cancel\", onClickClose: true },\n                {\n                  label: column.nested ? \"Update\" : \"Apply\",\n                  variant: \"filled\",\n                  color: \"action\",\n                  \"data-command\": \"SummariseColumn.apply\",\n                  onClick: () => {\n                    const newCol: ColumnConfigWInfo = {\n                      ...column,\n                      // name: funcDef? `${funcDef.label}(${column.name})` : column.name,\n                      show: true,\n                      computedConfig: {\n                        ...pickKeys(column.info!, [\"tsDataType\", \"udt_name\"]),\n                        column: column.name,\n                        funcDef,\n                        isColumn: true,\n                      },\n                    };\n                    onChange(\n                      columns.map((c) =>\n                        c.name === column.name ? newCol\n                        : hideOthers ? { ...c, show: false }\n                        : c,\n                      ),\n                    );\n                    setFuncDef(undefined);\n                    setShowPopup(undefined);\n                  },\n                },\n              ]\n            )\n          }\n        >\n          <FlexCol className={\"min-h-0\"}>\n            <FunctionSelector\n              selectedFunction={\n                funcDef?.key ?? column.computedConfig?.funcDef.key\n              }\n              className={funcDef ? \"p-1\" : \"\"}\n              column={column.name}\n              tableColumns={tableColumns}\n              wColumns={undefined}\n              onSelect={(funcDef) => {\n                setFuncDef(funcDef);\n                setHideOthers(!!funcDef?.isAggregate);\n              }}\n            />\n            {funcDef && (\n              <SwitchToggle\n                className=\"m-1\"\n                disabledInfo={\n                  otherColumnsShown ? undefined : \"No other columns are shown\"\n                }\n                label={\"Hide other columns\"}\n                checked={hideOthers}\n                onChange={setHideOthers}\n              />\n            )}\n          </FlexCol>\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/getNestedColumnTable.ts",
    "content": "import type {\n  DBSchemaTablesWJoins,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\nimport { getMinimalColumnInfo } from \"../tableUtils/tableUtils\";\nimport type { ColumnConfig } from \"./ColumnMenu\";\n\ntype Result = {\n  columns: ColumnConfig[];\n  table: DBSchemaTablesWJoins[number];\n  nestedColumn?: ColumnConfig;\n};\ntype ErrorResult = Partial<Record<keyof Result, undefined>>;\n\ntype MaybeResult =\n  | ({ error: string } & ErrorResult)\n  | ({ error?: undefined } & Result);\n\nexport type NestedColumnOpts =\n  | {\n      type: \"new\";\n      config: ColumnConfigWInfo;\n      /**\n       * A new nested column will be kept locally until the user decides to save it\n       */\n      onChange: (config: ColumnConfigWInfo) => void;\n    }\n  | {\n      type: \"existing\";\n      config: ColumnConfigWInfo;\n    };\nexport const getNestedColumnTable = (\n  nestedColumnOpts: NestedColumnOpts | undefined,\n  w: WindowSyncItem<\"table\">,\n  tables: DBSchemaTablesWJoins,\n): MaybeResult => {\n  const columns = w.$get()?.columns;\n  if (!columns) {\n    return {\n      error: \"w columns missing\",\n    };\n  }\n  const nestedColumnName = nestedColumnOpts?.config.name;\n  const nestedColumn =\n    !nestedColumnOpts ? undefined\n    : nestedColumnOpts.type === \"new\" ?\n      getMinimalColumnInfo([nestedColumnOpts.config])[0]\n    : columns.find((c) => c.name === nestedColumnName);\n  if (!nestedColumnOpts) {\n    const table = tables.find((t) => t.name === w.table_name);\n    if (!table) {\n      return {\n        error: `Table ${w.table_name} not found`,\n      };\n    }\n    return {\n      table,\n      columns,\n    };\n  }\n\n  if (!nestedColumn) {\n    return {\n      error: `Nested column ${nestedColumnName} not found`,\n    };\n  }\n  if (!nestedColumn.nested) {\n    return {\n      error: `Nested column name ${nestedColumnName} points to an invalid (non-nested) column`,\n    };\n  }\n\n  const nestedTableName = nestedColumn.nested.path.at(-1)?.table;\n  const table = tables.find((t) => t.name === nestedTableName);\n  if (!table) {\n    return {\n      error: `Nested column table ${nestedTableName} not found`,\n    };\n  }\n\n  return { table, nestedColumn, columns };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ColumnMenu/rgba2hex.ts",
    "content": "export function rgba2hex(color: string): string {\n  // Remove whitespace and parse RGB/RGBA values\n  const match = color\n    .replace(/\\s/g, \"\")\n    .match(/^rgba?\\((\\d+),(\\d+),(\\d+),?([^,\\s)]+)?/i);\n\n  if (!match) {\n    // Return original if parsing fails, or throw error for strict validation\n    return color.startsWith(\"#\") ? color : \"#000000\";\n  }\n\n  const [, r, g, b, alphaStr] = match as [\n    string,\n    string,\n    string,\n    string,\n    string?,\n  ];\n\n  // Convert RGB to hex\n  const toHex = (value: string): string => {\n    const num = parseInt(value, 10);\n    const clamped = Math.max(0, Math.min(255, num)); // Clamp to valid range\n    return clamped.toString(16).padStart(2, \"0\");\n  };\n\n  const hex = toHex(r) + toHex(g) + toHex(b);\n\n  // Handle alpha channel\n  const alpha = alphaStr ? parseFloat(alphaStr.trim()) : 1;\n  const alphaHex = Math.round(alpha * 255)\n    .toString(16)\n    .padStart(2, \"0\");\n\n  return `#${hex}${alphaHex}`;\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Table/JoinPathSelector/JoinPathItem.tsx",
    "content": "import React, { useState } from \"react\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type { JoinV2 } from \"../../Dashboard/dashboardUtils\";\nimport { FlexCol } from \"@components/Flex\";\nimport Btn from \"@components/Btn\";\nimport { mdiPencil } from \"@mdi/js\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\n\ntype JoinPathItemProps = JoinV2 & {\n  onChange: (newJoin: JoinV2) => void;\n  prevTableName: string | undefined;\n  tables: CommonWindowProps[\"tables\"];\n};\n\nexport const JoinPathItem = ({\n  tableName,\n  on: onValue,\n  tables,\n  prevTableName,\n  onChange,\n}: JoinPathItemProps) => {\n  const on =\n    tables\n      .find((t) => t.name === prevTableName)\n      ?.joinsV2.find((j) => j.tableName === tableName)?.on || [];\n  const condition = getJoinPathConditionStr({ tableName, on: onValue });\n  const [edit, setEdit] = useState<{ anchor: HTMLElement; on: JoinV2[\"on\"] }>();\n  const canChooseConditions = on.length > 1;\n\n  return (\n    <>\n      <FlexCol className={\"JoinPathItem relative gap-p25 f-0 \"}>\n        {canChooseConditions && (\n          <Btn\n            title=\"Choose join condition\"\n            style={{ position: \"absolute\", top: 0, right: 0 }}\n            className=\"show-on-parent-hover\"\n            variant=\"faded\"\n            color=\"action\"\n            iconPath={mdiPencil}\n            onClick={(e) => {\n              setEdit({ anchor: e.currentTarget, on });\n            }}\n          />\n        )}\n        <div className=\"font-bold\">{tableName}</div>\n        <div\n          style={{\n            /* Maintain height for first item */\n            height: \"1rem\",\n            /* Give a visual que for the fact that this condition refers to this and prev table */\n            marginLeft: \"-10%\",\n          }}\n        >\n          {condition}\n        </div>\n      </FlexCol>\n      {edit && (\n        <Popup\n          anchorEl={edit.anchor}\n          title=\"Select join conditions\"\n          positioning=\"beneath-left\"\n          onClose={() => setEdit(undefined)}\n          footerButtons={[\n            {\n              label: \"Cancel\",\n              onClick: () => {\n                onChange({ tableName, on: onValue });\n                setEdit(undefined);\n              },\n            },\n            {\n              label: \"Done\",\n              variant: \"filled\",\n              color: \"action\",\n              onClick: () => {\n                setEdit(undefined);\n              },\n            },\n          ]}\n        >\n          {on.map((condition, i) => {\n            const conditionStr = getJoinPathConditionStr({\n              tableName,\n              on: [condition],\n            });\n            const checked = onValue.some(\n              (condValue) =>\n                getJoinPathConditionStr({ tableName, on: [condValue] }) ===\n                conditionStr,\n            );\n            return (\n              <React.Fragment key={conditionStr}>\n                {i > 0 && <div>OR</div>}\n                <SwitchToggle\n                  className=\"p-1\"\n                  label={conditionStr}\n                  checked={checked}\n                  onChange={(newChecked) => {\n                    const newOn =\n                      newChecked ?\n                        [...onValue, condition]\n                      : onValue.filter(\n                          (condValue) =>\n                            getJoinPathConditionStr({\n                              tableName,\n                              on: [condValue],\n                            }) !== conditionStr,\n                        );\n                    if (!newOn.length) return;\n                    onChange({ tableName, on: newOn });\n                  }}\n                />\n              </React.Fragment>\n            );\n          })}\n        </Popup>\n      )}\n    </>\n  );\n};\n\nexport const getJoinPathConditionStr = (j: JoinV2) => {\n  return j.on\n    .map(\n      (fieldPairs, i) =>\n        \"(\" + fieldPairs.map(([f1, f2]) => `${f1} = ${f2}`).join(\" AND \") + \")\",\n    )\n    .join(\" OR \");\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/JoinPathSelector/JoinPathSelector.tsx",
    "content": "import { useEffectDeep } from \"prostgles-client\";\nimport React, { useState } from \"react\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type { JoinV2 } from \"../../Dashboard/dashboardUtils\";\nimport { getJoinTree, type JoinTree } from \"./getJoinTree\";\nimport { JoinPathItem, getJoinPathConditionStr } from \"./JoinPathItem\";\n\ntype P = {\n  tableName: string;\n  tables: CommonWindowProps[\"tables\"];\n  onSelect: (joinPath: JoinV2[]) => void;\n  variant?: \"hover\";\n  className?: string;\n  style?: React.CSSProperties;\n};\n\nexport const JoinPathSelector = (props: P) => {\n  const { tables, onSelect, className = \"\", style = {}, tableName } = props;\n  const [s, setS] = useState<{\n    jpath: JoinTree[];\n    joins?: JoinTree[];\n    currentJoin?: JoinTree;\n  }>({ jpath: [] });\n\n  useEffectDeep(() => {\n    if (s.joins) return;\n    const joins = getJoinTree({ tables, tableName });\n    setS({ jpath: s.jpath, joins });\n  }, [tables, tableName, s]);\n\n  const { joins } = s;\n  const [joinPath, setJoinPath] = useState<JoinV2[]>();\n  const currentTableName = joinPath?.at(-1)?.tableName ?? tableName;\n  const table = tables.find((t) => t.name === currentTableName);\n  if (!table) return null;\n  if (!joins) return <Loading />;\n\n  const { joinsV2 } = table;\n  return (\n    <FlexCol className={`JoinPathSelectorV3 ${className}`} style={style}>\n      <FlexRow className=\"p-1 bb b-color o-auto\">\n        <JoinPathItem\n          tableName={tableName}\n          prevTableName={undefined}\n          on={[]}\n          onChange={() => {}}\n          tables={tables}\n        />\n        {joinPath?.map((j, i) => (\n          <JoinPathItem\n            key={JSON.stringify(i)}\n            tables={tables}\n            prevTableName={joinPath[i - 1]?.tableName ?? tableName}\n            {...j}\n            onChange={(newJoin) => {\n              const newJoinPath = joinPath.map((j, ii) =>\n                ii === i ? newJoin : j,\n              );\n              setJoinPath(newJoinPath);\n              onSelect(newJoinPath);\n            }}\n          />\n        ))}\n      </FlexRow>\n      <SearchList\n        items={joinsV2.map((j) => ({\n          key: j.tableName,\n          label: j.tableName,\n          subLabel: getJoinPathConditionStr(j),\n          onPress: () => {\n            const newJoinPath = [...(joinPath || []), j];\n            setJoinPath(newJoinPath);\n            onSelect(newJoinPath);\n          },\n        }))}\n      />\n    </FlexCol>\n  );\n};\n\nexport const getHasJoins = (\n  tableName: string,\n  tables: CommonWindowProps[\"tables\"],\n) => {\n  return Boolean(\n    tables.find(\n      (t) =>\n        (t.name === tableName && t.columns.find((c) => c.references)) ||\n        (t.name !== tableName &&\n          t.columns.find((c) =>\n            c.references?.some((r) => r.ftable === tableName),\n          )),\n    ),\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/JoinPathSelector/getJoinTree.ts",
    "content": "import type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\n\nexport type JoinTree = {\n  table: string;\n  on: [string, string][];\n  joins?: JoinTree[];\n};\n\n/**\n * Returns a list of tables the given table can join to (based on foreign keys)\n */\nexport const getJoinTree = (args: {\n  tableName: string;\n  excludeTables?: string[];\n  tables: CommonWindowProps[\"tables\"];\n}): JoinTree[] => {\n  const { tables, tableName, excludeTables = [] } = args;\n  const table = tables.find((t) => t.name === tableName);\n  if (!table) throw \"Table info not found\";\n  const res: JoinTree[] = [];\n\n  /** This is to ensure no duplicates are added in cases where groups of columns are referenced (c.references?.fcols.length > 1) */\n  const addJT = (jt: JoinTree) => {\n    if (\n      !excludeTables.includes(jt.table) &&\n      !res.find(\n        (r) =>\n          r.table === jt.table &&\n          JSON.stringify(r.on) === JSON.stringify(jt.on),\n      )\n    ) {\n      res.push(jt);\n    }\n  };\n\n  table.columns.forEach((c) => {\n    c.references?.forEach((r) => {\n      const jt: JoinTree = {\n        table: r.ftable,\n        on: r.cols.map((col, i) => [col, r.fcols[i]!]),\n      };\n      addJT(jt);\n    });\n  });\n\n  tables.forEach((t) => {\n    if (t.name !== tableName) {\n      t.columns.forEach((c) => {\n        c.references?.forEach((r) => {\n          if (r.ftable === tableName) {\n            const jt: JoinTree = {\n              table: t.name,\n              on: r.fcols.map((fcol, i) => [fcol, r.cols[i]!]),\n            };\n            addJT(jt);\n          }\n        });\n      });\n    }\n  });\n\n  let result = res.map((r) => ({\n    ...r,\n    joins: getJoinTree({\n      tableName: r.table,\n      tables,\n\n      /** Need to only exclude tables from the current join path */\n      excludeTables: [tableName, ...excludeTables, r.table],\n    }),\n  }));\n\n  result = result.map((r) => ({\n    ...r,\n    // label:\n  }));\n\n  return result;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/NodeCountChecker.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport Btn from \"@components/Btn\";\nimport { mdiAlertOutline } from \"@mdi/js\";\nimport { InfoRow } from \"@components/InfoRow\";\n\nexport const NodeCountChecker = ({\n  parentNode,\n  dataAge,\n}: {\n  parentNode: HTMLElement | undefined;\n  dataAge: number | undefined;\n}) => {\n  const [nodeCount, setNodeCount] = useState({\n    count: 0,\n    tooHigh: false,\n    checked: Date.now(),\n  });\n  useEffect(() => {\n    const count = parentNode?.querySelectorAll(\"*\").length ?? 0;\n    setNodeCount({\n      count,\n      tooHigh: count > 3e3,\n      checked: Date.now(),\n    });\n  }, [parentNode, setNodeCount, dataAge]);\n\n  if (!nodeCount.tooHigh) return null;\n\n  return (\n    <PopupMenu\n      positioning=\"beneath-left\"\n      style={{\n        position: \"absolute\",\n        bottom: \"1em\",\n        left: \"1em\",\n        zIndex: 1,\n      }}\n      clickCatchStyle={{ opacity: 0.1 }}\n      button={\n        <Btn\n          iconPath={mdiAlertOutline}\n          color=\"warn\"\n          variant=\"filled\"\n          className=\"shadow\"\n        />\n      }\n      onClickClose={true}\n    >\n      <InfoRow variant=\"naked\" color=\"warning\">\n        Too much data displayed. Reduce the number of rows or columns to improve\n        responsiveness\n      </InfoRow>\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/ProstglesTable.css",
    "content": ".fullscreen {\n  position: fixed;\n  z-index: 2;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background: white;\n}\n\n.active-brush {\n  border: 1px solid #00d0ffcf;\n  background: #def9ff38;\n}\n\n/* #root > div > div > div.flex-row.f-1 > div.f-1.flex-col > div > div > span, */\n.react-resizable-handle.react-resizable-handle-se {\n  opacity: 0;\n}\n\n.d-row .show-on-row-hover:not(.hover) {\n  opacity: 0;\n}\n.d-row:hover .show-on-row-hover {\n  opacity: 1;\n}\n\n@keyframes rubberBand {\n  0% {\n    transform: scale(1);\n  }\n  30% {\n    transform: scaleX(1.25) scaleY(0.75);\n  }\n  40% {\n    transform: scaleX(0.75) scaleY(1.25);\n  }\n  60% {\n    transform: scaleX(1.15) scaleY(0.85);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n@keyframes colorFlick {\n  0% {\n    color: black;\n  }\n  30% {\n    color: red;\n  }\n  60% {\n    color: green;\n  }\n  100% {\n    color: initial;\n  }\n}\n.rubberBand {\n  animation-duration: 1.5s;\n  animation-fill-mode: both;\n  animation-iteration-count: 1;\n  animation-name: colorFlick;\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Table/QuickFilterGroupsControl.tsx",
    "content": "import { getFinalFilterInfo } from \"@common/filterUtils\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport React from \"react\";\nimport { isEmpty } from \"src/utils/utils\";\nimport type { W_TableProps } from \"./W_Table\";\n\nexport const QuickFilterGroupsControl = ({ w }: W_TableProps) => {\n  const { quickFilterGroups = {} } = w.options;\n  if (isEmpty(quickFilterGroups)) {\n    return null;\n  }\n  return (\n    <FlexRowWrap\n      data-command=\"QuickFilterGroupsControl\"\n      title=\"Quick filters\"\n      className=\"gap-p5 p-p5\"\n    >\n      {Object.entries(quickFilterGroups).map(\n        ([groupName, { filters, toggledFilterName }]) => {\n          return (\n            <Select\n              asRow={true}\n              key={groupName}\n              labelAsValue={true}\n              emptyLabel={groupName}\n              optional={true}\n              size=\"small\"\n              onChange={(filterName) => {\n                const newQuickFilterGroups = { ...quickFilterGroups };\n                newQuickFilterGroups[groupName]!.toggledFilterName = filterName;\n                w.$update(\n                  {\n                    options: {\n                      quickFilterGroups: newQuickFilterGroups,\n                    },\n                  },\n                  { deepMerge: true },\n                );\n              }}\n              btnProps={toggledFilterName ? { color: \"action\" } : {}}\n              value={toggledFilterName}\n              fullOptions={Object.entries(filters).map(\n                ([filterName, filter]) => {\n                  return {\n                    key: filterName,\n                    label: filterName,\n                    subLabel: getFinalFilterInfo(filter as any),\n                  };\n                },\n              )}\n            />\n          );\n        },\n      )}\n    </FlexRowWrap>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/RowCard.tsx",
    "content": "import type { TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React from \"react\";\nimport type { DetailedFilterBase } from \"@common/filterUtils\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport { SmartForm } from \"../SmartForm/SmartForm\";\nimport type { RowSiblingData } from \"./tableUtils/getEditColumn\";\nimport { getRowSiblingData } from \"./tableUtils/getEditColumn\";\nimport type { ReactiveState } from \"../../appUtils\";\nimport { useReactiveState } from \"../../appUtils\";\n\nexport type RowPanelProps =\n  | {\n      type: \"insert\";\n    }\n  | {\n      type: \"update\";\n      rowIndex: number;\n      filter: DetailedFilterBase[];\n      siblingData: RowSiblingData;\n      fixedUpdateData?: AnyObject;\n    };\n\ntype RowCardProps = Pick<CommonWindowProps, \"prgl\"> & {\n  tableName: string;\n  tableHandler: Partial<TableHandlerClient>;\n  onSuccess?: VoidFunction;\n  rows?: AnyObject[];\n  onPrevOrNext?: (newRowPanel: RowPanelProps) => void;\n  showR: ReactiveState<undefined | RowPanelProps>;\n};\n\nexport const RowCard = ({\n  prgl,\n  tableName,\n  rows,\n  tableHandler,\n  onSuccess,\n  onPrevOrNext,\n  showR,\n}: RowCardProps) => {\n  const { state: show, setState } = useReactiveState(showR);\n  if (!show) return null;\n\n  const canPrevOrNext =\n    onPrevOrNext &&\n    rows?.length &&\n    show.type === \"update\" &&\n    Object.values(show.siblingData).some((v) => v);\n\n  return (\n    <SmartForm\n      asPopup={true}\n      confirmUpdates={true}\n      db={prgl.db}\n      methods={prgl.methods}\n      tables={prgl.tables}\n      tableName={tableName}\n      fixedData={show.type === \"update\" ? show.fixedUpdateData : undefined}\n      rowFilter={show.type === \"update\" ? show.filter : undefined}\n      prevNext={{\n        prev: !!canPrevOrNext && !!show.siblingData.prevRow,\n        next: !!canPrevOrNext && !!show.siblingData.nextRow,\n      }}\n      onPrevOrNext={\n        !canPrevOrNext ? undefined : (\n          async (increment) => {\n            const newRowIndex = show.rowIndex + increment;\n            const newFilter =\n              increment === 1 ?\n                show.siblingData.nextRowFilter\n              : show.siblingData.prevRowFilter;\n            if (!newFilter) {\n              // alert(\"Reached end\");\n              return;\n            }\n            const table = prgl.tables.find((t) => t.name === tableName);\n            if (!table) return;\n\n            const siblingData = await getRowSiblingData(\n              rows,\n              newRowIndex,\n              table,\n              undefined,\n              tableHandler,\n            );\n            const newRowPanel: RowPanelProps = {\n              type: \"update\",\n              rowIndex: newRowIndex,\n              filter: newFilter,\n              siblingData,\n            };\n            onPrevOrNext(newRowPanel);\n          }\n        )\n      }\n      onSuccess={onSuccess}\n      onClose={() => setState(undefined)}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/AddChartMenu.tsx",
    "content": "import Btn, { type BtnProps } from \"@components/Btn\";\nimport { getSearchRanking } from \"@components/SearchList/searchMatchUtils/getSearchRanking\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiChartLine, mdiMap } from \"@mdi/js\";\nimport { useMemoDeep } from \"prostgles-client/dist/prostgles\";\nimport {\n  _PG_numbers,\n  includes,\n  isDefined,\n  type ParsedJoinPath,\n} from \"prostgles-types\";\nimport React from \"react\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type {\n  DBSchemaTablesWJoins,\n  OnAddChart,\n  WindowData,\n} from \"../../Dashboard/dashboardUtils\";\nimport { getRandomColor } from \"../../Dashboard/PALETTE\";\nimport { rgbaToString } from \"../../W_Map/getMapFeatureStyle\";\nimport type { ChartableSQL } from \"../../W_SQL/getChartableSQL\";\nimport type { ChartColumn, ColInfo } from \"./getChartCols\";\nimport { getChartCols } from \"./getChartCols\";\n\ntype P = Pick<CommonWindowProps, \"myLinks\" | \"childWindows\"> & {\n  onAddChart: OnAddChart;\n  tables: DBSchemaTablesWJoins;\n  btnClassName?: string;\n  size?: \"micro\";\n} & (\n    | {\n        type: \"sql\";\n        w: WindowData<\"sql\">;\n        chartableSQL: ChartableSQL;\n      }\n    | {\n        type: \"table\";\n        w: WindowData<\"table\">;\n        chartableSQL: undefined;\n      }\n  );\n\nexport const AddChartMenu = (props: P) => {\n  const {\n    type,\n    w,\n    onAddChart,\n    tables,\n    chartableSQL,\n    size,\n    myLinks,\n    childWindows,\n  } = props;\n\n  const isMicroMode = size === \"micro\";\n  const chartCols = useMemoDeep(() => {\n    const res = getChartCols(\n      type === \"table\" ?\n        { type: \"table\", w, tables }\n      : { type: \"sql\", chartableSQL, w },\n    );\n    return res;\n  }, [chartableSQL, tables, w, type]);\n\n  const { geoCols, dateCols, barCols, sql, withStatement = \"\" } = chartCols;\n\n  const tableName = w.table_name;\n  const onAdd = (\n    linkOpts: {\n      type: \"map\" | \"timechart\" | \"barchart\";\n      columns: ChartColumn[];\n    },\n    joinPath: ParsedJoinPath[] | undefined,\n  ) => {\n    const otherColumns = linkOpts.columns\n      .reduce((a, v) => {\n        v.otherColumns.forEach((vc) => {\n          if (!a.some((ac) => ac.name === vc.name)) {\n            a.push(vc);\n          }\n        });\n        return a;\n      }, [] as ColInfo[])\n      .map(({ name, udt_name, is_pkey }) => ({ name, udt_name, is_pkey }));\n\n    const firstNumericColumn = otherColumns.find(\n      (c) => !c.is_pkey && includes(_PG_numbers, c.udt_name),\n    )?.name;\n    const columnList = `(${linkOpts.columns.map((c) => c.name).join()})`;\n    const name =\n      joinPath ?\n        `${[tableName, ...joinPath.slice(0).map((p) => p.table)].join(\" > \")} ${columnList}`\n      : `${tableName || \"\"} ${columnList}`;\n    const usedColors = myLinks\n      .flatMap((l) =>\n        l.options.type !== \"table\" ?\n          l.options.columns.map((c) => c.colorArr)\n        : undefined,\n      )\n      .filter(isDefined);\n    const colorArr = getRandomColor(1, usedColors);\n    const type = linkOpts.type;\n    onAddChart({\n      name,\n      linkOpts: {\n        ...(type === \"timechart\" ?\n          {\n            type,\n            otherColumns,\n            columns: [\n              {\n                name: linkOpts.columns[0]!.name,\n                colorArr,\n                statType:\n                  firstNumericColumn ?\n                    {\n                      funcName: \"$avg\",\n                      numericColumn: firstNumericColumn,\n                    }\n                  : undefined,\n              },\n            ],\n          }\n        : type === \"barchart\" ?\n          {\n            type: \"barchart\",\n            statType:\n              firstNumericColumn ?\n                {\n                  funcName: \"$sum\",\n                  numericColumn: firstNumericColumn,\n                }\n              : undefined,\n            columns: linkOpts.columns.map(({ name }, i) => ({\n              name,\n              colorArr,\n            })),\n          }\n        : {\n            type,\n            columns: linkOpts.columns.map(({ name }, i) => ({\n              name,\n              colorArr,\n            })),\n          }),\n        dataSource:\n          sql ?\n            {\n              type: \"sql\",\n              sql,\n              withStatement,\n            }\n          : {\n              type: \"table\",\n              tableName: w.table_name!,\n              joinPath,\n            },\n      },\n    });\n  };\n\n  const charts: {\n    cols: ChartColumn[];\n    onAdd: (cols: ChartColumn[], path: ParsedJoinPath[] | undefined) => any;\n    label: \"Map\" | \"Timechart\" | \"Barchart\";\n    iconPath: string;\n  }[] = [\n    {\n      label: \"Map\",\n      iconPath: mdiMap,\n      cols: geoCols,\n      onAdd: (cols, path) => {\n        onAdd({ type: \"map\", columns: cols }, path);\n      },\n    },\n    {\n      label: \"Timechart\",\n      iconPath: mdiChartLine,\n      cols: dateCols,\n      onAdd: (cols, path) => {\n        onAdd(\n          {\n            type: \"timechart\",\n            columns: cols,\n          },\n          path,\n        );\n      },\n    },\n    // {\n    //   label: \"Barchart\",\n    //   iconPath: mdiChartBar,\n    //   cols: barCols,\n    //   onAdd: (cols, path) => {\n    //     onAdd(\n    //       {\n    //         type: \"barchart\",\n    //         columns: cols,\n    //       },\n    //       path,\n    //     );\n    //   },\n    // },\n  ];\n\n  return (\n    <>\n      {charts\n        .map((c) => {\n          const [firstCol] = c.cols;\n          const isMap = c.label === \"Map\";\n          const title = `Add ${c.label}`;\n          const layerAlreadyAdded = myLinks\n            .map(({ options: linkOpts, id }) => {\n              if (linkOpts.type === \"table\") {\n                return undefined;\n              }\n\n              const linkColumns = linkOpts.columns.map((col) => col.name);\n              const linkSql =\n                linkOpts.dataSource?.type === \"sql\" ?\n                  linkOpts.dataSource.sql\n                : undefined;\n              const matches =\n                linkOpts.type === c.label.toLowerCase() &&\n                ((w.type === \"sql\" && sql?.trim() === linkSql?.trim()) ||\n                  (w.type === \"table\" &&\n                    c.cols.some((col) => linkColumns.includes(col.name))));\n              if (matches) {\n                return linkOpts.columns[0]?.colorArr;\n              }\n            })\n            .find(isDefined);\n\n          const alreadyAddedButMinimisedOrNotVisibleChart =\n            layerAlreadyAdded &&\n            childWindows.some((cw) => {\n              const addedButMinimised =\n                cw.type === c.label.toLowerCase() && cw.minimised;\n              const addedButNotVisible =\n                cw.type === c.label.toLowerCase() &&\n                childWindows.some(\n                  (_cw) =>\n                    _cw.type !== cw.type &&\n                    !_cw.minimised &&\n                    Number(_cw.last_updated) > Number(cw.last_updated),\n                );\n              return addedButMinimised || addedButNotVisible;\n            });\n\n          const btnProps: BtnProps = {\n            title:\n              alreadyAddedButMinimisedOrNotVisibleChart ? \"Show chart\" : title,\n            size: size ?? \"small\",\n            iconPath: c.iconPath,\n            className: props.btnClassName,\n            style: {\n              minHeight: 0,\n              color:\n                layerAlreadyAdded ?\n                  rgbaToString(layerAlreadyAdded as any)\n                : undefined,\n            },\n            \"data-command\":\n              c.label === \"Map\" ? \"AddChartMenu.Map\" : \"AddChartMenu.Timechart\",\n          };\n\n          /**\n           * If map and no joined columns then add all columns for render\n           * Timechart can only render one date column\n           */\n          if (\n            !layerAlreadyAdded &&\n            ((c.label !== \"Map\" && c.cols.length > 1) ||\n              c.cols.some((_c) => _c.type === \"joined\"))\n          ) {\n            return (\n              <Select\n                key={c.label}\n                title={title}\n                data-command={btnProps[\"data-command\"]}\n                btnProps={{\n                  children: \"\",\n                  variant: \"icon\",\n                  ...btnProps,\n                }}\n                fullOptions={c.cols.map((c, i) => ({\n                  key: c.type === \"joined\" ? c.label : c.name,\n                  label:\n                    c.type === \"joined\" ? `> ${c.label} (${c.name})` : c.name,\n                  ranking: (searchTerm) =>\n                    getSearchRanking(\n                      searchTerm,\n                      c.type === \"joined\" ?\n                        c.path.map((p) => p.table)\n                      : [c.name],\n                    ),\n                }))}\n                onChange={(colNameOrLabel) => {\n                  const col = c.cols.find((col) =>\n                    col.type === \"joined\" ?\n                      col.label === colNameOrLabel\n                    : col.name === colNameOrLabel,\n                  );\n                  c.onAdd(\n                    [col!],\n                    col?.type === \"joined\" ? col.path : undefined,\n                  );\n                }}\n              />\n            );\n          }\n\n          if (!firstCol && isMicroMode) {\n            return undefined;\n          }\n\n          return (\n            <Btn\n              key={c.label}\n              disabledInfo={\n                alreadyAddedButMinimisedOrNotVisibleChart ? undefined\n                : layerAlreadyAdded ?\n                  t.AddChartMenu[\"Layer already added\"]\n                : !firstCol ?\n                  t.AddChartMenu[\n                    \"No {{chartColumnDataType}} columns available\"\n                  ]({\n                    chartColumnDataType:\n                      isMap ? \"geography/geometry\" : \"date/timestamp\",\n                  })\n                : undefined\n              }\n              {...btnProps}\n              onClick={() => {\n                if (alreadyAddedButMinimisedOrNotVisibleChart) {\n                  childWindows.forEach((cw) => {\n                    if (cw.type === c.label.toLowerCase()) {\n                      cw.$update({\n                        minimised: false,\n\n                        /** Hacky way to ensure it shows first if another chart is already visible */\n                        created: new Date().toISOString(),\n                      });\n                    }\n                  });\n                } else {\n                  c.onAdd(c.cols, undefined);\n                }\n              }}\n            />\n          );\n        })\n        .filter(isDefined)}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/AutoRefreshMenu.tsx",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport ButtonGroup from \"@components/ButtonGroup\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport type { WindowSyncItem } from \"../../Dashboard/dashboardUtils\";\nimport type { RefreshOptions } from \"./W_TableMenu\";\nimport React from \"react\";\nimport FormField from \"@components/FormField/FormField\";\nimport type { DBS } from \"../../Dashboard/DBS\";\n\nexport const AutoRefreshMenu = ({\n  w,\n  db,\n}: {\n  w:\n    | WindowSyncItem<\"sql\">\n    | WindowSyncItem<\"map\">\n    | WindowSyncItem<\"table\">\n    | WindowSyncItem<\"timechart\">;\n  db?: DBHandlerClient;\n}) => {\n  const { refresh } = w.options;\n  const {\n    type = \"None\",\n    intervalSeconds = 3,\n    throttleSeconds = 1,\n  } = refresh ?? {};\n\n  const update = (r: RefreshOptions[\"refresh\"]) => {\n    w.$update(\n      {\n        options: {\n          ...w.options,\n          refresh: r,\n        },\n      },\n      { deepMerge: true },\n    );\n  };\n\n  const REFRESH_OPTIONS = [\n    {\n      key: \"Realtime\",\n      disabledInfo:\n        w.type === \"table\" && !db?.[w.table_name]?.subscribe ?\n          \"Cannot subscribe\"\n        : undefined,\n    },\n    { key: \"Interval\" },\n    { key: \"None\" },\n  ] as const satisfies readonly {\n    key: NonNullable<RefreshOptions[\"refresh\"]>[\"type\"];\n    label?: string;\n    disabledInfo?: string;\n  }[];\n\n  return (\n    <div className=\"flex-col ai-start\">\n      <ButtonGroup\n        fullOptions={REFRESH_OPTIONS.filter(\n          (o) => w.type !== \"timechart\" || o.key !== \"Interval\",\n        )}\n        value={type}\n        style={{ marginRight: \"40px\", marginBottom: \"1em\" }}\n        onChange={(type) => {\n          update({ type, throttleSeconds: 1, intervalSeconds: 3 });\n        }}\n      />\n\n      <div className=\"text-1 noselect mt-0\">\n        {type === \"Realtime\" ?\n          <>\n            Changes shown as received from database. Needs super user to create\n            triggers\n            {w.table_name &&\n              db &&\n              db[w.table_name] &&\n              !db[w.table_name]?.subscribe && (\n                <InfoRow color=\"danger\" className=\"my-1\">\n                  Cannot subscribe\n                </InfoRow>\n              )}\n          </>\n        : type === \"Interval\" ?\n          \"Run the query every N seconds\"\n        : \"Data will not be refreshed\"}\n      </div>\n      {type === \"Interval\" && (\n        <FormField\n          label=\"Seconds\"\n          type=\"number\"\n          value={intervalSeconds}\n          hint=\"0 to disable\"\n          onChange={(v) => {\n            const intervalSeconds = +v;\n            if (!intervalSeconds || v < 0) {\n              update({ type: \"None\", intervalSeconds, throttleSeconds });\n            } else {\n              update({ type, intervalSeconds, throttleSeconds });\n            }\n          }}\n        />\n      )}\n      {type === \"Realtime\" && (\n        <FormField\n          label=\"Throttle seconds\"\n          type=\"number\"\n          value={throttleSeconds}\n          onChange={(throttleSeconds) => {\n            update({\n              type,\n              intervalSeconds,\n              throttleSeconds: +(throttleSeconds || 1),\n            });\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu.tsx",
    "content": "import {\n  mdiAccountMultiple,\n  mdiCog,\n  mdiContentSaveCogOutline,\n  mdiDatabaseSearch,\n  mdiFlash,\n  mdiInformationOutline,\n  mdiScript,\n  mdiShieldAccount,\n  mdiSyncCircle,\n  mdiViewColumnOutline,\n} from \"@mdi/js\";\nimport React from \"react\";\nimport type { TabItems } from \"@components/Tabs\";\nimport Tabs from \"@components/Tabs\";\nimport RTComp from \"../../RTComp\";\n\nimport type { ParsedJoinPath, TableInfo } from \"prostgles-types\";\nimport FormField from \"@components/FormField/FormField\";\nimport type { ColumnConfigWInfo, W_TableProps } from \"../W_Table\";\n\nimport type {\n  OnAddChart,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\n\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport { SQLSmartEditor } from \"../../SQLEditor/SQLSmartEditor\";\nimport type { ColumnConfig } from \"../ColumnMenu/ColumnMenu\";\nimport { ColumnsMenu } from \"../ColumnMenu/ColumnsMenu\";\nimport { AutoRefreshMenu } from \"./AutoRefreshMenu\";\nimport { W_TableMenu_AccessRules } from \"./W_TableMenu_AccessRules\";\nimport { W_TableMenu_Constraints } from \"./W_TableMenu_Constraints\";\nimport { W_TableMenu_CurrentQuery } from \"./W_TableMenu_CurrentQuery\";\nimport { W_TableMenu_DisplayOptions } from \"./W_TableMenu_DisplayOptions\";\nimport { W_TableMenu_Indexes } from \"./W_TableMenu_Indexes\";\nimport { W_TableMenu_Policies } from \"./W_TableMenu_Policies\";\nimport { W_TableMenu_TableInfo } from \"./W_TableMenu_TableInfo\";\nimport { W_TableMenu_Triggers } from \"./W_TableMenu_Triggers\";\nimport { getAndFixWColumnsConfig } from \"./getAndFixWColumnsConfig\";\nimport { getTableMeta, type W_TableInfo } from \"./getTableMeta\";\n\nexport type W_TableMenuProps = Pick<\n  W_TableProps,\n  \"workspace\" | \"prgl\" | \"externalFilters\" | \"joinFilter\"\n> & {\n  onAddChart?: OnAddChart;\n  w: WindowSyncItem<\"table\">;\n  onLinkTable?: (tableName: string, path: ParsedJoinPath[]) => any;\n  cols: ColumnConfigWInfo[];\n  suggestions: CommonWindowProps[\"suggestions\"];\n  onClose: () => any;\n};\nexport type Unpromise<T extends Promise<any>> =\n  T extends Promise<infer U> ? U : never;\n\nexport type RefreshOptions = {\n  readonly refresh?: {\n    readonly type: \"Realtime\" | \"None\" | \"Interval\";\n    intervalSeconds: number;\n    throttleSeconds: number;\n  };\n};\nexport type W_TableMenuState = {\n  schemaAge?: number;\n  indexes?: {\n    indexdef: string;\n    indexname: string;\n    schemaname: string;\n    tablename: string;\n  }[];\n  query?: {\n    title?: string;\n    contentTop?: React.ReactNode;\n    label?: string;\n    sql: string;\n    onSuccess?: VoidFunction;\n  };\n  l1Key?: string;\n  l2Key?: string;\n  running?: boolean;\n  error?: any;\n  initError?: any;\n  hint?: string;\n  columnsConfig?: ColumnConfig[];\n  infoQuery?: {\n    label: string;\n    query: string;\n  };\n  autoRefreshSeconds?: number;\n  constraints?: {}[];\n  tableMeta?: Unpromise<ReturnType<typeof getTableMeta>>;\n  tableInfo?: TableInfo;\n\n  linkTablePath?: string[];\n};\n\ntype D = {\n  w?: WindowSyncItem<\"table\">;\n};\n\nexport type W_TableMenuMetaProps = W_TableMenuProps & {\n  tableMeta: W_TableInfo | undefined;\n  onSetQuery: (newQuery: W_TableMenuState[\"query\"]) => void;\n};\n\nexport class W_TableMenu extends RTComp<W_TableMenuProps, W_TableMenuState, D> {\n  state: W_TableMenuState = {\n    // joins: [],\n    l1Key: undefined,\n    l2Key: undefined,\n    query: undefined,\n    running: undefined,\n    error: undefined,\n    initError: undefined,\n    hint: undefined,\n    indexes: undefined,\n    columnsConfig: undefined,\n    infoQuery: undefined,\n    autoRefreshSeconds: undefined,\n    tableMeta: undefined,\n  };\n\n  d: D = {\n    w: undefined,\n  };\n\n  onUnmount = async () => {\n    if (this.wSub) await this.wSub.$unsync();\n  };\n\n  getTableInfo() {\n    const {\n      prgl: { db, dbs, databaseId },\n      w,\n    } = this.props;\n    if (w.table_name && db.sql) {\n      getTableMeta(db, dbs, databaseId, w.table_name, w.table_oid)\n        .then((tableMeta) => {\n          this.setState({ tableMeta });\n        })\n        .catch((initError) => {\n          console.error(initError);\n        });\n    }\n  }\n\n  wSub?: ReturnType<Required<D>[\"w\"][\"$cloneSync\"]>;\n  autoRefresh: any;\n  loading = false;\n  onDelta = async (dP, dS) => {\n    const w = this.d.w || this.props.w;\n    const { table_name: tableName } = w;\n\n    if (tableName && (w as any).$cloneSync && !this.loading) {\n      this.loading = true;\n      this.wSub = await w.$cloneSync((w, delta) => {\n        this.setData({ w }, { w: delta });\n      });\n      this.getTableInfo();\n    }\n\n    if (dS && \"query\" in dS) {\n      this.setState({ error: undefined });\n    }\n  };\n\n  render() {\n    const {\n      onClose,\n      w,\n      prgl: { db, dbs, tables },\n      suggestions,\n    } = this.props;\n\n    const { infoQuery, l1Key, query, initError, tableMeta } = this.state;\n\n    if (initError) {\n      return (\n        <div className=\"p-1 relative\">\n          <ErrorComponent error={initError} />\n        </div>\n      );\n    }\n\n    const table = tables.find((t) => t.name === w.table_name);\n\n    const cardOptions =\n      w.options.viewAs?.type === \"card\" ? w.options.viewAs : undefined;\n\n    let queryForm: React.ReactNode = null;\n    if (query) {\n      queryForm = (\n        <SQLSmartEditor\n          key={query.sql}\n          sql={db.sql!}\n          query={query.sql}\n          title={query.title || \"Query\"}\n          contentTop={query.contentTop}\n          suggestions={suggestions}\n          onSuccess={() => {\n            query.onSuccess?.();\n            this.setState({ query: undefined, schemaAge: Date.now() });\n            this.getTableInfo();\n          }}\n          onCancel={() => {\n            this.setState({ query: undefined });\n          }}\n        />\n      );\n    }\n\n    let l1Opts: TabItems | undefined;\n\n    const commonProps = {\n      ...this.props,\n      tableMeta,\n      onSetQuery: (query) => this.setState({ query }),\n    };\n    if (w.table_name) {\n      l1Opts = {\n        ...(tableMeta && {\n          \"Table info\": {\n            label: table?.info.isView ? \"View info\" : undefined,\n            leftIconPath: mdiInformationOutline,\n            disabledText: db.sql ? undefined : \"Not enough privileges\",\n            content: <W_TableMenu_TableInfo {...commonProps} />,\n          },\n        }),\n\n        Columns: {\n          leftIconPath: mdiViewColumnOutline,\n          content: (\n            <ColumnsMenu\n              nestedColumnOpts={undefined}\n              w={w}\n              db={db}\n              tables={tables}\n              onClose={onClose}\n              suggestions={suggestions}\n            />\n          ),\n        },\n\n        \"Data refresh\": {\n          leftIconPath: mdiSyncCircle,\n          style:\n            (w.options.refresh?.type || \"None\") === \"None\" ?\n              {}\n            : { color: \"var(--active)\" },\n          content: <AutoRefreshMenu w={w} db={db} />,\n        },\n\n        ...(tableMeta && {\n          Triggers: {\n            label: \"Triggers \" + tableMeta.triggers.length,\n            disabledText: db.sql ? undefined : \"Not enough privileges\",\n            leftIconPath: mdiFlash,\n            content: <W_TableMenu_Triggers {...commonProps} />,\n          },\n\n          Constraints: {\n            label: \"Constraints \" + tableMeta.constraints.length,\n            leftIconPath: mdiContentSaveCogOutline,\n            disabledText: db.sql ? undefined : \"Not enough privileges\",\n            content: <W_TableMenu_Constraints {...commonProps} />,\n          },\n\n          Indexes: {\n            label: \"Indexes \" + tableMeta.indexes.length,\n            leftIconPath: mdiDatabaseSearch,\n            disabledText: db.sql ? undefined : \"Not enough privileges\",\n            content: <W_TableMenu_Indexes {...commonProps} />,\n          },\n\n          Policies: {\n            label: \"Policies \" + tableMeta.policiesCount,\n            leftIconPath: mdiShieldAccount,\n            disabledText: db.sql ? undefined : \"Not enough privileges\",\n            content: <W_TableMenu_Policies {...commonProps} />,\n          },\n\n          \"Access rules\": {\n            label: \"Access rules \" + tableMeta.accessRules.length,\n            leftIconPath: mdiAccountMultiple,\n            disabledText:\n              (dbs.access_control as any).find ?\n                undefined\n              : \"Not enough privileges\",\n            content: <W_TableMenu_AccessRules {...commonProps} />,\n          },\n\n          \"Current query\": {\n            leftIconPath: mdiScript,\n            content: <W_TableMenu_CurrentQuery {...commonProps} />,\n          },\n        }),\n        \"Display options\": {\n          leftIconPath: mdiCog,\n          content: <W_TableMenu_DisplayOptions {...this.props} />,\n        },\n      };\n    }\n    const tableName = w.table_name;\n\n    return (\n      <div\n        className=\"table-menu c-s-fit flex-col\"\n        style={{ maxHeight: \"calc(100vh - 65px)\", maxWidth: \"100vw\" }}\n      >\n        {queryForm}\n        <Tabs\n          key={this.state.schemaAge}\n          variant=\"vertical\"\n          contentClass={\n            \"max-w-700 min-h-0 max-h-100v flex-col min-w-0 \" +\n            (l1Key !== \"Columns\" ? \" p-1 \" : \"\") +\n            (l1Key === \"Columns\" ? \" \" : \" \")\n          }\n          items={l1Opts ?? {}}\n          compactMode={window.isMobileDevice ? \"hide-inactive\" : undefined}\n          activeKey={l1Key}\n          onChange={async (l1Key: any) => {\n            this.setState({ l1Key, query: undefined, infoQuery: undefined });\n            if (!this.d.w) return;\n\n            if (l1Key === \"View as card\") {\n              this.d.w.$update(\n                {\n                  options: {\n                    viewAs: {\n                      type: \"card\",\n                      maxCardWidth: cardOptions?.maxCardWidth ?? \"700px\",\n                    },\n                  },\n                },\n                { deepMerge: true },\n              );\n            } else if (l1Key === \"Columns\") {\n              const columnsConfig = await getAndFixWColumnsConfig(tables, w);\n              this.setState({ columnsConfig });\n            } else if (l1Key === \"Filter\") {\n              this.d.w.$update({\n                show_menu: false,\n                filter: [],\n              });\n            }\n          }}\n        />\n        {l1Key === \"Columns\" && tableName && db.sql && infoQuery && (\n          <div className=\"flex-col o-auto p-1\">\n            <FormField\n              className=\"mb-1\"\n              label={infoQuery.label}\n              readOnly={true}\n              asTextArea={true}\n              value={infoQuery.query}\n            />\n          </div>\n        )}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu_AccessRules.tsx",
    "content": "import React from \"react\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport type { W_TableMenuMetaProps } from \"./W_TableMenu\";\nimport { ACCESS_RULE_METHODS } from \"../../AccessControl/AccessRuleSummary\";\nimport { Link } from \"react-router-dom\";\nimport { getAccessControlHref } from \"../../AccessControl/useAccessControlSearchParams\";\nimport { LabeledRow } from \"@components/LabeledRow\";\nimport { mdiAccount, mdiPlus } from \"@mdi/js\";\nimport Btn from \"@components/Btn\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { isObject } from \"@common/publishUtils\";\nimport { RenderFilter } from \"../../RenderFilter\";\nimport { Label } from \"@components/Label\";\n\nexport const W_TableMenu_AccessRules = ({\n  tableMeta,\n  w,\n  prgl,\n}: W_TableMenuMetaProps) => {\n  if (!tableMeta) return null;\n  const { accessRules } = tableMeta;\n  return (\n    <FlexCol>\n      <ul className=\"no-decor flex-col gap-1\">\n        {accessRules.map((r) => {\n          const dbRule = r.dbPermissions;\n          const tableRule =\n            dbRule.type === \"Custom\" ?\n              dbRule.customTables.find((t) => t.tableName === w.table_name)\n            : undefined;\n          const href = getAccessControlHref({\n            connectionId: prgl.connectionId,\n            selectedRuleId: r.id.toString(),\n          });\n          return (\n            <Link key={r.id} to={href} className=\"no-decor \">\n              <div className=\" active-shadow-hover pointer rounded bg-color-0 shadow b b-color p-p5 \">\n                <LabeledRow\n                  icon={mdiAccount}\n                  title=\"User types\"\n                  className=\"ai-center\"\n                >\n                  <span className=\"text-0 font-20 bold\">\n                    {r.userTypes.join(\", \")}\n                  </span>\n                </LabeledRow>\n                <FlexCol className=\"p-1 text-0\">\n                  {dbRule.type === \"Run SQL\" ?\n                    \"Run SQL\"\n                  : dbRule.type === \"All views/tables\" ?\n                    dbRule.allowAllTables.join(\", \")\n                  : ACCESS_RULE_METHODS.map((ruleName) => {\n                      const rule = tableRule?.[ruleName];\n                      if (!rule) return null;\n                      const ruleFields =\n                        rule === true ? \"*\"\n                        : isObject(rule) && \"fields\" in rule && rule.fields ?\n                          rule.fields\n                        : undefined;\n                      const fieldsInfo =\n                        ruleFields === \"*\" ? \"All columns\"\n                        : Array.isArray(ruleFields) ?\n                          `Columns: ${ruleFields.join(\", \")}`\n                        : isObject(ruleFields) ?\n                          `${Object.values(ruleFields).some((v) => !v) ? \"Except columns:\" : \"Columns:\"} ${Object.keys(ruleFields)}`\n                        : undefined;\n                      return (\n                        <FlexCol key={ruleName} className=\"ta-left\">\n                          <FlexCol>\n                            <div className=\"font-22 text-0\">{ruleName}</div>\n                            <FlexCol className=\"pl-1\">\n                              {fieldsInfo}\n                              {isObject(rule) &&\n                                \"forcedFilterDetailed\" in rule && (\n                                  <FlexCol key={\"using\"} className=\"gap-p25\">\n                                    <Label variant=\"normal\">USING</Label>\n                                    <RenderFilter\n                                      contextData={undefined}\n                                      db={prgl.db}\n                                      itemName=\"condition\"\n                                      selectedColumns={undefined}\n                                      tableName={w.table_name}\n                                      tables={prgl.tables}\n                                      mode=\"minimised\"\n                                      filter={rule.forcedFilterDetailed}\n                                      onChange={() => {}}\n                                    />\n                                  </FlexCol>\n                                )}\n                              {isObject(rule) &&\n                                \"checkFilterDetailed\" in rule && (\n                                  <FlexCol key={\"check\"} className=\"gap-p25\">\n                                    <div>CHECK</div>\n                                    <RenderFilter\n                                      contextData={undefined}\n                                      db={prgl.db}\n                                      itemName=\"condition\"\n                                      selectedColumns={undefined}\n                                      tableName={w.table_name}\n                                      tables={prgl.tables}\n                                      mode=\"minimised\"\n                                      filter={rule.checkFilterDetailed}\n                                      onChange={() => {}}\n                                    />\n                                  </FlexCol>\n                                )}\n                            </FlexCol>\n                          </FlexCol>\n                        </FlexCol>\n                      );\n                    })\n                  }\n                </FlexCol>\n              </div>\n            </Link>\n          );\n        })}\n        {!accessRules.length && <InfoRow>No access rules</InfoRow>}\n      </ul>\n      <Btn\n        asNavLink={true}\n        href={getAccessControlHref({ connectionId: prgl.connectionId })}\n        iconPath={mdiPlus}\n        color=\"action\"\n        variant=\"filled\"\n      >\n        Create\n      </Btn>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu_Constraints.tsx",
    "content": "import { mdiDelete, mdiPlus } from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Select } from \"@components/Select/Select\";\nimport { SmartCardList } from \"../../SmartCardList/SmartCardList\";\nimport type { W_TableMenuProps, W_TableMenuState } from \"./W_TableMenu\";\nimport type { W_TableInfo } from \"./getTableMeta\";\n\ntype P = W_TableMenuProps & {\n  tableMeta: W_TableInfo | undefined;\n  onSetQuery: (newQuery: W_TableMenuState[\"query\"]) => void;\n};\nexport const W_TableMenu_Constraints = ({\n  tableMeta,\n  onSetQuery,\n  w,\n  prgl,\n}: P) => {\n  const tableName = w.table_name;\n\n  const listProps = useMemo(() => {\n    const tableNameProp = {\n      dataAge: prgl.dbKey,\n      sqlQuery: `\n        SELECT conname, pg_get_constraintdef(c.oid) as definition \n        FROM pg_catalog.pg_constraint c\n        INNER JOIN pg_catalog.pg_class rel\n          ON rel.oid = c.conrelid\n        INNER JOIN pg_catalog.pg_namespace nsp\n          ON nsp.oid = connamespace\n        WHERE nsp.nspname = 'public'\n        AND rel.relname = \\${tableName}\n      `,\n      args: { tableName },\n    };\n    const fieldConfigs = [\n      {\n        name: \"definition\",\n        label: \"\",\n        render: (definition, row) => (\n          <div className=\"ws-pre-line\">\n            <span className=\"text-2\">\n              ALTER TABLE {tableName}\n              <br></br>\n              ADD CONSTRAINT {row.conname}\n            </span>\n            <br></br>\n            {definition}\n          </div>\n        ),\n      },\n      {\n        name: \"conname\",\n        label: \"\",\n        className: \"show-on-parent-hover ml-auto\",\n        render: (conname) => (\n          <FlexRow className=\"\">\n            <Btn\n              title=\"Drop constraint...\"\n              color=\"danger\"\n              variant=\"faded\"\n              iconPath={mdiDelete}\n              onClick={() => {\n                onSetQuery({\n                  sql: `ALTER TABLE ${JSON.stringify(tableName)} DROP CONSTRAINT ${JSON.stringify(conname)}`,\n                });\n              }}\n            />\n          </FlexRow>\n        ),\n      },\n    ];\n    return {\n      tableName: tableNameProp,\n      fieldConfigs,\n    };\n  }, [onSetQuery, prgl.dbKey, tableName]);\n\n  if (!tableMeta || !tableName) return null;\n\n  return (\n    <FlexCol className=\"flex-col ai-start o-auto ws-pre\">\n      <div className=\"ta-left p-p5\">\n        Constraints set rules on the type of data that can be stored in columns\n      </div>\n      <SmartCardList\n        db={prgl.db}\n        methods={prgl.methods}\n        tables={prgl.tables}\n        noDataComponent={<InfoRow color=\"info\">No constraints</InfoRow>}\n        {...listProps}\n      />\n      <Select\n        title=\"Create constraint...\"\n        className=\"ml-p5\"\n        btnProps={{\n          iconPath: mdiPlus,\n          variant: \"faded\",\n          color: \"action\",\n          children: \"Create\",\n        }}\n        fullOptions={[\n          {\n            key: \"PRIMARY KEY\",\n            subLabel:\n              \"A column or a group of columns used to identify a row uniquely in a table\",\n          },\n          {\n            key: \"FOREIGN KEY\",\n            subLabel:\n              \"reference the primary key (or unique columns) of another table\",\n          },\n          {\n            key: \"CHECK\",\n            subLabel:\n              \"Condition for row values before they are inserted or updated to the column\",\n          },\n          {\n            key: \"UNIQUE\",\n            subLabel: \"Similar to primary key except it allows null values\",\n          },\n          {\n            key: \"WITH CHECK\",\n            subLabel:\n              \"Change condition used to validate INSERT and UPDATE queries\",\n          },\n        ]}\n        onChange={(val) => {\n          onSetQuery({\n            title: \"Add constraint\",\n            sql: `ALTER TABLE ${tableName} ADD ${val}`,\n          });\n        }}\n      />\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu_CurrentQuery.tsx",
    "content": "import React, { useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport CodeExample from \"../../CodeExample\";\nimport { getTableFilter } from \"../getTableData/getTableFilter\";\nimport { getTableSelect } from \"../tableUtils/getTableSelect\";\nimport { getSort } from \"../tableUtils/tableUtils\";\nimport type { W_TableMenuProps } from \"./W_TableMenu\";\n\nconst QUERY_TYPES = [\"SQL\", \"Prostgles API\", \"Full config\"] as const;\ntype QueryType = (typeof QUERY_TYPES)[number];\n\nexport const W_TableMenu_CurrentQuery = (props: W_TableMenuProps) => {\n  const {\n    w,\n    prgl: { tables, db },\n  } = props;\n  const [{ query, type }, setQuery] = useState<{\n    query: string;\n    type?: QueryType;\n  }>({ query: \"\", type: undefined });\n\n  return (\n    <FlexCol>\n      {QUERY_TYPES.map((queryType) => {\n        const isActive = queryType === type;\n        return (\n          <Btn\n            key={queryType}\n            onClickPromise={async () => {\n              if (queryType === \"Full config\") {\n                setQuery({\n                  query: JSON.stringify(w, null, 2),\n                  type: \"Full config\",\n                });\n                return;\n              }\n              const { filter, having } = getTableFilter(w, props);\n              const { select } = await getTableSelect(w, tables, db, filter);\n              const orderBy = getSort(tables, w);\n              let currentQuery = \"\";\n              const selectParams = { select, having, orderBy };\n              if (queryType === \"SQL\") {\n                const query = (await db[w.table_name]?.find?.(\n                  filter,\n                  selectParams,\n                  //@ts-ignore\n                  { returnQuery: true },\n                )) as unknown as string;\n                currentQuery = query;\n              } else {\n                currentQuery = `await db['${w.table_name}'].find(\\n  ${JSON.stringify(filter, null, 2)}, \\n  ${JSON.stringify(selectParams, null, 2)}\\n)`;\n              }\n              setQuery({ query: currentQuery, type: queryType });\n            }}\n            variant={isActive ? \"filled\" : \"faded\"}\n            color=\"action\"\n            disabledInfo={isActive ? \"Already shown\" : undefined}\n          >\n            {isActive ? \"Showing\" : \"Show\"} {queryType} query\n          </Btn>\n        );\n      })}\n\n      {query && (\n        <CodeExample\n          style={{\n            minWidth: \"500px\",\n            minHeight: \"500px\",\n          }}\n          language={type === \"SQL\" ? \"sql\" : \"javascript\"}\n          value={query}\n        />\n      )}\n    </FlexCol>\n  );\n};\n/*\n\nawait db['users'].find(\n  {}, \n  {\n  \"select\": {\n    \"id\": 1,\n    \"email\": 1,\n    \"password\": 1,\n    \"first_name\": 1,\n    \"last_name\": 1,\n    \"phone_number\": 1,\n    \"type\": 1,\n    \"location\": 1,\n    \"created_at\": 1,\n    \"customer_id_orders\": {\n      \"$leftJoin\": [\n        {\n          \"on\": [\n            {\n              \"id\": \"customer_id\"\n            }\n          ],\n          \"table\": \"orders\"\n        }\n      ],\n      \"filter\": { \"c\": { \">\": 0 } },\n      \"limit\": 20,\n      \"select\": {\n        \"c\": {\n          \"$countAll\": []\n        }\n      },\n      \"filter\": {}\n    }\n  },\n  \"limit\": 5,\n  \"orderBy\": []\n}\n)\n\nawait db['users'].find(\n  {\n  \"first_name\": \"Mirta\"\n}, \n  {\n  \"select\": {\n    \"id\": 1,\n    \"email\": 1,\n    \"password\": 1,\n    \"first_name\": 1,\n    \"last_name\": 1,\n    \"phone_number\": 1,\n    \"type\": 1,\n    \"location\": 1,\n    \"created_at\": 1,\n    \"deliverer_id_orders\": {\n      \"$leftJoin\": [\n        {\n          \"on\": [\n            {\n              \"id\": \"deliverer_id\"\n            }\n          ],\n          \"table\": \"orders\"\n        }\n      ],\n      \"limit\": 20,\n      \"select\": {\n        \"COUNT ALL\": {\n          \"$countAll\": []\n        }\n      },\n      \"filter\": {}\n    },\n    \"customer_id_orders\": {\n      \"$leftJoin\": [\n        {\n          \"on\": [\n            {\n              \"id\": \"customer_id\"\n            }\n          ],\n          \"table\": \"orders\"\n        }\n      ],\n      \"limit\": 20,\n      \"select\": {\n        \"COUNT ALL\": {\n          \"$countAll\": []\n        }\n      },\n      \"filter\": {}\n    }\n  },\n  \"orderBy\": []\n}\n)\n\n*/\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu_DisplayOptions.tsx",
    "content": "import React from \"react\";\nimport type { W_TableMenuProps } from \"./W_TableMenu\";\nimport FormField from \"@components/FormField/FormField\";\nimport { ColumnSelect } from \"../ColumnMenu/ColumnSelect/ColumnSelect\";\nimport { Select } from \"@components/Select/Select\";\nimport { includes } from \"../../W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\nimport { IconPalette } from \"@components/IconPalette/IconPalette\";\n\ntype P = W_TableMenuProps;\n\nexport const W_TableMenu_DisplayOptions = ({\n  w,\n  workspace,\n  prgl: { tables, connection, dbs },\n}: P) => {\n  const tableName = w.table_name;\n  if (!tableName) return null;\n  const table = tables.find((t) => t.name === tableName);\n\n  const cardOptions =\n    w.options.viewAs?.type === \"card\" ? w.options.viewAs : undefined;\n\n  return (\n    <div className=\"flex-col gap-1 ai-start mb-1 f-1 o-auto p-p25 \">\n      <FormField\n        className=\" w-fit f-0\"\n        label=\"Display mode\"\n        data-command=\"table.options.displayMode\"\n        value={w.options.viewAs?.type ?? \"table\"}\n        fullOptions={([\"table\", \"card\", \"json\"] as const).map((key) => ({\n          key,\n        }))}\n        onChange={(viewAsType) => {\n          w.$update(\n            {\n              options: {\n                viewAs:\n                  viewAsType === \"card\" ?\n                    {\n                      type: \"card\",\n                      maxCardWidth: window.isLowWidthScreen ? \"100%\" : \"700px\",\n                      hideEmptyCardCells: true,\n                    }\n                  : { type: viewAsType },\n              },\n            },\n            { deepMerge: true },\n          );\n        }}\n      />\n      <IconPalette\n        label={{\n          label: \"Icon\",\n          variant: \"normal\",\n          style: {\n            marginBottom: \"4px\",\n          },\n        }}\n        iconName={connection.table_options?.[w.table_name]?.icon}\n        onChange={(icon) => {\n          dbs.connections.update(\n            { id: connection.id },\n            {\n              table_options: {\n                $merge: [\n                  {\n                    [w.table_name]: {\n                      icon: icon ?? undefined,\n                    },\n                  },\n                ],\n              },\n            },\n          );\n        }}\n      />\n      <FormField\n        className=\"w-fit f-0\"\n        type=\"checkbox\"\n        label={{\n          label: \"Hide top count\",\n          info: \"If false will show the number of rows in the table header. Disable to improve performance\",\n        }}\n        value={w.options.hideCount ?? workspace.options.hideCounts ?? false}\n        onChange={(hideCount) => {\n          w.$update({ options: { hideCount } }, { deepMerge: true });\n        }}\n      />\n\n      {w.options.viewAs?.type !== \"json\" && (\n        <FormField\n          className=\"w-fit f-0\"\n          type=\"checkbox\"\n          label={{ label: \"Hide show row panel button\", info: \"First column\" }}\n          value={!!w.options.hideEditRow}\n          onChange={(hideEditRow) => {\n            w.$update({ options: { hideEditRow } }, { deepMerge: true });\n          }}\n        />\n      )}\n      {cardOptions && table && (\n        <>\n          <FormField\n            className=\"w-fit f-0\"\n            type=\"number\"\n            label=\"Cards per row\"\n            inputProps={{\n              step: 1,\n              min: 1,\n              max: 8,\n            }}\n            value={cardOptions.cardRows || 1}\n            onChange={(e) => {\n              w.$update(\n                { options: { viewAs: { cardRows: Math.round(+e) } } },\n                { deepMerge: true },\n              );\n            }}\n          />\n          <ColumnSelect\n            label=\"Cards group by column\"\n            data-command=\"table.options.cardView.groupBy\"\n            columns={table.columns}\n            value={cardOptions.cardGroupBy}\n            onChange={(c) => {\n              w.$update(\n                { options: { viewAs: { cardGroupBy: c } } },\n                { deepMerge: true },\n              );\n            }}\n          />\n          <ColumnSelect\n            label=\"Cards order by column\"\n            data-command=\"table.options.cardView.orderBy\"\n            columns={table.columns.map((c) =>\n              includes(c.udt_name, [\"numeric\", \"float4\", \"float8\"]) ? c : (\n                {\n                  ...c,\n                  disabledInfo:\n                    \"Only columns of data type numeric or float allowed\",\n                }\n              ),\n            )}\n            value={cardOptions.cardOrderBy}\n            onChange={(c) => {\n              w.$update(\n                { options: { viewAs: { cardOrderBy: c } } },\n                { deepMerge: true },\n              );\n            }}\n          />\n          <FormField\n            className=\"w-fit f-0\"\n            type=\"checkbox\"\n            label=\"Hide card column names\"\n            value={!!cardOptions.hideCardFieldNames}\n            onChange={(hideCardFieldNames) => {\n              w.$update(\n                { options: { viewAs: { hideCardFieldNames } } },\n                { deepMerge: true },\n              );\n            }}\n          />\n          <Select\n            label=\"Max card cell height in pixels\"\n            className=\"w-fit f-0\"\n            value={cardOptions.maxCardRowHeight ?? 800}\n            variant=\"div\"\n            options={[25, 50, 100, 150, 200, 300, 400, 500, 600, 700, 800]}\n            onChange={(maxCardRowHeight) => {\n              w.$update(\n                { options: { viewAs: { maxCardRowHeight } } },\n                { deepMerge: true },\n              );\n            }}\n          />\n          <Select\n            label=\"Card width\"\n            className=\"w-fit f-0\"\n            value={cardOptions.maxCardWidth ?? \"100%\"}\n            variant=\"div\"\n            options={[\n              \"100%\",\n              \"100px\",\n              \"200px\",\n              \"300px\",\n              \"400px\",\n              \"500px\",\n              \"600px\",\n              \"700px\",\n              \"900px\",\n            ]}\n            onChange={(maxCardWidth) => {\n              w.$update(\n                { options: { viewAs: { maxCardWidth } } },\n                { deepMerge: true },\n              );\n            }}\n          />\n          <Select\n            label=\"Card cell min width\"\n            className=\"w-fit f-0\"\n            value={cardOptions.cardCellMinWidth ?? \"\"}\n            variant=\"div\"\n            options={[\n              \"\",\n              ...[10, 20, 25, 30, 33, 40, 50, 60, 70, 80, 90, 100].map(\n                (v) => v + \"%\",\n              ),\n            ]}\n            onChange={(cardCellMinWidth) => {\n              w.$update(\n                { options: { viewAs: { cardCellMinWidth } } },\n                { deepMerge: true },\n              );\n            }}\n          />\n          <FormField\n            className=\"w-fit f-0\"\n            type=\"checkbox\"\n            label=\"Hide empty card cells\"\n            value={!!cardOptions.hideEmptyCardCells}\n            onChange={(hideEmptyCardCells) => {\n              w.$update(\n                { options: { viewAs: { hideEmptyCardCells } } },\n                { deepMerge: true },\n              );\n            }}\n          />\n        </>\n      )}\n      {w.options.viewAs?.type !== \"json\" && (\n        <>\n          <FormField\n            className=\"w-fit f-0\"\n            type=\"checkbox\"\n            label=\"Show data type sub headers\"\n            value={!!w.options.showSubLabel}\n            onChange={(showSubLabel) => {\n              w.$update({ options: { showSubLabel } }, { deepMerge: true });\n            }}\n          />\n\n          <FormField\n            className=\"w-fit f-0\"\n            type=\"number\"\n            label=\"Maximum number of characters per column\"\n            value={w.options.maxCellChars ?? 500}\n            onChange={(maxCellChars) => {\n              w.$update({ options: { maxCellChars } }, { deepMerge: true });\n            }}\n          />\n          {!cardOptions && (\n            <FormField\n              label=\"Max table row height in pixels\"\n              className=\"w-fit f-0\"\n              value={w.options.maxRowHeight ?? 100}\n              options={[25, 50, 100, 150, 200, 300, 400, 500, 600, 700, 800]}\n              onChange={(maxRowHeight) => {\n                w.$update({ options: { maxRowHeight } }, { deepMerge: true });\n              }}\n            />\n          )}\n        </>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu_Indexes.tsx",
    "content": "import { mdiDatabaseRefreshOutline, mdiDelete, mdiPlus } from \"@mdi/js\";\nimport { asName } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Select } from \"@components/Select/Select\";\nimport { SmartCardList } from \"../../SmartCardList/SmartCardList\";\nimport type { W_TableMenuProps, W_TableMenuState } from \"./W_TableMenu\";\nimport type { W_TableInfo } from \"./getTableMeta\";\n\ntype P = W_TableMenuProps & {\n  tableMeta: W_TableInfo | undefined;\n  onSetQuery: (newQuery: W_TableMenuState[\"query\"]) => void;\n};\nexport const W_TableMenu_Indexes = ({\n  tableMeta,\n  onSetQuery,\n  w,\n  cols,\n  prgl,\n}: P) => {\n  const tableName = w.table_name;\n\n  const listProps = useMemo(() => {\n    return {\n      tableName: {\n        sqlQuery: `\n      SELECT tablename, indexname, indexdef\n      FROM pg_indexes\n      WHERE schemaname = current_schema() AND format('%I', tablename) = \\${tableName}\n    `,\n        dataAge: prgl.dbKey,\n        args: { tableName },\n      },\n      fieldConfigs: [\n        {\n          name: \"indexdef\",\n          label: \"\",\n          render: (def) => (\n            <div className=\"ws-pre-line\">\n              {def.replace(\" ON \", \" \\nON \").replace(\" USING \", \" \\nUSING \")}\n            </div>\n          ),\n        },\n        {\n          name: \"indexname\",\n          label: \"\",\n          className: \"show-on-parent-hover ml-auto\",\n          render: (indexname) => (\n            <FlexRow className=\"\">\n              <Btn\n                title=\"Drop index...\"\n                color=\"danger\"\n                variant=\"faded\"\n                iconPath={mdiDelete}\n                onClick={() => {\n                  onSetQuery({\n                    sql: `DROP INDEX ${JSON.stringify(indexname)}`,\n                  });\n                }}\n              />\n              <Btn\n                title=\"Reindex\"\n                iconPath={mdiDatabaseRefreshOutline}\n                variant=\"faded\"\n                onClick={() => {\n                  onSetQuery({\n                    title: \"Reindex\",\n                    contentTop: (\n                      <>\n                        REINDEX is similar to a drop and recreate of the index\n                        in that the index contents are rebuilt from scratch.\n                        <br></br>\n                        If &quot;CONCURRENTLY&quot; is used PostgreSQL will\n                        rebuild the index without taking any locks that prevent\n                        concurrent inserts, updates, or deletes on the table\n                      </>\n                    ),\n                    sql: `REINDEX INDEX ${asName(indexname)}`,\n                  });\n                }}\n              />\n            </FlexRow>\n          ),\n        },\n      ],\n    };\n  }, [tableName, onSetQuery, prgl.dbKey]);\n\n  if (!tableMeta || !tableName) return null;\n\n  return (\n    <FlexCol>\n      <div className=\"ta-left p-p5\">\n        An index improves the speed of data retrieval operations on a table\n      </div>\n      <SmartCardList\n        db={prgl.db}\n        methods={prgl.methods}\n        tables={prgl.tables}\n        noDataComponent={<InfoRow color=\"info\">No indexes</InfoRow>}\n        {...listProps}\n      />\n      <FlexRow className=\"ai-center ml-p5 f-0\">\n        <Select\n          title=\"Alter this policy...\"\n          btnProps={{\n            iconPath: mdiPlus,\n            variant: \"faded\",\n            color: \"action\",\n            children: \"Create\",\n          }}\n          fullOptions={[\n            {\n              key: \"B-Tree\",\n              subLabel:\n                \"Default. B-trees can handle equality and range queries on data that can be sorted into some ordering. Operators: \\n<   <=   =   >=   >\",\n            },\n            {\n              key: \"Hash\",\n              subLabel:\n                \"Hash indexes store a 32-bit hash code derived from the value of the indexed column. Operators: \\n=\",\n            },\n            {\n              key: \"GiST\",\n              subLabel:\n                \"Has many different indexing strategies. Operators: \\n<<   &<   &>   >>   <<|   &<|   |&>   |>>   @>   <@   ~=   &&\",\n            },\n            {\n              key: \"SP-GiST\",\n              subLabel:\n                \"Has a wide range of different non-balanced disk-based data structures, such as quadtrees, k-d trees, and radix trees (tries). Operators: \\n<<   >>   ~=   <@   <<|   |>>\",\n            },\n            {\n              key: \"GIN\",\n              subLabel: `“Inverted indexes” which are appropriate for data values that contain multiple component values, such as arrays. Operators: \\n<@   @>   =   &&`,\n            },\n            {\n              key: \"BRIN\",\n              subLabel: `Most effective for columns whose values are well-correlated with the physical order of the table rows. Operators: \\n<   <=   =   >=   >`,\n            },\n          ]}\n          onChange={(val) => {\n            onSetQuery({\n              title: \"Create index\",\n              sql: `CREATE INDEX ON public.${tableName} \\nUSING ${val.toLowerCase().replaceAll(\"-\", \"\")} (${cols.map((c) => c.name).join(\", \")}) `,\n            });\n          }}\n        />\n        <Btn\n          iconPath={mdiDatabaseRefreshOutline}\n          variant=\"faded\"\n          onClick={() => {\n            onSetQuery({\n              title: \"Reindex table\",\n              sql: `REINDEX TABLE ${tableName} `,\n              contentTop: `REINDEX is similar to a drop and recreate of the index in that the index contents are rebuilt from scratch`,\n            });\n          }}\n        >\n          Reindex table\n        </Btn>\n      </FlexRow>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu_Policies.tsx",
    "content": "import { mdiDelete, mdiPencil, mdiPlus } from \"@mdi/js\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Select } from \"@components/Select/Select\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { SmartCardList } from \"../../SmartCardList/SmartCardList\";\nimport { PG_OBJECT_QUERIES } from \"../../SQLEditor/SQLCompletion/getPGObjects\";\nimport type { W_TableInfo } from \"./getTableMeta\";\nimport type { W_TableMenuProps, W_TableMenuState } from \"./W_TableMenu\";\nimport type { FieldConfig } from \"../../SmartCard/SmartCard\";\n\ntype P = W_TableMenuProps & {\n  tableMeta: W_TableInfo | undefined;\n  onSetQuery: (newQuery: W_TableMenuState[\"query\"]) => void;\n};\nexport const W_TableMenu_Policies = ({ tableMeta, onSetQuery, prgl, w }: P) => {\n  const tableName = w.table_name;\n\n  const listProps = useMemo(() => {\n    return {\n      tableName: {\n        dataAge: prgl.dbKey,\n        sqlQuery: PG_OBJECT_QUERIES.policies.sql(tableName),\n        args: { tableName },\n      },\n      fieldConfigs: [\n        {\n          name: \"definition\",\n          label: \"\",\n          renderMode: \"valueNode\",\n          render: (v) => <div className=\"ws-pre-line\">{v}</div>,\n        },\n        {\n          name: \"tablename\",\n          label: \"\",\n          className: \"show-on-parent-hover\",\n          renderMode: \"valueNode\",\n          render: (v, row: AnyObject) => (\n            <FlexCol>\n              <Select\n                title=\"Alter this policy...\"\n                btnProps={{\n                  iconPath: mdiPencil,\n                  variant: \"faded\",\n                  children: \"\",\n                }}\n                fullOptions={[\n                  {\n                    key: \"RENAME TO\",\n                    subLabel: \"Change the name of the policy\",\n                  },\n                  {\n                    key: \"TO\",\n                    subLabel: \"Change who the policy applies to\",\n                  },\n                  {\n                    key: \"USING\",\n                    subLabel:\n                      \"Change which rows (or the condition) the policy applies to\",\n                  },\n                  {\n                    key: \"WITH CHECK\",\n                    subLabel:\n                      \"Change condition used to validate INSERT and UPDATE queries\",\n                  },\n                ]}\n                onChange={(val) => {\n                  onSetQuery({\n                    sql:\n                      `ALTER POLICY ${row.escaped_identifier} ON ${tableName}\\n${val} ` +\n                      (val === \"WITH CHECK\" ? row.with_check\n                      : val === \"USING\" ? (row.using ?? \"\")\n                      : val === \"TO\" ? (row.roles ?? \"\")\n                      : \"\"),\n                    title: \"Alter policy\",\n                  });\n                }}\n              />\n              <Btn\n                title=\"Delete this policy...\"\n                iconPath={mdiDelete}\n                color=\"danger\"\n                variant=\"faded\"\n                onClick={() =>\n                  onSetQuery({\n                    sql: `DROP POLICY ${row.escaped_identifier} ON ${tableName}`,\n                  })\n                }\n              />\n            </FlexCol>\n          ),\n        },\n      ] satisfies FieldConfig[],\n    };\n  }, [tableName, prgl.dbKey, onSetQuery]);\n\n  if (!tableMeta || !tableName) return null;\n  const AlterQuery = `ALTER TABLE ${tableName}`;\n  return (\n    <FlexCol className=\"ai-start o-auto\">\n      <div className=\"ta-left p-p5\">\n        When row level security is enabled all normal access to the table for\n        selecting or modifying rows must be allowed by a row security policy.\n        <br />\n        <br />\n        Superusers and roles with the BYPASSRLS attribute always bypass the row\n        security system when accessing a table.\n        <br />\n        Table owners normally bypass row security as well, though a table owner\n        can choose to be subject to row security with ALTER TABLE ... FORCE ROW\n        LEVEL SECURITY.\n      </div>\n      <FlexRowWrap>\n        <SwitchToggle\n          label=\"Row Level Security\"\n          checked={tableMeta.relrowsecurity}\n          onChange={(val) => {\n            onSetQuery({\n              label: \"Row Level Security\",\n              sql: `${AlterQuery}\\n${val ? \"ENABLE\" : \"DISABLE\"} ROW LEVEL SECURITY;`,\n            });\n          }}\n        />\n        <SwitchToggle\n          label=\"Forced Row Level Security\"\n          checked={tableMeta.relforcerowsecurity}\n          onChange={(val) => {\n            onSetQuery({\n              label: \"Force Row Level Security\",\n              sql: `${AlterQuery}\\n${val ? \"\" : \"NO \"}FORCE ROW LEVEL SECURITY;`,\n            });\n          }}\n        />\n        <Select\n          emptyLabel={\"Toggle RLS for all tables\"}\n          fullOptions={[\n            {\n              key: \"ENABLE\",\n              subLabel: \"ENABLE ROW LEVEL SECURITY for all tables\",\n            },\n            {\n              key: \"DISABLE\",\n              subLabel: \"DISABLE ROW LEVEL SECURITY for all tables\",\n            },\n          ]}\n          onChange={(val) => {\n            onSetQuery({\n              title: `${val} Row Level Security for all tables in current schema`,\n              sql: getAllTableRLSQuery(val),\n            });\n          }}\n        />\n      </FlexRowWrap>\n      <SmartCardList\n        db={prgl.db}\n        methods={prgl.methods}\n        tables={prgl.tables}\n        noDataComponent={<InfoRow color=\"info\">No policies</InfoRow>}\n        {...listProps}\n      />\n      <Btn\n        color=\"action\"\n        variant=\"faded\"\n        className=\"ml-p5\"\n        iconPath={mdiPlus}\n        onClick={() =>\n          onSetQuery({\n            sql: [\n              `CREATE POLICY new_policy_name`,\n              `ON ${tableName}`,\n              `FOR ALL`,\n            ].join(\"\\n\"),\n          })\n        }\n      >\n        Create policy\n      </Btn>\n    </FlexCol>\n  );\n};\n\nconst getAllTableRLSQuery = (value: \"ENABLE\" | \"DISABLE\") => {\n  return `DO $$\nDECLARE\n  row record;\nBEGIN\n  FOR row IN SELECT tablename FROM pg_tables AS t\n    WHERE t.schemaname = CURRENT_SCHEMA -- Add custom filter here, if desired.\n  LOOP\n    EXECUTE format('ALTER TABLE %I ${value} ROW LEVEL SECURITY;', row.tablename); -- Toggle RLS for tables\n  END LOOP;\nEND; $$;`;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu_TableInfo.tsx",
    "content": "import {\n  mdiDatabaseRefreshOutline,\n  mdiDeleteOutline,\n  mdiPencil,\n} from \"@mdi/js\";\nimport { asName } from \"prostgles-types\";\nimport React from \"react\";\nimport type { W_TableMenuMetaProps } from \"./W_TableMenu\";\nimport Chip from \"@components/Chip\";\nimport Btn from \"@components/Btn\";\nimport CodeExample from \"../../CodeExample\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\n\nexport const W_TableMenu_TableInfo = ({\n  w,\n  tableMeta,\n  onSetQuery,\n  prgl,\n}: W_TableMenuMetaProps) => {\n  const tableName = w.table_name;\n  if (!tableMeta || !tableName) return null;\n\n  return (\n    <FlexCol className=\"f-1 ai-start o-auto \">\n      <div\n        data-command=\"W_TableMenu_TableInfo.name\"\n        className=\" flex-row mr-1 ai-center\"\n      >\n        <Chip className=\" \" variant=\"header\" label={\"Name\"} value={tableName} />\n        <Btn\n          iconPath={mdiPencil}\n          color=\"action\"\n          onClick={() => {\n            onSetQuery({\n              sql: `ALTER ${tableMeta.type.toUpperCase()} ${asName(tableName)} \\nRENAME TO new_name`,\n            });\n          }}\n        />\n      </div>\n      <div\n        data-command=\"W_TableMenu_TableInfo.comment\"\n        className=\" flex-row mr-1 ai-center\"\n      >\n        <Chip\n          className=\" \"\n          variant=\"header\"\n          label={\"Comment\"}\n          value={tableMeta.comment}\n        />\n        <Btn\n          color=\"action\"\n          iconPath={mdiPencil}\n          onClick={() => {\n            onSetQuery({\n              sql: `COMMENT ON ${tableMeta.type.toUpperCase()} ${asName(tableName)} IS 'My comment';`,\n            });\n          }}\n        />\n      </div>\n      <FlexRowWrap className=\"w-full  gap-0\">\n        <Chip\n          className=\"f-1\"\n          variant=\"header\"\n          label={\"OID\"}\n          data-command=\"W_TableMenu_TableInfo.oid\"\n          value={w.table_oid}\n        />\n        <Chip\n          data-command=\"W_TableMenu_TableInfo.type\"\n          className=\"f-1\"\n          variant=\"header\"\n          label={\"Type\"}\n          value={tableMeta.type}\n        />\n        <Chip\n          data-command=\"W_TableMenu_TableInfo.owner\"\n          className=\"f-1 \"\n          variant=\"header\"\n          label={\"Owner\"}\n          value={tableMeta.relowner_name}\n        />\n      </FlexRowWrap>\n      <FlexRowWrap\n        className=\"w-full\"\n        data-command=\"W_TableMenu_TableInfo.sizeInfo\"\n      >\n        <Chip\n          className=\"f-1\"\n          variant=\"header\"\n          label={\"Actual Size\"}\n          value={tableMeta.sizeInfo?.[\"Actual Size\"]}\n        />\n        <Chip\n          className=\"f-1\"\n          variant=\"header\"\n          label={\"Index Size\"}\n          value={tableMeta.sizeInfo?.[\"Index Size\"]}\n        />\n        <Chip\n          className=\"f-1\"\n          variant=\"header\"\n          label={\"Total Size\"}\n          value={tableMeta.sizeInfo?.[\"Total Size\"]}\n        />\n        <Chip\n          className=\"f-1\"\n          variant=\"header\"\n          label={\"Row count\"}\n          value={(+(tableMeta.sizeInfo?.[\"Row count\"] || 0)).toLocaleString()}\n        />\n      </FlexRowWrap>\n      {tableMeta.viewDefinition && (\n        <FlexCol\n          data-command=\"W_TableMenu_TableInfo.viewDefinition\"\n          className=\"w-full\"\n          style={{ height: \"400px\" }}\n        >\n          <div className=\"bold p-0 w-fit mt-1\">View definition</div>\n          <CodeExample\n            language=\"sql\"\n            value={tableMeta.viewDefinition}\n            style={{ width: \"100%\", height: \"400px\", maxHeight: \"60vh\" }}\n          />\n        </FlexCol>\n      )}\n      <div className=\"flex-row-wrap gap-p5 mr-1 ai-center mt-auto \">\n        {tableMeta.type === \"Table\" && (\n          <>\n            <Btn\n              className=\"mr-1\"\n              data-command=\"W_TableMenu_TableInfo.vacuum\"\n              iconPath={mdiDatabaseRefreshOutline}\n              variant=\"outline\"\n              onClick={() => {\n                onSetQuery({\n                  sql: `VACUUM ${asName(tableName)} `,\n                  title: `Garbage-collect and optionally analyze a database`,\n                });\n              }}\n            >\n              Vacuum\n            </Btn>\n            <Btn\n              className=\"mr-1\"\n              data-command=\"W_TableMenu_TableInfo.vacuumFull\"\n              iconPath={mdiDatabaseRefreshOutline}\n              variant=\"outline\"\n              onClick={() => {\n                onSetQuery({\n                  sql: `VACUUM FULL ${asName(tableName)} `,\n                  title: `Selects \"full\" vacuum, which can reclaim more space, but takes much longer and exclusively locks the table`,\n                });\n              }}\n            >\n              Vacuum Full\n            </Btn>\n          </>\n        )}\n\n        <Btn\n          iconPath={mdiDeleteOutline}\n          data-command=\"W_TableMenu_TableInfo.drop\"\n          color=\"danger\"\n          variant=\"faded\"\n          onClick={() => {\n            onSetQuery({\n              sql: `DROP ${tableMeta.type.toUpperCase()} ${tableName} \\n\"remove this line to confirm\"`,\n              title: `${tableMeta.type} will be deleted from the database`,\n              onSuccess: () => {\n                prgl.dbs.windows.update(\n                  { table_name: tableName },\n                  { closed: true },\n                );\n                w.$update({ closed: true });\n              },\n            });\n          }}\n        >\n          Drop...\n        </Btn>\n      </div>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/W_TableMenu_Triggers.tsx",
    "content": "import { mdiDelete, mdiPencil, mdiPlus } from \"@mdi/js\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { asName } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { SmartCardList } from \"../../SmartCardList/SmartCardList\";\nimport type { W_TableMenuProps, W_TableMenuState } from \"./W_TableMenu\";\nimport type { W_TableInfo } from \"./getTableMeta\";\n\ntype P = W_TableMenuProps & {\n  tableMeta: W_TableInfo | undefined;\n  onSetQuery: (newQuery: W_TableMenuState[\"query\"]) => void;\n};\nexport const W_TableMenu_Triggers = ({ tableMeta, onSetQuery, w, prgl }: P) => {\n  const tableName = w.table_name;\n\n  const listProps = useMemo(\n    () => ({\n      tableName: {\n        dataAge: prgl.dbKey,\n        sqlQuery: ` \n        SELECT event_object_table\n          ,trigger_name\n          ,event_manipulation\n          ,action_statement\n          ,action_timing\n          ,pg_get_triggerdef(oid) as trigger_def\n          ,tgenabled = 'D' as disabled\n          ,CASE WHEN pg_catalog.starts_with(action_statement, 'EXECUTE FUNCTION ') THEN pg_get_functiondef(RIGHT(action_statement, -17 )::regprocedure) ELSE '' END as function_def\n        FROM  information_schema.triggers t\n        LEFT JOIN pg_catalog.pg_trigger pt\n        ON t.trigger_name = pt.tgname\n        WHERE event_object_table = \\${tableName}\n        ORDER BY event_object_table, event_manipulation\n      `,\n        args: { tableName },\n      },\n      fieldConfigs: [\n        {\n          name: \"event_object_table\",\n          label: \"\",\n          render: (_, t: AnyObject) => (\n            <FlexRow>\n              <div className=\"ws-pre-line\">\n                {t.trigger_def\n                  .replace(\" BEFORE \", \"\\nBEFORE \")\n                  .replace(\" AFTER \", \"\\nAFTER \")\n                  .replace(\" ON \", \"\\nON \")\n                  .replace(\" REFERENCING \", \"\\nREFERENCING \")\n                  .replace(\" FOR \", \"\\nFOR \")\n                  .replace(\" EXECUTE \", \"\\nEXECUTE \")}\n              </div>\n            </FlexRow>\n          ),\n        },\n        {\n          name: \"trigger_name\",\n          label: \"\",\n          className: \"ml-auto show-on-parent-hover \",\n          render: (_, t: AnyObject) => (\n            <FlexRow className=\"\">\n              <Btn\n                iconPath={mdiPencil}\n                title=\"Edit trigger function\"\n                color=\"action\"\n                variant=\"faded\"\n                onClick={() => {\n                  onSetQuery({\n                    sql: t.function_def,\n                  });\n                }}\n              />\n\n              <Btn\n                title=\"Drop trigger\"\n                iconPath={mdiDelete}\n                color=\"danger\"\n                variant=\"faded\"\n                onClick={() => {\n                  onSetQuery({\n                    sql: `DROP TRIGGER ${asName(t.trigger_name)} ON ${asName(tableName)} ;`,\n                  });\n                }}\n              />\n            </FlexRow>\n          ),\n        },\n        {\n          name: \"disabled\",\n          label: \"\",\n          render: (_, t) => (\n            <SwitchToggle\n              title={t.disabled ? \"Disabled\" : \"Enabled\"}\n              checked={!t.disabled}\n              onChange={(checked) => {\n                onSetQuery({\n                  sql: `ALTER TABLE ${asName(tableName)} \\n${!checked ? \"DISABLE\" : \"ENABLE\"} TRIGGER ${JSON.stringify(t.trigger_name)};`,\n                });\n              }}\n            />\n          ),\n        },\n      ],\n    }),\n    [onSetQuery, prgl.dbKey, tableName],\n  );\n\n  if (!tableMeta || !tableName) return null;\n\n  return (\n    <FlexCol className=\" \">\n      <div className=\"ta-left p-p5\">\n        A trigger is used to execute a function before or after any of these\n        commands: INSERT, UPDATE, or DELETE\n      </div>\n      {!!tableMeta.triggers.length && (\n        <div className=\"flex-row ai-center \">\n          <Btn\n            className=\"f-0 mr-1 \"\n            color=\"action\"\n            variant=\"faded\"\n            disabledInfo={\n              tableMeta.triggers.every((t) => t.disabled) ?\n                \"Already disabled\"\n              : undefined\n            }\n            onClick={() => {\n              onSetQuery({\n                sql: `ALTER TABLE ${asName(tableName)} DISABLE TRIGGER ALL`,\n              });\n            }}\n          >\n            Disable all triggers\n          </Btn>\n          <Btn\n            // iconPath={mdiPlus}\n            className=\"  f-0\"\n            disabledInfo={\n              !tableMeta.triggers.every((t) => t.disabled) ?\n                \"Already enabled\"\n              : undefined\n            }\n            variant=\"outline\"\n            onClick={() => {\n              onSetQuery({\n                sql: `ALTER TABLE ${asName(tableName)} ENABLE TRIGGER ALL`,\n              });\n            }}\n          >\n            Enable all triggers\n          </Btn>\n        </div>\n      )}\n\n      <SmartCardList\n        db={prgl.db}\n        methods={prgl.methods}\n        tables={prgl.tables}\n        noDataComponent={<InfoRow color=\"info\">No triggers</InfoRow>}\n        {...listProps}\n      />\n      <Btn\n        iconPath={mdiPlus}\n        color=\"action\"\n        variant=\"faded\"\n        onClick={() => {\n          onSetQuery({ sql: getTriggerQuery(tableName) });\n        }}\n      >\n        Add trigger\n      </Btn>\n    </FlexCol>\n  );\n};\n\nconst getTriggerQuery = (tableName: string) => {\n  return `CREATE FUNCTION ${asName(\"trig_func_\" + tableName)}() \nRETURNS TRIGGER AS $func$ \n  DECLARE\n    name1 varchar(30);\n    name2 varchar(30);\n  BEGIN\n\n  RETURN NULL;    \n  END;\n$func$ LANGUAGE plpgsql;\n\nCREATE TRIGGER ${asName(\"trig_\" + tableName)}\nAFTER INSERT \nON ${asName(tableName)}\nREFERENCING NEW TABLE AS new\nFOR EACH ROW \nEXECUTE PROCEDURE ${asName(\"trig_func_\" + tableName)}();\n`;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/getAndFixWColumnsConfig.tsx",
    "content": "import { quickClone } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type {\n  DBSchemaTablesWJoins,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { ColumnConfig } from \"../ColumnMenu/ColumnMenu\";\nimport { isDefined } from \"../../../utils/utils\";\n\nconst getUpdatedColumnsConfig = (\n  table: DBSchemaTablesWJoins[number],\n  existingCols: ColumnConfig[] | null,\n) => {\n  try {\n    const tableColumnsConfig: ColumnConfig[] = table.columns.map((c) => ({\n      name: c.name,\n      show: true,\n      computed: false,\n    }));\n\n    if (existingCols && Array.isArray(existingCols)) {\n      const columnsHaveChanged =\n        existingCols\n          .map((c) => c.name)\n          .sort()\n          .join() !==\n        tableColumnsConfig\n          .map((c) => c.name)\n          .sort()\n          .join();\n      if (columnsHaveChanged) {\n        /* Remove missing columns */\n        const validWCols = quickClone(existingCols).filter(\n          ({ name, nested, computedConfig }) => {\n            return tableColumnsConfig.find((c1) => {\n              if (nested) {\n                return true;\n              }\n              if (computedConfig) {\n                return (\n                  !computedConfig.column || computedConfig.column === c1.name\n                );\n              }\n              return name === c1.name;\n            });\n          },\n        );\n\n        /* Add missing columns */\n        const newlyCreatedTableCols = tableColumnsConfig\n          .filter((c1) => !validWCols.find((nc) => nc.name === c1.name))\n          .map((c) => ({ name: c.name, show: true }));\n\n        return {\n          columns: validWCols.concat(newlyCreatedTableCols),\n          update: true,\n        };\n      }\n\n      return {\n        columns: existingCols,\n        update: false,\n      };\n    }\n\n    return {\n      columns: tableColumnsConfig,\n      update: true,\n    };\n  } catch (e) {\n    console.error(e);\n    throw e;\n  }\n};\n\nexport const getAndFixWColumnsConfig = async (\n  tables: DBSchemaTablesWJoins,\n  w: WindowSyncItem<\"table\">,\n): Promise<ColumnConfig[]> => {\n  const table = tables.find((t) => t.name === w.table_name);\n  if (!table) return [];\n  const { columns: rootColumns, update: updateRoot } = getUpdatedColumnsConfig(\n    table,\n    w.columns,\n  );\n  let update = updateRoot;\n  const columns = rootColumns\n    .map((c) => {\n      if (c.nested) {\n        const nestedTable = tables.find(\n          (t) => t.name === c.nested?.path.at(-1)?.table,\n        );\n\n        /** Table was dropped */\n        if (!nestedTable) return undefined;\n\n        const nestedColumns = getUpdatedColumnsConfig(\n          nestedTable,\n          c.nested.columns,\n        );\n        update = update || nestedColumns.update;\n        return {\n          ...c,\n          nested: {\n            ...c.nested,\n            columns: nestedColumns.columns,\n          },\n        };\n      }\n      return c;\n    })\n    .filter(isDefined);\n  if (update) {\n    await w.$update({ columns });\n  }\n  return columns;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/getChartCols.ts",
    "content": "import type { ParsedJoinPath, ValidatedColumnInfo } from \"prostgles-types\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type {\n  DBSchemaTablesWJoins,\n  WindowData,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { ChartableSQL } from \"../../W_SQL/getChartableSQL\";\nimport { getAllJoins } from \"../ColumnMenu/JoinPathSelectorV2\";\nimport { getColWInfo } from \"../tableUtils/getColWInfo\";\n\nexport type ColInfo = Pick<\n  ValidatedColumnInfo,\n  \"name\" | \"udt_name\" | \"is_pkey\"\n>;\ntype JoinedChartColumn = {\n  type: \"joined\";\n  label: string;\n  path: ParsedJoinPath[];\n  otherColumns: ColInfo[];\n} & ColInfo;\n\ntype NormalChartColumn = {\n  type: \"normal\";\n  otherColumns: ColInfo[];\n} & ColInfo;\n\nexport type ChartColumn = JoinedChartColumn | NormalChartColumn;\nexport const isGeoCol = (c: ColInfo) =>\n  [\"geography\", \"geometry\"].includes(c.udt_name);\nexport const isDateCol = (c: ColInfo) =>\n  c.udt_name.startsWith(\"timestamp\") || c.udt_name === \"date\";\n\ntype Args =\n  | {\n      type: \"table\";\n      w: WindowData<\"table\"> | WindowSyncItem<\"table\">;\n      tables: DBSchemaTablesWJoins;\n    }\n  | {\n      type: \"sql\";\n      w: WindowData<\"sql\"> | WindowSyncItem<\"sql\">;\n      chartableSQL: ChartableSQL;\n    };\nexport const getChartCols = (\n  args: Args,\n): {\n  geoCols: ChartColumn[];\n  dateCols: ChartColumn[];\n  barCols: ChartColumn[];\n  sql?: string;\n  withStatement?: string;\n} => {\n  if (args.type === \"sql\") {\n    return args.chartableSQL;\n  }\n  const { w, tables } = args;\n  const table = tables.find((t) => t.name === w.table_name);\n\n  const getOtherCols = (cols: ValidatedColumnInfo[]): ColInfo[] =>\n    cols.toSorted((b, a) => {\n      /** Sort primary keys down */\n      return (\n        Number(b.is_pkey || false) - Number(a.is_pkey || false) ||\n        (b.references?.length ?? 0) - (a.references?.length ?? 0)\n      );\n    });\n\n  const allJoins = getAllJoins({\n    tableName: w.table_name,\n    tables,\n    value: undefined,\n  });\n  const dateColsJoined = allJoins.allJoins\n    .flatMap((j) =>\n      j.table.columns.filter(isDateCol).map(\n        (c) =>\n          ({\n            type: \"joined\",\n            ...j,\n            is_pkey: c.is_pkey,\n            label: j.label,\n            name: c.name,\n            udt_name: c.udt_name,\n            otherColumns: getOtherCols(j.table.columns),\n          }) satisfies ChartColumn,\n      ),\n    )\n    .filter(isDefined);\n  const geoColsJoined = allJoins.allJoins\n    .flatMap((j) =>\n      j.table.columns.filter(isGeoCol).map(\n        (c) =>\n          ({\n            type: \"joined\",\n            ...j,\n            is_pkey: c.is_pkey,\n            name: c.name,\n            label: j.label,\n            udt_name: c.udt_name,\n            otherColumns: getOtherCols(j.table.columns),\n          }) satisfies ChartColumn,\n      ),\n    )\n    .filter(isDefined);\n\n  const cols =\n    !table ?\n      []\n    : getColWInfo(table, w.columns).map((c) => ({\n        ...c,\n        is_pkey: Boolean(c.info?.is_pkey),\n        udt_name: c.info?.udt_name || c.computedConfig?.udt_name || \"text\",\n      }));\n\n  const windowDateCols: ChartColumn[] = cols.filter(isDateCol).map((c) => ({\n    ...c,\n    type: \"normal\",\n    otherColumns: getOtherCols(\n      tables.find((t) => t.name === w.table_name)?.columns || [],\n    ),\n  }));\n  const windowGeoCols: ChartColumn[] = cols.filter(isGeoCol).map((c) => ({\n    ...c,\n    type: \"normal\",\n    otherColumns: getOtherCols(\n      tables.find((t) => t.name === w.table_name)?.columns || [],\n    ),\n  }));\n\n  const dateCols: ChartColumn[] = [...windowDateCols, ...dateColsJoined];\n  const geoCols: ChartColumn[] = [...windowGeoCols, ...geoColsJoined];\n  const barCols: ChartColumn[] = cols.map((c) => ({\n    ...c,\n    type: \"normal\",\n    otherColumns: getOtherCols(\n      tables.find((t) => t.name === w.table_name)?.columns || [],\n    ),\n  }));\n\n  return {\n    dateCols,\n    geoCols,\n    barCols,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TableMenu/getTableMeta.ts",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { ACCESS_CONTROL_SELECT } from \"../../AccessControl/AccessControl\";\nimport type { DBS } from \"../../Dashboard/DBS\";\nimport { PG_OBJECT_QUERIES } from \"../../SQLEditor/SQLCompletion/getPGObjects\";\n\nexport type W_TableInfo = {\n  comment: string;\n  type: \"Materialized view\" | \"View\" | \"Table\";\n  viewDefinition: string | null;\n  relowner: number;\n  relowner_name: string;\n  relowner_is_current_user: boolean;\n  relrowsecurity: boolean;\n  relforcerowsecurity: boolean;\n  constraints: {\n    conname: string;\n    definition: string;\n  }[];\n  indexes: {\n    tablename: string;\n    indexname: string;\n    indexdef: string;\n  }[];\n  policiesCount: number;\n  sizeInfo?: {\n    \"Total Size\": string;\n    \"Index Size\": string;\n    \"Actual Size\": string;\n    \"Row count\": string;\n  };\n  triggers: {\n    trigger_name: string;\n    event_manipulation: string;\n    action_statement: string;\n    action_timing: string;\n    function_def: string;\n    disabled: boolean;\n  }[];\n  accessRules: (DBSSchema[\"access_control\"] & {\n    userTypes: string[];\n  })[];\n};\n\nexport const getTableMeta = async (\n  db: DBHandlerClient,\n  dbs: DBS,\n  database_id: number,\n  tableName: string,\n  tableOid: number,\n): Promise<W_TableInfo> => {\n  if (!db.sql) throw \"db.sql not allowed\";\n\n  try {\n    const constraints: any = await db.sql(\n      `\n        SELECT conname, pg_get_constraintdef(c.oid) as definition \n        FROM pg_catalog.pg_constraint c\n          INNER JOIN pg_catalog.pg_class rel\n          ON rel.oid = c.conrelid\n          INNER JOIN pg_catalog.pg_namespace nsp\n          ON nsp.oid = connamespace\n          WHERE nsp.nspname = current_schema()\n              AND format('%I', rel.relname) = \\${tableName};`,\n      { tableName },\n      { returnType: \"rows\" },\n    );\n    const indexes: any = await db.sql(\n      `\n        SELECT tablename, indexname, indexdef\n        FROM pg_indexes\n        WHERE schemaname = current_schema() AND format('%I', tablename) = \\${tableName}`,\n      { tableName },\n      { returnType: \"rows\" },\n    );\n\n    const sizeInfo: any =\n      (await db.sql(\n        `\n        SELECT\n          relname  as table_name,\n          pg_size_pretty(pg_total_relation_size(relid)) As \"Total Size\",\n          pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) as \"Index Size\",\n          pg_size_pretty(pg_relation_size(relid)) as \"Actual Size\",\n          (SELECT COUNT(*) FROM ${tableName} ) as \"Row count\"\n        FROM pg_catalog.pg_statio_user_tables \n        WHERE format('%I', relname) = \\${tableName}\n        ORDER BY pg_total_relation_size(relid) DESC;\n      `,\n        { tableName },\n        { returnType: \"row\" },\n      )) || {};\n    const comment = await db.sql(\n      `SELECT obj_description(${tableOid}) as c FROM pg_class LIMIT 1`,\n      [tableName],\n      { returnType: \"value\" },\n    );\n\n    const triggers: any = await db.sql(\n      `\n        SELECT event_object_table\n          ,trigger_name\n          ,event_manipulation\n          ,action_statement\n          ,action_timing\n          ,tgenabled = 'D' as disabled\n          ,CASE WHEN pg_catalog.starts_with(action_statement, 'EXECUTE FUNCTION ') THEN pg_get_functiondef(RIGHT(action_statement, -17 )::regprocedure) ELSE '' END as function_def\n        FROM  information_schema.triggers t\n        LEFT JOIN pg_catalog.pg_trigger pt\n        ON t.trigger_name = pt.tgname\n        WHERE format('%I', event_object_table) = \\${tableName}\n        ORDER BY event_object_table\n        ,event_manipulation\n      `,\n      { tableName },\n      { returnType: \"rows\" },\n    );\n\n    const policiesCount = await db.sql(\n      `SELECT COUNT(*) FROM ( \\n${PG_OBJECT_QUERIES.policies.sql(tableName)} \\n) tt`,\n      { tableName },\n      { returnType: \"value\" },\n    );\n\n    const type = (await db.sql(\n      `\n        SELECT \n          CASE \n            WHEN  c.relkind = 'm' THEN 'Materialized view' \n            WHEN  c.relkind = 'r' THEN 'Table' \n            WHEN  c.relkind = 'f' THEN 'Foreign table' \n            WHEN  c.relkind = 'p' THEN 'Partitioned table' \n            WHEN  c.relkind = 'v' THEN 'View' \n            ELSE 'Table' \n          END as \"type\",\n          relowner,\n          (SELECT pgs.usename \n            FROM pg_user pgs \n            WHERE relowner = pgs.usesysid\n          ) as relowner_name,\n          relrowsecurity, \n          relforcerowsecurity\n        FROM pg_catalog.pg_class c\n          JOIN pg_namespace n ON n.oid = c.relnamespace\n        WHERE c.relkind in ('m', 'v', 'r', 'f', 'p')\n        AND n.nspname = \"current_schema\"()\n        AND format('%I', c.relname) = \\${tableName}\n      `,\n      { tableName },\n      { returnType: \"row\" },\n    )) as any;\n\n    const viewDefinition = await db.sql(\n      \"SELECT pg_get_viewdef(${tableOid})\",\n      { tableOid },\n      { returnType: \"value\" },\n    );\n\n    const filter = {\n      $and: [\n        { database_id },\n        // {\n        //   $or: [\n        //     { \"dbPermissions->>customTables\": tableName },\n        //     { \"dbPermissions->>type\": \"All views/tables\" },\n        //     { \"dbPermissions->>type\": \"Run SQL\" },\n        //   ]\n        // }\n      ],\n    };\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    const nonFilteredRules = await dbs.access_control?.find(\n      filter,\n      ACCESS_CONTROL_SELECT,\n    );\n    const rules = nonFilteredRules.filter(\n      (r) =>\n        r.dbPermissions.type !== \"Custom\" ||\n        r.dbPermissions.customTables.find((t) => t.tableName === tableName),\n    );\n    const accessRules: W_TableInfo[\"accessRules\"] = rules.map((r) => {\n      const userTypes = r.access_control_user_types.flatMap((d) =>\n        d.ids.flat(),\n      );\n      return {\n        ...r,\n        userTypes,\n      };\n    });\n    return {\n      constraints,\n      indexes,\n      sizeInfo,\n      comment,\n      triggers,\n      ...type,\n      policiesCount,\n      viewDefinition,\n      accessRules,\n    };\n  } catch (e) {\n    console.error(e);\n    throw e;\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/TooManyColumnsWarning.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { mdiAlertOutline } from \"@mdi/js\";\nimport React, { useEffect } from \"react\";\nimport type { WindowSyncItem } from \"../Dashboard/dashboardUtils\";\n\ntype P = {\n  w: WindowSyncItem<\"table\"> | WindowSyncItem<\"sql\">;\n  onHide: VoidFunction;\n  numberOfCols: number;\n  numberOfRows: number;\n};\nexport const TooManyColumnsWarning = ({\n  w,\n  onHide,\n  numberOfCols,\n  numberOfRows,\n}: P) => {\n  const cells = numberOfCols * numberOfRows;\n  const [show, setShow] = React.useState(false);\n  useEffect(() => {\n    const tooManyCells = cells > 500;\n  }, [cells]);\n\n  if (!show) return null;\n\n  return (\n    <PopupMenu\n      button={\n        <Btn\n          iconPath={mdiAlertOutline}\n          color=\"warn\"\n          variant=\"outline\"\n          className=\"shadow\"\n          style={{\n            position: \"absolute\",\n            bottom: \"1em\",\n            left: \"1em\",\n            zIndex: 1,\n          }}\n        />\n      }\n      footerButtons={[\n        { label: \"Cancel\", onClickClose: true },\n        {\n          label: \"Hide this warning\",\n          onClick: onHide,\n        },\n        {\n          label: \"Show only first 15 columns\",\n          color: \"action\",\n          variant: \"faded\",\n          onClick: () => {\n            w.$update({\n              columns: w.columns?.map((c, i) => ({\n                ...c,\n                show: i <= 15,\n              })),\n            });\n          },\n        },\n      ]}\n    >\n      <InfoRow variant=\"naked\" color=\"warning\">\n        There is a high number ({w.columns?.filter((c) => c.show).length}) of\n        rows/columns displayed. Reduce the number of viewed rows/columns from\n        the table menu to improve performance\n      </InfoRow>\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/W_Table.tsx",
    "content": "import { mdiAlertOutline, mdiPlus } from \"@mdi/js\";\nimport type { AnyObject, ParsedJoinPath } from \"prostgles-types\";\nimport { getKeys } from \"prostgles-types\";\n\nimport Loading from \"@components/Loader/Loading\";\nimport type { TableColumn, TableProps } from \"@components/Table/Table\";\nimport { PAGE_SIZES, Table, closest } from \"@components/Table/Table\";\nimport React from \"react\";\nimport type {\n  OnAddChart,\n  Query,\n  WindowData,\n  WindowSyncItem,\n  WorkspaceSyncItem,\n} from \"../Dashboard/dashboardUtils\";\nimport \"./ProstglesTable.css\";\n\nimport type { DeltaOf, DeltaOfData } from \"../RTComp\";\nimport RTComp from \"../RTComp\";\n\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport type { SingleSyncHandles } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { ValidatedColumnInfo } from \"prostgles-types/lib\";\nimport type {\n  ColumnConfig,\n  ColumnSort,\n  ColumnSortSQL,\n} from \"./ColumnMenu/ColumnMenu\";\nimport { ColumnMenu } from \"./ColumnMenu/ColumnMenu\";\n\nimport type { DetailedFilterBase } from \"@common/filterUtils\";\nimport { matchObj } from \"@common/utils\";\nimport { ClickCatchOverlayZIndex } from \"@components/ClickCatchOverlay\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport type { PaginationProps } from \"@components/Table/Pagination\";\nimport { isDefined, isEqual, pickKeys } from \"prostgles-types\";\nimport type { Command } from \"../../Testing\";\nimport { createReactiveState } from \"../../appUtils\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport { SmartFilterBar } from \"../SmartFilterBar/SmartFilterBar\";\nimport type { ProstglesQuickMenuProps } from \"../W_QuickMenu\";\nimport Window from \"../Window\";\nimport { CardView } from \"./CardView/CardView\";\nimport { NodeCountChecker } from \"./NodeCountChecker\";\nimport { QuickFilterGroupsControl } from \"./QuickFilterGroupsControl\";\nimport type { RowPanelProps } from \"./RowCard\";\nimport { RowCard } from \"./RowCard\";\nimport { W_TableMenu } from \"./TableMenu/W_TableMenu\";\nimport { getAndFixWColumnsConfig } from \"./TableMenu/getAndFixWColumnsConfig\";\nimport { TooManyColumnsWarning } from \"./TooManyColumnsWarning\";\nimport { W_Table_Content } from \"./W_Table_Content\";\nimport { getTableData } from \"./getTableData/getTableData\";\nimport type {\n  OnClickEditRow,\n  RowSiblingData,\n} from \"./tableUtils/getEditColumn\";\nimport { getFullColumnConfig } from \"./tableUtils/getFullColumnConfig\";\nimport { getTableCols } from \"./tableUtils/getTableCols\";\nimport { getTableSelect } from \"./tableUtils/getTableSelect\";\nimport { prepareColsForRender } from \"./tableUtils/prepareColsForRender\";\nimport { getSort, getSortColumn, updateWCols } from \"./tableUtils/tableUtils\";\n\nexport type W_TableProps = Omit<CommonWindowProps, \"w\"> & {\n  w: WindowSyncItem<\"table\">;\n  setLinkMenu: ProstglesQuickMenuProps[\"setLinkMenu\"];\n  childWindow: React.ReactNode | undefined;\n  onLinkTable?: (tableName: string, path: ParsedJoinPath[]) => void;\n  onClickRow?: TableProps<ColumnSort>[\"onRowClick\"];\n  filter?: any;\n  joinFilter?: AnyObject;\n  externalFilters: AnyObject[];\n  activeRow?: ActiveRow;\n  onAddChart?: OnAddChart;\n  activeRowColor?: React.CSSProperties[\"color\"];\n  workspace: WorkspaceSyncItem;\n};\nexport type ActiveRow = {\n  window_id: string;\n  table_name: string;\n  row_filter: { [key: string]: any };\n  timeChart?: {\n    min: Date;\n    max: Date;\n    center: Date;\n  };\n  barChart?: {\n    values: any[];\n  };\n};\n\nexport type MinMax<T = number> = {\n  min: T;\n  max: T;\n};\n/**\n * Used for cell timechart and barchart\n */\nexport type MinMaxVals = Record<string, MinMax>;\n\nexport function getFilter(filter: any = {}, activeRow?: ActiveRow): any {\n  return {\n    $and: [\n      filter,\n      !activeRow ? undefined : (\n        {\n          $existsJoined: {\n            // [`**.${activeRow.table_name}`]: activeRow.row_filter\n            path: [\"**\", activeRow.table_name],\n            filter: activeRow.row_filter,\n          },\n        }\n      ),\n    ].filter((f) => f),\n  };\n}\n\nexport type ProstglesColumn = TableColumn & { computed?: boolean } & Pick<\n    ValidatedColumnInfo,\n    \"name\" | \"tsDataType\" | \"udt_name\" | \"filter\"\n  >;\n\nexport type W_TableState = {\n  rowCount: number;\n  rowsLoaded: number;\n  table?: TableProps<ColumnSort> & Query;\n  sort?: ColumnSortSQL[];\n  loading: boolean;\n\n  rows?: AnyObject[];\n  filter: any;\n  pos?: { x: number; y: number };\n  size?: { w: number; h: number };\n  joins: string[];\n  runningQuerySince: number | undefined;\n  error?: string;\n  duration: number;\n  hideTable?: boolean;\n  sql: string;\n  rowPanel?:\n    | {\n        type: \"insert\";\n      }\n    | {\n        type: \"update\";\n        rowIndex: number;\n        filter: DetailedFilterBase[];\n        siblingData: RowSiblingData;\n        fixedUpdateData?: AnyObject;\n      };\n  rowDelta?: any;\n  onRowClick?: TableProps<ColumnSort>[\"onRowClick\"];\n\n  filterPopup?: boolean;\n\n  dataAge?: number;\n  barchartVals?: MinMaxVals;\n  columns?: ValidatedColumnInfo[];\n  totalRows?: number;\n  /**\n   * Stringified joinFilter that is set after the data has been downloaded.\n   * Used in setting activeRow styles to all rows adequately\n   */\n  joinFilterStr?: string;\n  localCols?: ColumnConfigWInfo[];\n  tooManyColumnsWarningWasShown?: boolean;\n};\n\nexport type ProstglesTableD = {\n  w?: WindowSyncItem<\"table\">;\n  pageSize: Required<PaginationProps>[\"pageSize\"];\n  page: number;\n  dataAge?: number;\n  wSync?: SingleSyncHandles<Required<WindowData<\"table\">>, true>;\n};\n\nexport type ColumnConfigWInfo = ColumnConfig & { info?: ValidatedColumnInfo };\n\nexport default class W_Table extends RTComp<\n  W_TableProps,\n  W_TableState,\n  ProstglesTableD\n> {\n  refHeader?: HTMLDivElement;\n  refResize?: HTMLElement;\n  ref?: HTMLElement;\n  refRowCount?: HTMLElement;\n\n  state: W_TableState = {\n    barchartVals: {},\n    rowsLoaded: 0,\n    runningQuerySince: undefined,\n    sql: \"\",\n    loading: false,\n    totalRows: 0,\n    sort: [],\n    joins: [],\n    filter: {},\n    rowCount: 0,\n    duration: 0,\n    error: \"\",\n    hideTable: true,\n    rowDelta: null,\n    filterPopup: false,\n    joinFilterStr: undefined,\n  };\n  d: ProstglesTableD = {\n    page: 0,\n    pageSize:\n      closest(Math.round((1.2 * window.innerHeight) / 55) || 25, PAGE_SIZES) ??\n      25,\n    w: undefined,\n    dataAge: 0,\n    wSync: undefined,\n  };\n\n  calculatedColWidths = false;\n\n  onMount() {\n    const { w } = this.props;\n\n    if (!Array.isArray(w.filter)) {\n      w.$update({ filter: [] });\n    }\n\n    this.d.wSync = w.$cloneSync((w, delta) => {\n      this.setData({ w }, { w: delta });\n    });\n  }\n\n  async onUnmount() {\n    this.d.wSync?.$unsync();\n    await this.dataSub?.unsubscribe?.();\n  }\n\n  /**\n   * To reduce the number of unnecessary data requests let's save the query signature and allow new queries only if different\n   */\n  currentDataRequestSignature = \"\";\n  static getTableDataRequestSignature(\n    args:\n      | {\n          select?: any;\n          filter?: AnyObject;\n          having?: AnyObject;\n          barchartVals?: AnyObject;\n          joinFilter?: AnyObject;\n          externalFilters?: any;\n          orderBy?: any;\n          limit?: number | null;\n          offset?: number;\n        }\n      | { sql: string },\n    dataAge: number,\n    dependencies: any[] = [],\n  ) {\n    const argKeyObj: typeof args & { dataAge: number; dependencies: any[] } = {\n      ...args,\n      dataAge,\n      dependencies,\n    };\n    const sigData = {};\n    Object.keys(argKeyObj)\n      .sort()\n      .forEach((key) => {\n        sigData[key] = argKeyObj[key];\n      });\n\n    return JSON.stringify(sigData);\n  }\n\n  onClickEditRow: OnClickEditRow = (\n    filter,\n    siblingData,\n    rowIndex,\n    fixedUpdateData,\n  ) => {\n    this.rowPanelRState.set({\n      type: \"update\",\n      rowIndex,\n      filter,\n      siblingData,\n      fixedUpdateData,\n    });\n  };\n\n  dataSub?: any;\n  dataSubFilter?: string;\n  dataAge?: number = 0;\n  autoRefresh?: any;\n  activeRowStr?: string;\n  currDbKey?: string;\n  onDelta = async (\n    dp: DeltaOf<W_TableProps>,\n    ds: DeltaOf<W_TableState>,\n    dd: DeltaOfData<ProstglesTableD>,\n  ) => {\n    const delta = { ...dp, ...ds, ...dd };\n\n    const { workspace } = this.props;\n    const { db } = this.props.prgl;\n    const { w } = this.d;\n    const { table_name: tableName, table_oid } = w || {};\n\n    let ns: Partial<W_TableState> | undefined;\n    if (!w || !tableName) return;\n\n    const tableHandler = db[tableName];\n\n    /** Show count if user requires it  */\n    const showCounts = !!(\n      (!workspace.options.hideCounts && !w.options.hideCount) ||\n      (workspace.options.hideCounts && w.options.hideCount === false)\n    );\n\n    /* Table was renamed. Replace from oid or fail gracefully */\n    if (tableName && table_oid && !tableHandler) {\n      const match = this.props.tables.find((ti) => ti.info.oid === table_oid);\n      if (match) {\n        await w.$update({ table_name: match.name });\n        return;\n      } else {\n        /** Reset schema related properties */\n        const emptyInfo = { columns: null, filter: [], having: [], sort: null };\n        const different =\n          !this.d.w ?\n            undefined\n          : Object.entries(emptyInfo).filter(\n              ([key, val]) => !isEqual(this.d.w![key], val),\n            );\n        if (different?.length) {\n          await w.$update(emptyInfo);\n        }\n      }\n    }\n\n    if (!tableHandler) return;\n\n    if (delta.w && (\"filter\" in delta.w || \"having\" in delta.w)) {\n      this.props.onForceUpdate();\n    }\n\n    const { dbKey } = this.props.prgl;\n    if (this.currDbKey !== dbKey) {\n      getAndFixWColumnsConfig(this.props.prgl.tables, w);\n      this.currDbKey = dbKey;\n    }\n\n    /* Simply re-render */\n    if (\n      [\"showSubLabel\", \"maxRowHeight\"].some(\n        (key) => delta.w?.options && key in delta.w.options,\n      )\n    ) {\n      ns = ns || {};\n    }\n\n    /** This is done to prevent errors due to renamed/altered columns */\n    if (\n      delta.w?.columns?.length &&\n      w.sort?.some((sort) => !getSortColumn(sort, delta.w?.columns ?? []))\n    ) {\n      w.$update({ sort: [] });\n    }\n\n    /** This is done to prevent errors due to renamed/altered columns */\n    if (\n      (delta.w?.filter || delta.w?.having) &&\n      !delta.w.id &&\n      !w.options.showFilters\n    ) {\n      w.$update({ options: { showFilters: true } }, { deepMerge: true });\n    }\n\n    /** This is done to prevent empty result due to page offset */\n    if ((delta.w?.filter || delta.w?.having) && this.d.page !== 0) {\n      this.setData({ page: 0 });\n    }\n\n    /** Trigger count on hideCount toggle */\n    if (\n      delta.w?.options &&\n      \"hideCount\" in delta.w.options &&\n      this.state.rows?.length\n    ) {\n      ns = {\n        ...ns,\n        dataAge: Date.now(),\n      };\n    }\n\n    const changedOpts = getKeys(delta.w?.options || {});\n\n    /**\n     * Get data\n     */\n    getTableData.bind(this)(dp, ds, dd, { showCounts });\n\n    /** Force update */\n    const rerenderOPTS: (keyof typeof w.options)[] = [\n      \"viewAs\",\n      \"hideEditRow\",\n      \"showFilters\",\n    ];\n    if (\n      !ns &&\n      (delta.w?.columns ||\n        (changedOpts.length &&\n          rerenderOPTS.some((k) => changedOpts.includes(k))))\n    ) {\n      ns ??= {};\n    }\n\n    if (ns) {\n      this.setState(ns as W_TableState);\n    }\n  };\n\n  getWCols = () => {\n    const { w } = this.d;\n    const { tables } = this.props;\n    const { rows } = this.state;\n    return !w ?\n        []\n      : getFullColumnConfig(tables, w, rows, this.ref?.offsetWidth);\n  };\n\n  getPaginationProps = (): PaginationProps => {\n    const { page, pageSize } = this.d;\n\n    const { rowCount } = this.state;\n\n    return {\n      page,\n      pageSize,\n      totalRows: rowCount,\n      onPageChange: (newPage) => {\n        this.setData({ page: newPage });\n      },\n      onPageSizeChange: (pageSize) => {\n        this.setData({ pageSize });\n      },\n    };\n  };\n\n  getPagination() {\n    const { pageSize, page } = this.d;\n    return {\n      limit: pageSize,\n      offset: this.props.joinFilter ? 0 : page * pageSize,\n    };\n  }\n\n  getMenu = (w, onClose) => {\n    const { prgl, onLinkTable, onAddChart } = this.props;\n\n    const cols = w.columns;\n\n    if (!cols) return <ErrorComponent error=\"Columns not defined\" />;\n\n    return (\n      <W_TableMenu\n        prgl={prgl}\n        workspace={this.props.workspace}\n        cols={cols.filter((c) => !c.computed)}\n        onAddChart={onAddChart}\n        w={w}\n        onLinkTable={onLinkTable}\n        suggestions={this.props.suggestions}\n        onClose={onClose}\n        externalFilters={this.props.externalFilters}\n        joinFilter={this.props.joinFilter}\n      />\n    );\n  };\n\n  onSort = async (sort: ColumnSort[]) => {\n    const { tables, db } = this.props.prgl;\n    const { w } = this.d;\n    if (!w) return null;\n    const { table_name: tableName } = w;\n    const tableHandler = db[tableName];\n\n    try {\n      // const columnsConfig = await getAndFixWColumnsConfig(tables, w); //columns: columnsConfig,\n      const orderBy = getSort(tables, { ...w, sort }) as any;\n      /** Ensure the sort is valid */\n      const { select } = await getTableSelect(w, tables, db, {}, true);\n      await tableHandler?.find!({}, { select, limit: 0, orderBy });\n      w.$update({ sort });\n    } catch (error: any) {\n      this.setState({ error });\n    }\n  };\n\n  rowPanelRState = createReactiveState<RowPanelProps | undefined>(undefined);\n\n  onColumnReorder = (newCols: ProstglesColumn[]) => {\n    const { w } = this.d;\n    if (!w) return null;\n    const nIdxes = newCols\n      .filter((c) => !(c.computed && c.key === \"edit_row\"))\n      .map((c) => c.name);\n    const columns = this.d.w?.columns\n      ?.slice(0)\n      .toSorted((a, b) => nIdxes.indexOf(a.name) - nIdxes.indexOf(b.name));\n    updateWCols(w, columns);\n  };\n\n  columnMenuState = createReactiveState<\n    | {\n        column: string;\n        clientX: number;\n        clientY: number;\n      }\n    | undefined\n  >(undefined);\n\n  render() {\n    const { loading, rows, runningQuerySince, error } = this.state;\n\n    const showTableNotFound = (tableName: string) => (\n      <div\n        className=\" p-2 flex-row ai-center text-danger\"\n        data-command={\"W_Table.TableNotFound\" satisfies Command}\n      >\n        <Icon path={mdiAlertOutline} className=\"mr-p5 \" />\n        Table {JSON.stringify(tableName)} not found\n      </div>\n    );\n\n    const { w } = this.d;\n    const tableName = w?.table_name ?? this.props.w.table_name;\n    const {\n      setLinkMenu,\n      joinFilter,\n      activeRow,\n      onAddChart,\n      activeRowColor,\n      prgl,\n      childWindow,\n      workspace,\n    } = this.props;\n    const { tables, db, dbs } = prgl;\n\n    const tableHandler = db[tableName];\n    if (!w) {\n      if (tableName && !tableHandler) {\n        return showTableNotFound(tableName);\n      }\n      return null;\n    }\n\n    const activeRowStyle: React.CSSProperties =\n      this.activeRowStr === JSON.stringify(joinFilter || {}) ?\n        { background: activeRowColor }\n      : {};\n\n    const FirstLoadCover = (\n      <div className=\"flex-col f-1 jc-center ai-center\">\n        <Loading className=\"m-auto absolute\" />\n      </div>\n    );\n\n    const wrapInWindow = (content: React.ReactNode) => {\n      return (\n        <Window\n          w={w}\n          layoutMode={workspace.layout_mode ?? \"editable\"}\n          quickMenuProps={{\n            tables,\n            prgl,\n            chartableSQL: undefined,\n            dbs,\n            setLinkMenu,\n            onAddChart,\n            myLinks: this.props.myLinks,\n            childWindows: this.props.childWindows,\n            show:\n              childWindow || this.props.workspace.layout_mode === \"fixed\" ?\n                { filter: true }\n              : undefined,\n          }}\n          getMenu={this.getMenu}\n        >\n          {content}\n        </Window>\n      );\n    };\n\n    const cardOpts =\n      w.options.viewAs?.type === \"card\" ? w.options.viewAs : undefined;\n    let content: React.ReactNode = null;\n    if (tableName && !tableHandler) {\n      content = showTableNotFound(tableName);\n    } else if (loading || (tableName && !tableHandler)) {\n      content = FirstLoadCover;\n    } else {\n      if (!rows) {\n        return wrapInWindow(FirstLoadCover);\n      }\n\n      const cols = getTableCols({\n        data: this.state.rows,\n        windowWidth: this.ref?.getBoundingClientRect().width,\n        tables,\n        db,\n        w: this.d.w,\n        onClickEditRow: this.onClickEditRow,\n        barchartVals: this.state.barchartVals,\n        suggestions: this.props.suggestions,\n        columnMenuState: this.columnMenuState,\n        opts: {\n          noRightBorder: this.props.workspace.layout_mode === \"fixed\",\n        },\n      });\n\n      let activeRowIndex = -1;\n      if (activeRow?.row_filter) {\n        activeRowIndex = rows.findIndex((r) =>\n          matchObj(activeRow.row_filter, r),\n        );\n      }\n\n      const canInsert = Boolean(tableHandler?.insert);\n      const showInsertButton = canInsert && w.options.hideInsertButton !== true;\n      const pkeys = cols\n        .map((c) => (c.show && c.info?.is_pkey ? c.info.name : undefined))\n        .filter(isDefined);\n      const rowKeys = pkeys.length ? pkeys : undefined;\n      content = (\n        <>\n          <div\n            className={`W_Table flex-col f-1 min-h-0 min-w-0 relative`}\n            ref={(r) => {\n              if (r) this.ref = r;\n            }}\n          >\n            <ColumnMenu\n              prgl={prgl}\n              db={db}\n              dbs={dbs}\n              columnMenuState={this.columnMenuState}\n              tables={tables}\n              suggestions={this.props.suggestions}\n              w={w}\n            />\n            {!this.state.tooManyColumnsWarningWasShown && (\n              <TooManyColumnsWarning\n                w={w}\n                numberOfCols={cols.length}\n                numberOfRows={rows.length}\n                onHide={() => {\n                  this.setState({ tooManyColumnsWarningWasShown: true });\n                }}\n              />\n            )}\n            <NodeCountChecker\n              parentNode={this.ref}\n              dataAge={this.state.rowsLoaded}\n            />\n\n            {!!w.options.showFilters && (\n              <FlexCol\n                key={\"W_Table_Filters\"}\n                className={`gap-p5 p-p5 bg-color-0 ${childWindow ? \" bb b-color \" : \"\"}`}\n                style={{\n                  /** Ensure it covers the attached timechart layer opts button */\n                  zIndex: 2,\n                }}\n                title=\"Edit filters\"\n              >\n                <SmartFilterBar\n                  {...prgl}\n                  methods={prgl.methods}\n                  w={this.d.w ?? this.props.w}\n                  rowCount={this.state.rowCount}\n                  className=\"\"\n                  extraFilters={this.props.externalFilters}\n                  hideSort={!cardOpts}\n                  showInsertUpdateDelete={{\n                    onSuccess: () => {\n                      this.setState({ dataAge: Date.now() });\n                    },\n                  }}\n                />\n              </FlexCol>\n            )}\n            <QuickFilterGroupsControl {...this.props} />\n\n            <W_Table_Content\n              key={\"W_Table_Content\"}\n              runningQuerySince={runningQuerySince}\n            >\n              {error && (\n                <ErrorComponent\n                  withIcon={true}\n                  style={{ flex: \"unset\", padding: \"2em\" }}\n                  error={error}\n                />\n              )}\n              {childWindow ?\n                childWindow\n              : cardOpts ?\n                <CardView\n                  key={`${cardOpts.cardGroupBy}-${cardOpts.cardOrderBy}-${this.state.dataAge}`}\n                  cols={cols}\n                  state={this.state}\n                  props={this.props}\n                  w={this.d.w}\n                  cardOpts={cardOpts}\n                  tableHandler={tableHandler!}\n                  paginationProps={{ ...this.getPaginationProps() }}\n                  onEditClickRow={this.onClickEditRow}\n                  onDataChanged={() => {\n                    this.setState({ dataAge: Date.now() });\n                  }}\n                />\n              : w.options.viewAs?.type === \"json\" ?\n                <CodeEditor\n                  language=\"json\"\n                  value={JSON.stringify(\n                    rows.map((r) =>\n                      pickKeys(\n                        r,\n                        cols.filter((c) => c.show).map((c) => c.name),\n                      ),\n                    ),\n                    null,\n                    2,\n                  )}\n                  className=\"b-unset\"\n                />\n              : <Table\n                  style={{\n                    flex: 1,\n                    boxShadow: \"unset\",\n                  }}\n                  // bodyClass={(joinFilter? \" active-brush \" : \"\") + (!rows.length? \"  \" : \"\")}\n                  // rowClass={((joinFilter && JSON.stringify(joinFilter) === joinFilterStr) ? \" active-brush \" : \"\") + (!rows.length ? \"  \" : \"\")}\n\n                  maxCharsPerCell={w.options.maxCellChars ?? 500}\n                  maxRowHeight={w.options.maxRowHeight}\n                  rowStyle={joinFilter ? activeRowStyle : {}}\n                  onSort={this.onSort}\n                  onColumnReorder={this.onColumnReorder}\n                  cols={prepareColsForRender(cols, this.getWCols, w)}\n                  rows={rows}\n                  rowKeys={rowKeys}\n                  sort={w.sort || undefined}\n                  tableStyle={{ borderRadius: \"unset\", border: \"unset\" }}\n                  pagination={this.getPaginationProps()}\n                  showSubLabel={w.options.showSubLabel}\n                  activeRowStyle={activeRowStyle}\n                  activeRowIndex={activeRowIndex}\n                  onRowClick={this.state.onRowClick}\n                  afterLastRowContent={\n                    showInsertButton &&\n                    !childWindow && (\n                      <Btn\n                        iconPath={mdiPlus}\n                        data-command=\"dashboard.window.rowInsert\"\n                        data-key={tableName}\n                        title={t.W_Table[\"Insert row\"]}\n                        className=\"shadow w-fit h-fit mt-1\"\n                        color=\"action\"\n                        variant={w.options.showFilters ? \"outline\" : \"filled\"}\n                        style={{\n                          position: \"sticky\",\n                          left: \"15px\",\n                          bottom: \"15px\",\n                          /** Below the filter search clickcatch */\n                          zIndex: ClickCatchOverlayZIndex - 1,\n                        }}\n                        onClick={() => {\n                          this.rowPanelRState.set({ type: \"insert\" });\n                        }}\n                      />\n                    )\n                  }\n                />\n              }\n            </W_Table_Content>\n          </div>\n\n          {tableHandler && (\n            <RowCard\n              showR={this.rowPanelRState}\n              rows={rows}\n              prgl={prgl}\n              tableName={tableName}\n              tableHandler={tableHandler}\n              onPrevOrNext={(newRowPanel) => {\n                this.rowPanelRState.set(newRowPanel);\n              }}\n              onSuccess={() => {\n                this.setState({ dataAge: Date.now() });\n              }}\n            />\n          )}\n        </>\n      );\n    }\n\n    return wrapInWindow(content);\n  }\n}\n\nexport function kFormatter(num: number) {\n  const abs = Math.abs(num);\n  if (abs > 1e12 - 1)\n    return Math.sign(num) * +(Math.abs(num) / 1e12).toFixed(1) + \" T\";\n  if (abs > 1e9 - 1)\n    return Math.sign(num) * +(Math.abs(num) / 1e9).toFixed(1) + \" B\";\n  if (abs > 1e6 - 1)\n    return Math.sign(num) * +(Math.abs(num) / 1e6).toFixed(1) + \" m\";\n  if (abs > 999)\n    return Math.sign(num) * +(Math.abs(num) / 1000).toFixed(1) + \" k\";\n  return num.toString();\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Table/W_Table_Content.tsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport type { DivProps } from \"@components/Flex\";\n\ntype P = Pick<DivProps, \"children\"> & {\n  runningQuerySince: number | undefined;\n};\nexport const W_Table_Content = ({ children, runningQuerySince }: P) => {\n  const [loading, setLoading] = useState(false);\n  const ref = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    const timeout = setTimeout(() => {\n      if (runningQuerySince && Date.now() - runningQuerySince > 500) {\n        setLoading(true);\n      } else {\n        setLoading(false);\n      }\n    }, 500);\n    return () => {\n      clearTimeout(timeout);\n      if (runningQuerySince) {\n        setLoading(false);\n      }\n    };\n  }, [runningQuerySince]);\n\n  useEffect(() => {\n    const parent = ref.current?.parentElement;\n    if (!parent) return;\n    parent.style.cursor = loading ? \"wait\" : \"auto\";\n  }, [loading]);\n\n  return (\n    <div\n      key={\"W_Table_Content\"}\n      ref={ref}\n      className={\"W_Table_Content flex-col oy-auto f-1 relative \"}\n      style={{\n        ...(loading ? loadingStyle : {}),\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n\nconst loadingStyle: React.CSSProperties = {\n  cursor: \"wait\",\n  pointerEvents: \"none\",\n  touchAction: \"none\",\n  opacity: 0.75,\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/colorBlend.ts",
    "content": "const r = Math.round;\n\nfunction toRGBA(d) {\n  const l = d.length;\n  const rgba = {};\n  if (d.slice(0, 3).toLowerCase() === \"rgb\") {\n    d = d.replace(\" \", \"\").split(\",\");\n    rgba[0] = parseInt(d[0].slice(d[3].toLowerCase() === \"a\" ? 5 : 4), 10);\n    rgba[1] = parseInt(d[1], 10);\n    rgba[2] = parseInt(d[2], 10);\n    rgba[3] = d[3] ? parseFloat(d[3]) : -1;\n  } else {\n    if (l < 6)\n      d = parseInt(\n        String(d[1]) +\n          d[1] +\n          d[2] +\n          d[2] +\n          d[3] +\n          d[3] +\n          (l > 4 ? String(d[4]) + d[4] : \"\"),\n        16,\n      );\n    else d = parseInt(d.slice(1), 16);\n    rgba[0] = (d >> 16) & 255;\n    rgba[1] = (d >> 8) & 255;\n    rgba[2] = d & 255;\n    rgba[3] =\n      l === 9 || l === 5 ? r((((d >> 24) & 255) / 255) * 10000) / 10000 : -1;\n  }\n  return rgba;\n}\n\nexport function blend(from: string, to: string, perc = 0.5) {\n  from = from.trim();\n  to = to.trim();\n  const b = perc < 0;\n  perc = b ? perc * -1 : perc;\n  const f = toRGBA(from);\n  const t = toRGBA(to);\n  if (to[0] === \"r\") {\n    const alpha =\n      f[3] > -1 && t[3] > -1 ? r(((t[3] - f[3]) * perc + f[3]) * 10000) / 10000\n      : t[3] < 0 ? f[3]\n      : t[3];\n    return (\n      \"rgb\" +\n      (to[3] === \"a\" ? \"a(\" : \"(\") +\n      r((t[0] - f[0]) * perc + f[0]) +\n      \",\" +\n      r((t[1] - f[1]) * perc + f[1]) +\n      \",\" +\n      r((t[2] - f[2]) * perc + f[2]) +\n      (f[3] < 0 && t[3] < 0 ? \"\" : \",\" + alpha) +\n      \")\"\n    );\n  }\n\n  return (\n    \"#\" +\n    (\n      0x100000000 +\n      (f[3] > -1 && t[3] > -1 ? r(((t[3] - f[3]) * perc + f[3]) * 255)\n      : t[3] > -1 ? r(t[3] * 255)\n      : f[3] > -1 ? r(f[3] * 255)\n      : 255) *\n        0x1000000 +\n      r((t[0] - f[0]) * perc + f[0]) * 0x10000 +\n      r((t[1] - f[1]) * perc + f[1]) * 0x100 +\n      r((t[2] - f[2]) * perc + f[2])\n    )\n      .toString(16)\n      .slice(f[3] > -1 || t[3] > -1 ? 1 : 3)\n  );\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Table/getTableData/getTableData.ts",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport { omitKeys, pickKeys } from \"prostgles-types\";\nimport type { DeltaOf, DeltaOfData } from \"../../RTComp\";\nimport type { ProstglesTableD, W_TableProps, W_TableState } from \"../W_Table\";\nimport W_Table from \"../W_Table\";\nimport { getTableSelect } from \"../tableUtils/getTableSelect\";\nimport { getSort } from \"../tableUtils/tableUtils\";\nimport { getTableFilter } from \"./getTableFilter\";\n\nexport async function getTableData(\n  this: W_Table,\n  dp: DeltaOf<W_TableProps>,\n  ds: DeltaOf<W_TableState>,\n  dd: DeltaOfData<ProstglesTableD>,\n  { showCounts }: { showCounts: boolean },\n) {\n  const delta = { ...dp, ...ds, ...dd };\n  const { rows } = this.state;\n  const {\n    prgl: { db },\n    joinFilter,\n    tables,\n  } = this.props;\n  const { w } = this.d;\n  if (!w) return;\n  const { table_name: tableName } = w;\n\n  let ns: Partial<W_TableState> | undefined;\n\n  const tableHandler = db[tableName];\n  if (!tableHandler) return;\n  try {\n    if (!tableHandler.find) {\n      if (this.state.rows?.length !== 0) {\n        ns = { rows: [] };\n      }\n    } else if (\n      !rows ||\n      delta.w?.options?.refresh ||\n      delta.w?.options?.quickFilterGroups ||\n      delta.dataAge ||\n      [\n        \"pageSize\",\n        \"page\",\n        \"sort\",\n        \"filter\",\n        \"having\",\n        \"joinFilter\",\n        \"externalFilters\",\n        \"limit\",\n        \"cols\",\n        \"dataAge\",\n        \"db\",\n        \"activeRow\",\n        \"columns\",\n      ].some((k) => k in delta || (delta.w && k in delta.w))\n    ) {\n      const tableFilterHaving = getTableFilter(w, this.props);\n      const { filter, having } = tableFilterHaving;\n      const {\n        select: selectWithoutData,\n        barchartVals: barchartValsWithoutData,\n      } = await getTableSelect(w, tables, db, filter, true);\n      const strFilter = JSON.stringify(tableFilterHaving);\n\n      const clearSub = () => {\n        return this.dataSub?.unsubscribe?.();\n      };\n      const clearInterval = () => {\n        if (this.autoRefresh) {\n          clearTimeout(this.autoRefresh);\n          this.autoRefresh = null;\n        }\n      };\n\n      const setSub = (throttleSeconds = 0) => {\n        if (this.dataSubFilter === strFilter) {\n          return;\n        }\n        this.dataSubFilter = strFilter;\n\n        return (async () => {\n          await clearSub();\n          clearInterval();\n\n          try {\n            /** Already getting data on first run */\n            let isInitialRun = true;\n            this.dataSub = await tableHandler.subscribe?.(\n              filter,\n              {\n                select: selectWithoutData,\n                limit: 0,\n                throttle: throttleSeconds * 1000,\n              },\n              () => {\n                if (isInitialRun) {\n                  isInitialRun = false;\n                  return;\n                }\n                this.setData({ dataAge: Date.now() });\n              },\n            );\n          } catch (error: any) {\n            console.error(\"Subscribe failed\", error);\n          }\n        })();\n      };\n\n      /** Resubscribe if filter changed or refresh cahnged */\n      if (w.options.refresh?.type === \"Realtime\" && tableHandler.subscribe) {\n        setSub(w.options.refresh.throttleSeconds);\n      }\n\n      /** Set data refresh */\n      if (\"refresh\" in (delta.w?.options || {})) {\n        const {\n          type = \"None\",\n          throttleSeconds = 0,\n          intervalSeconds = 3,\n        } = w.options.refresh || {};\n\n        /** Resubscribe if filter changed or refresh changed */\n        if (type === \"Realtime\") {\n          setSub(throttleSeconds);\n\n          /* Auto refresh settings */\n        } else if (type === \"Interval\" && intervalSeconds > 0) {\n          this.autoRefresh = setInterval(() => {\n            this.setState({ dataAge: Date.now() });\n          }, intervalSeconds * 1000);\n        } else if (type === \"None\") {\n          await clearSub();\n          clearInterval();\n        }\n      }\n\n      const orderBy = getSort(tables, w);\n      const { limit, offset } = this.getPagination();\n\n      const dataAge = delta.dataAge ?? this.dataAge ?? 0;\n      const cardOpts =\n        w.options.viewAs?.type === \"card\" ? w.options.viewAs : undefined;\n      const qSig = W_Table.getTableDataRequestSignature(\n        {\n          select: selectWithoutData,\n          barchartVals: barchartValsWithoutData,\n          orderBy,\n          limit,\n          offset,\n          joinFilter,\n          filter: filter,\n          having: having,\n        },\n        dataAge,\n        [cardOpts],\n      );\n\n      if (this.currentDataRequestSignature !== qSig) {\n        this.dataAge = dataAge;\n        this.currentDataRequestSignature = qSig;\n\n        if (!this.state.runningQuerySince) {\n          this.setState({ runningQuerySince: Date.now() });\n        }\n\n        const { select, barchartVals } = await getTableSelect(\n          w,\n          tables,\n          db,\n          filter,\n        );\n        if (barchartVals) {\n          ns = ns || ({} as any);\n          ns!.barchartVals = {\n            ...this.state.barchartVals,\n            ...barchartVals,\n          };\n        }\n\n        if (Object.keys(select).length) {\n          const findParams = {\n            select,\n            orderBy: orderBy as any,\n            limit,\n            offset,\n            having: having,\n          };\n          let rowCount: number | undefined;\n          try {\n            rowCount = await tableHandler.count?.(\n              filter,\n              pickKeys(findParams, [\"select\", \"having\"]),\n            );\n          } catch (error: any) {\n            console.error(\"Error getting rowCount\", error, error.query);\n            console.error(\"Error getting rowCount params\", filter, findParams);\n            throw error;\n          }\n          let initialRows: AnyObject[] = [];\n          if (cardOpts?.cardGroupBy) {\n            /**\n             * If card group mode then get top records for each group\n             */\n            const groupByColumn = cardOpts.cardGroupBy;\n            const groupByFindParams = {\n              ...omitKeys(findParams, [\"orderBy\"]),\n              ...(cardOpts.cardOrderBy ?\n                { orderBy: { [cardOpts.cardOrderBy]: true } }\n              : {}),\n            };\n            const groups =\n              rowCount == 0 ?\n                []\n              : await tableHandler.find(filter, {\n                  select: { [cardOpts.cardGroupBy]: 1 },\n                  groupBy: true,\n                  returnType: \"values\",\n                });\n            initialRows = (\n              await Promise.all(\n                groups.map((groupByValue) =>\n                  tableHandler.find!(\n                    { $and: [filter, { [groupByColumn]: groupByValue }] },\n                    groupByFindParams,\n                  ),\n                ),\n              )\n            ).flat();\n          } else {\n            initialRows =\n              rowCount == 0 ? [] : await tableHandler.find(filter, findParams);\n          }\n\n          this.activeRowStr = JSON.stringify(joinFilter || {});\n\n          const rows = initialRows.map((r) => ({\n            ...r,\n          }));\n\n          const nameTemplate =\n            w.title || `${w.table_name} ${showCounts ? \"${rowCount}\" : \"\"}`;\n          const newName = nameTemplate.replace(\n            \"${rowCount}\",\n            (showCounts ? (rowCount ?? \"\") : \"\").toLocaleString(),\n          );\n          if (newName !== w.name) {\n            w.$update({ name: newName });\n          }\n          ns = {\n            ...ns,\n            rows,\n            rowCount,\n            rowsLoaded: Date.now(),\n            totalRows: showCounts ? +((await tableHandler.count?.()) ?? 0) : 0,\n            onRowClick: (row, a2) => {\n              /** Must only include non computed columns  */\n              const rowHasNonComputedFields = this.d.w?.columns?.find(\n                (c) =>\n                  !c.computedConfig &&\n                  !c.format &&\n                  Object.keys(row ?? {}).includes(c.name),\n              );\n              if (row && rowHasNonComputedFields) {\n                this.props.onClickRow?.(row, a2);\n              } else {\n                this.props.onClickRow?.(undefined, a2);\n              }\n            },\n          };\n          if (joinFilter) {\n            ns.joinFilterStr = JSON.stringify(joinFilter);\n          } else {\n            ns.joinFilterStr = undefined;\n          }\n        } else {\n          ns = { ...ns, rows: [], rowCount: 0 };\n        }\n        ns.error = undefined;\n      }\n    }\n  } catch (error: any) {\n    console.error(error);\n    ns = { ...ns, error, rows: [] };\n  }\n\n  if (ns) {\n    ns.runningQuerySince = undefined;\n    this.setState(ns as any);\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/W_Table/getTableData/getTableFilter.ts",
    "content": "import {\n  getSmartGroupFilter,\n  simplifyFilter,\n  type DetailedFilter,\n} from \"@common/filterUtils\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { isDefined, isEmpty } from \"prostgles-types\";\nimport type { WindowData } from \"../../Dashboard/dashboardUtils\";\nimport type { W_TableProps } from \"../W_Table\";\nimport type { TableWindowInsertModel } from \"@common/DashboardTypes\";\n\nexport const getTableFilter = (\n  w: WindowData<\"table\">,\n  {\n    externalFilters,\n    joinFilter,\n  }: Pick<W_TableProps, \"joinFilter\" | \"externalFilters\">,\n) => {\n  const { filter: rawFilter, having: rawHaving } = w;\n\n  let filter: AnyObject = {};\n  let having: AnyObject = {};\n  /* Parse and Remove bad filters */\n  if (w.table_name) {\n    const quickFilterGroups = w.options\n      ?.quickFilterGroups as TableWindowInsertModel[\"quickFilterGroups\"];\n    const quickFilters = Object.values(quickFilterGroups ?? {})\n      .map(({ toggledFilterName, filters }) => {\n        if (!toggledFilterName) return;\n        const filter = filters[toggledFilterName];\n        if (!filter) return;\n        const [operand, fieldFilters] = (\n          \"$and\" in filter ? [\"and\", filter.$and]\n          : \"$or\" in filter ? [\"or\", filter.$or]\n          : [\"and\", [filter]]) satisfies [\"and\" | \"or\", AnyObject[]];\n        return getSmartGroupFilter(\n          fieldFilters as DetailedFilter[],\n          undefined,\n          operand,\n        );\n      })\n      .filter(isDefined);\n\n    filter = getSmartGroupFilter(\n      rawFilter || [],\n      { filters: quickFilters },\n      w.options?.filterOperand === \"OR\" ? \"or\" : undefined,\n    );\n\n    having = getSmartGroupFilter(\n      rawHaving || [],\n      undefined,\n      w.options?.havingOperand === \"OR\" ? \"or\" : undefined,\n    );\n  }\n\n  return {\n    filter:\n      simplifyFilter({\n        $and: [\n          !isEmpty(filter) ? filter : undefined,\n          joinFilter,\n          ...externalFilters,\n        ].filter(isDefined),\n      }) ?? {},\n    having:\n      simplifyFilter({\n        $and: [having].filter(isDefined),\n      }) ?? {},\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/StyledTableColumn.tsx",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport { _PG_date, _PG_numbers, includes, isDefined } from \"prostgles-types\";\nimport React from \"react\";\nimport { FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { CellBarchart } from \"@components/ProgressBar\";\nimport type { OnColRenderRowInfo } from \"@components/Table/Table\";\nimport { RenderValue } from \"../../SmartForm/SmartFormField/RenderValue\";\nimport type { ColumnConfig } from \"../ColumnMenu/ColumnMenu\";\nimport type {\n  ChipStyle,\n  ColumnValue,\n} from \"../ColumnMenu/ColumnStyleControls/ColumnStyleControls\";\nimport { kFormatter, type MinMax } from \"../W_Table\";\nimport { blend } from \"../colorBlend\";\nimport type { ProstglesTableColumn } from \"./getTableCols\";\nimport type { OnRenderColumnProps } from \"./onRenderColumn\";\nimport { SvgIcon } from \"@components/SvgIcon\";\n\ntype P = OnColRenderRowInfo &\n  Pick<OnRenderColumnProps, \"maxCellChars\" | \"c\" | \"barchartVals\">;\n\nexport const StyledTableColumn = ({\n  c,\n  value,\n  row,\n  barchartVals,\n  renderedVal,\n}: P) => {\n  if (c.style?.type === \"Icons\") {\n    const valueKey = value?.toString() ?? \"\";\n    const iconName = valueKey && c.style.valueToIconMap[valueKey];\n    const sizeNum = c.style.size ?? 24;\n    const iconNode = iconName && <SvgIcon icon={iconName} size={sizeNum} />;\n    return <FlexRow>{iconNode ?? value}</FlexRow>;\n  }\n  if (c.style?.type === \"Barchart\" && barchartVals?.[c.name]) {\n    const numVal = Number(value);\n    const numMin = Number(barchartVals[c.name]?.min ?? 0);\n    const numMax = Number(barchartVals[c.name]?.max ?? 0);\n    return (\n      <CellBarchart\n        style={{ marginTop: \"6px\" }}\n        min={numMin}\n        max={numMax}\n        barColor={c.style.barColor}\n        textColor={c.style.textColor}\n        value={numVal}\n        message={kFormatter(numVal)}\n      />\n    );\n  } else if (c.style?.type !== \"None\") {\n    const style = getCellStyle(c, c, row, barchartVals?.[c.name]);\n\n    if (\n      includes([\"Fixed\", \"Conditional\"], c.style?.type) &&\n      Array.isArray(value) &&\n      c.udt_name.startsWith(\"_\")\n    ) {\n      return (\n        <FlexRowWrap className=\"gap-p25\">\n          {value.map((v, i) => (\n            <StyledCell\n              key={i}\n              style={\n                c.style?.type === \"Scale\" ?\n                  { textColor: style?.textColor }\n                : style\n              }\n              renderedVal={\n                <RenderValue\n                  value={v}\n                  column={{\n                    udt_name: c.udt_name.slice(1) as any,\n                    tsDataType: c.tsDataType.slice(0, -2) as any,\n                  }}\n                  style={\n                    style?.textColor ? { color: style.textColor } : undefined\n                  }\n                  maxLength={55}\n                />\n              }\n              className={c.tsDataType === \"number\" ? \"as-end\" : \"\"}\n            />\n          ))}\n        </FlexRowWrap>\n      );\n    }\n    return (\n      <StyledCell\n        style={\n          c.style?.type === \"Scale\" ? { textColor: style?.textColor } : style\n        }\n        renderedVal={renderedVal}\n        className={includes(_PG_numbers, c.udt_name) ? \"as-end\" : \"\"}\n      />\n    );\n  }\n\n  return renderedVal;\n};\n\nexport const StyledCell = ({\n  style,\n  renderedVal,\n  className = \"\",\n}: {\n  renderedVal: any;\n  style: ChipStyle | undefined;\n  className?: string;\n}) => {\n  if (style) {\n    return (\n      <div\n        className={className}\n        style={{\n          ...(style.chipColor && {\n            backgroundColor: style.chipColor,\n            padding: \"4px 8px\",\n            borderRadius: \"12px\",\n            width: \"fit-content\",\n            whiteSpace: \"nowrap\",\n          }),\n          ...(style.cellColor && {\n            backgroundColor: style.cellColor,\n            padding: 0,\n            borderRadius: 0,\n            width: \"100%\",\n            height: \"100%\",\n          }),\n          ...(style.textColor && { color: style.textColor }),\n          ...(style.borderColor && {\n            border: `1px solid ${style.borderColor}`,\n          }),\n          /**\n           * Prevent left side overflow when showing numbers with \"as-end\"\n           */\n          maxWidth: \"100%\",\n          overflow: \"hidden\",\n          textOverflow: \"ellipsis\",\n        }}\n      >\n        {renderedVal}\n      </div>\n    );\n  }\n\n  return renderedVal;\n};\n\nexport const getCellStyle = (\n  col: ColumnConfig,\n  c: Pick<ProstglesTableColumn, \"tsDataType\" | \"udt_name\">,\n  row: AnyObject,\n  dataRange: MinMax | undefined,\n):\n  | {\n      textColor?: string;\n      chipColor?: string;\n      cellColor?: string;\n    }\n  | undefined => {\n  const { style } = col;\n  let res: ChipStyle = {};\n  if (!style || style.type === \"None\") {\n    res = {};\n  } else if (style.type === \"Fixed\") {\n    res = { ...style };\n  } else if (style.type === \"Conditional\") {\n    const val = row[col.name];\n\n    const match = style.conditions.find(({ operator, condition }) => {\n      const isNumeric =\n        c.udt_name === \"int4\" ||\n        c.udt_name === \"float8\" ||\n        c.udt_name === \"numeric\" ||\n        c.udt_name === \"int8\" ||\n        c.udt_name === \"int2\" ||\n        c.udt_name === \"float4\" ||\n        c.udt_name === \"money\";\n      const cval =\n        isNumeric ? +(condition as string) : (condition as ColumnValue);\n      if (operator === \"contains\") {\n        return val && `${JSON.stringify(val)}`.includes(cval?.toString() + \"\");\n      } else if (operator === \"=\") {\n        return val == cval;\n      } else if (operator === \">\") {\n        return cval !== undefined && cval !== null && val > cval;\n      } else if (operator === \">=\") {\n        return cval !== undefined && cval !== null && val >= cval;\n      } else if (operator === \"<=\") {\n        return cval !== undefined && cval !== null && val <= cval;\n      } else if (operator === \"<\") {\n        return cval !== undefined && cval !== null && val < cval;\n      } else if (operator === \"!=\") {\n        return val != cval;\n      } else if (operator === \"in\" || operator === \"not in\") {\n        const is_in = condition.includes(val);\n\n        if (operator === \"in\") return is_in;\n        else return !is_in;\n      }\n    });\n\n    if (!match && style.defaultStyle) {\n      res = {\n        ...style.defaultStyle,\n      };\n    }\n\n    if (match) {\n      res = {\n        ...style.defaultStyle,\n        ...match,\n      };\n    }\n  } else if (style.type === \"Scale\") {\n    const {\n      textColor = \"black\",\n      minColor = \"#63f717\",\n      maxColor = \"#46b5d5\",\n    } = style;\n    const val =\n      _PG_date.includes(c.udt_name as any) ?\n        +new Date(row[col.name])\n      : +row[col.name];\n    const { max, min } = dataRange ?? {};\n\n    if (isNumber(val) && isNumber(min) && isNumber(max)) {\n      const perc = (val - min) / (max - min);\n\n      res = {\n        textColor,\n        cellColor: blend(minColor, maxColor, perc),\n      };\n    }\n  }\n\n  return res;\n};\n\nexport const isNumber = (v: any): v is number => {\n  return Number.isFinite(v);\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/getColWInfo.ts",
    "content": "import type {\n  DBSchemaTableWJoins,\n  WindowData,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\n\nexport const getColWInfo = (\n  table: DBSchemaTableWJoins,\n  cols: WindowData<\"table\">[\"columns\"],\n): ColumnConfigWInfo[] => {\n  const tableColumns = table.columns.slice(0);\n  const isAdditionalComputed = (c: ColumnConfigWInfo) =>\n    c.computedConfig && !c.computedConfig.isColumn;\n\n  const columns: ColumnConfigWInfo[] = (cols ?? [])\n    .map((c) => ({\n      ...c,\n      info:\n        isAdditionalComputed(c) ? undefined : (\n          tableColumns.find((_c) => _c.select && _c.name === c.name)\n        ),\n    }))\n    .filter((c) => {\n      /** Remove dropped columns */\n      if (!c.computedConfig && !c.info && !c.nested) {\n        return false;\n      }\n      return true;\n    }) as ColumnConfigWInfo[];\n\n  const newCols = tableColumns.filter(\n    (c) => !columns.find((r) => r.info && r.name === c.name),\n  );\n\n  return structuredClone(\n    columns.concat(\n      newCols.map((c) => ({\n        info: c,\n        name: c.name,\n        show: true,\n      })),\n    ),\n  ).filter((c) => !c.info || c.info.select);\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/getColWidth.ts",
    "content": "import type { ProstglesTableColumn } from \"./getTableCols\";\nimport { _PG_numbers, includes } from \"prostgles-types\";\n\nconst minColumnWidth = 100;\nconst maxColumnWidth = 300;\n\nexport const getColWidth = <\n  T extends Pick<\n    ProstglesTableColumn,\n    \"tsDataType\" | \"udt_name\" | \"width\" | \"nested\"\n  >,\n  K extends keyof T,\n>(\n  cols: T[],\n  data: any[] = [],\n  key: K,\n  windowWidth?: number,\n): (T & { width: number })[] => {\n  /** If data AND no existing widths then calculate width based on row content length */\n  const someColumnHasWidth = cols.some((c) => Number.isFinite(c.width));\n  if (someColumnHasWidth) {\n    return cols as (T & { width: number })[];\n  }\n\n  const [firstCol] = cols;\n  const tableWidth = windowWidth ?? maxColumnWidth;\n  if (cols.length === 1 && !includes(_PG_numbers, firstCol?.udt_name)) {\n    return cols.map((c) => {\n      return {\n        ...c,\n        width: tableWidth - 10,\n      };\n    });\n  }\n\n  let assignedColumnWidth = 0;\n  return cols.map((c, i) => {\n    const isLastColumn = i === cols.length - 1;\n    let width = 20;\n\n    const udtNameWidths: Partial<Record<typeof c.udt_name, number>> = {\n      uuid: 160,\n      geography: 160,\n      geometry: 160,\n      timestamp: 200,\n      bool: 100,\n    };\n    const tsWidths: Partial<Record<typeof c.tsDataType, number>> = {\n      number: 100,\n    };\n\n    const fixedWidth = udtNameWidths[c.udt_name] ?? tsWidths[c.tsDataType];\n    if (fixedWidth) {\n      width = fixedWidth;\n    } else {\n      data.map((r) => {\n        const data = r[c[key]];\n        const dataIsSingleNestedColumnSoMustExcludePropertyNames =\n          c.nested?.columns.filter((nc) => nc.show).length === 1 &&\n          !c.nested.chart &&\n          Array.isArray(data);\n        const dataAsString = JSON.stringify(\n          dataIsSingleNestedColumnSoMustExcludePropertyNames ?\n            Object.values(data[0] ?? {})\n          : data || \"\",\n        );\n        const textContentWidth =\n          (dataAsString.length -\n            (dataIsSingleNestedColumnSoMustExcludePropertyNames ? 0 : 0)) *\n          8;\n\n        const existingWidth =\n          Number.isFinite(c.width) ? c.width! : textContentWidth;\n        /** Must be within 100px and 300px */\n        width = Math.min(\n          Math.max(width, textContentWidth, minColumnWidth, existingWidth),\n          maxColumnWidth,\n        );\n      });\n\n      /**\n       * TODO: should just workout top widest columns andd split free space between them.\n       *  If free space AND last column is lengthy then extend last column to fill it\n       * */\n      if (isLastColumn && !includes(_PG_numbers, c.udt_name)) {\n        const remainingWidth = tableWidth - assignedColumnWidth;\n        if (remainingWidth > 20) {\n          width = remainingWidth;\n        }\n      }\n    }\n    width = Math.max(width, minColumnWidth);\n\n    assignedColumnWidth += width;\n    return {\n      ...c,\n      width,\n    };\n  });\n};\n\n// const getColumnWidths = <\n//   T extends Pick<ProstglesTableColumn, \"tsDataType\" | \"udt_name\" | \"width\">,\n//   K extends keyof T,\n// >(\n//   colPropertyForData: K,\n//   cols: T[],\n//   data: any[],\n// ): { col: T; width: number; widthSource: \"fixed\" | \"fromData\" }[] => {\n//   const fixedWidths = new Map<T, number>();\n//   const fromDataWidths = new Map<T, number>();\n//   cols.forEach((col) => {\n//     if (Number.isFinite(col.width)) {\n//       fixedWidths.set(col, col.width!);\n//     } else {\n//       fromDataWidths.set(col, -1);\n//     }\n//   });\n//   data.forEach((r) => {\n//     let width = 20;\n//     fromDataWidths.forEach((_, c) => {\n//       const textContentWidth =\n//         JSON.stringify(r[c[colPropertyForData]] || \"\").length * 8;\n//       const existingWidth =\n//         Number.isFinite(c.width) ? c.width! : textContentWidth;\n//       /** Must be within 100px and 300px */\n//       width = Math.min(\n//         Math.max(width, textContentWidth, minColumnWidth, existingWidth),\n//         maxColumnWidth,\n//       );\n//       fromDataWidths.set(c, Math.max(fromDataWidths.get(c) ?? 0, width));\n//     });\n//   });\n\n//   return cols.map((c) => {\n//     const fixedWidth = fixedWidths.get(c);\n//     if (fixedWidth !== undefined) {\n//       return { col: c, width: fixedWidth, widthSource: \"fixed\" };\n//     } else {\n//       return {\n//         col: c,\n//         width: fromDataWidths.get(c) ?? minColumnWidth,\n//         widthSource: \"fromData\",\n//       };\n//     }\n//   });\n// };\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/getEditColumn.tsx",
    "content": "import { mdiOpenInNew, mdiPencilOutline } from \"@mdi/js\";\nimport type { TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject, ValidatedColumnInfo } from \"prostgles-types\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\n\nimport { type DetailedFilterBase } from \"@common/filterUtils\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\nimport type { AddColumnMenuProps } from \"../ColumnMenu/AddColumnMenu\";\nimport { AddColumnMenu } from \"../ColumnMenu/AddColumnMenu\";\nimport type { ProstglesColumn } from \"../W_Table\";\nimport { getRowFilter } from \"./getRowFilter\";\n\nexport const getUnknownColInfo = (\n  key: string,\n  label: string,\n  dataType: ValidatedColumnInfo[\"tsDataType\"],\n  computed,\n): ProstglesColumn => ({\n  key,\n  name: label,\n  label,\n  sortable: [\"string\", \"number\", \"boolean\", \"Date\"].includes(dataType),\n  tsDataType: dataType,\n  udt_name: \"text\",\n  filter: true,\n  computed,\n});\n\nexport type RowSiblingData = {\n  prevRow: AnyObject | undefined;\n  nextRow: AnyObject | undefined;\n  prevRowFilter: DetailedFilterBase[] | undefined;\n  nextRowFilter: DetailedFilterBase[] | undefined;\n};\nexport type OnClickEditRow = (\n  filter: DetailedFilterBase[],\n  siblingData: RowSiblingData,\n  rowIndex: number,\n  fixedUpdateData?: AnyObject,\n) => void;\n\ntype GetMenuColumnArgs = {\n  tableHandler: Partial<TableHandlerClient>;\n  onClickRow: OnClickEditRow;\n  table: DBSchemaTableWJoins;\n  columnConfig: { name: string }[] | undefined;\n  addColumnProps?: AddColumnMenuProps;\n};\nexport const getEditColumn = ({\n  tableHandler,\n  onClickRow,\n  addColumnProps,\n  table,\n  columnConfig,\n}: GetMenuColumnArgs): ProstglesColumn => {\n  const viewOnly = !tableHandler.update;\n  const title = viewOnly ? \"View row\" : \"View/Edit row\",\n    iconPath = viewOnly ? mdiOpenInNew : mdiPencilOutline;\n\n  const res: ProstglesColumn = {\n    ...getUnknownColInfo(\"edit_row\", \" \", \"any\", true),\n    filter: false,\n    sortable: false,\n    label: addColumnProps && <AddColumnMenu {...addColumnProps} />,\n    hidden: false,\n    width: 50,\n    getCellStyle: () => ({ padding: 0 }),\n    onRender: ({ row, nextRow, prevRow, rowIndex }) => (\n      <Btn\n        className={\n          \"h-full h-fit w-fit\" +\n          (window.isMobileDevice ? \" text-3 \" : \" show-on-row-hover  \")\n        }\n        title={title}\n        data-command=\"dashboard.window.viewEditRow\"\n        iconPath={iconPath}\n        style={{ padding: \"12px\" }}\n        color=\"action\"\n        onClickMessage={async (e, setM) => {\n          e.stopPropagation();\n\n          setM({ loading: 1 });\n          const { error, filter } = await getRowFilter(\n            row,\n            table,\n            columnConfig,\n            tableHandler,\n          );\n          if (error) {\n            alert(error);\n          } else if (filter) {\n            const siblingData = await getRowSiblingData(\n              [prevRow, row, nextRow],\n              1,\n              table,\n              columnConfig,\n              tableHandler,\n            );\n            onClickRow(filter, siblingData, rowIndex);\n          }\n          setM({ loading: 0 });\n        }}\n      />\n    ),\n  };\n\n  return res;\n};\n\nexport type CoreColInfo = Pick<\n  ValidatedColumnInfo,\n  \"filter\" | \"is_pkey\" | \"name\" | \"tsDataType\" | \"udt_name\"\n>;\n\nexport const getRowSiblingData = async (\n  rows: (AnyObject | undefined)[],\n  rowIndex: number,\n  table: DBSchemaTableWJoins,\n  columns: GetMenuColumnArgs[\"columnConfig\"],\n  tableHandler: Partial<TableHandlerClient<AnyObject, void>>,\n) => {\n  const prevRow = rows[rowIndex - 1];\n  const nextRow = rows[rowIndex + 1];\n\n  let prevRowFilter: undefined | DetailedFilterBase[];\n  let nextRowFilter: undefined | DetailedFilterBase[];\n  try {\n    if (prevRow)\n      prevRowFilter = (\n        await getRowFilter(prevRow, table, columns, tableHandler)\n      ).filter;\n    if (nextRow)\n      nextRowFilter = (\n        await getRowFilter(nextRow, table, columns, tableHandler)\n      ).filter;\n  } catch (e) {\n    console.error(e);\n  }\n  return { nextRow, prevRow, prevRowFilter, nextRowFilter };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/getFullColumnConfig.ts",
    "content": "import type { FileTable } from \"@common/utils\";\nimport type { AnyObject } from \"prostgles-types\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type { WindowData } from \"../../Dashboard/dashboardUtils\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\nimport { getColWInfo } from \"./getColWInfo\";\nimport { getColWidth } from \"./getColWidth\";\n\nexport const getFullColumnConfig = (\n  tables: CommonWindowProps[\"tables\"],\n  w: Pick<WindowData<\"table\">, \"columns\" | \"table_name\">,\n  data?: AnyObject[],\n  windowWidth?: number,\n): ColumnConfigWInfo[] => {\n  try {\n    const { table_name } = w;\n    const table = tables.find((t) => t.name === table_name);\n\n    if (!table) return [];\n    let colsWInfo = getColWInfo(table, w.columns);\n\n    /* Show file columns as Media format by default */\n    colsWInfo = colsWInfo.map((r) => {\n      const isFileColumn =\n        (table.info.isFileTable && r.name === \"url\") || r.info?.file;\n      return {\n        ...r,\n        format:\n          !r.format && isFileColumn ?\n            { type: \"Media\", params: { type: \"From URL Extension\" } }\n          : r.format,\n      };\n    });\n\n    try {\n      colsWInfo = getColWidth(\n        colsWInfo.map((r) => ({\n          ...r,\n          ...(r.info ?? { udt_name: \"text\", tsDataType: \"string\" }),\n        })),\n        data,\n        \"name\",\n        windowWidth,\n      ).map((c) => ({\n        ...c,\n        width: c.info?.udt_name === \"uuid\" ? 150 : c.width,\n      }));\n    } catch (e) {\n      console.error(e);\n    }\n\n    /* If media table then set url column display format to Media  */\n    if (!w.columns?.length && table.info.isFileTable) {\n      colsWInfo = structuredClone(colsWInfo);\n      const urlColumnIndex = colsWInfo.findIndex((c) => c.name === \"url\");\n      const urlColumn = colsWInfo.splice(urlColumnIndex, 1)[0];\n      const origNameColIdx = colsWInfo.findIndex(\n        ({ name }) => name === (\"original_name\" satisfies keyof FileTable),\n      );\n      const origNameCol = colsWInfo.splice(origNameColIdx, 1)[0];\n      if (origNameCol) {\n        colsWInfo.unshift(origNameCol);\n      }\n      if (urlColumn) {\n        urlColumn.format = {\n          type: \"Media\",\n          params: { type: \"From URL Extension\" },\n        };\n        colsWInfo.unshift(urlColumn);\n      }\n    }\n\n    return colsWInfo.slice(0);\n  } catch (e) {\n    console.error(e);\n    throw e;\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/getJoinPaths.ts",
    "content": "import type { ParsedJoinPath } from \"prostgles-types\";\nimport type { DBSchemaTablesWJoins } from \"../../Dashboard/dashboardUtils\";\n\nexport const flattenJoinPathsV2 = (\n  tableName: string,\n  tables: DBSchemaTablesWJoins,\n  prevTables: string[] = [],\n  maxDepth = 4,\n): ParsedJoinPath[][] => {\n  const _table = tables.find(\n    (t) => t.name === tableName && !prevTables.includes(tableName),\n  );\n\n  const depth = prevTables.length;\n  if (!_table || depth > maxDepth) return [];\n\n  const nextJoinsV2 = _table.joinsV2.filter(\n    (j) => !prevTables.includes(j.tableName),\n  );\n  const paths: ParsedJoinPath[][] = nextJoinsV2.flatMap((_j) => {\n    return _j.on.flatMap((singleOn) => {\n      const j: ParsedJoinPath = {\n        on: [Object.fromEntries(singleOn)],\n        table: _j.tableName,\n      };\n\n      const nestedJoins = flattenJoinPathsV2(_j.tableName, tables, [\n        ...prevTables,\n        tableName,\n      ]);\n      const nextPaths = nestedJoins.map((nj) => {\n        const nextPaths = Array.isArray(nj) ? nj : [nj];\n\n        const res: ParsedJoinPath[] = [j, ...nextPaths];\n        return res;\n      });\n      /** Split at current target and continue with the next joins */\n      return [[j], ...nextPaths];\n    });\n  });\n\n  return paths;\n};\n\nexport const getJoinPathStr = (jp: ParsedJoinPath[]) => {\n  return jp\n    .map((p) =>\n      [\n        p.table,\n        p.on\n          .map((cond) =>\n            Object.entries(cond).map((lrFields) => lrFields.join()),\n          )\n          .map((d) => d.sort().join()),\n      ].join(),\n    )\n    .join();\n};\nexport type TargetPath = {\n  table: DBSchemaTablesWJoins[number];\n  path: ParsedJoinPath[];\n  pathStr: string;\n};\nexport const getJoinPaths = (\n  tableName: string,\n  tables: DBSchemaTablesWJoins,\n): TargetPath[] => {\n  const getTable = (name: string) => {\n    return tables.find((t) => t.name === name);\n  };\n\n  const paths = flattenJoinPathsV2(tableName, tables);\n  const pathsWithInfo = paths\n    .map((path, idx) => {\n      const lastTableName = path.at(-1)!.table;\n      return {\n        table: getTable(lastTableName)!,\n        pathStr: getJoinPathStr(path),\n        path,\n      };\n    })\n    .sort(\n      (a, b) =>\n        a.path.length - b.path.length ||\n        a.table.name.localeCompare(b.table.name),\n    );\n\n  return pathsWithInfo;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/getRowFilter.ts",
    "content": "import type { TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { isEmpty } from \"../../../utils/utils\";\n\nimport {\n  getSmartGroupFilter,\n  type DetailedFilterBase,\n} from \"@common/filterUtils\";\nimport type { DBSchemaTableWJoins } from \"../../Dashboard/dashboardUtils\";\n\nexport const getRowFilter = async (\n  row: AnyObject,\n  table: DBSchemaTableWJoins,\n  columnConfig: { name: string }[] | undefined,\n  tableHandler: Partial<TableHandlerClient<AnyObject, void>>,\n): Promise<\n  | { filter: DetailedFilterBase[]; error: undefined }\n  | { filter: undefined; error: string }\n> => {\n  let rowFilter: DetailedFilterBase[] | undefined;\n  const { columns } = table;\n  let pkeys = columns.filter((c) => c.filter && c.is_pkey);\n  const uniqueColumnGroup = table.info.uniqueColumnGroups?.find((colNames) =>\n    colNames.every((colName) => columns.some((c) => c.name === colName)),\n  );\n  if (!pkeys.length && uniqueColumnGroup) {\n    pkeys = columns.filter(\n      (c) => c.filter && uniqueColumnGroup.includes(c.name),\n    );\n  }\n  if (\n    pkeys.length &&\n    (!columnConfig ||\n      columnConfig.some((c) => pkeys.find((pk) => pk.name === c.name)))\n    /** Allow using pkey/unique cols after func applied? */\n  ) {\n    pkeys.map((pkey) => {\n      rowFilter ??= [];\n      rowFilter.push({\n        fieldName: pkey.name,\n        value: row[pkey.name],\n      });\n    });\n  } else {\n    const dissallowedUdtTypes = [\"interval\"];\n    const filterCols = columns.filter(\n      (c) =>\n        !dissallowedUdtTypes.includes(c.udt_name) &&\n        c.filter &&\n        ([\"number\", \"string\", \"boolean\", \"Date\"].includes(c.tsDataType) ||\n          c.udt_name === \"jsonb\"),\n    );\n\n    /** Trim value if too long to avoid btrim error */\n    const getSlicedValue = (v) => {\n      if (typeof v === \"string\" && v.length > 400) {\n        return { $like: `${v.slice(0, 400)}%` };\n      }\n\n      return v;\n    };\n\n    rowFilter = filterCols.map((c) => {\n      const val = row[c.name];\n      return {\n        fieldName: c.name,\n        value:\n          (\n            c.udt_name.startsWith(\"json\") &&\n            typeof val === \"string\" &&\n            !val.startsWith('\"')\n          ) ?\n            JSON.stringify(val)\n          : getSlicedValue(val),\n      };\n    });\n    rowFilter = rowFilter.filter((f, i, arr) => {\n      const filterStrLen = JSON.stringify(\n        getSmartGroupFilter(arr.slice(0, i + 1)),\n      ).length;\n      return filterStrLen * 4 < 2104;\n    });\n  }\n\n  const _rowFilter = getSmartGroupFilter(rowFilter);\n  /** This check is needed for subscriptions */\n  if (JSON.stringify(_rowFilter).length * 4 > 2704 || isEmpty(_rowFilter)) {\n    return {\n      filter: undefined,\n      error:\n        \"Could not create filter for record\" +\n        (!pkeys.length ? \". Create a primary key to fix this issue\" : \"\"),\n    };\n  }\n\n  const [firstRecord, secondRecord] =\n    (await tableHandler.find?.(_rowFilter, { select: \"\", limit: 2 })) ?? [];\n  if (!firstRecord) {\n    console.log(_rowFilter);\n    return {\n      filter: undefined,\n      error: \"Could not create a single row filter. Record not found.\",\n    };\n  } else if (secondRecord) {\n    return {\n      filter: undefined,\n      error:\n        \"Could not create a single row filter. More than one record returned\" +\n        (!pkeys.length ? \". Create a primary key to fix this issue\" : \"\"),\n    };\n  } else {\n    return { filter: rowFilter ?? [], error: undefined };\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/getTableCols.tsx",
    "content": "import { mdiFilter, mdiFunction, mdiKey, mdiLink } from \"@mdi/js\";\nimport type { AnyObject } from \"prostgles-types\";\n\nimport React from \"react\";\n\nimport Btn from \"@components/Btn\";\n\nimport { FlexRow } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { quickClone } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type { WindowSyncItem } from \"../../Dashboard/dashboardUtils\";\nimport type W_Table from \"../W_Table\";\nimport type {\n  ColumnConfigWInfo,\n  MinMaxVals,\n  ProstglesColumn,\n  W_TableProps,\n} from \"../W_Table\";\nimport type { OnClickEditRow } from \"./getEditColumn\";\nimport { getEditColumn } from \"./getEditColumn\";\nimport { getFullColumnConfig } from \"./getFullColumnConfig\";\nimport { onRenderColumn } from \"./onRenderColumn\";\nimport { getCellStyle } from \"./StyledTableColumn\";\n\nexport type ProstglesTableColumn = ProstglesColumn & ColumnConfigWInfo;\n\ntype GetTableColsArgs = Pick<W_TableProps[\"prgl\"], \"db\" | \"tables\"> &\n  Pick<CommonWindowProps, \"suggestions\"> & {\n    data?: AnyObject[];\n    w?: WindowSyncItem<\"table\">;\n    windowWidth?: number;\n    onClickEditRow: OnClickEditRow;\n    barchartVals: MinMaxVals | undefined;\n    allowMediaSkip?: boolean;\n    hideEditRow?: boolean;\n    columnMenuState?: W_Table[\"columnMenuState\"];\n    opts?: Pick<Partial<ProstglesTableColumn>, \"noRightBorder\">;\n  };\nexport const getTableCols = ({\n  w,\n  db,\n  tables,\n  data,\n  windowWidth,\n  onClickEditRow,\n  barchartVals,\n  suggestions,\n  hideEditRow,\n  columnMenuState,\n  opts,\n}: GetTableColsArgs): ProstglesTableColumn[] => {\n  if (!w) return [];\n\n  const tableName = w.table_name;\n\n  const table = tables.find((t) => t.name === tableName);\n  const columns = table?.columns;\n\n  if (!columns) {\n    return [];\n  }\n\n  const _fullConfigCols = getFullColumnConfig(tables, w, data, windowWidth);\n  const fullConfigCols = _fullConfigCols\n    .filter((c) => c.show)\n    .map((_c) => {\n      const c: ColumnConfigWInfo = quickClone(_c);\n\n      const { tsDataType = \"any\", udt_name = \"text\" } =\n        (c.computedConfig ? c.computedConfig : c.info) ?? {};\n\n      return {\n        ...c,\n        tsDataType,\n        udt_name,\n      };\n    });\n\n  const tblCols: ProstglesTableColumn[] = fullConfigCols.map((c) => {\n    const nestedCols =\n      !c.nested ? null\n      : c.nested.chart ?\n        ` (${c.nested.chart.dateCol}, ${\n          c.nested.chart.yAxis.isCountAll ?\n            \"COUNT(*)\"\n          : `${c.nested.chart.yAxis.funcName.slice(1).toUpperCase()}(${c.nested.chart.yAxis.colName})`\n        })`\n      : \"\";\n    // ` (${sliceText(c.nested.columns.filter(c => c.show).map(c => c.name).join(\", \"), 20)})`;\n\n    const subLabel = (\n      <div className=\"flex-row ai-center\">\n        {(c.computedConfig || c.format) && (\n          <Icon\n            path={mdiFunction}\n            size={0.75}\n            className={\"color-action ml-p25\"}\n          />\n        )}\n        {c.info?.is_pkey ?\n          <div title=\"Primary key\" className=\"flex-row ai-center\">\n            <Icon path={mdiKey} size={0.75} className=\"mr-p25\" />\n            {c.info.udt_name}\n          </div>\n        : (nestedCols ?? (!c.computedConfig ? c.info?.udt_name : null))}\n        {(\n          !w.filter.some(\n            (f) => \"fieldName\" in f && f.fieldName === c.name && !f.disabled,\n          )\n        ) ?\n          null\n        : <Btn\n            title=\"Has active filter\"\n            iconPath={mdiFilter}\n            style={{ padding: 0 }}\n            size={\"micro\"}\n            className={\"color-action ml-p25\"}\n            onClick={(e) => {\n              e.stopPropagation();\n              e.preventDefault();\n\n              const _w = w;\n              _w.$update(\n                { options: { showFilters: !_w.options.showFilters } },\n                { deepMerge: true },\n              );\n            }}\n          />}\n      </div>\n    );\n\n    let labelText = c.info?.label || c.name;\n    let labelIcon = \"\";\n    if (c.computedConfig?.isColumn) {\n      labelIcon = mdiFunction;\n      labelText = `${c.computedConfig.funcDef.label}(${c.name})`;\n    } else if (c.info?.references || c.nested) {\n      labelIcon = mdiLink;\n    }\n    const title =\n      c.nested ?\n        `${c.name} (${c.nested.path.at(-1)?.table} data)`\n      : [\n          //@ts-ignore\n          c.name,\n          c.udt_name,\n          c.info?.comment || \"\",\n          c.info?.references ?\n            `references ${c.info.references.map((r) => r.ftable)}`\n          : \"\",\n        ]\n          .filter((v) => v)\n          .join(\"\\n\");\n    const tableColumn: ProstglesTableColumn = {\n      ...c,\n      filter: c.info?.filter ?? false,\n      sortable:\n        c.nested ?\n          {\n            column: c,\n            tables,\n            w,\n          }\n        : !!c.computedConfig ||\n          (!!c.info?.orderBy &&\n            c.info.udt_name !== \"json\" &&\n            (c.info as any)?.udt_name !== \"point\"),\n      computed: !!c.computedConfig,\n      key: c.name,\n      label:\n        labelIcon ?\n          <FlexRow className=\"ai-none jc-none gap-p5\">\n            <Icon className=\"f-0\" path={labelIcon} size={0.75} /> {labelText}\n          </FlexRow>\n        : labelText,\n      subLabelTitle: \"Data type\",\n      subLabel,\n      title,\n      hidden: c.name === \"$rowhash\",\n      width: c.width ?? 100,\n      noRightBorder: opts?.noRightBorder ?? false,\n      onRender: onRenderColumn({\n        c,\n        table,\n        tables,\n        barchartVals,\n        maxCellChars: w.options.maxCellChars,\n        getValues: () => {\n          return data?.map((r) => r[c.name]) ?? [];\n        },\n      }),\n\n      /**\n       * Set color based on data type?!\n       */\n      getCellStyle: (row) => {\n        if (c.style?.type === \"Scale\" && barchartVals?.[c.name]) {\n          const style = getCellStyle(c, c, row, barchartVals[c.name]);\n          if (!style?.cellColor && !style?.textColor) {\n            return {};\n          }\n          return {\n            ...(style.cellColor && {\n              backgroundColor: style.cellColor,\n              borderColor: style.cellColor,\n            }),\n            ...(style.textColor && { color: `${style.textColor}` }),\n          };\n        }\n        return c.format?.type === \"Media\" ? { display: \"flex\" } : {};\n      },\n      onContextMenu: (e: React.MouseEvent, n: HTMLElement) => {\n        e.preventDefault();\n        e.stopPropagation();\n        const { x, y } = e.currentTarget.getBoundingClientRect();\n        columnMenuState?.set({ column: c.name, clientX: x, clientY: y });\n\n        return false;\n      },\n    };\n\n    return tableColumn;\n  });\n  const tableHandler = db[tableName];\n\n  /* Can update table. Add update button */\n  if (tableHandler && !hideEditRow && !w.options.hideEditRow) {\n    const _columns = columns.filter(\n      (c) =>\n        !w.columns?.length ||\n        w.columns.some((wc) => wc.name === c.name && wc.show !== false),\n    );\n    const editColumn = getEditColumn({\n      table,\n      columnConfig: _columns,\n      tableHandler,\n      addColumnProps: {\n        w,\n        tables,\n        db,\n        suggestions,\n        nestedColumnOpts: undefined,\n      },\n      onClickRow: onClickEditRow,\n    });\n    tblCols.unshift(editColumn);\n  }\n\n  return tblCols;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/getTableSelect.ts",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { isDefined } from \"prostgles-types\";\nimport { getSmartGroupFilter } from \"@common/filterUtils\";\nimport { isEmpty } from \"../../../utils/utils\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type { WindowData } from \"../../Dashboard/dashboardUtils\";\nimport { getTimeChartSelectParams } from \"../../W_TimeChart/fetchData/getTimeChartSelectParams\";\nimport {\n  getDesiredTimeChartBinSize,\n  getTimeChartMinMax,\n} from \"../../W_TimeChart/fetchData/getTimeChartLayersWithBins\";\nimport type { ColumnConfig } from \"../ColumnMenu/ColumnMenu\";\nimport type { MinMax, MinMaxVals } from \"../W_Table\";\nimport { getFullColumnConfig } from \"./getFullColumnConfig\";\n\nexport const getTableSelect = async (\n  w: Pick<WindowData<\"table\">, \"columns\" | \"table_name\">,\n  tables: CommonWindowProps[\"tables\"],\n  db: DBHandlerClient,\n  filter: AnyObject,\n  withoutData = false,\n): Promise<{ barchartVals?: AnyObject; select: AnyObject }> => {\n  let select: AnyObject = {};\n  let barchartVals: MinMaxVals | undefined;\n\n  if (w.columns && Array.isArray(w.columns)) {\n    const fullColumns = getFullColumnConfig(tables, w);\n    await Promise.all(\n      fullColumns.map(async (c) => {\n        if (c.show) {\n          if (c.style && [\"Barchart\", \"Scale\"].includes(c.style.type)) {\n            barchartVals ??= {};\n            let minMax:\n              | {\n                  min: any;\n                  max: any;\n                }\n              | undefined;\n            let isDate = false;\n\n            if (withoutData) {\n              minMax = { min: -1, max: -1 };\n            } else if (c.computedConfig) {\n              const otherColumns = fullColumns.filter(\n                (oc) => oc.name !== c.name,\n              );\n              const otherColumnSelect = otherColumns\n                .filter((c) => c.show && !c.nested)\n                .reduce(\n                  (acc, otherCol) => ({\n                    ...acc,\n                    [otherCol.name]:\n                      otherCol.computedConfig ?\n                        getComputedColumnSelect(otherCol.computedConfig)\n                      : 1,\n                  }),\n                  {} as Record<string, any>,\n                );\n              const min = await db[w.table_name]?.findOne?.(filter, {\n                select: {\n                  ...otherColumnSelect,\n                  prostgles_min: getComputedColumnSelect(c.computedConfig),\n                },\n                orderBy: [{ key: \"prostgles_min\", asc: true, nulls: \"last\" }],\n              });\n              const max = await db[w.table_name]?.findOne?.(filter, {\n                select: {\n                  ...otherColumnSelect,\n                  prostgles_max: getComputedColumnSelect(c.computedConfig),\n                },\n                orderBy: [{ key: \"prostgles_max\", asc: false, nulls: \"last\" }],\n              });\n              minMax = {\n                min: min?.prostgles_min,\n                max: max?.prostgles_max,\n              };\n            } else {\n              minMax = await db[w.table_name]?.findOne?.(filter, {\n                select: {\n                  min: { $min: [c.name] },\n                  max: { $max: [c.name] },\n                },\n              });\n\n              isDate =\n                c.info?.udt_name.startsWith(\"timestamp\") ||\n                c.info?.udt_name === \"date\";\n            }\n            if (minMax) {\n              barchartVals[c.name] = {\n                min: isDate ? +new Date(minMax.min) : +minMax.min,\n                max: isDate ? +new Date(minMax.max) : +minMax.max,\n              };\n            }\n          }\n\n          if (c.computedConfig) {\n            select[c.name] = getComputedColumnSelect(c.computedConfig);\n          } else if (c.nested) {\n            const nestedSel = await getNestedColumnSelect(\n              c,\n              db,\n              tables,\n              withoutData,\n            );\n            if (nestedSel) {\n              if (nestedSel.dateExtent) {\n                barchartVals ??= {};\n                barchartVals[c.name] = nestedSel.dateExtent as any;\n              }\n              select[c.name] = nestedSel.select;\n            }\n          } else {\n            select[c.name] = 1;\n          }\n        }\n      }),\n    );\n  } else {\n    select = { \"*\": 1 };\n  }\n\n  return { barchartVals, select };\n};\n\nexport const getComputedColumnSelect = (\n  computedConfig: Required<ColumnConfig>[\"computedConfig\"],\n) => {\n  let funcName = computedConfig.funcDef.key;\n  let functionArgs: any[] = [computedConfig.column].filter(isDefined);\n  if (computedConfig.column || computedConfig.args) {\n    const { args } = computedConfig;\n    if (args?.$duration?.otherColumn) {\n      functionArgs = [computedConfig.column, args.$duration.otherColumn];\n      funcName = \"$age\";\n    } else if (funcName === \"$string_agg\") {\n      functionArgs = [\n        computedConfig.column,\n        args?.$string_agg?.separator || \", \",\n      ];\n    } else if (args?.$template_string) {\n      functionArgs = [args.$template_string];\n    }\n  }\n  return { [funcName]: functionArgs };\n};\n\nexport const getNestedColumnSelect = async (\n  c: ColumnConfig,\n  db: DBHandlerClient,\n  tables: CommonWindowProps[\"tables\"],\n  withoutData = false,\n): Promise<{ select: AnyObject; dateExtent?: MinMax<Date> } | undefined> => {\n  if (!c.nested) throw \"Impossible\";\n\n  let nestedSelect: AnyObject = {};\n  let dateExtent: MinMax<Date> | undefined;\n  if (c.nested.chart) {\n    const targetTable = c.nested.path.at(-1)!.table;\n    dateExtent =\n      withoutData ?\n        { min: new Date(), max: new Date() }\n      : await getTimeChartMinMax(db[targetTable]!, {}, c.nested.chart.dateCol);\n\n    const { bin } =\n      withoutData ?\n        { bin: \"day\" as const }\n      : await getDesiredTimeChartBinSize({\n          dataExtent: {\n            minDate: dateExtent.min,\n            maxDate: dateExtent.max,\n          },\n          manualBinSize: undefined,\n          pxPerPoint: 5,\n          viewPortExtent: undefined,\n          width: c.width ?? 100,\n        });\n    nestedSelect = getTimeChartSelectParams({\n      bin,\n      dateColumn: c.nested.chart.dateCol,\n      groupByColumn: undefined,\n      statType:\n        !c.nested.chart.yAxis.isCountAll ?\n          {\n            funcName: c.nested.chart.yAxis.funcName,\n            numericColumn: c.nested.chart.yAxis.colName,\n          }\n        : undefined,\n    }).select;\n  } else {\n    nestedSelect = (\n      await getTableSelect(\n        { columns: c.nested.columns, table_name: c.nested.path.at(-1)!.table },\n        tables,\n        db,\n        {},\n        withoutData,\n      )\n    ).select;\n    if (isEmpty(nestedSelect)) {\n      return undefined;\n    }\n  }\n\n  const filter = getSmartGroupFilter(c.nested.detailedFilter, undefined, \"and\");\n  const having = getSmartGroupFilter(c.nested.detailedHaving, undefined, \"and\");\n  return {\n    dateExtent,\n    select: {\n      [c.nested.joinType === \"inner\" ? \"$innerJoin\" : \"$leftJoin\"]:\n        c.nested.path,\n      limit: c.nested.limit,\n      select: nestedSelect,\n      orderBy: c.nested.sort && [c.nested.sort],\n      filter,\n      having,\n    },\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/onRenderColumn.tsx",
    "content": "import type {\n  AnyObject,\n  DBSchemaTable,\n  ValidatedColumnInfo,\n} from \"prostgles-types\";\nimport React from \"react\";\nimport { MediaViewer } from \"@components/MediaViewer/MediaViewer\";\nimport type { DBSchemaTablesWJoins } from \"../../Dashboard/dashboardUtils\";\nimport { RenderValue } from \"../../SmartForm/SmartFormField/RenderValue\";\nimport type { NestedTimeChartMeta } from \"../ColumnMenu/ColumnDisplayFormat/NestedColumnRender\";\nimport { NestedColumnRender } from \"../ColumnMenu/ColumnDisplayFormat/NestedColumnRender\";\nimport { DISPLAY_FORMATS } from \"../ColumnMenu/ColumnDisplayFormat/columnFormatUtils\";\nimport type { ColumnConfigWInfo, MinMaxVals } from \"../W_Table\";\nimport { StyledTableColumn } from \"./StyledTableColumn\";\nimport type { ProstglesTableColumn } from \"./getTableCols\";\nimport { ROUTES } from \"@common/utils\";\n\nexport type RenderedColumn = ColumnConfigWInfo &\n  Pick<ValidatedColumnInfo, \"tsDataType\" | \"udt_name\" | \"name\"> &\n  Pick<ProstglesTableColumn, \"format\">; // | \"noSanitize\" | \"contentConfig\" | \"allowedHTMLTags\">;\nexport type OnRenderColumnProps = {\n  c: RenderedColumn;\n  getValues: () => any[];\n  tables: DBSchemaTablesWJoins;\n  table: DBSchemaTable | undefined;\n  maxCellChars?: number;\n  barchartVals: MinMaxVals | undefined;\n  maximumFractionDigits?: number | undefined;\n};\nexport const onRenderColumn = (args: OnRenderColumnProps) => {\n  const {\n    c,\n    table,\n    tables,\n    maxCellChars = 500,\n    barchartVals,\n    getValues,\n    maximumFractionDigits,\n  } = args;\n  const formatRender = DISPLAY_FORMATS.find(\n    (df) =>\n      df.type !== \"NONE\" &&\n      ((table && df.match?.(table, c)) ?? df.type === c.format?.type),\n  );\n  const onRender: ProstglesTableColumn[\"onRender\"] =\n    c.nested ?\n      ({ value, row }) => {\n        // const nestedTimeChartDates: number[] | undefined = c.nested?.chart && value && value.flatMap(nr => isObject(nr)? +new Date(nr.date) : -1)\n        // const allDatesAreValid = nestedTimeChartDates && nestedTimeChartDates.every(d => Number.isFinite(d));\n        // const nestedTimeChartMeta: NestedTimeChartMeta | undefined = !allDatesAreValid? undefined : {\n        //   fullExtent: [\n        //     new Date(Math.min(...nestedTimeChartDates)),\n        //     new Date(Math.max(...nestedTimeChartDates)),\n        //   ]\n        // };\n        const chartLimits = barchartVals?.[c.name];\n        const nestedTimeChartMeta: NestedTimeChartMeta | undefined =\n          chartLimits ?\n            {\n              fullExtent: [\n                new Date(chartLimits.min),\n                new Date(chartLimits.max),\n              ],\n            }\n          : undefined;\n\n        return (\n          <NestedColumnRender\n            value={value}\n            row={row}\n            c={c}\n            tables={tables}\n            nestedTimeChartMeta={nestedTimeChartMeta}\n          />\n        );\n      }\n    : c.style && c.style.type !== \"None\" ?\n      (rowInfo) => (\n        <StyledTableColumn\n          {...rowInfo}\n          c={c}\n          maxCellChars={maxCellChars}\n          barchartVals={barchartVals}\n        />\n      )\n    : formatRender ?\n      ({ row }) => {\n        let value = row[c.name];\n\n        const connectionId = location.pathname\n          .split(\"/\")\n          .find((p, i, arr) => arr[i - 1] === \"connections\");\n        if (c.info?.file) {\n          if (!value && c.format?.type === \"Media\") return null;\n          value = `${ROUTES.STORAGE}/${connectionId}/${row[c.name]}`;\n        }\n        return formatRender.render(value, row, c, c.format!, maxCellChars);\n      }\n    : table?.info.isFileTable && c.name === \"url\" ?\n      ({ value, row }) => {\n        return <MediaViewer key={value} url={value} />;\n      }\n      // c.udt_name.startsWith(\"json\")?  ({ row }) =>  <JsonRenderer value={row[c.name]} /> :\n    : /** Not pretty enough */\n    c.udt_name === \"interval\" ?\n      ({ row }) =>\n        Object.keys(row[c.name] ?? {})\n          .map((k) => `${row[c.name][k]} ${k}`)\n          .join(\", \")\n    : /** c.tsDataType and c.udt_name SHOULD NOT BE MISSING AT THIS POINT! */\n      ({ value }) => (\n        <RenderValue\n          column={c.computedConfig ?? c}\n          value={value}\n          showTitle={true}\n          maxLength={maxCellChars}\n          maximumFractionDigits={maximumFractionDigits}\n          getValues={getValues}\n        />\n      );\n\n  return onRender;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/prepareColsForRender.ts",
    "content": "import type { WindowSyncItem } from \"../../Dashboard/dashboardUtils\";\nimport { isNumericColumn } from \"../../W_SQL/getSQLResultTableColumns\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\nimport type { ProstglesTableColumn } from \"./getTableCols\";\n\nexport const prepareColsForRender = (\n  cols: ProstglesTableColumn[],\n  getWCols: () => ColumnConfigWInfo[],\n  w: WindowSyncItem<\"table\">,\n) => {\n  return cols\n    .filter((c) => {\n      const wcols = getWCols();\n      if (Array.isArray(wcols)) {\n        const match = wcols.find((_c) => _c.name === c.name);\n        if (match) {\n          return Boolean(match.show);\n        }\n      }\n      return !c.hidden;\n    })\n    .toSorted((a, b) => {\n      const wcols = getWCols();\n      if (Array.isArray(wcols)) {\n        const _a = wcols.findIndex((c) => c.name === a.name),\n          _b = wcols.findIndex((c) => c.name === b.name);\n        if (_a > -1 && _b > -1) {\n          return _a - _b;\n        }\n      }\n      return 0;\n    })\n    .map((c) => ({\n      ...c,\n      /* Align numbers to right for an easier read */\n      headerClassname:\n        c.style?.type === \"Barchart\" ? \"\"\n        : (\n          isNumericColumn(c) ||\n          c.nested?.columns.filter(\n            (nc) =>\n              nc.show &&\n              nc.computedConfig &&\n              isNumericColumn(nc.computedConfig),\n          ).length === 1\n        ) ?\n          \" jc-end  \"\n        : \" \",\n      className: isNumericColumn(c) ? \" ta-right \" : \" \",\n      onResize: (width: number) => {\n        const wcols = getWCols();\n        const currentCols = wcols;\n\n        const newCols = currentCols.map((_c) => {\n          if (_c.name === c.key) {\n            try {\n              _c.width = width;\n            } catch (e) {\n              console.error(e);\n            }\n          }\n          return { ..._c };\n        });\n        w.$update({\n          columns: JSON.parse(JSON.stringify(newCols)) as typeof newCols,\n        });\n      },\n    }));\n};\n"
  },
  {
    "path": "client/src/dashboard/W_Table/tableUtils/tableUtils.ts",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { DBSchemaTable } from \"prostgles-types\";\nimport { getKeys, pickKeys } from \"prostgles-types\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport type {\n  Join,\n  JoinV2,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { ColumnConfig, ColumnSortSQL } from \"../ColumnMenu/ColumnMenu\";\nimport { SORTABLE_CHART_COLUMNS } from \"../ColumnMenu/NestedTimechartControls\";\nimport type { ColumnConfigWInfo } from \"../W_Table\";\n\n/** It's a record to ensure all keys are present */\nconst COLUMN_CONFIG_KEYS: Record<keyof ColumnConfig, 1> = {\n  idx: 1,\n  computedConfig: 1,\n  format: 1,\n  name: 1,\n  show: 1,\n  style: 1,\n  width: 1,\n  nested: 1,\n};\nexport const getMinimalColumnInfo = <CWI extends ColumnConfigWInfo>(\n  columns: CWI[],\n): Pick<CWI, keyof ColumnConfig>[] => {\n  const colconfigKeys = getKeys(COLUMN_CONFIG_KEYS);\n  return columns.map((c) => pickKeys(c, colconfigKeys, true));\n};\n\nexport const updateWCols = (\n  w: WindowSyncItem<\"table\">,\n  newCols: WindowSyncItem<\"table\">[\"columns\"] = null,\n  nestedColumnName?: string,\n) => {\n  const newMinimalCols =\n    newCols ?\n      getMinimalColumnInfo(newCols).map((c) => {\n        if (c.nested) {\n          return {\n            ...c,\n            nested: {\n              ...c.nested,\n              columns: getMinimalColumnInfo(c.nested.columns),\n            },\n          };\n        }\n\n        return c;\n      })\n    : null;\n  if (nestedColumnName) {\n    const currCols = w.$get()?.columns;\n    if (!currCols) {\n      console.error(\"No w cols\");\n      return;\n    }\n    if (!newMinimalCols) {\n      console.error(\"No newMinimalCols\");\n      return;\n    }\n    return w.$update({\n      columns: currCols.map((c) => {\n        if (c.name === nestedColumnName) {\n          if (!c.nested) {\n            throw \"nestedColumnName not pointing to a nested column\";\n          }\n          /** Prevent hiding all nested cols */\n          // const noColsSelected = !newMinimalCols.some(nc => nc.show)\n          return { ...c, nested: { ...c.nested, columns: newMinimalCols } };\n        }\n        return c;\n      }),\n    });\n  }\n  return w.$update({ sort: [], columns: newMinimalCols });\n};\n\nexport const getSortColumn = (\n  sort: ColumnSortSQL,\n  columns: ColumnConfig[],\n): ColumnConfig | undefined => {\n  return columns.find((c) => {\n    return (\n      c.name === sort.key ||\n      (typeof sort.key === \"number\" &&\n        typeof c.name === \"string\" &&\n        c.idx === sort.key) ||\n      (c.nested?.chart?.type === \"time\" &&\n        SORTABLE_CHART_COLUMNS.some(\n          (sortCol) => sort.key === `${c.name}.${sortCol}`,\n        )) ||\n      c.nested?.columns.some((nc) => sort.key === `${c.name}.${nc.name}`)\n    );\n  });\n};\n\nexport const getSort = (\n  tables: CommonWindowProps[\"tables\"],\n  w: Pick<WindowSyncItem<\"table\">, \"sort\" | \"columns\" | \"table_name\">,\n): ColumnSortSQL[] => {\n  const { sort } = w;\n\n  if (!sort) return [];\n\n  let _sort: ColumnSortSQL[] = sort.map((s) => ({ ...s }));\n\n  const cols = tables.find((t) => t.name === w.table_name)?.columns;\n  if (!cols) return [];\n\n  const wcols = w.columns;\n  _sort = _sort.filter((s) => {\n    if (!wcols) {\n      /** Sort key must match a valid table column */\n      return cols.some((c) => c.name === s.key);\n    } else {\n      const wcol = getSortColumn(s, wcols);\n      if (wcol?.nested) {\n        return true;\n      }\n      return cols.some((c) => {\n        if (wcol?.computedConfig) {\n          /** CountAll doesn't require a column */\n          return (\n            !wcol.computedConfig.column || wcol.computedConfig.column === c.name\n          );\n        } else if (wcol) {\n          return wcol.name === c.name;\n        }\n      });\n    }\n  });\n\n  return _sort;\n};\n\nexport const getJoinedTables = (\n  tables: DBSchemaTable[],\n  tableName: string,\n  db: DBHandlerClient,\n): { joins: Join[]; joinsV2: JoinV2[] } => {\n  const myCols = tables.find((t) => t.name === tableName)?.columns;\n  const upsertJoin = (joins: Join[], upsertedJoin: Join) => {\n    /** Do not show join if the table is not available to user */\n    if (!db[upsertedJoin.tableName]) return;\n\n    let found = false;\n\n    /** Joined tables are not duplicated?!! */\n    joins.forEach((join) => {\n      if (join.tableName === upsertedJoin.tableName) {\n        found = true;\n        join.on = [...join.on, ...upsertedJoin.on];\n      }\n    });\n\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    if (!found) {\n      joins.push(upsertedJoin);\n    }\n  };\n\n  const joinsV2: JoinV2[] = [];\n  const upsertJoinV2 = (tableName: string, condition: [string, string][]) => {\n    /** Do not show join if the table is not available to user */\n    if (!db[tableName]) return;\n\n    const tableJoinIdx = joinsV2.findIndex((j) => j.tableName === tableName);\n    if (tableJoinIdx > -1) {\n      const onIdx = joinsV2[tableJoinIdx]!.on.findIndex((cond) =>\n        cond.every((cols) =>\n          condition.some((newCols) => cols.join() === newCols.join()),\n        ),\n      );\n      if (onIdx < 0) {\n        joinsV2[tableJoinIdx]!.on.push(condition);\n      }\n    } else {\n      joinsV2.push({\n        tableName,\n        on: [condition],\n      });\n    }\n  };\n\n  const myReferencedJoins: ReturnType<typeof getJoinedTables> = {\n    joins: [],\n    joinsV2: [],\n  };\n  myCols\n    ?.filter((c) => c.references)\n    .forEach((c) => {\n      c.references?.forEach(({ ftable, cols, fcols }) => {\n        upsertJoin(myReferencedJoins.joins, {\n          tableName: ftable,\n          on: cols.map((c, i) => [c, fcols[i]!]),\n        });\n        upsertJoinV2(\n          ftable,\n          cols.map((c, idx) => [c, fcols[idx]!]),\n        );\n      });\n    });\n  const myReferees: ReturnType<typeof getJoinedTables> = {\n    joins: [],\n    joinsV2: [],\n  };\n  tables\n    .filter((t) => t.name !== tableName)\n    .forEach(({ name, columns }) => {\n      columns.forEach((c) => {\n        const matchingReferences = c.references?.filter(\n          ({ ftable }) => ftable === tableName,\n        );\n        if (matchingReferences?.length) {\n          matchingReferences.forEach(({ fcols, cols }) => {\n            upsertJoin(myReferees.joins, {\n              tableName: name,\n              hasFkeys: true,\n              on: [[fcols[0]!, c.name]],\n            });\n            upsertJoinV2(\n              name,\n              fcols.map((c, idx) => [c, cols[idx]!]),\n            );\n          });\n        }\n      });\n    });\n\n  return {\n    joins: [...myReferencedJoins.joins, ...myReferees.joins],\n    joinsV2,\n  };\n};\n\n// static getCellStyle(\n//   row: AnyObject,\n//   wcol: ColumnConfig,\n//   limits?: { minValue: any; maxValue: any; }\n// ): CellStyle {\n//   let res = {};\n\n//   if (wcol && wcol.style && wcol.style.type && wcol.style.type !== \"None\") {\n\n//     if(wcol.style.type === \"Conditional\"){\n\n//     /* Need to get min max */\n//     } else if (\n//       wcol.style.type === \"Barchart\" ||\n//       wcol.style.type === \"Scale\"\n//     ) {\n//       let { minValue, maxValue } = limits || {};\n//       wcol.style.minValue = minValue;\n//       wcol.style.maxValue = maxValue;\n//     }\n\n//     c.getCellStyle = (row, val, rv) => {\n\n//       const style = StyleColumn.getStyle(wcol, wcol..tsDataType, row);\n//       let res: React.CSSProperties = {}\n//       if (style.cellColor) {\n//         res = { ...res, background: style.cellColor };\n//       }\n//       if (style.textColor) {\n//         res = { ...res, color: style.textColor };\n//       }\n//       if(wcol.style.type === \"Barchart\"){\n//         res = { ...res, border: \"1px solid var(--gray-200)\" };\n//       }\n//       return res;\n//     }\n\n//       c.onRender = (row, val, renderedVal) => {\n//         const style = StyleColumn.getStyle(wcol, c.tsDataType, row);\n//         if (style && style.chipColor) {\n//           return <div style={{\n//             backgroundColor: style.chipColor,\n//             padding: \"6px 8px\",\n//             borderRadius: \"1em\",\n//             width: \"fit-content\"\n//           }}>\n//             {renderedVal}\n//           </div>\n//         }\n\n//         return renderedVal;\n//       }\n\n//   }\n\n//   return res;\n// }\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/AddTimeChartFilter.tsx",
    "content": "import {\n  mdiClose,\n  mdiFilterCogOutline,\n  mdiFilterPlus,\n  mdiGestureTap,\n} from \"@mdi/js\";\nimport { useEffectDeep } from \"prostgles-client\";\nimport React, { useCallback, useMemo, useRef, useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport type { PopupProps } from \"@components/Popup/Popup\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { TimeChart } from \"../Charts/TimeChart/TimeChart\";\nimport { RenderValue } from \"../SmartForm/SmartFormField/RenderValue\";\nimport type { ActiveRow } from \"../W_Table/W_Table\";\nconst filterColor = \"rgba(5, 176, 223, 0.1)\";\nconst filterColorOpaque = \"rgb(226 248 255)\";\ntype DateFilter = { min: Date; max: Date };\ntype P = {\n  filter: { min: number; max: number } | undefined | null;\n  chartRef: TimeChart;\n  onStart: VoidFunction;\n  onEnd: (filter: DateFilter | undefined) => void;\n  myActiveRow: ActiveRow | undefined;\n  activeRowColor: string | undefined;\n  onCancelActiveRow: VoidFunction;\n};\n\nexport const AddTimeChartFilter = ({\n  onStart,\n  onEnd,\n  chartRef,\n  filter,\n  myActiveRow,\n  activeRowColor = \"blue\",\n  onCancelActiveRow,\n}: P) => {\n  const [newFilter, setNewFilter] = useState<\n    | {\n        changed?: boolean;\n        x1?: number;\n        x2?: number;\n        dates?: DateFilter;\n        anchor?: Pick<PopupProps, \"anchorEl\" | \"anchorXY\" | \"positioning\">;\n      }\n    | undefined\n  >();\n\n  const divRef = useRef<HTMLDivElement>(null);\n  const btnRef = useRef<HTMLButtonElement>(null);\n\n  useEffectDeep(() => {\n    if (!newFilter || !chartRef.canv || !chartRef.data) return;\n    const canvasLeft = chartRef.canv.getBoundingClientRect().left;\n    const onPointerMove = (e: PointerEvent) => {\n      if (!divRef.current || newFilter.x1 === undefined) return;\n      const x2 = e.clientX - canvasLeft;\n      const x1 = newFilter.x1;\n      const xLeft = Math.min(x1, x2);\n      const width = Math.max(x1, x2) - xLeft;\n      divRef.current.style.left = `${xLeft}px`;\n      divRef.current.style.width = `${width}px`;\n      divRef.current.style.top = \"0\";\n      divRef.current.style.bottom = \"0\";\n      divRef.current.style.opacity = \"1\";\n    };\n    const onPointerDown = (e: PointerEvent) => {\n      const x = e.clientX - canvasLeft;\n      if (newFilter.x1 === undefined) {\n        setNewFilter({ x1: x });\n      } else {\n        if (!chartRef.data) return;\n        const x2 = x;\n        const { x1 } = newFilter;\n        setNewFilter({\n          x1,\n          x2,\n          changed: true,\n          anchor: {\n            anchorXY: {\n              x: e.pageX,\n              y: e.pageY,\n            },\n          },\n          dates: {\n            min: new Date(\n              Math.round(chartRef.data.xScale.invert(Math.min(x1, x2))),\n            ),\n            max: new Date(\n              Math.round(chartRef.data.xScale.invert(Math.max(x1, x2))),\n            ),\n          },\n        });\n      }\n    };\n\n    chartRef.canv.addEventListener(\"pointermove\", onPointerMove);\n    chartRef.canv.addEventListener(\"pointerdown\", onPointerDown);\n    return () => {\n      chartRef.canv?.removeEventListener(\"pointermove\", onPointerMove);\n      chartRef.canv?.removeEventListener(\"pointerdown\", onPointerDown);\n    };\n  }, [chartRef, divRef, newFilter, onEnd]);\n\n  const stop = useCallback(() => {\n    onEnd(undefined);\n    setNewFilter(undefined);\n  }, [onEnd]);\n\n  const activeRow = useMemo(() => {\n    if (!myActiveRow?.timeChart || !chartRef.canv || !chartRef.data) return;\n    const { min, max } = myActiveRow.timeChart;\n    const leftPoint = chartRef.getPointXY({ date: min, value: 0 });\n    const rightPoint = chartRef.getPointXY({ date: max, value: 0 });\n    if (!leftPoint || !rightPoint) return;\n    const [xLeft] = leftPoint;\n    const [xRight] = rightPoint;\n    return {\n      xLeft,\n      xRight,\n      min,\n      max,\n    };\n  }, [myActiveRow, chartRef]);\n\n  const mainButton = (\n    <Btn\n      _ref={btnRef}\n      title={filter ? \"Edit filter\" : \"Add filter\"}\n      data-command=\"W_TimeChart.AddTimeChartFilter\"\n      color={newFilter || filter ? \"action\" : undefined}\n      iconPath={filter ? mdiFilterCogOutline : mdiFilterPlus}\n      onClick={({ currentTarget }) => {\n        if (filter) {\n          setNewFilter({\n            anchor: {\n              anchorEl: currentTarget,\n              positioning: \"beneath-left\",\n            },\n            dates: {\n              min: new Date(filter.min),\n              max: new Date(filter.max),\n            },\n          });\n        } else {\n          const isStart = !newFilter;\n          if (isStart) {\n            onStart();\n            setNewFilter({});\n          } else {\n            stop();\n          }\n        }\n      }}\n    />\n  );\n\n  return (\n    <>\n      <FlexRow\n        className=\"AddTimeChartFilter\"\n        style={{ position: \"absolute\", right: 0, top: 0, zIndex: 1 }}\n      >\n        {newFilter && !newFilter.dates && (\n          <InfoRow\n            iconPath={mdiGestureTap}\n            className=\"shadow bg-color-0 px-1 py-p5\"\n            color=\"info\"\n            variant=\"naked\"\n          >\n            Tap two points you want to filter between\n          </InfoRow>\n        )}\n\n        {filter ?\n          <TimestampFilter\n            min={new Date(filter.min)}\n            max={new Date(filter.max)}\n            style={{ background: filterColorOpaque }}\n            endIcon={mainButton}\n          />\n        : mainButton}\n      </FlexRow>\n      {newFilter && !newFilter.dates && (\n        <div\n          ref={divRef}\n          style={{\n            position: \"absolute\",\n            borderLeft: `1px solid ${filterColor}`,\n            borderRight: `1px solid ${filterColor}`,\n            zIndex: 1,\n            background: filterColor,\n            opacity: 0,\n            pointerEvents: \"none\",\n          }}\n        />\n      )}\n      {activeRow && chartRef.chart && (\n        <FlexCol\n          data-command=\"W_TimeChart.ActiveRow\"\n          className=\"gap-0 absolute ws-nowrap\"\n          onClick={onCancelActiveRow}\n          style={{\n            zIndex: 1,\n            top: 0,\n            bottom: 0,\n            left: `${activeRow.xLeft}px`,\n          }}\n        >\n          <TimestampFilter\n            style={{\n              background: filterColorOpaque,\n              ...(chartRef.chart.getWH().w - activeRow.xLeft < 250 ?\n                {\n                  transform: \"translate(-228px, 0)\",\n                  borderBottomRightRadius: 0,\n                }\n              : {\n                  borderBottomLeftRadius: 0,\n                }),\n            }}\n            {...activeRow}\n          />\n          <div\n            style={{\n              borderLeft: `1px ${activeRowColor} blue`,\n              borderRight: `1px ${activeRowColor} blue`,\n              background: activeRowColor,\n              bottom: 0,\n              flex: 1,\n              height: \"100%\",\n              width: `${activeRow.xRight - activeRow.xLeft}px`,\n            }}\n          />\n        </FlexCol>\n      )}\n      {newFilter?.dates && newFilter.anchor && (\n        <Popup\n          title={filter ? \"Edit filter\" : \"Add filter\"}\n          {...newFilter.anchor}\n          clickCatchStyle={{ opacity: 0.2 }}\n          onClose={() => {\n            setNewFilter(undefined);\n          }}\n          footerButtons={[\n            {\n              label: \"Remove\",\n              color: \"danger\",\n              onClick: stop,\n            },\n            !filter || newFilter.changed ?\n              undefined\n            : {\n                label: \"On chart edit\",\n                color: \"action\",\n                variant: \"filled\",\n                onClick: () => {\n                  onStart();\n                  setNewFilter({});\n                },\n              },\n            !newFilter.changed && filter ?\n              undefined\n            : {\n                label: filter ? \"Update filter\" : \"Add filter\",\n                color: \"action\",\n                variant: \"filled\",\n                onClick: () => {\n                  onEnd(newFilter.dates);\n                  setNewFilter(undefined);\n                },\n              },\n          ]}\n        >\n          <FlexCol>\n            <FormField\n              type=\"text\"\n              value={newFilter.dates.min.toISOString()}\n              onChange={(v) =>\n                setNewFilter({\n                  ...newFilter,\n                  changed: true,\n                  dates: { ...newFilter.dates!, min: new Date(v) },\n                })\n              }\n            />\n            <FormField\n              type=\"text\"\n              value={newFilter.dates.max.toISOString()}\n              onChange={(v) =>\n                setNewFilter({\n                  ...newFilter,\n                  changed: true,\n                  dates: { ...newFilter.dates!, max: new Date(v) },\n                })\n              }\n            />\n          </FlexCol>\n        </Popup>\n      )}\n    </>\n  );\n};\n\ntype TimestampFilterProps = {\n  min: Date;\n  max: Date;\n  style: React.CSSProperties;\n  endIcon?: React.ReactNode;\n};\nconst TimestampFilter = ({\n  min,\n  max,\n  style,\n  endIcon,\n}: TimestampFilterProps) => {\n  return (\n    <FlexRow\n      className=\"TimestampFilter gap-1 p-p5 rounded ai-start\"\n      style={style}\n    >\n      <FlexCol className=\"gap-p5\">\n        <div style={{}}>\n          {RenderValue({\n            column: { udt_name: \"timestamptz\", tsDataType: \"any\" },\n            value: min,\n          })}\n        </div>\n        <div style={{}}>\n          {RenderValue({\n            column: { udt_name: \"timestamptz\", tsDataType: \"any\" },\n            value: max,\n          })}\n        </div>\n      </FlexCol>\n      {endIcon ?? (\n        <Btn\n          size=\"small\"\n          iconPath={mdiClose}\n          style={{ marginTop: \"-8px\", marginRight: \"-8px\" }}\n        />\n      )}\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/W_TimeChart.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { throttle } from \"@common/utils\";\nimport Loading from \"@components/Loader/Loading\";\nimport type { SingleSyncHandles } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { AnyObject, SubscriptionHandler } from \"prostgles-types\";\nimport { getKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport type { Command } from \"../../Testing\";\nimport type { DateExtent } from \"../Charts/TimeChart/getTimechartBinSize\";\nimport { getMainTimeBinSizes } from \"../Charts/TimeChart/getTimechartBinSize\";\nimport type { TimeChartLayer } from \"../Charts/TimeChart/TimeChart\";\nimport { TimeChart } from \"../Charts/TimeChart/TimeChart\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { WindowData, WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport RTComp, { type DeltaOf, type DeltaOfData } from \"../RTComp\";\nimport type { LayerBase } from \"../W_Map/W_Map\";\nimport type { ActiveRow } from \"../W_Table/W_Table\";\nimport Window from \"../Window\";\nimport { fetchAndSetTimechartLayerData } from \"./fetchData/fetchAndSetTimechartLayerData\";\nimport { getTimeChartLayerQueries } from \"./fetchData/getTimeChartLayers\";\nimport type { TimeChartLayerWithBinOrError } from \"./fetchData/getTimeChartLayersWithBins\";\nimport { getTimeChartSelectDate } from \"./fetchData/getTimeChartSelectParams\";\nimport { W_TimeChartHeaderControls } from \"./W_TimeChartHeaderControls\";\nimport type { TimeChartBinSize } from \"./W_TimeChartMenu\";\nimport { ProstglesTimeChartMenu } from \"./W_TimeChartMenu\";\n\nexport type TimeChartLinkOptions = Extract<\n  DBSSchema[\"links\"][\"options\"],\n  { type: \"timechart\" }\n>;\n\ntype LinkDataOptions =\n  | Extract<TimeChartLinkOptions[\"dataSource\"], { type: \"sql\" }>\n  | (Extract<TimeChartLinkOptions[\"dataSource\"], { type: \"table\" }> & {\n      tableName: string;\n      externalFilters: AnyObject[];\n    })\n  | Extract<TimeChartLinkOptions[\"dataSource\"], { type: \"local-table\" }>;\n\nexport type ProstglesTimeChartLayer = Pick<\n  LayerBase,\n  \"_id\" | \"linkId\" | \"disabled\"\n> & {\n  title?: string;\n  dateColumn: string;\n  groupByColumn: string | undefined;\n\n  /**\n   * If none then COUNT(*) will be used\n   */\n  statType:\n    | {\n        funcName: Required<\n          TimeChartLinkOptions[\"columns\"][number]\n        >[\"statType\"][\"funcName\"];\n        numericColumn: string;\n      }\n    | undefined;\n  color?: string;\n} & LinkDataOptions;\n\nexport type W_TimeChartProps = Omit<CommonWindowProps, \"w\"> & {\n  onClickRow: (\n    row: AnyObject | undefined,\n    tableName: string,\n    values: ActiveRow[\"timeChart\"],\n  ) => void;\n  myActiveRow: ActiveRow | undefined;\n  activeRowColor: string | undefined;\n  w: WindowSyncItem<\"timechart\">;\n};\n\nexport type W_TimeChartStateLayer = TimeChartLayer & {\n  linkId: string;\n  extFilter:\n    | {\n        filter: any;\n        paddedEdges: [Date, Date];\n      }\n    | undefined;\n  /** This is used in caching results (extent filter is excluded) */\n  dataSignature: string;\n};\n\nexport type W_TimeChartState = {\n  loading: boolean;\n  wSync: SingleSyncHandles<Required<WindowData<\"timechart\">>, true> | null;\n  error?: unknown;\n  layers: W_TimeChartStateLayer[];\n  erroredLayers?: TimeChartLayerWithBinOrError[];\n  columns: any[];\n  xExtent?: [Date, Date];\n  visibleDataExtent?: DateExtent;\n  viewPortExtent?: DateExtent;\n  resetExtent?: number;\n  binSize?: TimeChartBinSize;\n  loadingData: boolean;\n  addingFilter?: boolean;\n};\n\ntype D = {\n  extent?: DateExtent;\n  w?: WindowSyncItem<\"timechart\">;\n  lCols: {\n    [key: string]: W_TimeChartProps[\"tables\"][number][\"columns\"];\n  };\n  dataAge: number;\n};\n\nexport class W_TimeChart extends RTComp<W_TimeChartProps, W_TimeChartState, D> {\n  refHeader?: HTMLDivElement;\n  refResize?: HTMLElement;\n  ref?: HTMLElement;\n\n  state: W_TimeChartState = {\n    resetExtent: 0,\n    xExtent: undefined,\n    visibleDataExtent: undefined,\n    loading: false,\n    wSync: null,\n    layers: [],\n    error: null,\n    loadingData: true,\n    columns: [],\n  };\n\n  onMount() {\n    const { w } = this.props;\n    if (!this.state.wSync) {\n      const wSync = w.$cloneSync((w, delta) => {\n        this.setData({ w }, { w: delta });\n      });\n\n      this.setState({ wSync });\n    }\n  }\n\n  onUnmount() {\n    this.state.wSync?.$unsync();\n    Object.values(this.layerSubscriptions).forEach(({ sub }) => {\n      void sub?.unsubscribe();\n    });\n  }\n\n  onDelta(\n    dp: DeltaOf<W_TimeChartProps>,\n    ds: DeltaOf<W_TimeChartState>,\n    dd: DeltaOfData<D>,\n  ): void {\n    const deltaKeys = getKeys({ ...dp!, ...ds!, ...dd! });\n    const filterChanged = dd?.w?.options && \"filter\" in dd.w.options;\n    if (\n      filterChanged ||\n      dd?.w?.options?.binSize ||\n      (\n        [\n          \"myLinks\",\n          \"w\",\n          \"resetExtent\",\n          \"viewPortExtent\",\n          \"prgl\",\n          \"dataAge\",\n        ] as const\n      ).find((k) => deltaKeys.includes(k))\n    ) {\n      void this.setLayerData();\n    }\n\n    if (filterChanged) {\n      this.props.onForceUpdate();\n    }\n  }\n\n  d: D = {\n    lCols: {},\n    w: undefined,\n    extent: undefined,\n    dataAge: 0,\n  };\n\n  /* Throttle data updates */\n  dataAge?: number;\n  setDataAge = (dataAge: number) => {\n    this.dataAge = dataAge;\n    this._setDataAge();\n  };\n  _setDataAge = throttle(() => {\n    this.setData({ dataAge: this.dataAge });\n  }, 300);\n\n  layerSubscriptions: Record<\n    string,\n    {\n      externalFilters: any;\n      realtimeOpts: any;\n      sub: SubscriptionHandler | undefined;\n      /**\n       * Data age of the fetched data\n       */\n      dataAge: number;\n      /**\n       * Received from subscription\n       */\n      latestDataAge: number;\n      isLoading: boolean;\n    }\n  > = {};\n\n  dataStr?: string;\n  setLayerData = fetchAndSetTimechartLayerData.bind(this);\n\n  settingExtent: NodeJS.Timeout | null = null;\n  private visibleDataExtent?: DateExtent;\n  private viewPortExtent?: DateExtent;\n  setVisibleExtent(\n    data: W_TimeChartState[\"visibleDataExtent\"],\n    viewPort: DateExtent,\n  ) {\n    this.visibleDataExtent = data;\n    this.viewPortExtent = viewPort;\n    if (this.settingExtent) clearTimeout(this.settingExtent);\n\n    this.settingExtent = setTimeout(() => {\n      this.setState({\n        visibleDataExtent: this.visibleDataExtent,\n        viewPortExtent: this.viewPortExtent,\n      });\n      this.visibleDataExtent = undefined;\n      this.viewPortExtent = undefined;\n      this.settingExtent = null;\n    }, 100);\n  }\n\n  get layerQueries() {\n    const { getLinksAndWindows, myLinks, w, active_row } = this.props;\n    const { links, windows } = getLinksAndWindows();\n    return getTimeChartLayerQueries({ active_row, links, myLinks, w, windows });\n  }\n\n  getMenu = (w: WindowSyncItem<\"timechart\">) => {\n    return <ProstglesTimeChartMenu w={w} autoBinSize={this.state.binSize} />;\n  };\n\n  onColorLegendChanged = () => {\n    this.setData({ dataAge: Date.now() });\n  };\n\n  menuAnchor?: HTMLDivElement;\n  chartRef?: TimeChart;\n  render() {\n    const {\n      layers: fetchedLayers = [],\n      erroredLayers,\n      error: fetchingError,\n      loadingData,\n      addingFilter = false,\n    } = this.state;\n\n    const { onClickRow, workspace } = this.props;\n    const { w } = this.d;\n    if (!w) return <Loading className=\"m-auto f-1\" />;\n\n    const { layerQueries } = this;\n    /** This ensures layers are removed instantly */\n    const layers = fetchedLayers.filter((l) =>\n      layerQueries.find((lq) => lq._id === l.linkId),\n    );\n\n    const resetExtent = (refetchData?: true) => {\n      if (this.chartRef?.chart) {\n        this.chartRef.chart.setView({ xO: 0, xScale: 1, yO: 0, yScale: 1 });\n        this.setState({\n          visibleDataExtent: undefined,\n          viewPortExtent: undefined,\n          ...(refetchData && {\n            resetExtent: Date.now(),\n          }),\n        });\n      }\n    };\n\n    const binSizeName = this.state.binSize;\n    const binSize =\n      !binSizeName || binSizeName === \"auto\" ?\n        undefined\n      : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n        getMainTimeBinSizes()[binSizeName]?.size;\n\n    return (\n      <Window\n        w={w}\n        getMenu={this.getMenu}\n        layoutMode={workspace.layout_mode ?? \"editable\"}\n      >\n        <div\n          ref={(r) => {\n            if (r) this.menuAnchor = r;\n          }}\n          style={{ width: \"1px\", height: \"1px\" }}\n        />\n\n        <div\n          className=\"W_TimeChart relative f-1 flex-col min-h-0 min-w-0 noselect \"\n          data-command={\"W_TimeChart\" satisfies Command}\n          style={{\n            backgroundColor: \"var(--color-timechart-bg)\",\n          }}\n          ref={(r) => {\n            if (r) this.ref = r;\n          }}\n        >\n          {this.chartRef && (\n            <W_TimeChartHeaderControls\n              {...this.props}\n              w={w}\n              layers={layers}\n              loadingData={loadingData}\n              onColorLegendChanged={this.onColorLegendChanged}\n              fetchingError={fetchingError}\n              layerQueries={layerQueries}\n              erroredLayers={erroredLayers}\n              chartRef={this.chartRef}\n              visibleDataExtent={this.state.visibleDataExtent}\n              setAddingFilter={(addingFilter) => {\n                this.setState({ addingFilter });\n              }}\n              resetExtent={resetExtent}\n            />\n          )}\n          <TimeChart\n            key={this.state.resetExtent}\n            layers={layers}\n            chartRef={(ref) => {\n              this.chartRef = ref;\n            }}\n            yAxisScaleMode={w.options.yScaleMode}\n            onExtentChanged={(extent, viewPort, opts) => {\n              if (opts.resetExtent) {\n                resetExtent();\n              } else {\n                this.setVisibleExtent(extent, viewPort);\n              }\n              onClickRow(undefined, \"\", undefined);\n            }}\n            onClick={({ dateMillis, isMinDate }) => {\n              if (this.state.addingFilter) return;\n              const [firstLink, ...otherLinks] = this.props.myLinks;\n              if (\n                firstLink?.options.type === \"timechart\" &&\n                !otherLinks.length &&\n                binSize &&\n                this.state.binSize &&\n                this.state.binSize !== \"auto\"\n              ) {\n                const { windows } = this.props.getLinksAndWindows();\n                const myTable = windows.find(\n                  (w) =>\n                    w.id !== this.d.w?.id &&\n                    [firstLink.w1_id, firstLink.w2_id].includes(w.id),\n                );\n                if (myTable?.type === \"table\" && myTable.table_name) {\n                  const dateColumn = firstLink.options.columns[0]?.name;\n                  if (dateColumn) {\n                    const min = new Date(dateMillis);\n                    const max = new Date(dateMillis + binSize);\n                    const dateSelect = getTimeChartSelectDate({\n                      dateColumn,\n                      bin: this.state.binSize,\n                    });\n                    const filter = {\n                      $filter: [\n                        dateSelect,\n                        isMinDate ? \"<=\" : \"=\",\n                        new Date(dateMillis),\n                      ],\n                    };\n                    onClickRow(filter, myTable.table_name, {\n                      min,\n                      max,\n                      center: new Date(dateMillis),\n                    });\n                  }\n                }\n              }\n            }}\n            zoomPanDisabled={addingFilter}\n            tooltipPosition={w.options.tooltipPosition ?? \"auto\"}\n            renderStyle={w.options.renderStyle ?? \"line\"}\n            showBinLabels={w.options.showBinLabels ?? \"off\"}\n            showGradient={w.options.showGradient ?? true}\n            binValueLabelMaxDecimals={w.options.binValueLabelMaxDecimals}\n            binSize={binSize}\n          />\n        </div>\n      </Window>\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/W_TimeChartHeaderControls.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexRow } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { mdiAlertCircleOutline, mdiUndo } from \"@mdi/js\";\nimport { isDefined } from \"prostgles-types\";\nimport React from \"react\";\nimport type { TimeChart } from \"../Charts/TimeChart/TimeChart\";\nimport { DataLayerManager } from \"../WindowControls/DataLayerManager/DataLayerManager\";\nimport { AddTimeChartFilter } from \"./AddTimeChartFilter\";\nimport type {\n  ProstglesTimeChartLayer,\n  W_TimeChartProps,\n  W_TimeChartState,\n} from \"./W_TimeChart\";\nimport { W_TimeChartLayerLegend } from \"./W_TimeChartLayerLegend\";\n\ntype P = Pick<\n  W_TimeChartProps,\n  | \"w\"\n  | \"onClickRow\"\n  | \"activeRowColor\"\n  | \"myActiveRow\"\n  | \"myLinks\"\n  | \"getLinksAndWindows\"\n> & {\n  chartRef: TimeChart;\n  layerQueries: ProstglesTimeChartLayer[];\n  fetchingError: unknown;\n  visibleDataExtent: W_TimeChartState[\"visibleDataExtent\"];\n  erroredLayers: W_TimeChartState[\"erroredLayers\"];\n  layers: W_TimeChartState[\"layers\"];\n  loadingData: W_TimeChartState[\"loadingData\"];\n  onColorLegendChanged: VoidFunction;\n  setAddingFilter: (adding: boolean) => void;\n  resetExtent: (refetchData?: true) => void;\n};\nexport const W_TimeChartHeaderControls = (props: P) => {\n  const {\n    chartRef,\n    onClickRow,\n    myActiveRow,\n    activeRowColor,\n    visibleDataExtent,\n    erroredLayers,\n    w,\n    layerQueries,\n    fetchingError,\n    layers,\n    loadingData,\n    onColorLegendChanged,\n    setAddingFilter,\n    resetExtent,\n  } = props;\n  const error =\n    fetchingError ?? (erroredLayers?.[0]?.hasError && erroredLayers[0].error);\n\n  const onCancelActiveRow = () => onClickRow(undefined, \"\", undefined);\n  const infoSection = (\n    <FlexRow\n      className=\"W_TimeChart_TopBar gap-0 relative f-1 m-auto \"\n      style={{\n        position: \"absolute\",\n        top: \"0\",\n        left: \"0\",\n        /** Ensure it doesn't clash with right add filter button */\n        maxWidth: \"calc(100% - 60px)\",\n        /* Ensure it doesn't cover the tooltip active row brush */\n        zIndex: 1,\n      }}\n    >\n      <DataLayerManager\n        {...props}\n        w={w}\n        type=\"timechart\"\n        asMenuBtn={{}}\n        layerQueries={layerQueries}\n      />\n      {isDefined(error) && (\n        <PopupMenu\n          button={<Btn color=\"danger\" iconPath={mdiAlertCircleOutline} />}\n        >\n          <div className=\"bg-color-0\">\n            <ErrorComponent error={error} findMsg={true} />\n          </div>\n        </PopupMenu>\n      )}\n      <Btn\n        title=\"Reset extent\"\n        data-command=\"W_TimeChart.resetExtent\"\n        style={{\n          opacity: visibleDataExtent ? 1 : 0,\n        }}\n        iconPath={mdiUndo}\n        onClick={() => resetExtent(true)}\n      />\n      <W_TimeChartLayerLegend\n        {...props}\n        layers={layers}\n        layerQueries={layerQueries}\n        onChanged={onColorLegendChanged}\n      />\n    </FlexRow>\n  );\n\n  return (\n    <>\n      {loadingData && w.options.refresh?.type !== \"Realtime\" && (\n        <Loading variant=\"cover\" delay={1500} />\n      )}\n      {infoSection}\n      <AddTimeChartFilter\n        activeRowColor={activeRowColor}\n        myActiveRow={myActiveRow}\n        filter={w.options.filter}\n        chartRef={chartRef}\n        onCancelActiveRow={onCancelActiveRow}\n        onStart={() => {\n          onCancelActiveRow();\n          setAddingFilter(true);\n        }}\n        onEnd={(filter) => {\n          const newFilter =\n            !filter ? null : (\n              {\n                min: +filter.min,\n                max: +filter.max,\n              }\n            );\n          w.$update({\n            options: { ...w.options, filter: newFilter },\n          });\n          setAddingFilter(false);\n          resetExtent();\n        }}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/W_TimeChartLayerLegend.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { mdiClose } from \"@mdi/js\";\nimport React from \"react\";\nimport type { CommonWindowProps } from \"../Dashboard/Dashboard\";\nimport type { WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { ColorByLegend } from \"../WindowControls/ColorByLegend/ColorByLegend\";\nimport type { ChartLinkOptions } from \"../WindowControls/DataLayerManager/DataLayer\";\nimport { useSortedLayerQueries } from \"../WindowControls/DataLayerManager/useSortedLayerQueries\";\nimport { LayerColorPicker } from \"../WindowControls/LayerColorPicker\";\nimport { TimeChartLayerOptions } from \"../WindowControls/TimeChartLayerOptions\";\nimport type {\n  ProstglesTimeChartLayer,\n  W_TimeChartStateLayer,\n} from \"./W_TimeChart\";\n\ntype P = Pick<CommonWindowProps, \"getLinksAndWindows\" | \"myLinks\"> & {\n  layerQueries: ProstglesTimeChartLayer[];\n  layers: W_TimeChartStateLayer[];\n  onChanged: VoidFunction;\n  w: WindowSyncItem<\"timechart\">;\n};\n\nexport const W_TimeChartLayerLegend = ({\n  layerQueries,\n  layers,\n  onChanged,\n  ...props\n}: P) => {\n  const { w, myLinks } = props;\n\n  const activeLayerQueries = useSortedLayerQueries({\n    layerQueries,\n    myLinks,\n  }).filter((l) => !l.disabled);\n\n  return (\n    <ScrollFade className=\"W_TimeChartLayerLegend flex-row gap-1 min-w-0 o-auto no-scroll-bar\">\n      {activeLayerQueries.map(\n        ({ _id, linkId, dateColumn, groupByColumn, link }) => {\n          return (\n            <FlexRow key={_id} className=\"W_TimeChartLayerLegend_Item gap-0\">\n              {!groupByColumn && (\n                <LayerColorPicker\n                  btnProps={{ size: \"nano\" }}\n                  title={\"layerDesc\"}\n                  column={dateColumn}\n                  linkOptions={link.options as ChartLinkOptions}\n                  onChange={(newOptions) => {\n                    link.$update({ options: newOptions }, { deepMerge: true });\n                  }}\n                />\n              )}\n\n              <TimeChartLayerOptions\n                w={w}\n                getLinksAndWindows={props.getLinksAndWindows}\n                link={link}\n                myLinks={myLinks}\n                column={dateColumn}\n                mode=\"on-screen\"\n              />\n              {groupByColumn && (\n                <ColorByLegend\n                  {...props}\n                  className=\"ml-1\"\n                  layers={layers}\n                  layerLinkId={linkId}\n                  groupByColumn={groupByColumn}\n                  onChanged={onChanged}\n                />\n              )}\n              <Btn\n                iconPath={mdiClose}\n                size=\"micro\"\n                onClick={() => {\n                  const isLastLayer = activeLayerQueries.length === 1;\n                  link.$update({ closed: true, deleted: true });\n                  if (isLastLayer && w.parent_window_id) {\n                    w.$update({ closed: true, deleted: true });\n                  }\n                }}\n              />\n            </FlexRow>\n          );\n        },\n      )}\n    </ScrollFade>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/W_TimeChartMenu.tsx",
    "content": "import { mdiPanHorizontal, mdiSyncCircle } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport type { WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { AutoRefreshMenu } from \"../W_Table/TableMenu/AutoRefreshMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { includes } from \"../W_SQL/W_SQLBottomBar/W_SQLBottomBar\";\n\ntype P = {\n  w: WindowSyncItem<\"timechart\">;\n  autoBinSize: string | undefined;\n};\n\nconst BIN_LABEL_OPTIONS = [\n  { key: \"off\", label: \"Off\" },\n  { key: \"all points\", label: \"All points\" },\n  { key: \"peaks and troughs\", label: \"Peaks and troughs\" },\n  { key: \"latest point\", label: \"Last point\" },\n] as const;\nexport type ShowBinLabelsMode = (typeof BIN_LABEL_OPTIONS)[number][\"key\"];\n\nconst TooltipPositions = [\n  { key: \"auto\", label: \"Auto\", subLabel: \"Shows closest to the points\" },\n  { key: \"top\", label: \"Top\", subLabel: \"Top of chart\" },\n  { key: \"middle\", label: \"Middle\", subLabel: \"Middle of chart\" },\n  { key: \"bottom\", label: \"Bottom\", subLabel: \"Bottom of chart\" },\n  { key: \"hidden\", label: \"Hidden\", subLabel: \"No tooltip\" },\n] as const;\nexport type TooltipPosition = (typeof TooltipPositions)[number][\"key\"];\n\nconst MissingBinsOptions = [\n  { key: \"show 0\", subLabel: \"Empty bins will be shown as 0\" },\n  { key: \"ignore\", subLabel: \"Lines will join existing points\" },\n  {\n    key: \"show nearest\",\n    subLabel: \"Each missing bin will show the nearest existing bin\",\n  },\n] as const;\nexport type MissingBinsOption = (typeof MissingBinsOptions)[number][\"key\"];\n\nexport const TimechartRenderStyles = [\n  { key: \"scatter plot\", label: \"Scatter plot\" },\n  { key: \"line\", label: \"Line chart\" },\n  { key: \"smooth\", label: \"Smooth line chart\" },\n  { key: \"bars\", label: \"Bar chart\" },\n] as const;\nexport type TimechartRenderStyle =\n  (typeof TimechartRenderStyles)[number][\"key\"];\n\nexport const ProstglesTimeChartMenu = ({ w, autoBinSize }: P) => {\n  const {\n    binSize = \"auto\",\n    tooltipPosition = \"auto\",\n    missingBins = \"ignore\",\n    renderStyle = \"line\",\n    showBinLabels = \"off\",\n    showGradient = true,\n    binValueLabelMaxDecimals = null,\n  } = w.options;\n\n  const displayedBinSize =\n    binSize === \"auto\" && autoBinSize !== undefined ?\n      `Auto (${autoBinSize})`\n    : TIMECHART_BIN_SIZES.find((o) => o.key === binSize)?.label;\n\n  return (\n    <FlexCol className=\"p-1\">\n      <FormField\n        type=\"text\"\n        label={\"Name\"}\n        value={w.name || \"\"}\n        onChange={(newTitle) => {\n          w.$update({ name: newTitle });\n        }}\n      />\n      <Select\n        className=\"w-fit\"\n        label=\"Bin size\"\n        value={\n          binSize === \"auto\" && autoBinSize !== undefined ?\n            `Auto (${autoBinSize})`\n          : binSize\n        }\n        btnProps={{\n          children: displayedBinSize,\n          title: \"Bin size\",\n          color: \"action\",\n          iconPath: \"\", // mdiPanHorizontal,\n        }}\n        iconPath={mdiPanHorizontal}\n        fullOptions={TIMECHART_BIN_SIZES}\n        onChange={(binSize) => {\n          w.$update({ options: { binSize } }, { deepMerge: true });\n        }}\n      />\n      <Select\n        label=\"Tooltip\"\n        value={tooltipPosition}\n        fullOptions={TooltipPositions}\n        onChange={(tooltipPosition) => {\n          w.$update({ options: { tooltipPosition } }, { deepMerge: true });\n        }}\n      />\n      <FlexRow>\n        <Select\n          label=\"Chart style\"\n          value={renderStyle}\n          fullOptions={TimechartRenderStyles}\n          onChange={(renderStyle) => {\n            w.$update({ options: { renderStyle } }, { deepMerge: true });\n          }}\n        />\n        {includes(renderStyle, [\"line\", \"smooth\"]) && (\n          <SwitchToggle\n            label={\"Show gradient\"}\n            checked={showGradient}\n            variant=\"col\"\n            onChange={(showGradient) => {\n              w.$update({ options: { showGradient } }, { deepMerge: true });\n            }}\n          />\n        )}\n      </FlexRow>\n\n      <FlexRow>\n        <Select\n          label=\"Show value labels\"\n          value={showBinLabels}\n          fullOptions={BIN_LABEL_OPTIONS}\n          onChange={(showBinLabels) => {\n            w.$update({ options: { showBinLabels } }, { deepMerge: true });\n          }}\n        />\n        <FormField\n          label=\"Max decimals\"\n          value={binValueLabelMaxDecimals}\n          disabledInfo={\n            showBinLabels === \"off\" ?\n              \"Must enable 'Show value labels'\"\n            : undefined\n          }\n          nullable={true}\n          style={{ maxWidth: \"150px\" }}\n          inputProps={{ min: 0, max: 1e3, step: 1 }}\n          onChange={(v) => {\n            const binValueLabelMaxDecimals = v ? +v : v;\n            w.$update(\n              { options: { binValueLabelMaxDecimals } },\n              { deepMerge: true },\n            );\n          }}\n        />\n      </FlexRow>\n\n      {renderStyle === \"line\" && (\n        <Select\n          label=\"Missing bins\"\n          // disabledInfo={\"No other modes supported at the moment\"}\n          value={missingBins}\n          fullOptions={MissingBinsOptions}\n          onChange={(missingBins) => {\n            w.$update({ options: { missingBins } }, { deepMerge: true });\n          }}\n        />\n      )}\n      <PopupMenu\n        onClickClose={false}\n        button={\n          <Btn\n            variant=\"faded\"\n            color={\n              w.options.refresh?.type === \"Realtime\" ? \"action\" : undefined\n            }\n            iconPath={mdiSyncCircle}\n          >\n            Data refresh\n          </Btn>\n        }\n      >\n        <AutoRefreshMenu w={w} />\n      </PopupMenu>\n    </FlexCol>\n  );\n};\n\nexport const TIMECHART_BIN_SIZES = [\n  { key: \"auto\", label: \"Auto\" },\n  { key: \"year\", label: \"1 Year\" },\n  { key: \"month\", label: \"1 Month\" },\n  { key: \"week\", label: \"1 Week\" },\n  { key: \"day\", label: \"1 Day\" },\n  { key: \"8hour\", label: \"8 Hours\" },\n  { key: \"4hour\", label: \"4 Hours\" },\n  { key: \"2hour\", label: \"2 Hours\" },\n  { key: \"hour\", label: \"1 Hour\" },\n  { key: \"30minute\", label: \"30 Minutes\" },\n  { key: \"15minute\", label: \"15 Minutes\" },\n  { key: \"5minute\", label: \"5 Minutes\" },\n  { key: \"minute\", label: \"1 Minute\" },\n  { key: \"30second\", label: \"30 Seconds\" },\n  { key: \"15second\", label: \"15 Seconds\" },\n  { key: \"5second\", label: \"5 Seconds\" },\n  { key: \"second\", label: \"1 Second\" },\n  { key: \"millisecond\", label: \"1 Millisecond\" },\n  { key: \"5millisecond\", label: \"5 Milliseconds\" },\n  { key: \"10millisecond\", label: \"10 Milliseconds\" },\n  { key: \"100millisecond\", label: \"100 Milliseconds\" },\n  { key: \"250millisecond\", label: \"250 Milliseconds\" },\n  { key: \"500millisecond\", label: \"500 Milliseconds\" },\n\n  // \"Auto\", \"1 second\", \"1 minute\", \"1 hour\", \"1 day\", \"1 week\", \"1 month\", \"1 year\", \"5 years\", \"10 years\"\n] as const;\n\nexport type TimeChartBinSize = (typeof TIMECHART_BIN_SIZES)[number][\"key\"];\nexport const TIMECHART_STAT_TYPES = [\n  { label: \"Count All\", func: \"$countAll\" },\n  { label: \"Min\", func: \"$min\" },\n  { label: \"Max\", func: \"$max\" },\n  { label: \"Sum\", func: \"$sum\" },\n  { label: \"Avg\", func: \"$avg\" },\n] as const;\nexport type StatType = (typeof TIMECHART_STAT_TYPES)[number][\"label\"];\nexport type StatFunction = (typeof TIMECHART_STAT_TYPES)[number][\"func\"];\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/constants.ts",
    "content": "export const TIMECHART_FIELD_NAMES = {\n  date: \"date\",\n  value: \"value\",\n  group_by: \"group_by\",\n} as const;\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/fetchAndSetTimechartLayerData.ts",
    "content": "import { MILLISECOND } from \"../../Charts\";\nimport { getTimeChartData } from \"./getTimeChartData\";\nimport type { W_TimeChart } from \"../W_TimeChart\";\nimport { getMainTimeBinSizes } from \"src/dashboard/Charts/TimeChart/getTimechartBinSize\";\n\nexport const fetchAndSetTimechartLayerData = async function (\n  this: W_TimeChart,\n) {\n  this.setState({ loadingData: true });\n  try {\n    const timechartData = await getTimeChartData.bind(this)();\n    if (timechartData) {\n      const { error, layers: rawLayers, erroredLayers } = timechartData;\n      const binSize =\n        timechartData.binSize ?\n          getMainTimeBinSizes()[timechartData.binSize].size\n        : undefined;\n      const layers = rawLayers.map((l) => {\n        const sortedParsedData = l.data\n          .map((d) => {\n            return {\n              ...d,\n              value: +d.value,\n              date: +new Date(d.date),\n            };\n          })\n          .sort((a, b) => a.date - b.date);\n\n        /** Add empty bins */\n        let filledData = sortedParsedData.slice(0, 0);\n        const { missingBins = \"ignore\", renderStyle = \"line\" } =\n          this.d.w?.options ?? {};\n        if (binSize) {\n          if (missingBins === \"ignore\" || renderStyle !== \"line\") {\n            filledData = sortedParsedData.slice(0);\n          } else if (missingBins === \"show nearest\") {\n            filledData = sortedParsedData.slice(0);\n            for (let i = sortedParsedData.length - 1; i >= 0; i--) {\n              const d = sortedParsedData[i];\n              const nextD = sortedParsedData[i + 1];\n\n              if (d && nextD) {\n                const gapSize = nextD.date - d.date;\n                const gapSizeInBins = Math.floor(gapSize / binSize);\n                const halfGapSizeInBins = Math.floor(gapSizeInBins / 2);\n                if (gapSize > binSize + MILLISECOND) {\n                  filledData.splice(i + 1, 0, {\n                    value: nextD.value,\n                    date: nextD.date - halfGapSizeInBins * binSize,\n                  });\n                  /** If not exactly in the middle then also add the left point */\n                  if (gapSizeInBins % 2 !== 0) {\n                    const leftGapSizeInBins = Math.floor(gapSizeInBins / 2);\n                    filledData.splice(i, 0, {\n                      value: d.value,\n                      date: d.date + leftGapSizeInBins * binSize,\n                    });\n                  }\n                }\n              }\n            }\n          } else {\n            sortedParsedData.forEach((d) => {\n              let preLastItem = filledData.at(-2);\n              let lastItem = filledData.at(-1);\n              if (!lastItem) {\n                filledData.push(d);\n              } else {\n                while (d.date - lastItem!.date > binSize + MILLISECOND) {\n                  const emptyItem = {\n                    value: 0,\n                    date: lastItem!.date + binSize,\n                  };\n                  /** Update last point if drawing a straigh line to avoid too many points */\n                  if (\n                    lastItem &&\n                    preLastItem &&\n                    Number(preLastItem.value) === 0 &&\n                    Number(lastItem.value) === 0\n                  ) {\n                    lastItem.date = emptyItem.date;\n                  } else {\n                    filledData.push(emptyItem);\n                  }\n                  preLastItem = filledData.at(-2);\n                  lastItem = filledData.at(-1);\n                }\n                filledData.push(d);\n              }\n            });\n          }\n        }\n\n        const { ref, chartRef } = this;\n        if (ref && chartRef) {\n          setTimeout(() => {\n            const renderedData = filledData.map((d) => {\n              const [x, y] =\n                chartRef.getPointXY({\n                  date: new Date(d.date),\n                  value: +d.value,\n                }) ?? [];\n              return {\n                x,\n                y,\n                value: d.value,\n              };\n            });\n            ref._renderedData = renderedData;\n          }, 0);\n        }\n\n        return {\n          ...l,\n          data: filledData,\n        };\n      });\n\n      this.setState({\n        loadingData: false,\n        loading: false,\n        binSize: timechartData.binSize,\n        error,\n        layers,\n        erroredLayers,\n      });\n    }\n  } catch (error) {\n    this.setState({ loading: false, error, loadingData: false });\n  }\n};\n\ndeclare global {\n  interface HTMLElement {\n    _renderedData?: {\n      x: number | undefined;\n      y: number | undefined;\n      value: number;\n    }[];\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/fetchTimechartLayer.ts",
    "content": "import type { SyncDataItem } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { asName, type PG_COLUMN_UDT_DATA_TYPE } from \"prostgles-types\";\nimport type {\n  DataItem,\n  TimeChartLayer,\n} from \"../../Charts/TimeChart/TimeChart\";\nimport type { WindowData } from \"../../Dashboard/dashboardUtils\";\nimport { getSQLQuerySemicolon } from \"../../SQLEditor/SQLCompletion/completionUtils/getQueryReturnType\";\nimport { getGroupByValueColor } from \"../../WindowControls/ColorByLegend/getGroupByValueColor\";\nimport type {\n  W_TimeChartProps,\n  W_TimeChartState,\n  W_TimeChartStateLayer,\n} from \"../W_TimeChart\";\nimport { TIMECHART_STAT_TYPES } from \"../W_TimeChartMenu\";\nimport { TIMECHART_FIELD_NAMES } from \"./constants\";\nimport { getYLabelFunc, type FetchedLayerData } from \"./getTimeChartData\";\nimport { type TimeChartLayerWithBinOrError } from \"./getTimeChartLayersWithBins\";\nimport { getTimeChartSelectParams } from \"./getTimeChartSelectParams\";\nimport { getTimeLayerDataSignature } from \"./getTimeLayerDataSignature\";\nimport { getTimechartExtentFilter } from \"./getTimechartExtentFilter\";\nimport { getMainTimeBinSizes } from \"src/dashboard/Charts/TimeChart/getTimechartBinSize\";\nimport type { ColumnValue } from \"src/dashboard/W_Table/ColumnMenu/ColumnStyleControls/ColumnStyleControls\";\n\ntype getTChartLayerArgs = Pick<\n  W_TimeChartState,\n  \"viewPortExtent\" | \"visibleDataExtent\"\n> &\n  Pick<W_TimeChartProps, \"getLinksAndWindows\" | \"myLinks\" | \"tables\"> & {\n    layer: TimeChartLayerWithBinOrError;\n    bin: FetchedLayerData[\"binSize\"];\n    binSize: FetchedLayerData[\"binSize\"] | \"auto\";\n    desiredBinCount: number;\n    db: DBHandlerClient;\n    w: SyncDataItem<Required<WindowData<\"timechart\">>, true>;\n  };\nexport async function fetchTimechartLayer({\n  bin,\n  binSize,\n  desiredBinCount,\n  layer,\n  db,\n  w,\n  tables,\n  getLinksAndWindows,\n  myLinks,\n  viewPortExtent,\n  visibleDataExtent,\n}: getTChartLayerArgs): Promise<\n  undefined | W_TimeChartStateLayer | W_TimeChartStateLayer[]\n> {\n  let rows: DataItem[] = [];\n  let cols: TimeChartLayer[\"cols\"] = [];\n\n  const extentFilter = getTimechartExtentFilter(\n    { viewPortExtent, visibleDataExtent },\n    getMainTimeBinSizes()[bin!].size,\n  );\n  const dataSignature = getTimeLayerDataSignature(layer, w, [extentFilter]);\n\n  if (layer.hasError) {\n    throw layer.error;\n  }\n  const { dateColumn, statType, groupByColumn } = layer;\n  if (layer.type === \"table\" || layer.type === \"local-table\") {\n    const tableName =\n      layer.type === \"table\" ?\n        (layer.joinPath?.at(-1)?.table ?? layer.tableName)\n      : layer.localTableName;\n\n    const tableHandler = db[tableName];\n    if (!tableHandler?.findOne || !tableHandler.find) {\n      throw `Cannot query table ${tableName}: Missing or disallowed`;\n    }\n\n    const { request } = layer;\n    const { tableFilters } = request;\n    const { select, orderBy } = getTimeChartSelectParams({\n      statType,\n      groupByColumn,\n      dateColumn,\n      bin,\n    });\n\n    const finalFilter = {\n      $and: [\n        tableFilters,\n        extentFilter?.filter,\n        { [TIMECHART_FIELD_NAMES.date]: { \"<>\": null } },\n      ].filter((f) => f),\n    };\n    rows = await tableHandler.find(finalFilter, {\n      select,\n      orderBy,\n      limit:\n        (binSize !== \"auto\" ? 1e3 : undefined) ??\n        Math.max(desiredBinCount * 10, 1e4), // Returned row count can vary considerably from the desiredBinCount\n    });\n\n    /** If too zoomed in and no data then add edges */\n    const firstVal = rows[0];\n    const lastVal = rows.at(-1);\n    if (\n      // this.state.visibleDataExtent &&\n      viewPortExtent &&\n      (!rows.length ||\n        (firstVal && +new Date(firstVal.date) > +viewPortExtent.minDate) ||\n        (lastVal && +new Date(lastVal.date) < +viewPortExtent.maxDate))\n    ) {\n      const { minDate, maxDate } = viewPortExtent;\n      const leftValues = await tableHandler.find(\n        {\n          $and: [\n            tableFilters,\n            { [TIMECHART_FIELD_NAMES.date]: { \"<\": minDate.toISOString() } },\n          ],\n        },\n        {\n          select,\n          orderBy: [\n            { key: TIMECHART_FIELD_NAMES.date, asc: false, nulls: \"last\" },\n          ],\n          limit: 2,\n        },\n      );\n      const rightValues = await tableHandler.find(\n        {\n          $and: [\n            tableFilters,\n            { [TIMECHART_FIELD_NAMES.date]: { \">\": maxDate.toISOString() } },\n          ],\n        },\n        {\n          select,\n          orderBy: [\n            { key: TIMECHART_FIELD_NAMES.date, asc: true, nulls: \"last\" },\n          ],\n          limit: 2,\n        },\n      );\n      rows = [...leftValues.reverse(), ...rows, ...rightValues];\n    }\n\n    rows.map((r) => ({ ...r, value: +r.value }));\n\n    const _cols = tables.find((t) => t.name === tableName)?.columns;\n    if (!_cols) {\n      throw `Columns not found for table ${tableName}`;\n    }\n    cols = _cols;\n  } else {\n    const { dateColumn, sql, withStatement, statType, groupByColumn } = layer;\n\n    if (!db.sql) {\n      console.error(\"Not enough privileges to run query\");\n      return;\n    }\n\n    const queryWithoutSemicolon = getSQLQuerySemicolon(sql, false);\n    const plainResult = await db.sql(`\n        ${withStatement}\n        SELECT * FROM (\n          ${queryWithoutSemicolon}\n        ) prostgles_chart_table \n        LIMIT 0 \n      `);\n    cols = plainResult.fields.map((f) => ({\n      ...f,\n      key: f.name,\n      label: f.name,\n      subLabel: f.dataType,\n      udt_name: f.dataType as PG_COLUMN_UDT_DATA_TYPE,\n    }));\n\n    let statField = \"COUNT(*)\";\n    if (statType && statType.funcName !== \"$countAll\") {\n      const stat = TIMECHART_STAT_TYPES.find(\n        (s) => s.func === statType.funcName,\n      );\n      if (stat) {\n        statField = `${stat.label}(${asName(statType.numericColumn)})`;\n      }\n    }\n\n    const binValue = bin ?? \"hour\";\n    const binInfo = getMainTimeBinSizes()[binValue];\n    const binUnit = binInfo.unit;\n    const prevBinUnit = {\n      millisecond: \"second\",\n      second: \"minute\",\n      minute: \"hour\",\n      hour: \"day\",\n      day: \"week\",\n      week: \"month\",\n      month: \"year\",\n      year: \"year\",\n    }[binUnit];\n\n    /**\n     * For fractional bins we use date_bin function\n     */\n    const escDateCol = asName(dateColumn);\n    const escGroupByCol = groupByColumn && asName(groupByColumn);\n    const dateBinCol =\n      binInfo.increment === 1 ?\n        `date_trunc(\\${bin}, ${escDateCol}::TIMESTAMPTZ)`\n      : `date_bin('${binInfo.increment}${binInfo.unit}', ${escDateCol}::TIMESTAMPTZ, date_trunc('${prevBinUnit}', ${escDateCol}::TIMESTAMPTZ))`;\n    const topSelect = [\n      `${dateBinCol} as ${JSON.stringify(TIMECHART_FIELD_NAMES.date)}`,\n      escGroupByCol &&\n        `${escGroupByCol} as ${JSON.stringify(TIMECHART_FIELD_NAMES.group_by)}`,\n      `${statField} as ${JSON.stringify(TIMECHART_FIELD_NAMES.value)}`,\n    ]\n      .filter((v) => v)\n      .join(\", \");\n    const dataQuery = [\n      withStatement,\n      `SELECT ${topSelect}`,\n      `FROM (`,\n      queryWithoutSemicolon,\n      `) t `,\n      `WHERE ${escDateCol} IS NOT NULL `,\n      `GROUP BY 1 ${escGroupByCol ? `, 2` : \"\"}`,\n      `ORDER BY 2`,\n    ].join(\"\\n\");\n\n    rows = (await db.sql(\n      dataQuery,\n      { dateColumn, bin: binInfo.unit, statField },\n      { returnType: \"rows\" },\n    )) as DataItem[];\n  }\n\n  const color = layer.color || \"red\";\n  const renderedLayer: W_TimeChartStateLayer = {\n    linkId: layer._id,\n    color,\n    getYLabel: getYLabelFunc(\"\", !layer.statType),\n    data: rows,\n    cols,\n    fullExtent: [layer.request.min, layer.request.max],\n    label:\n      layer.title ??\n      (layer.type === \"sql\" ? layer.sql.slice(0, 50)\n      : layer.type === \"local-table\" ? layer.localTableName\n      : (layer.joinPath?.at(-1)?.table ?? layer.tableName)),\n    extFilter: extentFilter,\n    dataSignature,\n  };\n\n  if (groupByColumn) {\n    const { getColor } = getGroupByValueColor({\n      getLinksAndWindows,\n      myLinks,\n      layerLinkId: layer.linkId,\n      groupByColumn,\n    });\n\n    const groupByValues = Array.from(\n      new Set(\n        renderedLayer.data.map(\n          (d) => d[TIMECHART_FIELD_NAMES.group_by] as ColumnValue,\n        ),\n      ),\n    );\n    return groupByValues.map((groupByValue, gbi) => {\n      return {\n        ...renderedLayer,\n        getYLabel: getYLabelFunc(\n          `  ${groupByValue?.toString()}`,\n          !layer.statType,\n        ),\n        data: rows.filter(\n          (r) => r[TIMECHART_FIELD_NAMES.group_by] === groupByValue,\n        ),\n        color: getColor(groupByValue, gbi),\n        groupByValue,\n      } satisfies TimeChartLayer;\n    });\n  }\n\n  return renderedLayer;\n}\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/getTimeChartData.ts",
    "content": "import { isDefined, tryCatchV2 } from \"prostgles-types\";\nimport type {\n  DateExtent,\n  getMainTimeBinSizes,\n} from \"../../Charts/TimeChart/getTimechartBinSize\";\nimport type {\n  W_TimeChartState,\n  W_TimeChartStateLayer,\n  W_TimeChart,\n} from \"../W_TimeChart\";\nimport { fetchTimechartLayer } from \"./fetchTimechartLayer\";\nimport {\n  getDesiredTimeChartBinSize,\n  getTimeChartLayersWithBins,\n  type TimeChartLayerWithBin,\n  type TimeChartLayerWithBinOrError,\n} from \"./getTimeChartLayersWithBins\";\n\ntype TChartLayer = W_TimeChartState[\"layers\"][number];\ntype TChartLayers = TChartLayer[];\n\nexport type FetchedLayerData = {\n  layers: TChartLayers;\n  erroredLayers: TimeChartLayerWithBinOrError[];\n  error: unknown;\n  binSize: keyof ReturnType<typeof getMainTimeBinSizes> | undefined;\n};\n\nexport async function getTimeChartData(\n  this: W_TimeChart,\n): Promise<FetchedLayerData | undefined> {\n  let layers: TChartLayers = [];\n  let bin: FetchedLayerData[\"binSize\"];\n  let erroredLayers: TimeChartLayerWithBinOrError[] = [];\n  try {\n    const {\n      prgl: { db },\n    } = this.props;\n    const { w } = this.d;\n    if (!w) {\n      return undefined;\n    }\n    const layerExtentBinsRes = await getTimeChartLayersWithBins.bind(this)();\n    if (!layerExtentBinsRes) return undefined;\n\n    const { layerExtentBins } = layerExtentBinsRes;\n    const nonErroredLayers = layerExtentBins.filter(\n      (l): l is TimeChartLayerWithBin => !l.hasError,\n    );\n    erroredLayers = layerExtentBins.filter((l) => l.hasError);\n    if (!layerExtentBins.length) {\n      return {\n        layers: [],\n        erroredLayers,\n        error: undefined,\n        binSize: undefined,\n      };\n    }\n\n    const { chartRef } = this;\n    const { chart } = chartRef ?? {};\n    if (!chartRef || !chart) {\n      return undefined;\n    }\n\n    const min = Math.min(...nonErroredLayers.map((l) => +l.request.min));\n    const max = Math.max(...nonErroredLayers.map((l) => +l.request.max));\n    const dataExtent: DateExtent = {\n      minDate: new Date(min),\n      maxDate: new Date(max),\n    };\n\n    const { binSize = \"auto\", renderStyle = \"line\" } = this.d.w?.options ?? {};\n    // const pxPerPoint = showBinLabels? 50 : renderStyle === \"line\"? 4 : renderStyle === \"scatter plot\"? 2 : 20;\n    const pxPerPoint =\n      renderStyle === \"line\" ? 4\n      : renderStyle === \"scatter plot\" ? 2\n      : 20;\n    const size = chart.getWH();\n    const { getLinksAndWindows, myLinks, tables } = this.props;\n    const { viewPortExtent, visibleDataExtent } = this.state;\n    const binOpts = getDesiredTimeChartBinSize({\n      width: size.w,\n      pxPerPoint,\n      manualBinSize: binSize,\n      dataExtent,\n      viewPortExtent: this.state.viewPortExtent,\n    });\n    bin = binOpts.bin;\n    const { desiredBinCount } = binOpts;\n    layers = (\n      await Promise.all(\n        nonErroredLayers.map(async (layer) => {\n          const {\n            data: fetchedLayer,\n            hasError,\n            error,\n          } = await tryCatchV2(async () => {\n            const fetchedLayer = await fetchTimechartLayer({\n              getLinksAndWindows,\n              myLinks,\n              tables,\n              viewPortExtent,\n              visibleDataExtent,\n              layer,\n              bin,\n              binSize,\n              db,\n              w,\n              desiredBinCount,\n            });\n            return fetchedLayer;\n          });\n          const layerSubscription = this.layerSubscriptions[layer._id];\n          if (layerSubscription) {\n            layerSubscription.isLoading = false;\n          }\n          if (hasError && !erroredLayers.some((el) => el._id === layer._id)) {\n            erroredLayers.push({\n              ...layer,\n              hasError,\n              error,\n              request: undefined,\n            });\n          }\n\n          return hasError ? undefined : fetchedLayer;\n        }),\n      )\n    )\n      .filter(isDefined)\n      .flat();\n  } catch (error) {\n    console.error(error);\n    return { error, erroredLayers, layers: [], binSize: undefined };\n  }\n\n  return { layers, erroredLayers, error: undefined, binSize: bin };\n}\n\n/**\n * Given the y-axis value range, format the number for best comprehension\n */\nexport const getYLabelFunc = (\n  endText: string,\n  asIs?: boolean,\n): W_TimeChartStateLayer[\"getYLabel\"] => {\n  return ({ value, min, max }) => {\n    const result =\n      Math.abs(min - max) > 1 || min === max || asIs ?\n        `${value.toLocaleString()}`\n      : `${value.toFixed(\n          Math.max(\n            2,\n            /* Show enough decimal places */\n            2 - Math.round(Math.log10(Math.abs(max - min))),\n          ),\n        )}`;\n\n    return result + endText;\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/getTimeChartLayers.ts",
    "content": "import { isDefined, tryCatchV2 } from \"prostgles-types\";\nimport type {\n  LinkSyncItem,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport { getCrossFilters } from \"../../joinUtils\";\nimport { getLinkColor } from \"../../W_Map/fetchData/getMapLayerQueries\";\nimport type { ActiveRow } from \"../../W_Table/W_Table\";\nimport type { ProstglesTimeChartLayer } from \"../W_TimeChart\";\n\ntype Args = {\n  links: LinkSyncItem[];\n  myLinks: LinkSyncItem[];\n  windows: WindowSyncItem[];\n  active_row: ActiveRow | undefined;\n  w: WindowSyncItem<\"timechart\">;\n};\n\nexport const getTimeChartLayer = ({\n  links,\n  link,\n  windows,\n  active_row,\n  w,\n}: Args & { link: LinkSyncItem }): ProstglesTimeChartLayer[] => {\n  const l = link;\n  const parentWindow = windows.find(\n    (_w) =>\n      (_w.type === \"table\" || _w.type === \"sql\") &&\n      _w.id !== w.id &&\n      [l.w1_id, l.w2_id].includes(_w.id),\n  ) as WindowSyncItem<\"table\"> | WindowSyncItem<\"sql\"> | undefined;\n\n  const lOpts = l.options;\n  if (lOpts.type !== \"timechart\") {\n    throw \"Not expected\";\n  }\n\n  const { dataSource } = lOpts;\n  return lOpts.columns\n    .flatMap(({ name: dateColumn, colorArr, statType }, columnIndex) => {\n      const color = getLinkColor(colorArr).colorStr;\n      const commonOpts = {\n        _id: `${l.id}-${columnIndex}`,\n        title: lOpts.title,\n        linkId: l.id,\n        disabled: !!l.disabled,\n        groupByColumn: lOpts.groupByColumn,\n        statType,\n        dateColumn,\n      } as const;\n\n      const localTableName =\n        dataSource?.type === \"local-table\" ?\n          dataSource.localTableName\n        : undefined;\n      const joinPath =\n        dataSource?.type === \"table\" ? dataSource.joinPath : undefined;\n      if (parentWindow?.type === \"table\" || localTableName) {\n        if (dataSource?.type === \"local-table\") {\n          return {\n            ...commonOpts,\n            type: \"local-table\",\n            localTableName: dataSource.localTableName,\n            smartGroupFilter: dataSource.smartGroupFilter,\n            color,\n          } satisfies ProstglesTimeChartLayer;\n        }\n\n        if (!parentWindow || !parentWindow.table_name) {\n          throw \"Unexpected: wTable or table_name missing\";\n        }\n\n        /** Map will always join to the same table name. Use that table */\n        const jf = getCrossFilters(w, active_row, links, windows);\n\n        const layer: ProstglesTimeChartLayer = {\n          ...commonOpts,\n          type: \"table\",\n          tableName: parentWindow.table_name,\n          joinPath,\n          externalFilters: jf.all,\n          color,\n        };\n\n        return layer;\n      } else if (parentWindow) {\n        const linkSql =\n          lOpts.dataSource?.type === \"sql\" ? lOpts.dataSource.sql : undefined;\n        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n        if (parentWindow.type !== \"sql\" || !linkSql) {\n          throw \"Unexpected: sql/window missing\";\n        }\n        const layer: ProstglesTimeChartLayer = {\n          ...commonOpts,\n          type: \"sql\",\n          sql: linkSql,\n          withStatement:\n            lOpts.dataSource?.type === \"sql\" ?\n              lOpts.dataSource.withStatement\n            : \"\",\n          color,\n        };\n\n        return layer;\n      } else if (dataSource?.type === \"sql\") {\n        const layer: ProstglesTimeChartLayer = {\n          ...commonOpts,\n          type: \"sql\",\n          sql: dataSource.sql,\n          withStatement: \"\",\n          color,\n        };\n\n        return layer;\n      } else if (dataSource) {\n        throw (\n          \"Unexpected timechart layer source: \" + JSON.stringify(dataSource)\n        );\n      }\n    })\n    .filter(isDefined);\n};\n\nexport const getTimeChartLayerQueries = (args: Args) => {\n  const { myLinks } = args;\n  const layerQueries: ProstglesTimeChartLayer[] = myLinks\n    .flatMap((link) => {\n      const { data = [] } = tryCatchV2(() =>\n        getTimeChartLayer({ ...args, link }),\n      );\n      return data;\n    })\n    .filter(isDefined);\n\n  return layerQueries;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/getTimeChartLayersWithBins.ts",
    "content": "import { type TableHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { asName, isEqual, tryCatchV2 } from \"prostgles-types\";\nimport { isDefined, quickClone } from \"../../../utils/utils\";\nimport type {\n  WindowData,\n  WindowSyncItem,\n} from \"../../Dashboard/dashboardUtils\";\nimport type { ProstglesTimeChartLayer, W_TimeChart } from \"../W_TimeChart\";\nimport type { TimeChartBinSize } from \"../W_TimeChartMenu\";\nimport { getTimeLayerDataSignature } from \"./getTimeLayerDataSignature\";\nimport { getSQLQuerySemicolon } from \"../../SQLEditor/SQLCompletion/completionUtils/getQueryReturnType\";\nimport { getTimechartExtentFilter } from \"./getTimechartExtentFilter\";\nimport {\n  getTimechartBinSize,\n  type DateExtent,\n} from \"src/dashboard/Charts/TimeChart/getTimechartBinSize\";\nimport {\n  getSmartGroupFilter,\n  getTableFilterFromDetailedGroupFilter,\n} from \"@common/filterUtils\";\n\nexport const getTimeChartFilters = (\n  w: WindowData<\"timechart\"> | WindowSyncItem<\"timechart\">,\n  dateColumn: string,\n) => {\n  return w.options?.filter ?\n      [\n        { [dateColumn]: { \">\": new Date(w.options.filter.min) } },\n        { [dateColumn]: { \"<\": new Date(w.options.filter.max) } },\n      ]\n    : [];\n};\n\nexport type TimeChartLayerWithBinError = ProstglesTimeChartLayer & {\n  hasError: true;\n  error: unknown;\n  request?: undefined;\n};\nexport type TimeChartLayerWithBin = ProstglesTimeChartLayer & {\n  hasError?: false;\n  request: {\n    min: Date;\n    max: Date;\n    dateExtent: number;\n    finalFilter: AnyObject;\n    dataSignature: string;\n    tableFilters: AnyObject;\n  };\n};\n\nexport type TimeChartLayerWithBinOrError =\n  | TimeChartLayerWithBin\n  | TimeChartLayerWithBinError;\n\nasync function getTimeChartLayerWithBin(\n  this: W_TimeChart,\n  layer: ProstglesTimeChartLayer,\n) {\n  const {\n    prgl: { db },\n  } = this.props;\n  const { w } = this.d;\n  if (!w) return undefined;\n\n  const extentFilter = getTimechartExtentFilter(this.state, undefined);\n  const dataSignature = getTimeLayerDataSignature(layer, w, [extentFilter]);\n  const layerSubscription = this.layerSubscriptions[layer._id];\n  if (layerSubscription) layerSubscription.isLoading = true;\n  if (layer.type === \"table\" || layer.type === \"local-table\") {\n    const tableName =\n      layer.type === \"table\" ?\n        (layer.joinPath?.at(-1)?.table ?? layer.tableName)\n      : layer.localTableName;\n    const { dateColumn } = layer;\n\n    const tableHandler = db[tableName];\n    if (!tableHandler?.findOne || !tableHandler.find) {\n      throw `Cannot query table ${tableName}: Missing or disallowed`;\n    }\n\n    // TODO low priority: Cache layers when panning\n    // const cachedLayers = this.state.layers.filter(l => l.dataSignature === dataSignature);\n    // extentFilter = getExtentFilter(extent, dateColumn);\n\n    /** Subscribe */\n    const externalFilters =\n      layer.type === \"table\" ? layer.externalFilters\n      : !layer.smartGroupFilter ? []\n      : [getTableFilterFromDetailedGroupFilter(layer.smartGroupFilter)];\n    const timeChartFilters = getTimeChartFilters(w, dateColumn);\n    const tableFilters = {\n      $and: [...externalFilters, ...timeChartFilters].filter(isDefined),\n    };\n    const existingSubscription = this.layerSubscriptions[layer._id];\n    const realtimeOpts = w.options.refresh;\n    if (\n      tableHandler.subscribe &&\n      (!existingSubscription ||\n        !isEqual(existingSubscription.externalFilters, externalFilters) ||\n        !isEqual(existingSubscription.realtimeOpts, realtimeOpts))\n    ) {\n      await existingSubscription?.sub?.unsubscribe();\n      const firstDataAge = Date.now();\n      this.layerSubscriptions[layer._id] = {\n        externalFilters,\n        realtimeOpts: quickClone(realtimeOpts),\n        sub:\n          realtimeOpts?.type !== \"Realtime\" ?\n            undefined\n          : await tableHandler.subscribe(\n              tableFilters,\n              {\n                select: \"\",\n                limit: 0,\n                throttle: +realtimeOpts.throttleSeconds * 1000,\n              },\n              () => {\n                if (!this.mounted) {\n                  void this.layerSubscriptions[layer._id]?.sub?.unsubscribe();\n                  console.error(\"TOOD: refactor w_timechart to hooks\");\n                  return;\n                }\n                const now = Date.now();\n                const prevAge =\n                  this.layerSubscriptions[layer._id]?.latestDataAge;\n                this.layerSubscriptions[layer._id]!.latestDataAge = now;\n\n                // Skip first\n                if (prevAge === firstDataAge) {\n                  return;\n                }\n\n                // Skip when already fetching. Will re-fetch when done\n                if (this.layerSubscriptions[layer._id]?.isLoading) {\n                  return;\n                }\n\n                this.setDataAge(now);\n              },\n            ),\n        dataAge: firstDataAge,\n        latestDataAge: firstDataAge,\n        isLoading: true,\n      };\n    }\n\n    /* Get date delta to work out bin size */\n    const finalFilter = {\n      $and: [tableFilters, extentFilter?.filter].filter((f) => f),\n    };\n    const dateRange = await getTimeChartMinMax(\n      tableHandler,\n      tableFilters,\n      dateColumn,\n    );\n    const res: TimeChartLayerWithBinOrError = {\n      ...layer,\n      request: {\n        dataSignature,\n        tableFilters,\n        finalFilter,\n        ...dateRange,\n        // limit: optsBinSize ? 1000 : undefined,\n      },\n    };\n    return res;\n  } else {\n    const { dateColumn, sql, withStatement } = layer;\n\n    if (!db.sql) {\n      console.error(\"Not enough privileges to run query\");\n      return;\n    }\n\n    // const extentFilter = \"\";\n    // let bin: { key: typeof optsBinSize, size: number; } | undefined = !optsBinSize? undefined : { ...MainTimeBinSizes[optsBinSize], key: optsBinSize };\n    // if(extent){\n    //   const { leftDate, rightDate } = extent;\n    //   if(leftDate && rightDate) {\n    //     extentFilter = await db.sql(\n    //       \" WHERE ${dateColumn:name} >= ${leftDate} AND ${dateColumn:name} <= ${leftDate}  \",\n    //       { dateColumn, leftDate, rightDate },\n    //       { returnType: \"statement\" }\n    //     );\n    //     bin = this.getBin(leftDate, rightDate);\n    //   }\n    // }\n\n    const escDateCol = asName(dateColumn);\n\n    const queryWithoutSemicolon = getSQLQuerySemicolon(sql, false);\n    const minMaxQuery = `\n      ${withStatement}\n      SELECT \n        MIN(${escDateCol}) as min, \n        MAX(${escDateCol}) as max \n      FROM (\n        ${queryWithoutSemicolon}\n      ) t\n    `;\n    const rows = await db.sql(\n      minMaxQuery,\n      { dateColumn },\n      { returnType: \"rows\" },\n    );\n    const [firstRow] = rows;\n    if (!firstRow) {\n      console.warn(\"No min max\");\n      return;\n    }\n    const minMaxDateStr = firstRow as { min: number; max: number };\n    const min = new Date(minMaxDateStr.min);\n    const max = new Date(minMaxDateStr.max);\n\n    const res: TimeChartLayerWithBinOrError = {\n      ...layer,\n      request: {\n        tableFilters: {},\n        dataSignature,\n        finalFilter: extentFilter ?? {},\n        max,\n        min,\n        dateExtent: +max - +min,\n      },\n    };\n    return res;\n  }\n}\n\nexport async function getTimeChartLayersWithBins(this: W_TimeChart) {\n  const { w } = this.d;\n  if (!w) {\n    return undefined;\n  }\n\n  const activeLayers = this.layerQueries.filter((l) => !l.disabled);\n\n  let layerExtentBins: TimeChartLayerWithBinOrError[] = [];\n\n  layerExtentBins = (\n    await Promise.all(\n      activeLayers.map(async (layer) => {\n        const {\n          data: layerWithBin,\n          error,\n          hasError,\n        } = await tryCatchV2(async () =>\n          getTimeChartLayerWithBin.bind(this)(layer),\n        );\n        const layerSubscription = this.layerSubscriptions[layer._id];\n        if (hasError && layerSubscription) {\n          layerSubscription.isLoading = false;\n        }\n\n        return hasError ? { ...layer, hasError, error } : layerWithBin;\n      }),\n    )\n  )\n    .filter(isDefined)\n    .flat();\n\n  return { layerExtentBins };\n}\n\ntype GetDesiredTimeChartBinSizeArgs = {\n  width: number;\n  pxPerPoint: number;\n  manualBinSize: TimeChartBinSize | undefined;\n  dataExtent: DateExtent;\n  viewPortExtent: DateExtent | undefined;\n};\nexport const getDesiredTimeChartBinSize = ({\n  width,\n  pxPerPoint,\n  manualBinSize,\n  dataExtent,\n  viewPortExtent,\n}: GetDesiredTimeChartBinSizeArgs) => {\n  const desiredBinCount = Math.round(width / pxPerPoint);\n\n  const bin =\n    manualBinSize && manualBinSize !== \"auto\" ?\n      manualBinSize\n    : getTimechartBinSize({\n        data: dataExtent,\n        viewPort: viewPortExtent,\n        bin_count: desiredBinCount,\n      }).key;\n  return { bin, desiredBinCount };\n};\n\nexport const getTimeChartMinMax = async (\n  tableHandler:\n    | TableHandlerClient\n    | Partial<TableHandlerClient<AnyObject, void>>,\n  tableFilters,\n  dateColumn: string,\n) => {\n  const minMax = (await tableHandler.findOne!(tableFilters, {\n    select: { min: { $min: [dateColumn] }, max: { $max: [dateColumn] } },\n  }).catch(console.error)) as { min: string; max: string };\n\n  const min = new Date(minMax.min);\n  const max = new Date(minMax.max);\n  const dateExtent = +max - +min;\n\n  return {\n    min,\n    max,\n    dateExtent,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/getTimeChartSelectParams.ts",
    "content": "import type { ProstglesTimeChartLayer } from \"../W_TimeChart\";\nimport { TIMECHART_STAT_TYPES } from \"../W_TimeChartMenu\";\nimport { TIMECHART_FIELD_NAMES } from \"./constants\";\nimport type { FetchedLayerData } from \"./getTimeChartData\";\n\nexport const getTimeChartSelectDate = ({\n  dateColumn,\n  bin,\n}: Pick<GetTimeChartSelectArgs, \"bin\" | \"dateColumn\">) => {\n  return { [\"$date_trunc_\" + bin]: [dateColumn, { timeZone: true }] };\n};\n\nexport type GetTimeChartSelectArgs = Pick<\n  ProstglesTimeChartLayer,\n  \"statType\" | \"groupByColumn\" | \"dateColumn\"\n> & {\n  bin: FetchedLayerData[\"binSize\"];\n};\nexport const getTimeChartSelectParams = ({\n  statType,\n  groupByColumn,\n  dateColumn,\n  bin,\n}: GetTimeChartSelectArgs) => {\n  const stat =\n    statType && TIMECHART_STAT_TYPES.find((s) => s.func === statType.funcName);\n  const valueSelect =\n    stat ? { [stat.func]: [statType.numericColumn] } : { $countAll: [] };\n  const select = {\n    [TIMECHART_FIELD_NAMES.value]: valueSelect,\n    ...(groupByColumn && {\n      [TIMECHART_FIELD_NAMES.group_by]: { $column: [groupByColumn] },\n    }),\n    [TIMECHART_FIELD_NAMES.date]: getTimeChartSelectDate({ bin, dateColumn }),\n  };\n\n  return {\n    select,\n    orderBy: { [TIMECHART_FIELD_NAMES.date]: 1 },\n  } as const;\n};\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/getTimeLayerDataSignature.ts",
    "content": "import type { WindowData } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport type { ProstglesTimeChartLayer } from \"../W_TimeChart\";\n\nexport const getTimeLayerDataSignature = (\n  l: ProstglesTimeChartLayer,\n  w: WindowData<\"timechart\">,\n  dependencies: any[],\n) => {\n  if (l.type === \"table\") {\n    return JSON.stringify({\n      ...l,\n      wopts: w.options,\n      dependencies,\n    });\n  } else {\n    return JSON.stringify({\n      ...l,\n      wopts: w.options,\n      dependencies,\n    });\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/W_TimeChart/fetchData/getTimechartExtentFilter.ts",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport { SECOND } from \"../../Charts\";\nimport type { W_TimeChartState } from \"../W_TimeChart\";\nimport { TIMECHART_FIELD_NAMES } from \"./constants\";\n\nexport type TimechartExtentFilter = {\n  filter: AnyObject;\n  paddedEdges: [Date, Date];\n};\n\nexport const getTimechartExtentFilter = (\n  state: Pick<W_TimeChartState, \"viewPortExtent\" | \"visibleDataExtent\">,\n  binSize: number | undefined,\n): TimechartExtentFilter | undefined => {\n  const { visibleDataExtent, viewPortExtent } = state;\n  if (visibleDataExtent) {\n    const { minDate, maxDate } = visibleDataExtent;\n    const padd10perc =\n      binSize ?\n        Math.max(10 * SECOND, binSize * 20)\n      : Math.round((+maxDate - +minDate) / 10);\n\n    /**\n     * Edges are padded to ensure edges do not show inexisting gaps\n     * Must ensure that the padding INCLUDES the bin size\n     */\n    const leftPadded = new Date(+minDate - padd10perc);\n    const rightPadded = new Date(+maxDate + padd10perc);\n\n    const $and: AnyObject[] = [];\n    const leftEdgeVisible =\n      viewPortExtent && visibleDataExtent.minDate >= viewPortExtent.minDate;\n    const rightEdgeVisible =\n      viewPortExtent && visibleDataExtent.maxDate <= viewPortExtent.maxDate;\n\n    if (!leftEdgeVisible) {\n      $and.push({ [TIMECHART_FIELD_NAMES.date]: { $gte: leftPadded } });\n    }\n    if (!rightEdgeVisible) {\n      $and.push({ [TIMECHART_FIELD_NAMES.date]: { $lte: rightPadded } });\n    }\n\n    return {\n      paddedEdges: [leftPadded, rightPadded],\n      filter: { $and },\n    };\n  }\n};\n"
  },
  {
    "path": "client/src/dashboard/Window.tsx",
    "content": "import {\n  mdiArrowCollapse,\n  mdiClose,\n  mdiCog,\n  mdiDotsVertical,\n  mdiOpenInNew,\n} from \"@mdi/js\";\n\nimport type { SingleSyncHandles } from \"prostgles-client/dist/SyncedTable/SyncedTable\";\nimport type { ReactNode } from \"react\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport Btn from \"@components/Btn\";\nimport { ErrorTrap } from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport Popup from \"@components/Popup/Popup\";\nimport { t } from \"../i18n/i18nUtils\";\nimport type { WindowData, WindowSyncItem } from \"./Dashboard/dashboardUtils\";\nimport type { DeepPartial } from \"./RTComp\";\nimport RTComp from \"./RTComp\";\nimport type { ReactSilverGridNode } from \"./SilverGrid/SilverGrid\";\nimport { getSilverGridTitleNode } from \"./SilverGrid/SilverGridChildHeader\";\nimport type { ProstglesQuickMenuProps } from \"./W_QuickMenu\";\nimport { W_QuickMenu } from \"./W_QuickMenu\";\n\ntype P<W extends WindowSyncItem> = {\n  w?: W;\n  onWChange?: (w: W, delta: DeepPartial<W>) => any;\n\n  children?: ReactNode;\n  getMenu?: (w: W, onClose: () => any) => ReactNode;\n  layoutMode: \"fixed\" | \"editable\";\n  quickMenuProps?: W extends WindowSyncItem<\"table\"> | WindowSyncItem<\"sql\"> ?\n    Omit<ProstglesQuickMenuProps, \"w\">\n  : undefined;\n};\n\ntype S<W extends WindowSyncItem> = {\n  showMenu: boolean;\n  w?: W;\n};\n\ntype D = {\n  w?: WindowSyncItem;\n  wSync?: SingleSyncHandles<WindowData>;\n};\n\nexport default class Window<W extends WindowSyncItem> extends RTComp<\n  P<W>,\n  S<W>,\n  D\n> {\n  state: S<W> = {\n    showMenu: false,\n  };\n\n  d: D = {};\n\n  static getTitle(_w: WindowSyncItem) {\n    const w = _w.$get() as WindowSyncItem | undefined;\n    const title =\n      !w ? undefined : (\n        w.name ||\n        w.title?.replace(\"${rowCount}\", \"\") ||\n        w.table_name ||\n        w.method_name ||\n        w.id\n      );\n    return title || \"Empty\";\n  }\n\n  onDelta = (dp) => {\n    const { w } = this.d;\n    const { onWChange } = this.props;\n    if (this.ref && w) {\n      const titleDiv = getSilverGridTitleNode(w.id);\n      const title = Window.getTitle(w);\n      if (titleDiv && titleDiv.innerText !== title) {\n        titleDiv.innerText = title;\n        titleDiv.title = title;\n      }\n    }\n\n    if (dp?.onWChange && this.d.w) {\n      onWChange?.(this.d.w as any, this.d.w as any);\n    }\n\n    if (this.props.w && !this.d.wSync) {\n      const wSync = this.props.w.$cloneSync((_w, delta) => {\n        const w = _w;\n        this.setData({ w }, { w: delta });\n        onWChange?.(w, delta);\n        this.setState({ w });\n      });\n\n      this.setData({ wSync });\n    }\n  };\n\n  onUnmount = async () => {\n    await this.d.wSync?.$unsync();\n    this.d.wSync = undefined;\n  };\n\n  getTitleIcon() {\n    const { quickMenuProps } = this.props;\n    const { w } = this.d;\n\n    if (!quickMenuProps || !w) return null;\n    if (w.type !== \"table\" && w.type !== \"sql\") {\n      return null;\n    }\n\n    return <W_QuickMenu {...quickMenuProps} w={w} />;\n  }\n\n  ref?: HTMLDivElement;\n  render(): ReactSilverGridNode | null {\n    const { children, getMenu, layoutMode = \"editable\" } = this.props;\n    const { showMenu } = this.state;\n    const { w = this.props.w } = this.state;\n\n    if (!w) return null;\n\n    let menuPortal;\n    const menuIconContainer = this.ref?.parentElement?.querySelector(\n      \":scope > .silver-grid-item-header > .silver-grid-item-header--icon\",\n    );\n    if (getMenu && menuIconContainer) {\n      menuPortal = ReactDOM.createPortal(\n        <>\n          {layoutMode === \"fixed\" ?\n            <div style={{ width: \".65em\" }}></div>\n          : <Btn\n              className=\"f-0\"\n              iconPath={mdiDotsVertical}\n              title={t.Window[\"Open menu\"]}\n              data-command=\"dashboard.window.menu\"\n              onContextMenu={(e) => {\n                navigator.clipboard.writeText(w.id);\n              }}\n              onClick={() => {\n                this.setState({ showMenu: !showMenu });\n              }}\n            />\n          }\n          {this.getTitleIcon()}\n        </>,\n        menuIconContainer,\n      );\n    }\n\n    const closeMenu = () => {\n      this.setState({ showMenu: false });\n    };\n\n    const windowContent = (\n      <>\n        {menuPortal}\n        <div\n          key={w.id + \"-content\"}\n          className=\"Window flex-col f-1 min-h-0 min-w-0 relative\"\n          ref={(e) => {\n            if (e) {\n              let forceUpdate;\n              if (!this.ref) {\n                forceUpdate = true;\n              }\n              this.ref = e;\n              if (forceUpdate) this.forceUpdate();\n            }\n          }}\n        >\n          <ErrorTrap>{children}</ErrorTrap>\n        </div>\n\n        {showMenu && getMenu && (\n          <Popup\n            title={window.isLowWidthScreen ? t.Window.Menu : undefined}\n            fixedTopLeft={true}\n            anchorEl={this.ref}\n            positioning={\"inside\"}\n            rootStyle={{ padding: 0 }}\n            clickCatchStyle={{ opacity: 0.5, backdropFilter: \"blur(1px)\" }}\n            contentClassName=\"\"\n            contentStyle={{\n              overflow: \"unset\",\n            }}\n            onClose={closeMenu}\n          >\n            <ErrorTrap>{getMenu(w, closeMenu)}</ErrorTrap>\n          </Popup>\n        )}\n      </>\n    );\n\n    if (w.parent_window_id && this.props.layoutMode === \"editable\") {\n      return (\n        <FlexCol\n          data-command=\"Window.ChildChart\"\n          className=\"f-1 gap-0 min-s-0 o-hidden\"\n        >\n          <FlexRow data-command=\"Window.ChildChart.toolbar\" className=\"p-p5\">\n            <Btn\n              className=\"f-0\"\n              title={t.Window[\"Open menu\"]}\n              variant=\"outline\"\n              color=\"action\"\n              iconPath={mdiCog}\n              data-command=\"dashboard.window.chartMenu\"\n              onClick={() => {\n                this.setState({ showMenu: !showMenu });\n              }}\n              children={\n                window.isLowWidthScreen ? null : t.Window[\"Chart options\"]\n              }\n            />\n\n            <Btn\n              className=\"ml-auto\"\n              iconPath={mdiArrowCollapse}\n              color=\"action\"\n              title={t.Window[\"Collapse chart\"]}\n              data-command=\"dashboard.window.collapseChart\"\n              onClick={() => {\n                w.$update({ minimised: true });\n              }}\n            />\n            <Btn\n              variant=\"outline\"\n              iconPath={mdiOpenInNew}\n              color=\"action\"\n              data-command=\"dashboard.window.detachChart\"\n              onClick={() => w.$update({ parent_window_id: null })}\n              children={\n                window.isLowWidthScreen ? null : t.Window[\"Detach chart\"]\n              }\n            />\n            <Btn\n              variant=\"outline\"\n              data-command=\"dashboard.window.closeChart\"\n              iconPath={mdiClose}\n              onClick={() => w.$update({ closed: true })}\n              children={\n                window.isLowWidthScreen ? null : t.Window[\"Close chart\"]\n              }\n            />\n          </FlexRow>\n          {windowContent}\n        </FlexCol>\n      );\n    }\n\n    return windowContent;\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/AddChartLayer.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport { isDefined } from \"prostgles-types\";\nimport React, { useMemo, useState } from \"react\";\nimport { Select } from \"@components/Select/Select\";\nimport type { MapLayerManagerProps } from \"./DataLayerManager/DataLayerManager\";\nimport { FlexRow } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport Btn from \"@components/Btn\";\nimport { MapOSMQuery } from \"../W_Map/controls/MapOSMQuery\";\nimport type { Extent } from \"../Map/DeckGLMap\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\n\nexport const defaultWorldExtent: Extent = [-180, -90, 180, 90];\n\nexport const AddChartLayer = (props: MapLayerManagerProps) => {\n  const { type, w } = props;\n  const isMap = type === \"map\";\n  let osmBbox = \"\";\n  const { dbs, tables } = usePrgl();\n  if (w.type === \"map\") {\n    const [b, a, b1, a1] = w.options.extent ?? defaultWorldExtent;\n    osmBbox = [a, b, a1, b1].join(\",\");\n  }\n  const [_, setError] = useState();\n  const chartTables = useMemo(() => {\n    return tables\n      .flatMap((t) => {\n        const chartCols = t.columns.filter((c) =>\n          c.udt_name.startsWith(isMap ? \"geo\" : \"timestamp\"),\n        );\n        if (chartCols.length) {\n          return chartCols.map((c) => ({\n            key: `${t.name}.${c.name}`,\n            tableName: t.name,\n            column: c.name,\n          }));\n        }\n        return undefined;\n      })\n      .filter(isDefined)\n      .sort((a, b) => a.key.localeCompare(b.key));\n  }, [tables, isMap]);\n\n  return (\n    <FlexRow className=\"AddChartLayer\">\n      <Select\n        data-command=\"ChartLayerManager.AddChartLayer.addLayer\"\n        btnProps={{\n          iconPath: mdiPlus,\n          color: \"action\",\n          variant: \"filled\",\n          children: \"Add layer\",\n        }}\n        fullOptions={chartTables.map((t) => ({\n          key: t.key,\n          label: t.tableName,\n          subLabel: t.column,\n        }))}\n        onChange={async (key) => {\n          const chartTableFromKey = chartTables.find((gt) => gt.key === key);\n          if (chartTableFromKey) {\n            const colorArr = [100, 20, 57];\n\n            await dbs.links\n              .insert({\n                w1_id: w.id,\n                w2_id: w.id,\n                workspace_id: w.workspace_id,\n                options: {\n                  type,\n                  dataSource: {\n                    type: \"local-table\",\n                    localTableName: chartTableFromKey.tableName,\n                  },\n                  columns: [\n                    {\n                      name: chartTableFromKey.column,\n                      colorArr,\n                    },\n                  ],\n                },\n                last_updated: undefined as any,\n                user_id: undefined as any,\n              })\n              .catch((e) => {\n                console.error(e);\n                setError(() => {\n                  throw new Error(e);\n                });\n              });\n          }\n        }}\n      />\n      {type === \"map\" && (\n        <PopupMenu\n          title=\"Add OSM Layer\"\n          contentClassName=\"p-1\"\n          data-command=\"ChartLayerManager.AddChartLayer.addOSMLayer\"\n          button={\n            <Btn iconPath={mdiPlus} variant=\"faded\" color=\"action\">\n              Add OSM Layer\n            </Btn>\n          }\n          onClickClose={false}\n        >\n          {osmBbox && (\n            <MapOSMQuery\n              {...props}\n              bbox={osmBbox}\n              onData={(_, osmLayerQuery) => {\n                void dbs.links.insert({\n                  w1_id: w.id,\n                  w2_id: w.id,\n                  workspace_id: w.workspace_id,\n                  options: {\n                    type,\n                    columns: [],\n                    osmLayerQuery,\n                  },\n                  last_updated: undefined as any,\n                  user_id: undefined as any,\n                });\n              }}\n            />\n          )}\n        </PopupMenu>\n      )}\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/ColorByLegend/ColorByLegend.tsx",
    "content": "import { getSmartGroupFilter } from \"@common/filterUtils\";\nimport type { DivProps } from \"@components/Flex\";\nimport { FlexRow, classOverride } from \"@components/Flex\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport { useEffectDeep } from \"prostgles-client/dist/prostgles\";\nimport React, { useCallback, useMemo } from \"react\";\nimport { chipColors } from \"src/dashboard/W_Table/ColumnMenu/ColumnDisplayFormat/ChipStylePalette\";\nimport {\n  DefaultConditionalStyleLimit,\n  fetchColumnValues,\n} from \"src/dashboard/W_Table/ColumnMenu/ColumnStyleControls/getValueColors\";\nimport { isDefined } from \"../../../utils/utils\";\nimport type { CommonWindowProps } from \"../../Dashboard/Dashboard\";\nimport { ColorPicker } from \"../../W_Table/ColumnMenu/ColorPicker\";\nimport {\n  getRandomElement,\n  type ColumnValue,\n} from \"../../W_Table/ColumnMenu/ColumnStyleControls/ColumnStyleControls\";\nimport type { W_TimeChartStateLayer } from \"../../W_TimeChart/W_TimeChart\";\nimport { getGroupByValueColor } from \"./getGroupByValueColor\";\n\ntype P = DivProps &\n  Pick<CommonWindowProps, \"getLinksAndWindows\" | \"myLinks\" | \"w\"> & {\n    layerLinkId: string;\n    groupByColumn: string;\n    onChanged: VoidFunction;\n    layers: W_TimeChartStateLayer[];\n  };\nexport const ColorByLegend = ({ className, style, onChanged, ...props }: P) => {\n  const { groupByColumn, layers } = props;\n  const { db, theme } = usePrgl();\n  const {\n    getColor,\n    oldLayerWindow,\n    thisLink,\n    valueStyles,\n    thisLinkTimechartOptions,\n  } = getGroupByValueColor(props);\n\n  const layerGroupByValues = useMemo(\n    () => Array.from(new Set(layers.map((l) => l.groupByValue))),\n    [layers],\n  );\n\n  const linkOptions = thisLinkTimechartOptions;\n  const tableName = oldLayerWindow?.table_name;\n  const groupByColumnColors = linkOptions?.groupByColumnColors;\n\n  const updateGroupByColumnColors = useCallback(\n    (groupByColumnColors: { value: unknown; color: string }[]) => {\n      if (!thisLink || !linkOptions) throw \"Not expected\";\n      thisLink.$update({\n        options: {\n          ...linkOptions,\n          groupByColumnColors,\n        },\n      });\n      onChanged();\n    },\n    [thisLink, linkOptions, onChanged],\n  );\n\n  /** Add group by colors */\n  useEffectDeep(() => {\n    const missingLabels =\n      !valueStyles ? undefined : (\n        layerGroupByValues.filter(\n          (groupByValue) => !valueStyles.some((s) => s.value === groupByValue),\n        )\n      );\n    if (\n      !valueStyles?.length ||\n      (missingLabels?.length &&\n        valueStyles.length < DefaultConditionalStyleLimit)\n    ) {\n      const parentW = props\n        .getLinksAndWindows()\n        .windows.find((w) => w.id === props.w.parent_window_id);\n      const filter = getSmartGroupFilter(parentW?.filter || []);\n      void fetchColumnValues(\n        linkOptions?.dataSource?.type === \"sql\" ?\n          {\n            type: \"sql\",\n            db,\n            query: linkOptions.dataSource.sql,\n            columnName: groupByColumn,\n            theme,\n          }\n        : {\n            type: \"table\",\n            db,\n            tableName: tableName!,\n            columnName: groupByColumn,\n            filter,\n            theme,\n          },\n      ).then((values) => {\n        if (!values) return;\n        const prevSyleIndexes = new Set<number>();\n        updateGroupByColumnColors(\n          values.map((value) => {\n            const nonPickedStyles =\n              prevSyleIndexes.size === chipColors.length ?\n                chipColors\n              : chipColors.filter((_, i) => !prevSyleIndexes.has(i));\n            const { elem: style, index } = getRandomElement(nonPickedStyles);\n            prevSyleIndexes.add(index);\n            return {\n              color: style.color,\n              value,\n            };\n          }),\n        );\n      });\n    }\n  }, [\n    db,\n    groupByColumn,\n    layerGroupByValues,\n    linkOptions,\n    tableName,\n    props,\n    theme,\n    updateGroupByColumnColors,\n    valueStyles,\n  ]);\n\n  if (!valueStyles?.length) return null;\n  const labels = [\n    ...layers.map(\n      (l) =>\n        valueStyles.find((s) => s.value === l.groupByValue) ??\n        ({\n          value: l.groupByValue,\n          color: l.color,\n        } satisfies (typeof valueStyles)[number]),\n    ),\n    ...valueStyles\n      .filter((s) => !layers.some((l) => l.groupByValue === s.value))\n      .slice(0, 3),\n  ].filter(isDefined);\n  const getConditionLabel = (\n    condition: ColumnValue | ColumnValue[],\n  ): string => {\n    if (Array.isArray(condition))\n      return condition.map((c) => getConditionLabel(c)).join(\", \");\n    if (condition === null) return \"null\";\n    if (condition === undefined) return \"undefined\";\n    return condition.toString();\n  };\n\n  return (\n    <FlexRow\n      className={classOverride(\"ColorByLegend\", className)}\n      style={style}\n    >\n      {labels.map((s, i) => {\n        const isInData = layers.some((l) => l.groupByValue === s.value);\n        return (\n          <ColorPicker\n            key={i}\n            style={isInData ? {} : { opacity: 0.25 }}\n            value={getColor(s.value, i)}\n            label={getConditionLabel(s.value)}\n            variant=\"legend\"\n            btnProps={{ size: \"micro\" }}\n            onChange={(newColor) => {\n              const newGroupByColumnColors =\n                groupByColumnColors?.map((c) => {\n                  return c.value === s.value ?\n                      {\n                        value: c.value,\n                        color: newColor,\n                      }\n                    : c;\n                }) ?? [];\n              if (!newGroupByColumnColors.some((c) => c.value === s.value)) {\n                newGroupByColumnColors.push({\n                  ...s,\n                  color: newColor,\n                });\n              }\n              updateGroupByColumnColors(newGroupByColumnColors);\n            }}\n          />\n        );\n      })}\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/ColorByLegend/getGroupByValueColor.ts",
    "content": "import { getProperty } from \"@common/utils\";\nimport { includes } from \"prostgles-types\";\nimport type { CommonWindowProps } from \"src/dashboard/Dashboard/Dashboard\";\nimport type { WindowSyncItem } from \"../../Dashboard/dashboardUtils\";\nimport { PALETTE } from \"../../Dashboard/PALETTE\";\n\nexport const getGroupByValueColor = ({\n  getLinksAndWindows,\n  myLinks,\n  layerLinkId,\n}: Pick<CommonWindowProps, \"getLinksAndWindows\" | \"myLinks\"> & {\n  layerLinkId: string;\n  groupByColumn: string;\n}) => {\n  const { windows } = getLinksAndWindows();\n  const thisLink = myLinks.find((l) => l.id === layerLinkId);\n  const thisLinkTimechartOptions =\n    thisLink?.options.type === \"timechart\" ? thisLink.options : undefined;\n  const valueStyles =\n    thisLinkTimechartOptions?.type === \"timechart\" ?\n      thisLinkTimechartOptions.groupByColumnColors\n    : undefined;\n  const oldLayerWindow =\n    !thisLink ? undefined : (\n      (windows.find(\n        (w) =>\n          includes([\"sql\", \"table\"], w.type) &&\n          [thisLink.w1_id, thisLink.w2_id].includes(w.id),\n      ) as WindowSyncItem<\"table\"> | WindowSyncItem<\"sql\">)\n    );\n  const layerWindow = oldLayerWindow?.$get();\n\n  const getColor = (value: unknown, valueIndex: number) => {\n    const valueStyle = valueStyles?.find((c) => c.value === value);\n    if (valueStyle) {\n      return valueStyle.color;\n    }\n    const c = getProperty(PALETTE, `c${valueIndex}`);\n    return (c ?? PALETTE.c1).getStr();\n  };\n\n  return {\n    getColor,\n    valueStyles,\n    layerWindow,\n    oldLayerWindow,\n    thisLink,\n    thisLinkTimechartOptions,\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/DataLayerManager/DataLayer.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport {\n  mdiClose,\n  mdiEye,\n  mdiEyeOff,\n  mdiScript,\n  mdiSetCenter,\n  mdiTable,\n} from \"@mdi/js\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\nimport React, { useCallback } from \"react\";\nimport { RenderFilter } from \"src/dashboard/RenderFilter\";\nimport type { Link, LinkSyncItem } from \"../../Dashboard/dashboardUtils\";\nimport type { LayerQuery, W_MapProps } from \"../../W_Map/W_Map\";\nimport type {\n  ProstglesTimeChartLayer,\n  W_TimeChartProps,\n} from \"../../W_TimeChart/W_TimeChart\";\nimport { LayerColorPicker } from \"../LayerColorPicker\";\nimport { OSMLayerOptions } from \"../OSMLayerOptions\";\nimport { SQLChartLayerEditor } from \"../SQLChartLayerEditor\";\nimport { TimeChartLayerOptions } from \"../TimeChartLayerOptions\";\nimport type { MapLayerManagerProps } from \"./DataLayerManager\";\n\nexport type ChartLinkOptions = Exclude<\n  DBSSchema[\"links\"][\"options\"],\n  { type: \"table\" }\n>;\ntype P =\n  | (Pick<W_TimeChartProps, \"w\" | \"getLinksAndWindows\" | \"myLinks\"> & {\n      type: \"timechart\";\n      layer: NonNullable<\n        ProstglesTimeChartLayer & {\n          link: LinkSyncItem;\n        }\n      >;\n    })\n  | (Pick<W_MapProps, \"w\" | \"getLinksAndWindows\" | \"myLinks\"> & {\n      type: \"map\";\n      w: MapLayerManagerProps[\"w\"];\n      layer: NonNullable<\n        LayerQuery & {\n          link: LinkSyncItem;\n        }\n      >;\n    });\nexport const DataLayer = (props: P) => {\n  const { tables, db } = usePrgl();\n  const { myLinks, layer, w, getLinksAndWindows } = props;\n\n  const thisLink = myLinks.find((l) => l.id === layer.linkId);\n  const linkOptions = thisLink?.options;\n  if (!linkOptions || linkOptions.type === \"table\") return null;\n\n  const { dataSource } = linkOptions;\n\n  const tableName =\n    props.layer.type === \"table\" ? props.layer.tableName\n    : props.layer.type === \"local-table\" ? props.layer.localTableName\n    : undefined;\n  const joinPath =\n    dataSource?.type === \"table\" ? dataSource.joinPath : undefined;\n  const column =\n    props.type === \"map\" ? props.layer.geomColumn : props.layer.dateColumn;\n  const osmOrSQLQuery =\n    dataSource?.type === \"osm\" ? dataSource.osmLayerQuery\n    : dataSource?.type === \"sql\" ? dataSource.sql\n    : undefined;\n  const layerDesc =\n    osmOrSQLQuery ?? `${joinPath?.at(-1)?.table || tableName} (${column})`;\n\n  const updateOptions = useCallback(\n    (newOptions: ChartLinkOptions) => {\n      if (thisLink.options.type === \"table\") return;\n      thisLink.$update(\n        {\n          options: newOptions,\n        },\n        { deepMerge: true },\n      );\n    },\n    [thisLink],\n  );\n  return (\n    <FlexRowWrap\n      key={layer._id}\n      className={`LayerQuery bg-color-0 ai-center gap-1 ta-left b b-color rounded ${window.isMobileDevice ? \"p-p5\" : \"p-1\"}`}\n    >\n      <LayerColorPicker\n        onChange={updateOptions}\n        title={layerDesc}\n        column={column}\n        linkOptions={linkOptions}\n      />\n\n      {dataSource?.type === \"osm\" ?\n        <OSMLayerOptions link={thisLink} dataSource={dataSource} />\n      : <Label\n          variant=\"header\"\n          iconPath={\n            dataSource?.type === \"local-table\" ? mdiTable\n            : dataSource?.type === \"table\" ?\n              mdiSetCenter\n            : mdiScript\n          }\n          info={\n            dataSource?.type === \"local-table\" ? \"Local table\"\n            : dataSource?.type === \"table\" ?\n              `${dataSource.joinPath?.length ? \"Linked table\" : \"Table\"}: ${[{ table: tableName }, ...(dataSource.joinPath ?? [])].map((p) => p.table).join(\" -> \")} (${column})`\n            : <SQLChartLayerEditor link={thisLink} />\n          }\n          className={\"ws-nowrap f-1 min-w-0\"}\n          title={\n            dataSource?.type === \"table\" || dataSource?.type === \"local-table\" ?\n              `Table name`\n            : \"SQL Script\"\n          }\n        >\n          <div className=\"text-ellipsis\">{layerDesc}</div>\n        </Label>\n      }\n\n      <TimeChartLayerOptions\n        w={w}\n        getLinksAndWindows={getLinksAndWindows}\n        link={thisLink}\n        myLinks={myLinks}\n        column={column}\n      />\n\n      {dataSource?.type === \"local-table\" && (\n        <RenderFilter\n          db={db}\n          tables={tables}\n          title=\"Manage filters\"\n          mode=\"micro\"\n          selectedColumns={undefined}\n          itemName=\"filter\"\n          tableName={dataSource.localTableName}\n          contextData={undefined}\n          filter={dataSource.smartGroupFilter}\n          onChange={(andOrFilter) => {\n            updateOptions({\n              ...linkOptions,\n              dataSource: {\n                ...dataSource,\n                smartGroupFilter: andOrFilter,\n              },\n            });\n          }}\n        />\n      )}\n\n      <Btn\n        title=\"Toggle layer on/off\"\n        data-command=\"ChartLayerManager.toggleLayer\"\n        className={`ml-auto ${thisLink.disabled ? \"\" : \"show-on-parent-hover\"} `}\n        iconPath={thisLink.disabled ? mdiEyeOff : mdiEye}\n        color={\"action\"}\n        onClick={() => {\n          if (thisLink.options.type === \"table\") return;\n          thisLink.$update({ disabled: !thisLink.disabled });\n        }}\n      />\n\n      <Btn\n        color=\"danger\"\n        title=\"Remove layer\"\n        data-command=\"ChartLayerManager.removeLayer\"\n        className=\"show-on-parent-hover\"\n        onClickPromise={() => {\n          if (thisLink.options.type === \"table\") return;\n          const opts = thisLink.options;\n          const newOpts: Link[\"options\"] = {\n            ...opts,\n            columns: opts.columns.filter((c) => c.name !== column),\n          };\n          if (newOpts.columns.length === 0) {\n            thisLink.$update({ closed: true });\n          } else {\n            updateOptions(newOpts);\n          }\n        }}\n        iconPath={mdiClose}\n      />\n    </FlexRowWrap>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/DataLayerManager/DataLayerManager.tsx",
    "content": "import type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiLayers } from \"@mdi/js\";\nimport React from \"react\";\nimport type { LinkSyncItem } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport { MapBasemapOptions } from \"../../W_Map/controls/MapBasemapOptions\";\nimport { MapOpacityMenu } from \"../../W_Map/controls/MapOpacityMenu\";\nimport type { LayerQuery, W_MapProps } from \"../../W_Map/W_Map\";\nimport type {\n  ProstglesTimeChartLayer,\n  W_TimeChartProps,\n} from \"../../W_TimeChart/W_TimeChart\";\nimport { AddChartLayer } from \"../AddChartLayer\";\nimport { DataLayer } from \"./DataLayer\";\nimport { useSortedLayerQueries } from \"./useSortedLayerQueries\";\n\nexport type MapLayerManagerProps = (\n  | ({\n      type: \"timechart\";\n      layerQueries: ProstglesTimeChartLayer[];\n    } & Pick<W_TimeChartProps, \"myLinks\" | \"w\" | \"getLinksAndWindows\">)\n  | ({\n      type: \"map\";\n    } & Pick<\n      W_MapProps,\n      \"myLinks\" | \"w\" | \"getLinksAndWindows\" | \"layerQueries\"\n    >)\n) & {\n  asMenuBtn?: BtnProps<void>;\n};\n\n// TODO: Show columns grouped by their link\nexport const DataLayerManager = (props: MapLayerManagerProps) => {\n  const { myLinks, type, asMenuBtn, w, layerQueries = [] } = props;\n\n  const sortedLayerQueries = useSortedLayerQueries({\n    layerQueries,\n    myLinks,\n  });\n  const content = (\n    <FlexCol>\n      <FlexCol className=\"ChartLayerManager_LayerList\">\n        {sortedLayerQueries.map((layer) => {\n          if (props.type === \"timechart\") {\n            return (\n              <DataLayer\n                {...props}\n                type=\"timechart\"\n                key={layer._id}\n                layer={\n                  layer as ProstglesTimeChartLayer & { link: LinkSyncItem }\n                }\n              />\n            );\n          }\n          return (\n            <DataLayer\n              {...props}\n              type=\"map\"\n              key={layer._id}\n              layer={layer as LayerQuery & { link: LinkSyncItem }}\n            />\n          );\n        })}\n      </FlexCol>\n      <FlexRow>\n        <AddChartLayer {...props} />\n        {type === \"timechart\" && (\n          <Select\n            className=\"ml-auto\"\n            label={\"Y Scale mode\"}\n            asRow={true}\n            value={w.options.yScaleMode ?? \"multiple\"}\n            fullOptions={[\n              {\n                key: \"single\",\n                label: \"Single\",\n                subLabel: \"Default (shared Y axis)\",\n              },\n              {\n                key: \"multiple\",\n                label: \"Multiple\",\n                subLabel: \"Per layer (separate Y axis for each layer)\",\n              },\n            ]}\n            onChange={(yScaleMode) => {\n              w.$update(\n                {\n                  options: { yScaleMode },\n                },\n                { deepMerge: true },\n              );\n            }}\n          />\n        )}\n      </FlexRow>\n      {type === \"map\" && (\n        <FlexCol className=\"mt-2\">\n          <MapBasemapOptions {...props} asPopup={true} />\n          <MapOpacityMenu {...props} />\n        </FlexCol>\n      )}\n    </FlexCol>\n  );\n\n  if (!asMenuBtn) {\n    return content;\n  }\n\n  const title = \"Manage layers\";\n  return (\n    <PopupMenu\n      title={title}\n      data-command=\"ChartLayerManager\"\n      button={\n        <Btn iconPath={mdiLayers} title={title} color=\"action\" {...asMenuBtn} />\n      }\n      contentClassName=\"bg-color-1 p-1\"\n      render={() => content}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/DataLayerManager/useSortedLayerQueries.ts",
    "content": "import { isDefined } from \"prostgles-types\";\nimport type { LinkSyncItem } from \"src/dashboard/Dashboard/dashboardUtils\";\nimport type { LayerQuery } from \"src/dashboard/W_Map/W_Map\";\nimport type { ProstglesTimeChartLayer } from \"src/dashboard/W_TimeChart/W_TimeChart\";\n\ntype Args<T extends ProstglesTimeChartLayer[] | LayerQuery[]> = {\n  layerQueries: T;\n  myLinks: LinkSyncItem[];\n};\nexport const useSortedLayerQueries = <\n  T extends ProstglesTimeChartLayer[] | LayerQuery[],\n>({\n  layerQueries,\n  myLinks,\n}: Args<T>): (T[number] & {\n  link: LinkSyncItem;\n})[] => {\n  return layerQueries\n    .map((lq) => {\n      const link = myLinks.find((l) => l.id === lq.linkId);\n      if (!link) return undefined;\n      return {\n        ...lq,\n        link,\n      };\n    })\n    .filter(isDefined)\n    .sort((a, b) => {\n      return (\n        new Date(a.link.created ?? 0).getTime() -\n        new Date(b.link.created ?? 0).getTime()\n      );\n    });\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/LayerColorPicker.tsx",
    "content": "import type { BtnProps } from \"@components/Btn\";\nimport React from \"react\";\nimport { ColorPicker } from \"../W_Table/ColumnMenu/ColorPicker\";\nimport type { ChartLinkOptions } from \"./DataLayerManager/DataLayer\";\nimport { MapLayerStyling } from \"./MapLayerStyling\";\n\nexport type LayerColorPickerProps = {\n  linkOptions: ChartLinkOptions;\n  onChange: (newOptions: ChartLinkOptions) => void;\n  column: string;\n  title?: string;\n  btnProps?: BtnProps;\n};\n\nexport const LayerColorPicker = ({\n  linkOptions,\n  column,\n  title,\n  btnProps,\n  onChange,\n}: LayerColorPickerProps) => {\n  const rgba = linkOptions.columns.find((c) => c.name === column)?.colorArr ?? [\n    100, 100, 100,\n  ];\n\n  if (linkOptions.type === \"map\") {\n    return (\n      <MapLayerStyling\n        linkOptions={linkOptions}\n        onChange={onChange}\n        column={column}\n        title={title}\n      />\n    );\n  }\n\n  return (\n    <ColorPicker\n      data-command=\"LayerColorPicker\"\n      style={{ flex: \"none\" }}\n      btnProps={btnProps}\n      title={title}\n      required={true}\n      className=\"w-fit m-p5 text-2\"\n      value={`rgba(${rgba.join(\", \")})`}\n      onChange={(colorStr, colorArr) => {\n        const updatedColumns = linkOptions.columns.map((c) => ({\n          ...c,\n          colorArr: c.name === column ? colorArr : c.colorArr,\n        }));\n        onChange({\n          ...linkOptions,\n          columns: updatedColumns,\n        });\n      }}\n    />\n  );\n};\n\ndeclare global {\n  interface Array<T> {\n    // Override map to handle union array types better\n    map<U>(\n      callbackfn: (value: T, index: number, array: T[]) => U,\n      thisArg?: any,\n    ): U[];\n  }\n}\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/MapLayerStyling.tsx",
    "content": "import { FlexRow } from \"@components/Flex\";\nimport { IconPalette } from \"@components/IconPalette/IconPalette\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport React from \"react\";\nimport type { LinkSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { ColorCircle, ColorPicker } from \"../W_Table/ColumnMenu/ColorPicker\";\nimport type { LayerColorPickerProps } from \"./LayerColorPicker\";\nimport type { ChartLinkOptions } from \"./DataLayerManager/DataLayer\";\nimport { usePrgl } from \"@pages/ProjectConnection/PrglContextProvider\";\n\ntype P = Pick<LayerColorPickerProps, \"column\" | \"title\"> & {\n  linkOptions: Extract<LinkSyncItem[\"options\"], { type: \"map\" }>;\n  onChange: (newOptions: ChartLinkOptions) => void;\n};\nexport const MapLayerStyling = ({\n  linkOptions,\n  onChange,\n  column,\n  title,\n}: P) => {\n  const { tables } = usePrgl();\n  const { dataSource } = linkOptions;\n  const tableName =\n    dataSource?.type === \"table\" ? dataSource.tableName\n    : dataSource?.type === \"local-table\" ? dataSource.localTableName\n    : undefined;\n  const table =\n    !tableName ? undefined : tables.find((t) => t.name === tableName);\n\n  const linkColor = `rgba(${getLinkColor(linkOptions)})`;\n\n  return (\n    <PopupMenu\n      title={title}\n      button={<ColorCircle color={linkColor} />}\n      render={() => (\n        <FlexRow>\n          <ColorPicker\n            style={{ flex: \"none\" }}\n            label={{\n              label: \"Layer color\",\n              variant: \"normal\",\n              className: \"mb-p5\",\n            }}\n            title={title}\n            required={true}\n            className=\"w-fit m-p5 text-2\"\n            value={linkColor}\n            onChange={(__, _, colorArr) => {\n              onChange({\n                ...linkOptions,\n                mapColorMode: {\n                  type: \"fixed\",\n                  colorArr,\n                },\n                /** Is this used only for timechart? */\n                columns: linkOptions.columns.map((c) => ({\n                  ...c,\n                  colorArr: c.name === column ? colorArr : c.colorArr,\n                })),\n              });\n            }}\n          />\n          <IconPalette\n            label={{ label: \"Icon\", variant: \"normal\", className: \"mb-p5\" }}\n            iconName={\n              linkOptions.mapIcons?.type === \"fixed\" ?\n                linkOptions.mapIcons.iconPath\n              : undefined\n            }\n            onChange={(iconPath) => {\n              onChange({\n                ...linkOptions,\n                mapIcons:\n                  !iconPath ? undefined : (\n                    {\n                      type: \"fixed\",\n                      iconPath,\n                    }\n                  ),\n              });\n            }}\n          />\n          {linkOptions.mapIcons && (\n            <Select\n              label={\"Display\"}\n              fullOptions={[{ key: \"Circle and Icon\" }, { key: \"Icon\" }]}\n              value={\n                linkOptions.mapIcons.display === \"icon\" ?\n                  \"Icon\"\n                : \"Circle and Icon\"\n              }\n              onChange={(v) => {\n                onChange({\n                  ...linkOptions,\n                  mapIcons: {\n                    ...linkOptions.mapIcons!,\n                    display: v === \"Icon\" ? \"icon\" : \"icon+circle\",\n                  },\n                });\n              }}\n            />\n          )}\n          {table && (\n            <Select\n              label={\"Labels\"}\n              value={linkOptions.mapShowText?.columnName}\n              fullOptions={table.columns.map((c) => ({\n                key: c.name,\n                subLabel: c.data_type,\n                disabledInfo:\n                  (\n                    ![\n                      \"text\",\n                      \"int4\",\n                      \"int8\",\n                      \"date\",\n                      \"timestamp\",\n                      \"varchar\",\n                      \"name\",\n                    ].includes(c.udt_name)\n                  ) ?\n                    \"Only text columns can be used for labels\"\n                  : undefined,\n              }))}\n              optional={true}\n              onChange={(columnName) => {\n                onChange({\n                  ...linkOptions,\n                  mapShowText:\n                    columnName ?\n                      {\n                        columnName,\n                      }\n                    : undefined,\n                });\n              }}\n            />\n          )}\n        </FlexRow>\n      )}\n    />\n  );\n};\n\nconst getLinkColor = (options: LinkSyncItem[\"options\"]) => {\n  if (options.type === \"table\") {\n    return options.colorArr;\n  }\n  if (options.type === \"map\") {\n    if (options.mapColorMode?.type === \"fixed\") {\n      return options.mapColorMode.colorArr;\n    }\n    if (options.mapColorMode?.type === \"scale\") {\n      return options.mapColorMode.minColorArr;\n    }\n    if (options.mapColorMode?.type === \"conditional\") {\n      return options.mapColorMode.conditions[0]?.colorArr;\n    }\n  }\n  return options.columns[0]?.colorArr;\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/OSMLayerOptions.tsx",
    "content": "import React from \"react\";\nimport type { LinkSyncItem } from \"../Dashboard/dashboardUtils\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport Btn from \"@components/Btn\";\nimport { mdiMap } from \"@mdi/js\";\nimport { OverpassQuery } from \"../W_Map/OSM/OverpassQuery\";\nimport { isDefined } from \"../../utils/utils\";\nimport { FlexRow } from \"@components/Flex\";\nimport { getRandomColor } from \"../Dashboard/PALETTE\";\n\ntype P = {\n  link: LinkSyncItem;\n  dataSource: Extract<\n    Extract<LinkSyncItem[\"options\"], { type: \"map\" }>[\"dataSource\"],\n    { type: \"osm\" }\n  >;\n};\nexport const OSMLayerOptions = ({ link, dataSource }: P) => {\n  const opts = link.options;\n  if (opts.type !== \"map\") return null;\n  const query = dataSource.osmLayerQuery;\n  if (!isDefined(query)) return null;\n  return (\n    <PopupMenu\n      showFullscreenToggle={{}}\n      title=\"OSM Layer Options\"\n      button={\n        <FlexRow>\n          <Btn title=\"Edit Overpass Query\" iconPath={mdiMap} />\n          <div\n            className=\"f-1 text-ellipsis text-1 font-18\"\n            style={{ fontWeight: 500 }}\n          >\n            {query}\n          </div>\n        </FlexRow>\n      }\n      onClickClose={false}\n    >\n      <OverpassQuery\n        query={query}\n        onChange={(osmLayerQuery) => {\n          link.$update(\n            {\n              options: {\n                dataSource: {\n                  ...dataSource,\n                  osmLayerQuery,\n                },\n                mapColorMode: {\n                  type: \"fixed\",\n                  colorArr: getRandomColor(1),\n                },\n              },\n            },\n            { deepMerge: true },\n          );\n        }}\n      />\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/SQLChartLayerEditor.tsx",
    "content": "import React from \"react\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport { LANG } from \"../SQLEditor/W_SQLEditor\";\nimport type { LinkSyncItem } from \"../Dashboard/dashboardUtils\";\n\ntype P = {\n  link: LinkSyncItem;\n};\nexport const SQLChartLayerEditor = ({ link }: P) => {\n  const dataSource =\n    link.options.type !== \"table\" ? link.options.dataSource : undefined;\n  if (dataSource?.type !== \"sql\") {\n    return <>Data source is not SQL</>;\n  }\n  return (\n    <CodeEditor\n      language={LANG}\n      value={dataSource.sql}\n      style={{ minWidth: \"min(600px, 90vw)\" }}\n      onChange={(v) => {}}\n      onSave={(v) => {\n        link.$update(\n          {\n            options: {\n              ...link.options,\n              type: \"timechart\",\n              dataSource: {\n                type: \"sql\",\n                sql: v,\n              },\n            },\n          },\n          { deepMerge: true },\n        );\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WindowControls/TimeChartLayerOptions.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiSigma, mdiTableColumn } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport { _PG_numbers, includes, tryCatchV2 } from \"prostgles-types\";\nimport React from \"react\";\nimport { usePrgl } from \"src/pages/ProjectConnection/PrglContextProvider\";\nimport type { LinkSyncItem, WindowSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { windowIs } from \"../Dashboard/dashboardUtils\";\nimport { RenderFilter } from \"../RenderFilter\";\nimport { getTableExpressionReturnType } from \"../SQLEditor/SQLCompletion/completionUtils/getQueryReturnType\";\nimport { getTimeChartLayer } from \"../W_TimeChart/fetchData/getTimeChartLayers\";\nimport { TIMECHART_STAT_TYPES } from \"../W_TimeChart/W_TimeChartMenu\";\nimport type { MapLayerManagerProps } from \"./DataLayerManager/DataLayerManager\";\nimport { SQLChartLayerEditor } from \"./SQLChartLayerEditor\";\nimport FormField from \"@components/FormField/FormField\";\n\ntype TimeChartLayerOptionsProps = Pick<\n  MapLayerManagerProps,\n  \"myLinks\" | \"getLinksAndWindows\"\n> & {\n  link: LinkSyncItem;\n  column: string;\n  w: WindowSyncItem;\n  mode?: \"on-screen\";\n};\nexport const TimeChartLayerOptions = ({\n  link,\n  column,\n  getLinksAndWindows,\n  myLinks,\n  w: wMapOrTimechart,\n  mode,\n}: TimeChartLayerOptionsProps) => {\n  const { db, tables } = usePrgl();\n  const sqlHandler = db.sql;\n  const linkOpts = link.options;\n  const sqlDataSourceColumns = usePromise(async () => {\n    if (\n      !sqlHandler ||\n      linkOpts.type !== \"timechart\" ||\n      linkOpts.dataSource?.type !== \"sql\"\n    )\n      return [];\n    const { colTypes, error } = await getTableExpressionReturnType(\n      linkOpts.dataSource.sql,\n      sqlHandler,\n    );\n    if (error) console.warn(error);\n    return (\n      colTypes?.map((c) => {\n        return {\n          ...c,\n          name: c.column_name,\n        };\n      }) ?? []\n    );\n  }, [linkOpts, sqlHandler]);\n\n  if (!windowIs(wMapOrTimechart, \"timechart\")) {\n    return null;\n  }\n  const w = wMapOrTimechart as WindowSyncItem<\"timechart\">;\n  if (linkOpts.type !== \"timechart\") {\n    return <>Invalid link type: {linkOpts.type}</>;\n  }\n\n  const colOpts = linkOpts.columns.find((dc) => dc.name === column);\n  if (!colOpts) {\n    return <>Column not found: {column}</>;\n  }\n\n  const { windows, links } = getLinksAndWindows();\n  const { data: lq } = tryCatchV2(() =>\n    getTimeChartLayer({\n      active_row: undefined,\n      link,\n      windows,\n      links,\n      myLinks,\n      w,\n    }).find((l) => l.dateColumn === column),\n  );\n  const parentW = windows.find(\n    (_w) => _w.id !== w.id && [link.w1_id, link.w2_id].includes(_w.id),\n  );\n  const table =\n    lq?.type === \"table\" ?\n      tables.find((t) => t.name === lq.tableName)\n    : undefined;\n  const dataSource = linkOpts.dataSource;\n\n  // TODO: this needs refactoring\n  const cols =\n    dataSource?.type === \"sql\" ? (sqlDataSourceColumns ?? [])\n    : dataSource?.type === \"local-table\" ?\n      (tables.find((t) => t.name === dataSource.localTableName)?.columns ?? [])\n    : ((parentW?.type === \"sql\" ?\n        (linkOpts.otherColumns ?? parentW.options.sqlResultCols)\n      : parentW?.type === \"table\" ? table?.columns\n      : []) ?? []);\n\n  const numericCols = cols.filter((c) => includes(_PG_numbers, c.udt_name));\n  const statType = colOpts.statType ?? {\n    funcName: \"$countAll\",\n    numericColumn: undefined,\n  };\n\n  const updateLinkOpts = (newOpts: Partial<typeof linkOpts>) => {\n    link.$update(\n      {\n        options: newOpts,\n      },\n      { deepMerge: true },\n    );\n  };\n\n  const updateCol = (col: string, newColOpts: Partial<typeof colOpts>) => {\n    updateLinkOpts({\n      ...linkOpts,\n      columns: linkOpts.columns.map((c) =>\n        c.name === col ? { ...c, ...newColOpts } : c,\n      ),\n    });\n  };\n  const isOnScreen = mode === \"on-screen\";\n  const activeStat = TIMECHART_STAT_TYPES.find(\n    (s) => s.func === statType.funcName,\n  );\n  const activeStatLabel = activeStat?.label ?? statType.funcName;\n  const boldTextNode = (text: string) => (\n    <strong style={{ margin: \".6px\" }}>{text}</strong>\n  );\n  const fadedTextNode = (text: string) => (\n    <span style={{ opacity: 0.7 }}>{text}</span>\n  );\n  const activeStatLabelDesc =\n    activeStatLabel === \"Count All\" ? \"count(*), \" : (\n      <FlexRow className=\"gap-0\">\n        {fadedTextNode(`${activeStatLabel}(`)}\n        {boldTextNode(colOpts.statType?.numericColumn ?? \"\")}\n        {fadedTextNode(`),`)}\n      </FlexRow>\n    );\n  const groupByCols = cols.filter(\n    (c) =>\n      c.name !== lq?.statType?.numericColumn &&\n      c.name !== lq?.dateColumn &&\n      c.udt_name !== \"timestamp\" &&\n      c.udt_name !== \"timestamptz\",\n  );\n\n  const title = linkOpts.title || (\n    <>\n      {activeStatLabelDesc}\n      {isOnScreen ? `${lq?.dateColumn}` : \"\"}\n    </>\n  );\n\n  return (\n    <>\n      <PopupMenu\n        title=\"Layer options\"\n        data-command=\"TimeChartLayerOptions.yAxis\"\n        showFullscreenToggle={{}}\n        rootChildClassname=\"f-1\"\n        contentClassName=\"p-1\"\n        clickCatchStyle={{ opacity: 1 }}\n        button={\n          <FlexRow className=\"gap-p5 font-14\">\n            <Btn\n              color=\"action\"\n              variant={isOnScreen ? \"text\" : \"faded\"}\n              iconPath={isOnScreen ? \"\" : mdiSigma}\n              title=\"Aggregate function\"\n              data-command=\"TimeChartLayerOptions.aggFunc\"\n              style={{\n                paddingRight: isOnScreen ? \"0\" : undefined,\n              }}\n            >\n              {title}\n            </Btn>\n          </FlexRow>\n        }\n        render={() => (\n          <FlexCol className=\"gap-2 f-1 \">\n            <FormField\n              type=\"text\"\n              label={\"Title (optional)\"}\n              value={linkOpts.title}\n              onChange={(newTitle) => {\n                updateLinkOpts({ title: newTitle });\n              }}\n            />\n            <FlexRowWrap>\n              <Select\n                label=\"Aggregation type\"\n                variant=\"div\"\n                className=\"w-fit\"\n                data-command=\"TimeChartLayerOptions.aggFunc.select\"\n                btnProps={{\n                  iconPath: mdiSigma,\n                  color: \"action\",\n                  iconPosition: \"left\",\n                }}\n                fullOptions={TIMECHART_STAT_TYPES.map((s) => ({\n                  key: s.func,\n                  label: s.label,\n                  disabledInfo:\n                    numericCols.length || s.func === \"$countAll\" ?\n                      undefined\n                    : \"Requires a numeric column\",\n                }))}\n                value={statType.funcName}\n                onChange={(funcName) => {\n                  updateCol(colOpts.name, {\n                    statType: {\n                      funcName,\n                      numericColumn:\n                        statType.numericColumn ?? numericCols[0]!.name,\n                    },\n                  });\n                }}\n              />\n              <Select\n                label=\"Aggregation field\"\n                variant=\"div\"\n                className=\"w-fit \"\n                btnProps={{\n                  color: \"action\",\n                }}\n                data-command=\"TimeChartLayerOptions.numericColumn\"\n                fullOptions={numericCols.map((c) => ({\n                  key: c.name,\n                  subLabel: c.udt_name,\n                  ...c,\n                }))}\n                disabledInfo={\n                  statType.funcName === \"$countAll\" ?\n                    \"Requires a different aggregation function\"\n                  : !numericCols.length ?\n                    \"No numeric columns available\"\n                  : undefined\n                }\n                value={colOpts.statType?.numericColumn}\n                onChange={(numericColumn) => {\n                  updateCol(colOpts.name, {\n                    statType: { ...statType, numericColumn },\n                  });\n                }}\n              />\n            </FlexRowWrap>\n            <Select\n              label=\"Group by field\"\n              variant=\"div\"\n              className=\"w-fit \"\n              data-command=\"TimeChartLayerOptions.groupBy\"\n              optional={true}\n              disabledInfo={\n                groupByCols.length ? undefined : (\n                  \"No groupable columns available\"\n                )\n              }\n              btnProps={{\n                iconPath: mdiTableColumn,\n                color: !lq?.groupByColumn ? undefined : \"action\",\n                iconPosition: \"left\",\n              }}\n              fullOptions={[\n                {\n                  key: undefined,\n                  label: \"NONE\",\n                },\n                ...groupByCols.map((s) => ({\n                  key: s.name,\n                  label: \"label\" in s ? s.label : s.name,\n                  subLabel: s.udt_name,\n                })),\n              ]}\n              value={lq?.groupByColumn}\n              onChange={(groupByColumn) => {\n                updateLinkOpts({ groupByColumn });\n              }}\n            />\n            {isOnScreen && (\n              <FlexCol\n                className={\"gap-p5 \" + (lq?.type === \"sql\" ? \"f-1\" : \"\")}\n              >\n                <Label variant=\"normal\">\n                  {lq?.type === \"sql\" ? \"Query\" : \"Table\"}\n                </Label>\n                {lq?.type === \"sql\" ?\n                  <SQLChartLayerEditor link={link} />\n                : <code className=\"ta-start ws-pre-line bg-color-2 rounded p-p5\">\n                    {lq?.type === \"table\" ?\n                      (lq.joinPath?.at(-1)?.table ?? lq.tableName)\n                    : lq?.localTableName}\n                  </code>\n                }\n              </FlexCol>\n            )}\n            {lq?.type === \"local-table\" &&\n              dataSource?.type === \"local-table\" && (\n                <FlexCol className=\"gap-p5\">\n                  <Label variant=\"normal\">Filter</Label>\n                  <RenderFilter\n                    title=\"Manage filters\"\n                    mode=\"compact\"\n                    selectedColumns={undefined}\n                    itemName=\"filter\"\n                    contextData={undefined}\n                    onChange={(smartGroupFilter) => {\n                      updateLinkOpts({\n                        dataSource: {\n                          ...dataSource,\n                          smartGroupFilter,\n                        },\n                      });\n                    }}\n                    db={db}\n                    tableName={lq.localTableName}\n                    filter={dataSource.smartGroupFilter}\n                    tables={tables}\n                  />\n                </FlexCol>\n              )}\n          </FlexCol>\n        )}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WorkspaceMenu/WorkspaceAddBtn.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport React, { useCallback, useState } from \"react\";\nimport type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport { isObject } from \"prostgles-types\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport type { WorkspaceSchema } from \"../Dashboard/dashboardUtils\";\nimport type { Prgl } from \"../../App\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { useIsMounted } from \"prostgles-client\";\n\ntype WorkspaceDeleteBtnProps = Pick<Prgl, \"dbs\"> & {\n  connection_id: string;\n  setWorkspace: (w: Required<WorkspaceSchema>) => void;\n  btnProps?: BtnProps<void>;\n  className?: string;\n};\nexport const WorkspaceAddBtn = ({\n  dbs,\n  connection_id,\n  setWorkspace,\n  btnProps,\n  className,\n}: WorkspaceDeleteBtnProps) => {\n  const [error, setError] = useState<any>();\n  const [name, setName] = useState(\"\");\n\n  const getIsMounted = useIsMounted();\n  const insertNewWorkspace = useCallback(async () => {\n    try {\n      const newWsp = await dbs.workspaces.insert(\n        {\n          name,\n          connection_id,\n        } as DBSSchema[\"workspaces\"],\n        { returning: \"*\" },\n      );\n      if (!getIsMounted()) return;\n      setWorkspace(newWsp);\n    } catch (newWspErr: any) {\n      if (\n        isObject(newWspErr) &&\n        newWspErr.columns?.join?.() ===\n          [\"user_id\", \"connection_id\", \"name\"].join()\n      ) {\n        setError(\"Already exists\");\n      } else {\n        setError(newWspErr);\n      }\n    }\n  }, [dbs, name, connection_id, setError, setWorkspace, getIsMounted]);\n\n  return (\n    <PopupMenu\n      style={{}}\n      onClickClose={false}\n      className={className}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\") {\n          insertNewWorkspace();\n        }\n      }}\n      autoFocusFirst={\"content\"}\n      positioning=\"inside\"\n      data-command=\"WorkspaceAddBtn\"\n      button={\n        <Btn\n          title=\"Add new workspace\"\n          iconPath={mdiPlus}\n          size=\"small\"\n          variant=\"filled\"\n          color=\"action\"\n          {...btnProps}\n        />\n      }\n      content={\n        <div>\n          <FormField\n            label=\"New workspace name\"\n            value={name}\n            onChange={(name) => {\n              setName(name);\n              setError(undefined);\n            }}\n            error={error}\n          />\n        </div>\n      }\n      footerButtons={[\n        {\n          label: \"Cancel\",\n          onClickClose: true,\n        },\n        {\n          color: \"action\",\n          label: \"Create\",\n          variant: \"filled\",\n          iconPath: mdiPlus,\n          \"data-command\": \"WorkspaceAddBtn.Create\",\n          onClickPromise: insertNewWorkspace,\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WorkspaceMenu/WorkspaceDeleteBtn.tsx",
    "content": "import { mdiDelete } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport Btn, { type BtnProps } from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { pageReload } from \"@components/Loader/Loading\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport type { Workspace } from \"../Dashboard/dashboardUtils\";\nimport { ROUTES } from \"@common/utils\";\n\ntype WorkspaceDeleteBtnProps = Pick<Prgl, \"dbs\"> &\n  Pick<BtnProps, \"disabledInfo\"> & {\n    w: Workspace;\n    activeWorkspaceId: string;\n  };\nexport const WorkspaceDeleteBtn = ({\n  dbs,\n  w,\n  activeWorkspaceId,\n  disabledInfo,\n}: WorkspaceDeleteBtnProps) => {\n  const [error, setError] = useState<any>();\n\n  return (\n    <PopupMenu\n      style={{\n        height: \"100%\",\n      }}\n      onClickClose={false}\n      contentStyle={{ maxWidth: \"300px\" }}\n      clickCatchStyle={{ opacity: 0.2 }}\n      positioning=\"beneath-center\"\n      button={\n        <Btn\n          title=\"Delete this workspace\"\n          iconPath={mdiDelete}\n          className=\"delete-workspace\"\n          disabledInfo={disabledInfo}\n          data-command=\"WorkspaceDeleteBtn\"\n          color=\"danger\"\n        />\n      }\n      content={\n        <div className=\"flex-col gap-p5\">\n          <div>\n            Are you sure you want to delete this workspace and all related data\n            (windows, links)?\n          </div>\n          {error && <ErrorComponent error={error} />}\n        </div>\n      }\n      footerButtons={[\n        {\n          label: \"Cancel\",\n          onClickClose: true,\n        },\n        {\n          color: \"danger\",\n          label: \"Delete workspace\",\n          variant: \"faded\",\n          \"data-command\": \"WorkspaceDeleteBtn.Confirm\",\n          onClick: async (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            try {\n              await dbs.workspaces.update({ id: w.id }, { deleted: true });\n              if (w.id === activeWorkspaceId) {\n                const path = [ROUTES.CONNECTIONS, w.connection_id]\n                  .filter((v) => v)\n                  .join(\"/\");\n                window.location.href = path;\n              } else {\n                pageReload(\"Workspace deleted\");\n              }\n            } catch (newWspErr) {\n              setError(error);\n            }\n          },\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WorkspaceMenu/WorkspaceMenu.css",
    "content": ".add-wsp-btn button:hover {\n  color: var(--action) !important;\n}\n\n#dashboard-menu-button {\n  background-color: rgb(240 254 255);\n  border: \"1px solid #00d0ff\";\n}\n\n.dark-theme #dashboard-menu-button {\n  background-color: #002537;\n  border-color: rgb(0 80 99);\n}\n"
  },
  {
    "path": "client/src/dashboard/WorkspaceMenu/WorkspaceMenu.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FlexRow, classOverride } from \"@components/Flex\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport { onWheelScroll } from \"@components/Table/Table\";\nimport { mdiPencil, mdiViewDashboard, mdiViewDashboardEdit } from \"@mdi/js\";\nimport React, { useEffect, useRef } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport type { Command } from \"../../Testing\";\nimport type { WorkspaceSyncItem } from \"../Dashboard/dashboardUtils\";\nimport { useSetActiveWorkspace, useWorkspaces } from \"./useWorkspaces\";\nimport \"./WorkspaceMenu.css\";\nimport { WorkspaceMenuDropDown } from \"./WorkspaceMenuDropDown\";\n\ntype P = {\n  workspace: WorkspaceSyncItem;\n  prgl: Prgl;\n  className?: string;\n};\n\nexport const WorkspaceMenu = (props: P) => {\n  const {\n    workspace,\n    className,\n    prgl: { dbs, user },\n  } = props;\n  const listRef = useRef<HTMLDivElement>(null);\n\n  const { setWorkspace } = useSetActiveWorkspace(workspace.id);\n  const userId = user?.id;\n  const { workspaces } = useWorkspaces(dbs, userId!, workspace.connection_id);\n\n  useEffect(() => {\n    setTimeout(() => {\n      listRef.current?.querySelector(\"li.active\")?.scrollIntoView();\n    }, 100);\n  }, [workspace.id, workspaces]);\n\n  const renderedWorkspaces =\n    window.isLowWidthScreen ?\n      workspaces.filter((w) => w.id === workspace.id)\n    : workspaces;\n\n  return (\n    <FlexRow\n      ref={listRef}\n      className={classOverride(\n        \"WorkspaceMenu as-end jc-center text-white ai-center f-1  min-w-0 o-auto h-fit\",\n        className,\n      )}\n      style={{\n        gap: \"1px\",\n      }}\n    >\n      <ul\n        className={\n          \"no-decor o-auto f-1 min-w-0 max-w-fit flex-row no-scroll-bar ai-end\"\n        }\n        onWheel={onWheelScroll()}\n        data-command={\"WorkspaceMenu.list\" satisfies Command}\n      >\n        {renderedWorkspaces.map((w) => (\n          <li\n            key={w.id}\n            className={\n              \"workspace-list-item text-1 relative flex-row \" +\n              (workspace.id === w.id ? \"active\" : \"\")\n            }\n          >\n            <Btn\n              title={\n                (w.published && !w.isMine ? \"Shared workspace\" : \"Workspace\") +\n                (w.isMine ? \"\" : \" (readonly)\")\n              }\n              iconNode={w.icon ? <SvgIcon icon={w.icon} /> : undefined}\n              style={{\n                padding: \"16px\",\n                borderBottomStyle: \"solid\",\n                borderBottomWidth: \"4px\",\n                borderBottomColor: \"transparent\",\n                ...(workspace.id === w.id && {\n                  borderBottomColor: \"var(--active)\",\n                  fontWeight: 600,\n                }),\n                borderRadius: 0,\n                whiteSpace: \"nowrap\",\n              }}\n              onClick={() => {\n                setWorkspace(w);\n              }}\n            >\n              {w.name}\n            </Btn>\n          </li>\n        ))}\n      </ul>\n\n      {user?.type === \"admin\" && !window.isLowWidthScreen && (\n        <Btn\n          iconPath={\n            workspace.layout_mode === \"fixed\" ?\n              mdiViewDashboardEdit\n            : mdiViewDashboard\n          }\n          title={\"Toggle Layout Mode\"}\n          data-command=\"WorkspaceMenu.toggleWorkspaceLayoutMode\"\n          onClick={() => {\n            workspace.$update({\n              layout_mode:\n                workspace.layout_mode === \"fixed\" ? \"editable\" : \"fixed\",\n            });\n          }}\n        />\n      )}\n      <WorkspaceMenuDropDown\n        {...props}\n        setWorkspace={setWorkspace}\n        workspaces={workspaces}\n      />\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WorkspaceMenu/WorkspaceMenuDropDown.tsx",
    "content": "import {\n  mdiAccountMultiple,\n  mdiChevronDown,\n  mdiContentCopy,\n  mdiViewCarousel,\n} from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SearchList } from \"@components/SearchList/SearchList\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport { cloneWorkspace } from \"../Dashboard/cloneWorkspace\";\nimport { WorkspaceAddBtn } from \"./WorkspaceAddBtn\";\nimport { WorkspaceDeleteBtn } from \"./WorkspaceDeleteBtn\";\nimport \"./WorkspaceMenu.css\";\nimport { WorkspaceSettings } from \"./WorkspaceSettings\";\nimport type { WorkspaceSyncItem } from \"../Dashboard/dashboardUtils\";\nimport type { Prgl } from \"src/App\";\nimport type { useSetActiveWorkspace, useWorkspaces } from \"./useWorkspaces\";\n\ntype P = {\n  workspace: WorkspaceSyncItem;\n  prgl: Prgl;\n} & ReturnType<typeof useWorkspaces> &\n  ReturnType<typeof useSetActiveWorkspace>;\n\nexport const WorkspaceMenuDropDown = ({\n  prgl,\n  workspace,\n  workspaces,\n  setWorkspace,\n}: P) => {\n  const { dbs, dbsTables, dbsMethods, user } = prgl;\n  const isAdmin = user?.type === \"admin\";\n  const sortedWorkspaces = useMemo(\n    () =>\n      workspaces.sort(\n        (a, b) =>\n          new Date(a.created!).getTime() - new Date(b.created!).getTime() ||\n          a.name.localeCompare(b.name),\n      ),\n    [workspaces],\n  );\n  return (\n    <PopupMenu\n      title=\"Workspaces\"\n      rootStyle={{\n        maxHeight: `100%`,\n        marginRight: \"1em\",\n      }}\n      positioning=\"beneath-right\"\n      button={\n        <Btn\n          title=\"Manage Workspaces\"\n          iconPath={mdiChevronDown}\n          className={\"text-0\"}\n          data-command=\"WorkspaceMenuDropDown\"\n          style={\n            window.isLowWidthScreen ?\n              {}\n            : {\n                padding: \"12px\",\n              }\n          }\n        />\n      }\n      contentStyle={{\n        overflow: \"hidden\",\n        padding: 0,\n        borderRadius: 0,\n      }}\n      render={(closePopup) => (\n        <FlexCol\n          className={\"flex-col f-1 min-h-0 gap-0\"}\n          style={{ paddingTop: 0 }}\n          onKeyDown={(e) => {\n            if (e.key === \"Escape\") {\n              closePopup();\n            }\n          }}\n        >\n          {!workspaces.length ?\n            <div className=\"text-2\">No other workspaces</div>\n          : <SearchList\n              id=\"search-list-queries\"\n              data-command=\"WorkspaceMenu.SearchList\"\n              className={\" b-t f-1 min-h-0 \"}\n              style={{ minHeight: \"120px\", maxHeight: \"30vh\" }}\n              placeholder={\"Workspaces\"}\n              items={sortedWorkspaces.map((w) => ({\n                key: w.name,\n                label: w.name,\n                labelStyle: {},\n                rowStyle:\n                  workspace.id === w.id ?\n                    {\n                      background: \"var(--bg-li-selected)\",\n                    }\n                  : {},\n                contentLeft: (\n                  <div\n                    className=\"flex-col ai-start f-0 mr-1 text-2\"\n                    style={\n                      workspace.id === w.id ?\n                        { color: \"var(--active)\" }\n                      : undefined\n                    }\n                  >\n                    {w.icon ?\n                      <SvgIcon icon={w.icon} />\n                    : <Icon path={mdiViewCarousel} size={1} />}\n                  </div>\n                ),\n                contentRight: (\n                  <div className=\"flex-row gap-p5 pl-1 show-on-parent-hover\">\n                    {w.published && isAdmin && (\n                      <Btn\n                        title=\"Published\"\n                        iconPath={mdiAccountMultiple}\n                        color=\"action\"\n                        asNavLink={true}\n                        href={`/connection-config/${w.connection_id}?section=access_control`}\n                      />\n                    )}\n                    <WorkspaceDeleteBtn\n                      w={w}\n                      dbs={dbs}\n                      activeWorkspaceId={workspace.id}\n                      disabledInfo={\n                        isAdmin || w.isMine ?\n                          undefined\n                        : \"You can not delete a published workspace\"\n                      }\n                    />\n                    <Btn\n                      iconPath={mdiContentCopy}\n                      title=\"Clone workspace\"\n                      data-command=\"WorkspaceMenu.CloneWorkspace\"\n                      onClickPromise={async () => {\n                        await cloneWorkspace(dbs, w.id).then((d) => {\n                          setWorkspace(d.clonedWsp);\n                        });\n                      }}\n                    />\n                    {(isAdmin || w.isMine) && (\n                      <>\n                        <WorkspaceSettings\n                          w={w}\n                          dbs={prgl.dbs}\n                          dbsTables={dbsTables}\n                          dbsMethods={dbsMethods}\n                        />\n                      </>\n                    )}\n                  </div>\n                ),\n                onPress: (e) => {\n                  if (\n                    w.id === workspace.id ||\n                    (e.target as Element | null)?.closest(\n                      \".delete-workspace\",\n                    ) ||\n                    (e.target as Element | null)?.closest(\n                      \".workspace-settings\",\n                    ) ||\n                    (e.target as Element | null)?.closest(\".clickcatchcomp\")\n                  )\n                    return;\n\n                  setWorkspace(w);\n                  closePopup();\n                },\n              }))}\n            />\n          }\n        </FlexCol>\n      )}\n      footer={() => (\n        <WorkspaceAddBtn\n          dbs={dbs}\n          connection_id={workspace.connection_id}\n          setWorkspace={setWorkspace}\n          btnProps={{\n            children: \"New workspace\",\n            \"data-command\": \"WorkspaceMenuDropDown.WorkspaceAddBtn\",\n          }}\n        />\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WorkspaceMenu/WorkspaceSettings.tsx",
    "content": "import { mdiCog } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React from \"react\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport { IconPalette } from \"@components/IconPalette/IconPalette\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport type {\n  DBSchemaTablesWJoins,\n  Workspace,\n} from \"../Dashboard/dashboardUtils\";\nimport { SmartForm } from \"../SmartForm/SmartForm\";\n\ntype WorkspaceSettingsProps = Pick<Prgl, \"dbs\" | \"dbsMethods\"> & {\n  w: Workspace;\n  dbsTables: DBSchemaTablesWJoins;\n};\nexport const WorkspaceSettings = ({\n  dbs,\n  dbsTables,\n  w,\n  dbsMethods,\n}: WorkspaceSettingsProps) => {\n  return (\n    <PopupMenu\n      title={\"Workspace settings\"}\n      style={{\n        height: \"100%\",\n      }}\n      clickCatchStyle={{ opacity: 1 }}\n      onClickClose={false}\n      positioning=\"top-center\"\n      data-command=\"WorkspaceSettings\"\n      button={\n        <Btn\n          title=\"Workspace settings\"\n          iconPath={mdiCog}\n          className=\"workspace-settings\"\n          onContextMenu={async () => {\n            const workspaceData = await dbs.workspaces.findOne(\n              { id: w.id },\n              {\n                select: {\n                  name: true,\n                  options: true,\n                  layout: true,\n                  windows: {\n                    id: false,\n                    user_id: false,\n                    workspace_id: false,\n                    created: false,\n                    last_updated: false,\n                  },\n                },\n              },\n            );\n            void navigator.clipboard.writeText(\n              JSON.stringify(workspaceData, null, 2),\n            );\n            alert(\"Workspace data copied to clipboard\");\n          }}\n        />\n      }\n      contentStyle={{ padding: 0 }}\n      render={(popupClose) => (\n        <div className=\"flex-col gap-p5  min-h-0\">\n          <SmartForm\n            db={dbs as DBHandlerClient}\n            showJoinedTables={false}\n            label=\"\"\n            contentClassname=\"p-1\"\n            tableName=\"workspaces\"\n            tables={dbsTables}\n            methods={dbsMethods}\n            confirmUpdates={true}\n            columns={{\n              name: 1,\n              published: 1,\n              layout_mode: 1,\n              icon: {\n                onRender: (value: string, onChange) => {\n                  return <IconPalette iconName={value} onChange={onChange} />;\n                },\n              },\n            }}\n            disabledActions={[\"clone\", \"delete\"]}\n            rowFilter={[{ fieldName: \"id\", value: w.id }]}\n            onClose={popupClose}\n          />\n        </div>\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/dashboard/WorkspaceMenu/useWorkspaces.ts",
    "content": "import { ROUTES } from \"@common/utils\";\nimport type { DBS } from \"../Dashboard/DBS\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { useCallback, useMemo } from \"react\";\nimport type { Workspace } from \"../Dashboard/dashboardUtils\";\n\nexport const useWorkspaces = (\n  dbs: DBS,\n  userId: string,\n  connectionId: string,\n) => {\n  const unsortedWorkspaces = useWorkspacesSync(dbs, connectionId);\n  const workspaces = useMemo(() => {\n    return (\n      unsortedWorkspaces\n        .slice(0)\n        .sort((a, b) => +new Date(a.created!) - +new Date(b.created!))\n        .map((wsp) => ({\n          ...wsp,\n          isMine: wsp.user_id === userId,\n        }))\n        /** Exclude editable original workspaces */\n        .filter(\n          (wsp) => wsp.isMine || !wsp.published || wsp.layout_mode === \"fixed\",\n        )\n    );\n  }, [unsortedWorkspaces, userId]);\n\n  return { workspaces };\n};\n\nconst WorkspaceIdSearchParam = \"workspaceId\" as const;\nexport const getWorkspacePath = (\n  w: Pick<Workspace, \"id\" | \"connection_id\">,\n) => {\n  return [\n    ROUTES.CONNECTIONS,\n    `${w.connection_id}?${WorkspaceIdSearchParam}=${w.id}`,\n  ]\n    .filter((v) => v)\n    .join(\"/\");\n};\n\nexport const useSetActiveWorkspace = (\n  currentWorkspaceId: string | undefined,\n) => {\n  const navigate = useNavigate();\n  const [searchParams, setSearchParams] = useSearchParams();\n  const setWorkspace = useCallback(\n    (w: Pick<Workspace, \"id\" | \"connection_id\"> | undefined) => {\n      if (!w) {\n        searchParams.delete(WorkspaceIdSearchParam);\n        setSearchParams(searchParams);\n        return;\n      }\n      if (w.id === currentWorkspaceId) {\n        return;\n      }\n      const path = getWorkspacePath(w);\n\n      navigate(path);\n    },\n    [currentWorkspaceId, navigate, searchParams, setSearchParams],\n  );\n\n  return { setWorkspace };\n};\n\n/**\n * The purpose of this is to ensure that we reuse sync'd workspaces\n */\nexport const useWorkspacesSync = (dbs: DBS, connection_id: string) => {\n  const { data: unsortedWorkspaces = [] } = dbs.workspaces.useSync!(\n    { connection_id, deleted: false },\n    { handlesOnData: true, select: \"*\", patchText: false },\n  );\n  return unsortedWorkspaces;\n};\n"
  },
  {
    "path": "client/src/dashboard/joinUtils.ts",
    "content": "import { getFinalFilter } from \"@common/filterUtils\";\nimport type { AnyObject, ParsedJoinPath } from \"prostgles-types\";\nimport { reverseParsedPath } from \"prostgles-types\";\nimport { isDefined, quickClone } from \"../utils/utils\";\nimport type {\n  Link,\n  WindowData,\n  WindowSyncItem,\n} from \"./Dashboard/dashboardUtils\";\nimport W_Map from \"./W_Map/W_Map\";\nimport type { ActiveRow } from \"./W_Table/W_Table\";\nimport { getTimeChartFilters } from \"./W_TimeChart/fetchData/getTimeChartLayersWithBins\";\n\ntype SyncWindow =\n  | WindowSyncItem<\"table\">\n  | WindowSyncItem<\"map\">\n  | WindowSyncItem<\"sql\">\n  | WindowSyncItem<\"timechart\">;\n\nexport type CrossFilters = {\n  crossFilters: AnyObject[];\n  activeRowFilter?: AnyObject;\n  all: AnyObject[];\n};\ntype CrossFilterWindow =\n  | Pick<WindowSyncItem<\"table\">, \"id\" | \"table_name\" | \"type\">\n  | Pick<WindowSyncItem<\"map\">, \"id\" | \"table_name\" | \"type\">\n  | Pick<WindowSyncItem<\"timechart\">, \"id\" | \"table_name\" | \"type\">;\n\nexport type GetJoinFiltersResult = {\n  l: Link;\n  f: AnyObject | undefined;\n  w: SyncWindow;\n  parsedPath: ParsedJoinPath[];\n  activeRowFilter: AnyObject | undefined;\n};\n\nexport const getJoinFilters = (\n  w: CrossFilterWindow,\n  activeRow: ActiveRow | undefined = undefined,\n  links: Link[],\n  _windows: SyncWindow[],\n  previousWids: string[] = [],\n  previousPath: ParsedJoinPath[] = [],\n): GetJoinFiltersResult[] => {\n  const windows = _windows.map((w) => w.$get()) as SyncWindow[]; // To ensure we get latest data\n\n  const myLinks: GetJoinFiltersResult[] = links\n    .map((l) => {\n      const lwids = [l.w1_id, l.w2_id];\n      const lws = windows.filter(\n        (w) => !w.closed && !w.deleted && lwids.includes(w.id),\n      );\n      const otherW = lws.find((lw) => lw.id !== w.id);\n      const getTableFilters = (table: WindowData<\"table\">) =>\n        table.filter?.map((f) => getFinalFilter({ ...f })).filter(isDefined) ??\n        [];\n      if (\n        otherW &&\n        lws.length === 2 &&\n        lwids.includes(w.id) &&\n        !previousWids.some((pw) => lwids.includes(pw))\n      ) {\n        const activeRowFilter =\n          otherW.id === activeRow?.window_id ? activeRow.row_filter : undefined;\n        const getChartFilters = (\n          chartFilters: AnyObject[],\n          reverseToTable: string | undefined,\n        ) => {\n          if (l.options.type === \"map\" || l.options.type === \"timechart\") {\n            const { dataSource } = l.options;\n            const joinPath =\n              dataSource?.type === \"table\" ? dataSource.joinPath : undefined;\n            if (joinPath?.length || previousPath.length) {\n              const parsedPath =\n                !joinPath ? []\n                : reverseToTable ?\n                  reverseParsedPath(joinPath.slice(0), reverseToTable)\n                : joinPath.slice(0);\n              const f = {\n                $existsJoined: {\n                  path: [...previousPath, ...parsedPath],\n                  filter: { $and: chartFilters.concat({}) },\n                },\n              };\n\n              return { parsedPath, f };\n            }\n          }\n          return { parsedPath: [], f: { $and: chartFilters } };\n        };\n\n        /** Table getting chart filters */\n        if (\n          w.type === \"table\" &&\n          w.type !== otherW.type &&\n          otherW.type === l.options.type\n        ) {\n          const chartCol = l.options.columns[0]?.name;\n          if (!chartCol) return undefined;\n          let chartFilters: AnyObject[] = [];\n          if (otherW.type === \"map\") {\n            if (otherW.options.extent) {\n              chartFilters =\n                otherW.options.extentBehavior !== \"filterToMapBounds\" ?\n                  []\n                : [W_Map.extentToFilter(otherW.options.extent, chartCol)];\n            }\n          } else {\n            chartFilters = getTimeChartFilters(otherW, chartCol);\n          }\n\n          return {\n            l,\n            w: otherW,\n            ...getChartFilters(chartFilters, undefined),\n            activeRowFilter:\n              activeRowFilter ?\n                getChartFilters([activeRowFilter], undefined).f\n              : undefined,\n          };\n\n          /** Table to table */\n        } else if (otherW.type === \"table\" && l.options.type === \"table\") {\n          const isLTR = l.w1_id === w.id;\n          const { tablePath } = l.options;\n          const currentParsedPath =\n            isLTR ?\n              tablePath.slice(0)\n            : reverseParsedPath(tablePath.slice(0), otherW.table_name);\n          const parsedPath = [...previousPath, ...currentParsedPath];\n          return {\n            l,\n            w: otherW,\n            parsedPath,\n            f: {\n              $existsJoined: {\n                path: parsedPath,\n                filter: {\n                  $and: getTableFilters(otherW).concat({}),\n                },\n              },\n            },\n            activeRowFilter:\n              otherW.id === activeRow?.window_id ?\n                {\n                  $existsJoined: {\n                    path: parsedPath,\n                    filter: activeRow.row_filter,\n                  },\n                }\n              : undefined,\n          };\n          /** Chart to their table */\n        } else if (\n          otherW.type === \"table\" &&\n          l.options.type === w.type &&\n          w.type !== \"table\"\n        ) {\n          return {\n            l,\n            w: otherW,\n            ...getChartFilters(getTableFilters(otherW), otherW.table_name),\n            activeRowFilter:\n              activeRowFilter ?\n                getChartFilters([activeRowFilter], otherW.table_name).f\n              : undefined,\n          };\n        } else {\n          throw \"Unexpected window/link combination\";\n        }\n      }\n    })\n    .filter(isDefined);\n\n  const nextLinks: typeof myLinks = myLinks.flatMap((d) => {\n    if (d.w.type !== \"table\") {\n      return [];\n    }\n    return getJoinFilters(\n      d.w,\n      activeRow,\n      links,\n      _windows,\n      [...previousWids, w.id],\n      [...previousPath, ...d.parsedPath],\n    );\n  });\n\n  return [...myLinks, ...nextLinks];\n};\nexport const getCrossFilters = (\n  w: CrossFilterWindow,\n  activeRow: ActiveRow | undefined = undefined,\n  links: Link[],\n  _windows: SyncWindow[] | WindowSyncItem[],\n): CrossFilters => {\n  // To ensure we get latest data\n  const windows = _windows.map((w) => w.$get()) as SyncWindow[];\n\n  const joinFilters = getJoinFilters(w, activeRow, links, windows);\n\n  const crossFilters = joinFilters\n    .map((d) => d.f && quickClone({ ...d.f }))\n    .filter(isDefined);\n  const activeRowFilter = joinFilters.find(\n    (d) => d.activeRowFilter,\n  )?.activeRowFilter;\n\n  return {\n    activeRowFilter,\n    crossFilters,\n    all: [activeRowFilter, ...crossFilters].filter(isDefined),\n  };\n};\n"
  },
  {
    "path": "client/src/dashboard/localSettings.ts",
    "content": "import { isObject } from \"prostgles-types\";\nimport { useEffect, useState } from \"react\";\n\nexport type LocalSettings = {\n  centeredLayout?: {\n    enabled: boolean;\n    maxWidth: number;\n  };\n  themeOverride?: \"light\" | \"dark\";\n};\n\ntype LocalSettingsListener = (s: LocalSettings) => void;\n\nlet localSettingsListeners: LocalSettingsListener[] = [];\n\nconst LOCALSTORAGE_KEY = \"localSettings\" as const;\n\nconst parseLocalSettings = () => {\n  const localSettings: LocalSettings = {};\n  try {\n    const savedSettings = window.localStorage.getItem(LOCALSTORAGE_KEY);\n    if (savedSettings) {\n      const parsedSavedSettings: LocalSettings | undefined =\n        JSON.parse(savedSettings);\n      if (parsedSavedSettings && isObject(parsedSavedSettings)) {\n        const { centeredLayout, themeOverride } = parsedSavedSettings;\n        if (\n          centeredLayout &&\n          typeof ((centeredLayout as any).enabled ?? false) === \"boolean\" &&\n          Number.isFinite(centeredLayout.maxWidth)\n        ) {\n          localSettings.centeredLayout = centeredLayout;\n        }\n        if (themeOverride && [\"light\", \"dark\"].includes(themeOverride)) {\n          localSettings.themeOverride = themeOverride;\n        }\n      }\n      return localSettings;\n    }\n  } catch (e) {\n    console.error(\"localSettings error: \", e);\n  }\n  return {};\n};\n\nwindow.addEventListener(\n  \"storage\",\n  function (event) {\n    // if (event.storageArea === localStorage) {\n    const s = parseLocalSettings();\n    localSettingsListeners.forEach((l) => l(s));\n    // }\n  },\n  false,\n);\n\nexport const localSettings = {\n  add: (l: LocalSettingsListener) => {\n    if (!localSettingsListeners.some((ll) => ll === l)) {\n      localSettingsListeners.push(l);\n    }\n  },\n  remove: (l: LocalSettingsListener) => {\n    localSettingsListeners = localSettingsListeners.filter((ll) => ll !== l);\n  },\n  get: () => ({\n    ...parseLocalSettings(),\n    $set: (ls: Partial<LocalSettings>) => {\n      localStorage.setItem(\n        LOCALSTORAGE_KEY,\n        JSON.stringify({\n          ...parseLocalSettings(),\n          ...ls,\n        }),\n      );\n      window.dispatchEvent(new Event(\"storage\"));\n    },\n  }),\n} as const;\n\nexport const useLocalSettings = () => {\n  const [ls, setLS] = useState(localSettings.get());\n\n  useEffect(() => {\n    const onChange = (newSettings: LocalSettings) => {\n      setLS({ ...newSettings, $set: ls.$set });\n    };\n    localSettings.add(onChange);\n\n    return () => localSettings.remove(onChange);\n  }, [ls]);\n\n  return ls;\n};\n"
  },
  {
    "path": "client/src/dashboard/setPan.ts",
    "content": "import { vibrateFeedback } from \"./Dashboard/dashboardUtils\";\n\nexport type PanEvent = {\n  x: number;\n  y: number;\n  xOffset: number;\n  yOffset: number;\n  xStart: number;\n  yStart: number;\n  xDiff: number;\n  yDiff: number;\n  xTravel: number;\n  yTravel: number;\n  /**\n   * x position within the element.\n   */\n  xNode: number;\n  /**\n   * y position within the element.\n   */\n  yNode: number;\n  xNodeStart: number;\n  yNodeStart: number;\n  triggered: boolean;\n  node: HTMLDivElement;\n};\nexport type PanListeners = {\n  threshold?: number;\n  doubleTapThreshold?: number;\n  tapThreshold?: number;\n  onPress?: (\n    e: React.PointerEvent<HTMLDivElement>,\n    node: HTMLDivElement,\n  ) => void;\n  onDoubleTap?: (\n    args: { x: number; y: number },\n    e: React.PointerEvent<HTMLDivElement>,\n    node: HTMLDivElement,\n  ) => void;\n  onRelease?: (\n    e: React.PointerEvent<HTMLDivElement>,\n    node: HTMLDivElement,\n  ) => void;\n  onPointerMove?: (\n    pev: Pick<PanEvent, \"xNode\" | \"yNode\">,\n    e: React.PointerEvent<HTMLDivElement>,\n    node: HTMLDivElement,\n  ) => void;\n  onPanStart?: (pe: PanEvent, e: React.PointerEvent<HTMLDivElement>) => void;\n  onPan: (pe: PanEvent, e: React.PointerEvent<HTMLDivElement>) => void;\n  onPanEnd?: (pe: PanEvent, e: React.PointerEvent<HTMLDivElement>) => void;\n  onPinch?: (ev: {\n    x1: number;\n    y1: number;\n    x2: number;\n    y2: number;\n    deltaX: number;\n    deltaY: number;\n  }) => void;\n  onSinglePinch?: (ev: {\n    x: number;\n    y: number;\n    w: number;\n    h: number;\n    delta: number;\n    totalDelta: number;\n    goingUp: boolean;\n  }) => void;\n};\n\nexport function setPan(node: HTMLDivElement, evs: PanListeners) {\n  let panEvents: {\n    node: HTMLDivElement;\n    events: PanListeners;\n    handlers: {\n      _onPress: (ev: React.PointerEvent<HTMLDivElement>) => void;\n      _onRelease: (ev: React.PointerEvent<HTMLDivElement>) => void;\n      onMove: (ev: React.PointerEvent<HTMLDivElement>) => void;\n    };\n  }[] = [];\n  let _pointerdown: PanEvent | null = null;\n  let _panning: PanEvent | null = null;\n  let lastMove: React.PointerEvent<HTMLDivElement> | undefined;\n  let currEv: (typeof panEvents)[number] | undefined;\n  let lastRelease:\n    | {\n        duration: number;\n        ended: number;\n      }\n    | undefined;\n  let lastPress:\n    | {\n        ev: React.PointerEvent<HTMLDivElement>;\n        started: number;\n        onSinglePinch?: boolean;\n      }\n    | undefined;\n  const _onPress = (ev: React.PointerEvent<HTMLDivElement>) => {\n      currEv = panEvents.find((p) => p.node.contains(ev.target as any));\n      if (!currEv) return;\n\n      lastPress = {\n        ev,\n        started: Date.now(),\n      };\n\n      /** Ignore right clicks */\n      if (ev.button && ev.pointerType !== \"touch\") return;\n\n      currEv.events.onPress?.(ev, currEv.node);\n\n      ev.preventDefault();\n      ev.stopPropagation();\n      const rect = node.getBoundingClientRect();\n      const { clientX: x, clientY: y } = ev;\n      const [xStart, yStart] = [x, y];\n      const [xOffset, yOffset] = [rect.x, rect.y];\n      const xNode = x - xOffset; //x position within the element.\n      const yNode = y - yOffset; //y position within the element.\n      const xNodeStart = x - rect.left;\n      const yNodeStart = y - rect.top;\n      const [xDiff, yDiff] = [0, 0];\n      const [xTravel, yTravel] = [0, 0];\n\n      _pointerdown = {\n        node,\n        x,\n        y,\n        xStart,\n        yStart,\n        xNode,\n        yNode,\n        xNodeStart,\n        yNodeStart,\n        xDiff,\n        yDiff,\n        xOffset,\n        yOffset,\n        xTravel,\n        yTravel,\n        triggered: false,\n      };\n    },\n    _onRelease = (ev: React.PointerEvent<HTMLDivElement>) => {\n      if (!currEv) return;\n\n      const { onSinglePinch, onDoubleTap, onRelease, onPanEnd } = currEv.events;\n      if (onSinglePinch || onDoubleTap) {\n        if (\n          onDoubleTap &&\n          lastRelease &&\n          lastRelease.duration < 200 &&\n          Date.now() - lastRelease.ended < 400\n        ) {\n          const r = node.getBoundingClientRect();\n\n          onDoubleTap(\n            {\n              x: ev.clientX - r.x,\n              y: ev.clientY - r.y,\n            },\n            ev,\n            currEv.node,\n          );\n          lastRelease = undefined;\n        } else {\n          lastRelease = {\n            duration: Date.now() - (lastPress?.started ?? 0),\n            ended: Date.now(),\n          };\n        }\n      }\n\n      onRelease?.(ev, currEv.node);\n\n      if (_pointerdown && _pointerdown.triggered && _panning) {\n        ev.preventDefault();\n        onPanEnd?.({ ..._panning }, ev);\n        currEv = undefined;\n        lastMove = undefined;\n      }\n      _pointerdown = null;\n    },\n    onMove = (ev: React.PointerEvent<HTMLDivElement>) => {\n      if (!currEv || !lastPress) {\n        if (evs.onPointerMove && node.contains(ev.target as any)) {\n          const rect = node.getBoundingClientRect();\n          const { clientX: x, clientY: y } = ev;\n          const [xOffset, yOffset] = [rect.x, rect.y];\n          evs.onPointerMove(\n            { xNode: x - xOffset, yNode: y - yOffset },\n            ev,\n            node,\n          );\n        }\n        return;\n      }\n\n      /** onSinglePinch */\n      const {\n        onSinglePinch,\n        onPinch,\n        onPanStart,\n        onPan,\n        threshold = 15,\n      } = currEv.events;\n      if (\n        onSinglePinch &&\n        lastMove &&\n        lastRelease &&\n        _pointerdown &&\n        (lastPress.onSinglePinch ||\n          (lastRelease.duration < 200 && Date.now() - lastRelease.ended < 400))\n      ) {\n        lastPress.onSinglePinch = true;\n\n        ev.preventDefault();\n        ev.stopPropagation();\n        const r = node.getBoundingClientRect();\n        const dist = Math.hypot(\n          ev.clientX - lastMove.clientX,\n          ev.clientY - lastMove.clientY,\n        );\n\n        onSinglePinch({\n          x: lastPress.ev.clientX - r.x,\n          y: lastPress.ev.clientY - r.y,\n          w: r.width,\n          h: r.height,\n          totalDelta: Math.hypot(\n            ev.clientX - lastPress.ev.clientX,\n            ev.clientY - lastPress.ev.clientY,\n          ),\n          delta: (ev.clientY < lastMove.clientY ? 1 : -1) * dist,\n          goingUp: ev.clientY < lastMove.clientY,\n        });\n        vibrateFeedback(15);\n\n        /** Pinch zoom */\n        // } else if(\"touches\" in ev && ev.touches.length >= 2){\n        //   ev.preventDefault();\n        //   ev.stopPropagation();\n        //   if(onPinch){\n        // let [ev1, ev2] = Object.values(moveEvents).sort((a, b) => a.clientX - b.clientX);\n        // const r = node.getBoundingClientRect();\n        // const o = {\n        //   x1: ev1.clientX - r.x,\n        //   y1: ev1.clientY - r.y,\n        //   x2: ev2.clientX - r.x,\n        //   y2: ev2.clientY - r.y,\n        // };\n        // moveEvents[ev.pointerId] = ev;\n        // [ev1, ev2] = Object.values(moveEvents).sort((a, b) => a.clientX - b.clientX);\n        // const n = {\n        //   x1: ev1.clientX - r.x,\n        //   y1: ev1.clientY - r.y,\n        //   x2: ev2.clientX - r.x,\n        //   y2: ev2.clientY - r.y,\n        // };\n        // onPinch({\n        //   ...n,\n        //   deltaX: n.x1 - n.x2,\n        //   deltaY: n.y1 - n.y2,\n        // })\n        // }\n\n        /** Pan */\n      } else if (_pointerdown) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        const {\n          xOffset,\n          yOffset,\n          xStart,\n          yStart,\n          xDiff,\n          yDiff,\n          xTravel,\n          yTravel,\n        } = _pointerdown;\n        const { clientX: x, clientY: y } = ev;\n        _panning = {\n          ..._pointerdown,\n          x,\n          y,\n          xNode: x - xOffset,\n          yNode: y - yOffset,\n          xDiff: x - xStart,\n          yDiff: y - yStart,\n        };\n        _panning.xTravel = xTravel + Math.abs(_panning.xDiff);\n        _panning.yTravel = yTravel + Math.abs(_panning.yDiff);\n\n        if (\n          !_pointerdown.triggered &&\n          (_panning.xTravel >= threshold || _panning.yTravel > threshold)\n        ) {\n          _pointerdown.triggered = true;\n          clearTextSelection();\n          onPanStart?.({ ..._panning }, ev);\n        } else if (_pointerdown.triggered) {\n          clearTextSelection();\n          onPan({ ..._panning }, ev);\n        }\n      }\n\n      lastMove = ev;\n    };\n\n  /* Required for pointerup to fire */\n  node.style.touchAction = \"none\";\n  window.document.body.style.touchAction = \"none\";\n\n  /* Prevent chrome back/forward nav */\n  window.document.documentElement.style.overscrollBehavior = \"none\";\n\n  const onReleaseCleanup = addEvent(\n    window.document.body,\n    \"pointerup\",\n    _onRelease,\n  );\n  const onMoveCleanup = addEvent(window.document.body, \"pointermove\", onMove);\n\n  const onPointerDownCleanup = addEvent(node, \"pointerdown\", _onPress);\n  panEvents.push({\n    node,\n    events: evs,\n    handlers: { _onPress, _onRelease, onMove },\n  });\n\n  return function () {\n    onPointerDownCleanup();\n    onReleaseCleanup();\n    onMoveCleanup();\n    panEvents = panEvents.filter(\n      (pev) => pev.events.onPan !== evs.onPan && pev.node !== node,\n    );\n  };\n}\n\nexport function addEvent(node: HTMLElement, type, func) {\n  const wrappedEvent = function (ev) {\n    if (!node.isConnected) {\n      node.removeEventListener(type, wrappedEvent);\n    } else {\n      func(ev);\n    }\n  };\n  node.removeEventListener(type, wrappedEvent);\n  node.addEventListener(type, wrappedEvent, { passive: false, capture: false });\n  return function () {\n    node.removeEventListener(type, wrappedEvent);\n  };\n}\n\nexport const clearTextSelection = () => {\n  window.getSelection()?.empty();\n};\n"
  },
  {
    "path": "client/src/dashboard/shortestPath.ts",
    "content": "const shortestDistanceNode = (distances, visited) => {\n  let shortest: string | null = null;\n\n  for (const node in distances) {\n    const currentIsShortest =\n      shortest === null || distances[node] < distances[shortest];\n    if (currentIsShortest && !visited.includes(node)) {\n      shortest = node;\n    }\n  }\n  return shortest;\n};\nexport type Graph = {\n  [key: string]: { [key: string]: number };\n};\n\nexport const makeReversibleGraph = (\n  links: [string, string, number?][],\n): Graph => {\n  const g: Record<string, any> = {};\n  links.map(([id1, id2, distance = 1]) => {\n    g[id1] = g[id1] || {};\n    g[id1][id2] = distance;\n    g[id2] = g[id2] || {};\n    g[id2][id1] = distance;\n  });\n  return g;\n};\n\nexport const findShortestPath = (\n  graph: Graph,\n  startNode: string,\n  endNode: string,\n): { distance: number; path: string[] } => {\n  // establish object for recording distances from the start node\n  let distances = {};\n  distances[endNode] = \"Infinity\";\n  distances = Object.assign(distances, graph[startNode]);\n\n  // track paths\n  const parents = { endNode: null };\n  for (const child in graph[startNode]) {\n    parents[child] = startNode;\n  }\n\n  // track nodes that have already been visited\n  const visited: string[] = [];\n\n  // find the nearest node\n  let node = shortestDistanceNode(distances, visited);\n\n  // for that node\n  while (node) {\n    // find its distance from the start node & its child nodes\n    const distance = distances[node];\n    const children = graph[node];\n    // for each of those child nodes\n    for (const child in children) {\n      // make sure each child node is not the start node\n      if (String(child) === String(startNode)) {\n        continue;\n      } else {\n        // save the distance from the start node to the child node\n        const newdistance = distance + children[child];\n        // if there's no recorded distance from the start node to the child node in the distances object\n        // or if the recorded distance is shorter than the previously stored distance from the start node to the child node\n        // save the distance to the object\n        // record the path\n        if (!distances[child] || distances[child] > newdistance) {\n          distances[child] = newdistance;\n          parents[child] = node;\n        }\n      }\n    }\n    // move the node to the visited set\n    visited.push(node);\n    // move to the nearest neighbor node\n    node = shortestDistanceNode(distances, visited);\n  }\n\n  // using the stored paths from start node to end node\n  // record the shortest path\n  const shortestPath = [endNode];\n  let parent = parents[endNode];\n  while (parent) {\n    shortestPath.push(parent);\n    parent = parents[parent];\n  }\n  shortestPath.reverse();\n\n  // return the shortest path from start node to end node & its distance\n  const results = {\n    distance: distances[endNode],\n    path: shortestPath,\n  };\n\n  return results;\n};\n\n/* Usage: \n\nconst graph = {\n\tstart: { A: 5, B: 2 },\n\tA: { start: 1, C: 4, D: 2 },\n\tB: { A: 8, D: 7 },\n\tC: { D: 6, end: 3 },\n\tD: { end: 1 },\n\tend: {},\n};\n\nfindShortestPath(graph, 'start', 'end');\n\n{\n  \"distance\": 8,\n  \"path\": [\n    \"start\",\n    \"A\",\n    \"D\",\n    \"end\"\n  ]\n}\n\n\n\n\n// The graph is unidirectional\n\nconst graph = {\n\tstart: { A: 5 },\n\tend: { A: 1 },\n\tA: { start: 1, end: 1 },\n};\n\nfindShortestPath(graph, 'start', 'end');\n\n\n*/\n"
  },
  {
    "path": "client/src/demo/AppVideoDemo.tsx",
    "content": "import { mdiPlay } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport { r_useAppVideoDemo, useReactiveState, type Prgl } from \"../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport Popup from \"@components/Popup/Popup\";\nimport type { DBS } from \"../dashboard/Dashboard/DBS\";\nimport { startWakeLock, VIDEO_DEMO_DB_NAME } from \"../dashboard/W_SQL/TestSQL\";\nimport { getKeys } from \"../utils/utils\";\nimport { accessControlDemo } from \"./scripts/accessControlDemo\";\nimport { backupDemo } from \"./scripts/backupDemo\";\nimport { dashboardDemo } from \"./scripts/dashboardDemo\";\nimport { fileDemo } from \"./scripts/fileDemo\";\nimport { sqlDemo } from \"./scripts/sqlVideoDemo\";\nimport { schemaDiagramDemo } from \"./scripts/schemaDiagramDemo\";\nimport { AIAssistantDemo } from \"./scripts/AIAssistantDemo\";\n\nconst loadTest = () => {\n  const dbs: DBS = (window as any).dbs;\n  console.log(dbs);\n};\n\nconst VIDEO_DEMO_SCRIPTS = {\n  AIAssistantDemo,\n  schemaDiagramDemo,\n  backupDemo,\n  fileDemo,\n  accessControlDemo,\n  sqlDemo,\n  dashboardDemo,\n  loadTest,\n};\nconst demoScripts = getKeys(VIDEO_DEMO_SCRIPTS);\ntype DEMO_NAME = keyof typeof VIDEO_DEMO_SCRIPTS;\n\nconst videoTimings: { videoName: string; start: number; end: number }[] = [];\nlet currVideo: (typeof videoTimings)[number] | undefined;\nconst startVideoDemo = (videoName: string) => {\n  if (currVideo) {\n    videoTimings.push({ ...currVideo, end: Date.now() });\n  }\n  currVideo = {\n    videoName,\n    start: Date.now(),\n    end: 0,\n  };\n};\n\nexport const AppVideoDemo = ({ connection: { db_name } }: Prgl) => {\n  const isOnDemoDatabase = db_name === VIDEO_DEMO_DB_NAME;\n  const {\n    state: { demoStarted },\n  } = useReactiveState(r_useAppVideoDemo);\n  const [showDemoOptions, setShowDemoOptions] =\n    useState<HTMLButtonElement | null>(null);\n  const startDemo = async (name?: DEMO_NAME) => {\n    setShowDemoOptions(null);\n\n    if (!isOnDemoDatabase) {\n      throw new Error(\"Cannot start demo on a non-demo database\");\n    }\n    r_useAppVideoDemo.set({ demoStarted: true });\n    const { stopWakeLock } = await startWakeLock();\n    if (name) {\n      await VIDEO_DEMO_SCRIPTS[name]();\n    } else {\n      const {\n        AIAssistantDemo,\n        schemaDiagramDemo,\n        sqlDemo,\n        accessControlDemo,\n        backupDemo,\n        fileDemo,\n        dashboardDemo,\n      } = VIDEO_DEMO_SCRIPTS;\n      startVideoDemo(\"AI Assistant\");\n      await AIAssistantDemo();\n      startVideoDemo(\"Schema Diagram\");\n      await schemaDiagramDemo();\n      startVideoDemo(\"SQL\");\n      await sqlDemo();\n      startVideoDemo(\"Access Control\");\n      await accessControlDemo();\n      startVideoDemo(\"Dashboard\");\n      await dashboardDemo();\n      startVideoDemo(\"Backups\");\n      await backupDemo();\n      startVideoDemo(\"the end\");\n    }\n    stopWakeLock();\n  };\n\n  if (demoStarted || !isOnDemoDatabase) {\n    return null;\n  }\n  return (\n    <>\n      <Btn\n        //@ts-ignore\n        _ref={(node) => {\n          if (!node) return;\n          node.start = () => {\n            return startDemo();\n          };\n        }}\n        onContextMenu={(e) => {\n          e.preventDefault();\n          setShowDemoOptions(e.currentTarget);\n        }}\n        // style={{ opacity: isOnDemoDatabase? 1 : 0 }}\n        data-command=\"AppDemo.start\"\n        onClick={() => startDemo()}\n        iconPath={mdiPlay}\n      />\n      {showDemoOptions && (\n        <Popup anchorEl={showDemoOptions} positioning=\"beneath-left\">\n          <FlexCol className=\"gap-0\">\n            {demoScripts.map((key) => (\n              <Btn key={key} onClick={() => startDemo(key)}>\n                {key}\n              </Btn>\n            ))}\n          </FlexCol>\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/demo/MousePointer.tsx",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { getModalRoot } from \"@components/Popup/Popup\";\nimport { setPointer } from \"./demoUtils\";\n\nconst mousePointerStyle: React.CSSProperties = {\n  position: \"fixed\",\n  display: \"none\",\n  // display: \"block\",\n  zIndex: 219999,\n  left: \"0\",\n  top: \"0\",\n  // left: \"220px\",\n  // top: \"220px\",\n  width: \"20px\",\n  height: \"20px\",\n  // backgroundColor: \"#2f2e2e69\",\n  backgroundColor: \"var(--text-0)\",\n  opacity: 0.25,\n  borderRadius: \"50%\",\n  transform: \"translate(-50%, -50%)\",\n  pointerEvents: \"none\",\n};\n\nexport const MousePointer = () => {\n  const pointerNode = (\n    <div\n      ref={(e) => {\n        if (e) {\n          setPointer(e);\n        }\n      }}\n      style={mousePointerStyle}\n      className=\"shadow\"\n    />\n  );\n\n  return ReactDOM.createPortal(pointerNode, getModalRoot(true));\n};\n"
  },
  {
    "path": "client/src/demo/demoUtils.ts",
    "content": "import { scrollIntoViewIfNeeded } from \"src/utils/utils\";\nimport {\n  type Command,\n  getCommandElemSelector,\n  getDataKeyElemSelector,\n} from \"../Testing\";\nimport { tout } from \"../pages/ElectronSetup/ElectronSetup\";\n\nlet pointer: HTMLDivElement | null = null;\n\nexport const movePointer = async (x: number, y: number) => {\n  if (pointer) {\n    pointer.style.display = \"block\";\n    pointer.style.transition = \".5s ease\";\n    pointer.style.left = x + \"px\";\n    pointer.style.top = y + \"px\";\n    await tout(500);\n    const targetEl = document.elementFromPoint(x, y);\n    if (targetEl) {\n      const evt = new PointerEvent(\"pointermove\", {\n        bubbles: true,\n        cancelable: true,\n        clientX: x,\n        clientY: y,\n        view: window,\n      });\n\n      targetEl.dispatchEvent(evt);\n    }\n  }\n};\ntype GetElemOpts = {\n  noSpaceBetweenSelectors?: boolean;\n  /**\n   * Zero based index of the element to click\n   */\n  nth?: number;\n};\nexport const getElement = <T extends Element>(\n  testId: Command | \"\",\n  endSelector = \"\",\n  { noSpaceBetweenSelectors, nth = -1 }: GetElemOpts = {},\n) => {\n  const testIdSelector = testId ? getCommandElemSelector(testId) : \"\";\n  const allMatchingElements = Array.from(\n    document.querySelectorAll<T>(\n      `${testIdSelector}${noSpaceBetweenSelectors ? \"\" : \" \"}${endSelector}`,\n    ),\n  );\n  /** Only the last matching element is clicked to ensure only the top ClickCatchOverlay is clicked to maintain correct context */\n  return allMatchingElements.at(nth);\n};\ntype ClickOpts = GetElemOpts & {\n  timeout?: number;\n  noTimeToWait?: boolean;\n  whenReady?: boolean;\n};\n\nexport const waitForElement = async <T extends Element>(\n  testId: Command | \"\",\n  endSelector = \"\",\n  { noTimeToWait, timeout = 15e3, ...otherOpts }: ClickOpts = {},\n) => {\n  !noTimeToWait && (await tout(100));\n  let elem = getElement<T>(testId, endSelector, otherOpts);\n  if (!elem && timeout) {\n    const waitTime = noTimeToWait ? 20 : 100;\n    let timeoutLeft = timeout;\n    while (!elem && timeoutLeft > 0) {\n      await tout(waitTime);\n      elem = getElement<T>(testId, endSelector, otherOpts);\n      timeoutLeft -= waitTime;\n    }\n  }\n  if (!elem) {\n    throw `Could not find ${[testId, endSelector].filter(Boolean).join(\" \")} element to click`;\n  }\n  return elem;\n};\n\nexport const clickWhenReady = async (elem: HTMLElement, timeout = 5e3) => {\n  if (elem instanceof HTMLButtonElement && elem.disabled) {\n    let timeoutLeft = timeout;\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    while (elem.disabled && timeoutLeft > 0) {\n      await tout(100);\n      timeoutLeft -= 100;\n    }\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    if (elem.disabled) {\n      throw `Element ${elem.nodeName + elem.className} is still disabled after ${timeout}ms`;\n    }\n  }\n  elem.scrollIntoView({ behavior: \"smooth\" });\n  await tout(150);\n  elem.click();\n};\n\nexport const goToElem = async <ElemType = HTMLElement>(\n  testId: Command | \"\",\n  endSelector = \"\",\n  opts: ClickOpts = {},\n): Promise<ElemType> => {\n  const elem = await waitForElement<HTMLButtonElement>(\n    testId,\n    endSelector,\n    opts,\n  );\n  const bbox = elem.getBoundingClientRect();\n  scrollIntoViewIfNeeded(elem, { behavior: \"smooth\" });\n  !opts.noTimeToWait && (await tout(200));\n  await movePointer(\n    bbox.left + Math.min(60, bbox.width / 2),\n    bbox.top + bbox.height / 2,\n  );\n  if (!elem.isConnected) {\n    return goToElem(testId, endSelector, opts);\n  }\n  return elem as any;\n};\nexport const click = async (\n  testId: Command | \"\",\n  endSelector = \"\",\n  opts: ClickOpts = {},\n): Promise<void> => {\n  const elem = await goToElem(testId, endSelector, opts);\n  /** In some cases react has since replaced the node content */\n  const latestElem = getElement<HTMLButtonElement>(testId, endSelector, opts);\n  const finalElem = latestElem ?? elem;\n  if (opts.whenReady) {\n    await clickWhenReady(finalElem, opts.timeout);\n  } else {\n    finalElem.click();\n  }\n};\nexport const openConnection = async (\n  name: \"prostgles_video_demo\" | \"food_delivery\",\n) => {\n  await click(\n    \"Connections\",\n    getDataKeyElemSelector(name) +\n      \" \" +\n      getCommandElemSelector(\"Connection.openConnection\"),\n  );\n};\n// window._click = click;\n\nexport const type = async (\n  value: string,\n  testId: Command | \"\",\n  endSelector = \"\",\n) => {\n  const input = await goToElem<HTMLInputElement>(testId, endSelector);\n  input.focus();\n  // input.value = \"\";\n  // input.focus();\n  // const chars = value.split(\"\");\n  // for (const char of chars) {\n  //   input.value += char;\n  //   await tout(100);\n  // }\n  // await tout(200);\n  // input.value = value;\n  //@ts-ignore\n  await input.forceDemoValue?.(value);\n  await tout(500);\n};\n\nlet lastHovered:\n  | {\n      elem: Element;\n      hoverElem: Element | null;\n    }\n  | undefined;\nlet hoverCheckInterval: NodeJS.Timeout;\nexport const setPointer = (p: HTMLDivElement | null) => {\n  pointer = p;\n\n  if (!p) return;\n  p.ontransitionstart = () => {\n    hoverCheckInterval = setInterval(() => {\n      const bbox = p.getBoundingClientRect();\n      const hovered = document.elementFromPoint(\n        bbox.left + bbox.width / 2,\n        bbox.top + bbox.height / 2,\n      );\n      if (hovered !== lastHovered?.elem) {\n        lastHovered?.hoverElem?.classList.toggle(\"hover\", false);\n        if (!hovered) {\n          lastHovered = undefined;\n          return;\n        }\n        const hoverElem = getClosestHovereableElem(hovered);\n        lastHovered = { elem: hovered, hoverElem };\n        hoverElem?.classList.toggle(\"hover\", true);\n      }\n    }, 100);\n  };\n  p.ontransitionend = (e) => {\n    clearInterval(hoverCheckInterval);\n  };\n};\n\nconst hoverTriggerClasses = [\n  \"show-on-hover\",\n  \"show-on-row-hover\",\n  \"show-on-parent-hover\",\n  \"show-on-trigger-hover\",\n  \"list-comp li\",\n];\nconst getClosestHovereableElem = (elem: Element) => {\n  return elem.closest(hoverTriggerClasses.map((v) => `.${v}`).join(\", \"));\n};\n"
  },
  {
    "path": "client/src/demo/recordDemoUtils.ts",
    "content": "const clickedPaths: { selector: string; timestamp: number }[] = [];\nexport const startRecordingDemo = () => {\n  window.addEventListener(\"pointerdown\", (e) => {\n    const path = getClickPath(e.target);\n    const selector = getClickSelector(path);\n    const firstPath = clickedPaths[0];\n    clickedPaths.push({\n      selector,\n      timestamp: Date.now() - (!firstPath ? 0 : firstPath.timestamp),\n    });\n    // console.log(clickedPaths.at(-1));\n  });\n};\n\ntype ClickedNode = {\n  type: string;\n  node: Element;\n  value?: string;\n};\n\nconst getClickSelector = (path: ClickedNode[]) => {\n  return path\n    .slice(0)\n    .reverse()\n    .map((n) =>\n      n.value ? `[data-${n.type}=${JSON.stringify(n.value)}]` : n.type,\n    )\n    .join(\" \");\n};\n\nconst getClickPath = (el: HTMLElement | Element | EventTarget | null) => {\n  if (!el || !(el instanceof Element)) {\n    return [];\n  }\n\n  /** Used to ignore paths/svgs inside buttons */\n  const closestButton = el.closest(\"button\");\n  const closestAnchor = el.closest(\"a\");\n  const closestListItem = el.closest(\"li\");\n  const currAction = getClosestActionElement(\n    closestButton ?? closestAnchor ?? closestListItem ?? el,\n    true,\n  );\n  const path: ClickedNode[] = [\n    currAction ?? {\n      type: el.nodeName,\n      node: el,\n    },\n  ];\n  let currElem = getClosestActionElement(undefined);\n  do {\n    currElem = getClosestActionElement(path.at(-1)?.node);\n    if (currElem) path.push(currElem);\n  } while (currElem);\n  return path;\n};\n\nconst getClosestActionElement = (\n  eventTarget: HTMLElement | Element | EventTarget | undefined,\n  onlySelf = false,\n): ClickedNode | undefined => {\n  if (!(eventTarget instanceof Element)) {\n    return undefined;\n  }\n  const el = onlySelf ? eventTarget : eventTarget.parentElement;\n  if (!el || !(\"closest\" in el)) {\n    return undefined;\n  }\n\n  const DATA_TEST_SELECTORS = [\"key\", \"command\", \"table-name\"];\n  let actionNode: ClickedNode | undefined;\n  DATA_TEST_SELECTORS.find((selector) => {\n    const attributeName = `data-${selector}`;\n    const closest = el.closest(`[${attributeName}]`);\n    if (closest && (!onlySelf || closest === el)) {\n      actionNode ??= {\n        type: selector,\n        node: closest,\n        value: (closest as HTMLElement).getAttribute(attributeName) ?? \"\",\n      };\n      return actionNode;\n    }\n    return undefined;\n  });\n\n  return actionNode;\n};\n"
  },
  {
    "path": "client/src/demo/scripts/AIAssistantDemo.ts",
    "content": "import { tout } from \"src/utils/utils\";\nimport {\n  click,\n  movePointer,\n  openConnection,\n  waitForElement,\n} from \"../demoUtils\";\n\nexport const AIAssistantDemo = async () => {\n  await click(\"dashboard.goToConnections\");\n  await openConnection(\"food_delivery\");\n  await click(\"AskLLM\");\n\n  await click(\"LLMChatOptions.Model\");\n  const modelSearch = await waitForElement<HTMLTextAreaElement>(\n    \"SearchList\",\n    \"input\",\n    { nth: -1 },\n  );\n  await naturalType(\"prost\", modelSearch);\n  pressEnter(modelSearch);\n\n  const el = await waitForElement<HTMLTextAreaElement>(\n    \"AskLLM.popup\",\n    \"textarea\",\n  );\n  const elRect = el.getBoundingClientRect();\n  await tout(500);\n  await movePointer(elRect.left + elRect.width, elRect.top + 10);\n  await tout(500);\n  await naturalType(\"which tables should I vacuum and reindex?\", el);\n  pressEnter(el);\n  await tout(1000);\n};\n\nconst naturalType = async (text: string, el: HTMLTextAreaElement) => {\n  for (const char of text.split(\"\")) {\n    el.value += char;\n    el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n\n    // Variable delays to simulate human typing\n    let delay = 40 + Math.random() * 40;\n\n    // Longer pauses after punctuation and spaces\n    if (char === \" \") delay += 50 + Math.random() * 100;\n    if (char === \".\" || char === \",\" || char === \"!\")\n      delay += 100 + Math.random() * 200;\n\n    // Slightly longer for uppercase letters (shift key)\n    if (char >= \"A\" && char <= \"Z\") delay += 20;\n\n    await tout(delay);\n  }\n};\n\nconst pressEnter = (el: HTMLTextAreaElement) => {\n  el.dispatchEvent(\n    new KeyboardEvent(\"keydown\", { key: \"Enter\", bubbles: true }),\n  );\n};\n"
  },
  {
    "path": "client/src/demo/scripts/accessControlDemo.ts",
    "content": "import { tout } from \"../../pages/ElectronSetup/ElectronSetup\";\nimport type { Command } from \"../../Testing\";\nimport { click, getElement } from \"../demoUtils\";\nimport { videoDemoAccessControlScripts } from \"./videoDemoAccessControlScripts\";\n\nexport const accessControlDemo = async () => {\n  await click(\"dashboard.goToConnConfig\");\n  await click(\"config.ac\");\n  await tout(1000);\n  const existingRule = getElement<HTMLDivElement>(\"\", `[data-key=\"default\"]`);\n  if (existingRule) {\n    existingRule.click();\n    await tout(200);\n    await click(\"config.ac.removeRule\");\n    await click(\"Btn.ClickConfirmation.Confirm\");\n  }\n\n  for (const { selector, timestamp } of videoDemoAccessControlScripts) {\n    await click(\"\", selector);\n    console.log(selector);\n    await tout(450);\n  }\n\n  await tout(1e3);\n  await click(\"config.ac\");\n  await click(\"\", `[data-key=\"default\"] .ExistingAccessRules_Item_Header`);\n  await tout(2500);\n  await click(\n    \"SearchList.List\",\n    `[data-key=\"messages\"] [data-command=${JSON.stringify(\"selectRuleAdvanced\" satisfies Command)}]`,\n  );\n  await tout(1500);\n  await click(\"MenuList\", `[data-key=\"insert\"]`);\n  await tout(1500);\n  await click(\"MenuList\", `[data-key=\"update\"]`);\n  await tout(1500);\n  await click(\"MenuList\", `[data-key=\"delete\"]`);\n  await click(\"Popup.close\");\n  await click(\"ComparablePGPolicies\");\n  await tout(1500);\n  await click(\"Popup.close\");\n  await click(\"dashboard.goToConnConfig\");\n  await tout(2500);\n};\n"
  },
  {
    "path": "client/src/demo/scripts/backupDemo.ts",
    "content": "import { tout } from \"../../pages/ElectronSetup/ElectronSetup\";\nimport { click, getElement, openConnection } from \"../demoUtils\";\n\nexport const backupDemo = async () => {\n  await click(\"dashboard.goToConnections\");\n  await openConnection(\"prostgles_video_demo\");\n  await click(\"dashboard.goToConnConfig\");\n  await tout(1e3);\n  if (getElement(\"BackupControls.Restore\")) {\n    await tout(1e3);\n    return;\n  }\n  await click(\"config.bkp\");\n  await tout(500);\n  const deleteAllBtn = getElement<HTMLButtonElement>(\n    \"BackupControls.DeleteAll\",\n    \"button\",\n  );\n  if (deleteAllBtn) {\n    deleteAllBtn.click();\n    const code = getElement<HTMLDivElement>(\n      \"\",\n      `[title=\"confirmation-code\"]`,\n    )?.innerText;\n    const input = getElement<HTMLInputElement>(\"\", `[name=\"confirmation\"]`);\n    (input as any)?.forceDemoValue(code);\n    await click(\"BackupControls.DeleteAll.Confirm\");\n  }\n\n  await click(\"config.bkp.create\");\n  await click(\"config.bkp.create.start\");\n  await tout(1e3);\n  await click(\"BackupControls.Restore\");\n  await tout(2e3);\n\n  await click(\"ClickCatchOverlay\");\n  await tout(500);\n  await click(\"config.bkp.AutomaticBackups\");\n  await tout(500);\n  await click(\"AutomaticBackups.destination\");\n  await tout(500);\n  await click(\"AutomaticBackups.destination\", `[data-key=Local]`);\n  await tout(500);\n  await click(\"AutomaticBackups.frequency\");\n  await tout(500);\n  await click(\"AutomaticBackups.frequency\", `[data-key=daily]`);\n  await tout(500);\n  await click(\"AutomaticBackups.hourOfDay\");\n  await tout(2e3);\n};\n"
  },
  {
    "path": "client/src/demo/scripts/dashboardDemo.ts",
    "content": "import type { DeckGLMapDivDemoControls } from \"../../dashboard/Map/DeckGLMap\";\nimport { runDbSQL } from \"../../dashboard/W_SQL/getDemoUtils\";\nimport { tout } from \"../../pages/ElectronSetup/ElectronSetup\";\nimport {\n  click,\n  getElement,\n  movePointer,\n  openConnection,\n  type,\n  waitForElement,\n} from \"../demoUtils\";\n\n/** Close previous windows */\nexport const closeAllViews = async () => {\n  let windowCloseBtn: HTMLElement | null;\n  do {\n    windowCloseBtn = getElement(\"dashboard.window.close\") as HTMLElement | null;\n    windowCloseBtn?.click();\n    await tout(400);\n    const deleteSql = getElement<HTMLButtonElement>(\"CloseSaveSQLPopup.delete\");\n    deleteSql?.click();\n  } while (windowCloseBtn);\n};\n\nexport const dashboardDemo = async () => {\n  await tout(500);\n\n  const DEMO_WSP_PREFIX = \"Demo Workspace \";\n  const demoWspNameFilter = { \"name.$like\": `${DEMO_WSP_PREFIX}%` };\n  await (window as any).dbs.workspaces.update(demoWspNameFilter, {\n    deleted: true,\n  });\n  await (window as any).dbs.workspaces.delete(demoWspNameFilter);\n\n  await click(\"dashboard.goToConnections\");\n  await tout(500);\n  // document\n  //   .querySelector<HTMLAnchorElement>(\"[data-key^=food_delivery] a\")!\n  //   .click();\n\n  await openConnection(\"food_delivery\");\n\n  await closeAllViews();\n\n  const createWorkspace = async (name: string) => {\n    await click(\"WorkspaceMenuDropDown\");\n    await click(\"WorkspaceMenuDropDown.WorkspaceAddBtn\");\n    const wspName = getElement(\"Popup.content\", \"input\");\n    (wspName as any)?.forceDemoValue(\n      name || DEMO_WSP_PREFIX + Math.random().toFixed(2),\n    );\n    await click(\"WorkspaceAddBtn.Create\");\n  };\n\n  const openTable = async (tableName: string) => {\n    await tout(500);\n    const tableList = getElement(\"dashboard.menu.tablesSearchList\");\n    if (!tableList) {\n      await click(\"dashboard.menu\");\n      // await click(\"dashboard.menu\");\n      // await click(\"DashboardMenuHeader.togglePinned\");\n    }\n    await click(\"dashboard.menu.tablesSearchList\", `[data-key=${tableName}]`);\n    // await click(\"DashboardMenuHeader.togglePinned\");\n  };\n\n  // await createWorkspace();\n\n  await closeAllViews();\n  await openTable(\"users\");\n  await tout(500);\n  await click(\"AddColumnMenu\");\n  await click(\"AddColumnMenu\", \"[data-key=Referenced]\");\n  await click(\"JoinPathSelectorV2\");\n  await click(\"JoinPathSelectorV2\", `[data-key=\"(id = customer_id) orders\"]`);\n\n  await click(\"QuickAddComputedColumn\");\n  await click(\"QuickAddComputedColumn\", `[data-key=\"$countAll`);\n  await click(\"QuickAddComputedColumn.Add\");\n\n  await type(\"Customer Order Count\", \"\", \"#nested-col-name\");\n  await click(\"LinkedColumn.Add\");\n\n  /** Descending order count */\n  await click(\"\", `[role=\"columnheader\"]:nth-child(2)`);\n  await click(\"\", `[role=\"columnheader\"]:nth-child(2)`);\n  await tout(1e3);\n\n  await click(\"dashboard.window.viewEditRow\", undefined, {\n    nth: 0,\n    noTimeToWait: true,\n  });\n  await click(\"JoinedRecords.SectionToggle\", `[data-key=\"orders\"] `);\n  await tout(2e3);\n  await click(\"JoinedRecords\", `[data-command=\"SmartCard.viewEditRow\"]`, {\n    nth: 0,\n  });\n  await tout(1e3);\n  await click(\"JoinedRecords.SectionToggle\", `[data-key=\"order_items\"] `);\n  await tout(2e3);\n  await click(\"Popup.close\");\n  await click(\"Popup.close\");\n\n  /** Add Map */\n  await click(\"AddChartMenu.Map\");\n  await click(\"AddChartMenu.Map\", `[data-key=\"location\"]`);\n  await tout(2e3);\n  const mapDiv = document.querySelector(\n    \".DeckGLMapDiv\",\n  ) as DeckGLMapDivDemoControls;\n  const point = {\n    /** Park */\n    // latitude: 51.536,\n    // longitude: -.1568\n    /** Maida Vale */\n    latitude: 51.5276,\n    longitude: -0.1906,\n  };\n  const { x, y } = mapDiv.getLatLngXY(point);\n  await movePointer(x, y);\n  await mapDiv.zoomTo({ ...point, zoom: 19 });\n  await tout(5e3);\n  await click(\"ChartLayerManager\");\n  await click(\"ChartLayerManager.AddChartLayer.addLayer\");\n  // await click(\"ChartLayerManager.AddChartLayer.addLayer\", `[data-key=${JSON.stringify(`routes.geog`)}]`);\n  await click(\n    \"ChartLayerManager.AddChartLayer.addLayer\",\n    `[data-key=${JSON.stringify(`\"london_restaurants.geojson\".geometry`)}]`,\n  );\n  await click(\"Popup.close\");\n\n  /** Set to col layout, add table, clone wsp */\n  await click(\"dashboard.menu.settingsToggle\");\n  await click(\"dashboard.menu.settings.defaultLayoutType\");\n  await click(\"dashboard.menu.settings.defaultLayoutType\", `[data-key=\"col\"]`);\n  await click(\"Popup.close\");\n  await openTable(\"orders\");\n  await click(\"WorkspaceMenuDropDown\");\n  await click(\"WorkspaceMenu.CloneWorkspace\");\n\n  await tout(1e3);\n  await waitForElement(\"\", \".DeckGLMapDiv\");\n  await waitForElement(\"\", `.SilverGridChild[data-table-name=\"orders\"]`);\n\n  /** TODO: Fails due to page refresh \n   * Delete all workspaces and expect to be returned to a new blank workspace \n  await click(\"WorkspaceMenuDropDown\");\n  await click(\"WorkspaceDeleteBtn\");\n  await click(\"WorkspaceDeleteBtn.Confirm\");\n  await tout(1e3);\n  if(document.querySelectorAll(`button[title=\"Workspace\"]`).length !== 1){\n    throw new Error(\"Expected a total of 2 workspaces\");\n  }\n  await click(\"WorkspaceMenuDropDown\");\n  await click(\"WorkspaceDeleteBtn\");\n  await click(\"WorkspaceDeleteBtn.Confirm\");\n  */\n\n  await tout(1e3);\n  await waitForElement(\"dashboard.menu.sqlEditor\");\n\n  await tout(2e3);\n\n  await click(\"dashboard.goToConnections\");\n  await click(\"\", \"[data-key^=crypto] a\", { nth: 0 });\n\n  // await createWorkspace();\n\n  await closeAllViews();\n  await runDbSQL(\n    \"DELETE FROM futures WHERE (now() - timestamp) > interval '30 minutes'\",\n  );\n  await openTable(\"futures\");\n\n  await click(\"dashboard.window.toggleFilterBar\");\n  await type(\"btcusd\", \"\", \".SmartFilterBar input\");\n  await click(\"\", `[data-label=\"BTCUSDC\"]`);\n\n  await click(\"\", `[title=\"Click to expand/collapse\"]`);\n  await type(\"btcu\", \"\", \".FilterWrapper input.custom-input\");\n  await click(\"\", `[data-key=\"BTCUSDT\"]`);\n\n  await click(\"SmartAddFilter\");\n  await click(\"SmartAddFilter\", `[data-key=\"timestamp\"]`);\n  await click(\"AgeFilter.comparator\");\n  await click(\"AgeFilter.comparator\", `[data-key=\"<\"]`);\n  await type(\"7 minutes\", \"\", \".AgeFilter input \");\n\n  await click(\"\", `[title=\"Expand/Collapse filters\"]`);\n  await click(\"AddChartMenu.Timechart\");\n  await click(\"AddChartMenu.Timechart\", `[data-key=\"timestamp\"]`);\n  await click(\"ChartLayerManager\");\n  await click(\"TimeChartLayerOptions.aggFunc\");\n  await click(\"TimeChartLayerOptions.aggFunc.select\");\n  await click(\"TimeChartLayerOptions.aggFunc.select\", `[data-key=\"$avg\"]`);\n  await click(\"TimeChartLayerOptions.numericColumn\");\n  await click(\"TimeChartLayerOptions.numericColumn\", `[data-key=\"price\"]`);\n  await click(\"TimeChartLayerOptions.groupBy\");\n  await click(\"TimeChartLayerOptions.groupBy\", `[data-key=\"symbol\"]`);\n  await click(\"Popup.close\");\n  await click(\"Popup.close\");\n  await tout(5e3);\n};\n"
  },
  {
    "path": "client/src/demo/scripts/fileDemo.ts",
    "content": "import { tout } from \"../../pages/ElectronSetup/ElectronSetup\";\nimport { click, type } from \"../demoUtils\";\n\nexport const fileDemo = async () => {\n  await tout(2e3);\n  await click(\"config.goToConnDashboard\");\n  await tout(1e3);\n  if (!document.querySelector(`[data-table-name=\"messages\"]`)) {\n    await click(\"dashboard.menu.tablesSearchList\", \"[data-key='messages']\");\n    await tout(2e3);\n  }\n  await click(\"AddColumnMenu\");\n  await tout(1e3);\n  await click(\"AddColumnMenu\", \"[data-key='CreateFileColumn']\");\n\n  await tout(500);\n  await type(\"attachement\", \"Popup.content\", \"input\");\n  await tout(1e3);\n  await click(\"CreateFileColumn.confirm\");\n  await tout(500);\n  await click(\"dashboard.window.rowInsert\");\n  await tout(500);\n  await click(\"SmartFormFieldOptions.AttachFile\");\n};\n"
  },
  {
    "path": "client/src/demo/scripts/schemaDiagramDemo.ts",
    "content": "import { tout } from \"src/utils/utils\";\nimport { click, movePointer, openConnection } from \"../demoUtils\";\n\nexport const schemaDiagramDemo = async () => {\n  await click(\"dashboard.goToConnections\");\n  await openConnection(\"food_delivery\");\n  await click(\"SchemaGraph\");\n  await tout(1000);\n  await movePointer(424, 730);\n  await movePointer(816, 756);\n  await tout(1000);\n  await click(\"Popup.close\");\n};\n"
  },
  {
    "path": "client/src/demo/scripts/sqlVideoDemo.ts",
    "content": "import { fixIndent } from \"@common/utils\";\nimport {\n  getDemoUtils,\n  type DemoScript,\n  type TypeAutoOpts,\n} from \"../../dashboard/W_SQL/getDemoUtils\";\nimport { VIDEO_DEMO_DB_NAME } from \"../../dashboard/W_SQL/TestSQL\";\nimport { tout } from \"../../utils/utils\";\nimport { closeAllViews } from \"./dashboardDemo\";\nimport {\n  click,\n  clickWhenReady,\n  movePointer,\n  openConnection,\n  waitForElement,\n} from \"../demoUtils\";\n\nexport { fixIndent };\nconst sqlVideoDemo: DemoScript = async (args) => {\n  const {\n    runDbSQL,\n    fromBeginning,\n    typeAuto,\n    moveCursor,\n    getEditor,\n    actions,\n    testResult,\n    runSQL,\n    newLine,\n  } = args;\n\n  const hasTable = await runDbSQL(\n    `SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = current_schema() AND tablename = 'chats'`,\n    {},\n    { returnType: \"value\" },\n  );\n  const existingUsers: string[] = await runDbSQL(\n    `SELECT usename FROM pg_catalog.pg_user `,\n    {},\n    { returnType: \"values\" },\n  );\n  if (!hasTable) {\n    // alert(\"Creating demo tables. Must run demo script again\");\n    const usersTable = `CREATE TABLE IF NOT EXISTS users  (\n      \"id\" UUID PRIMARY KEY DEFAULT null,\n      \"status\" TEXT NOT NULL DEFAULT 'active'::text,\n      \"username\" TEXT NOT NULL DEFAULT null,\n      \"password\" TEXT NOT NULL DEFAULT gen_random_uuid(),\n      \"type\" TEXT NOT NULL DEFAULT 'default'::text,\n      \"passwordless_admin\" BOOL  DEFAULT null,\n      \"created\" TIMESTAMP  DEFAULT now(),\n      \"last_updated\" INT8  DEFAULT (EXTRACT(epoch FROM now()) * (1000)::numeric),\n      \"options\" JSONB  DEFAULT null,\n      \"2fa\" JSONB  DEFAULT null,\n      \"has_2fa_enabled\" BOOLEAN GENERATED ALWAYS AS ( (\"2fa\"->>'enabled')::BOOLEAN ) STORED\n      );\n    `;\n    const demoSchema = [\n      `DROP TABLE IF EXISTS users, chats, chat_users, chat_members, contacts, orders, messages CASCADE;`,\n      existingUsers.includes(\"mynewuser\") ? \"\" : `CREATE USER mynewuser;`,\n      `GRANT ALL ON ALL TABLES IN SCHEMA public TO mynewuser;`,\n      existingUsers.includes(\"vid_demo_user\") ? \"\" : (\n        `CREATE USER vid_demo_user;`\n      ),\n      usersTable,\n      `INSERT INTO users(id, status, username, password, type, options) VALUES (gen_random_uuid(), 'active', 'user78679', '****', 'default', '{ \"theme\": \"light\", \"viewedSqlTips\": true, \"language\": \"en-US\", \"timeZone\": \"America/New_York\" }'::JSONB);`,\n      `INSERT INTO users(id, status, username, password, type, options) VALUES (gen_random_uuid(), 'active', 'user219', '****', 'customer', '{ \"theme\": \"from-system\", \"viewedSqlTips\": false }'::JSONB);`,\n      `INSERT INTO users(id, status, username, password, type, options) VALUES (gen_random_uuid(), 'active', 'user219', '****', 'defaultl', '{ \"theme\": \"dark\", \"viewedSqlTips\": true }'::JSONB);`,\n      `CREATE TABLE IF NOT EXISTS orders (id TEXT PRIMARY KEY, user_id UUID NOT NULL REFERENCES users, total_price DECIMAL(12,2) CHECK(total_price >= 0), created_at TIMESTAMP  DEFAULT now());`,\n      `CREATE TABLE IF NOT EXISTS chats (id BIGSERIAL PRIMARY KEY);`,\n      `CREATE TABLE IF NOT EXISTS chat_members (chat_id BIGINT NOT NULL REFERENCES chats, user_id UUID NOT NULL REFERENCES users, UNIQUE(chat_id, user_id));`,\n      `CREATE TABLE IF NOT EXISTS contacts ( user_id UUID REFERENCES users, contact_user_id UUID REFERENCES users, added_on TIMESTAMP DEFAULT now(), PRIMARY KEY (user_id, contact_user_id));`,\n      `CREATE TABLE IF NOT EXISTS messages (id BIGSERIAL PRIMARY KEY, chat_id BIGINT REFERENCES chats, sender_id UUID REFERENCES users ON DELETE CASCADE, message_text TEXT NOT NULL CHECK (length(trim(message_text)) > 0), timestamp TIMESTAMP DEFAULT now(), seen_at TIMESTAMP);`,\n      `REVOKE ALL ON ALL TABLES IN SCHEMA public FROM vid_demo_user;`,\n      `\n    -- Alter Default Privileges for Future Tables\n      ALTER DEFAULT PRIVILEGES IN SCHEMA public FOR ROLE vid_demo_user\n      REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM vid_demo_user;\n      `,\n    ];\n    await runDbSQL(demoSchema.join(\"\\n\"));\n  }\n\n  await runDbSQL(`DROP POLICY IF EXISTS read_own_data ON users;`);\n\n  /** Force monaco to show the text in docker */\n  const focusEditor = () => {\n    getEditor().editor.querySelector<HTMLDivElement>(\".monaco-editor\")?.click();\n  };\n  focusEditor();\n\n  const waitAccept = 1e3;\n  const waitBeforeAccept = 0.5e3;\n  const showScript = async (\n    title: string,\n    script: string,\n    logic: () => Promise<void>,\n  ) => {\n    fromBeginning(false, `/* ${title} */\\n${script}`);\n    await tout(1e3);\n    await logic();\n    await tout(1e3);\n  };\n\n  const typeQuick = (text: string, opts?: TypeAutoOpts) => {\n    return typeAuto(text, {\n      msPerChar: 0,\n      waitBeforeAccept: 0.5e3,\n      waitAccept: 0,\n      ...opts,\n    });\n  };\n\n  await timeChartDemo(args);\n\n  /** Join complete */\n  await showScript(\n    `Joins autocomplete`,\n    `SELECT * \\nFROM users u`,\n    async () => {\n      await typeQuick(`\\nleft`);\n      await typeQuick(` `, { msPerChar: 40, waitAccept: 0, nth: 2 });\n    },\n  );\n\n  /** Context aware suggestions */\n  const script = fixIndent(`\n    /** Schema and context aware suggestions */\n    SELECT u.id, latest_orders.*\n    FROM users u\n    LEFT JOIN LATERAL (\n      SELECT *\n      FROM orders o\n      WHERE u.id = o.user_id\n      ORDER BY\n      LIMIT 10\n    ) latest_orders\n      ON TRUE\n    `);\n  await fromBeginning(false, script);\n  await moveCursor.lineEnd();\n  await moveCursor.up(3);\n  await moveCursor.lineEnd();\n  await tout(500);\n  await typeAuto(\" o.\");\n  await typeAuto(\" d\");\n  testResult(script.replace(\"ORDER BY\", \"ORDER BY o.created_at DESC\"));\n\n  /** Documentation and ALL CATALOGS SUGGESTED */\n  // await showScript(`Schema and context aware suggestions`, \"\", async () => {\n  //   await typeQuick(`SEL` );\n  //   await typeQuick(` query`, { nth: 1 });\n  //   await typeQuick(`, md` );\n  //   await typeAuto(`(`, { dontAccept: true });\n  //   triggerParamHints();\n  //   await tout(500);\n  //   await typeQuick(\"q\" );\n  // });\n\n  await showScript(\n    `Data aware suggestions, jsonb support`,\n    `SELECT`,\n    async () => {\n      await typeQuick(` opt`);\n      await moveCursor.lineEnd();\n      await typeQuick(`, age(cr`, { waitBeforeAccept: 1500 });\n      await moveCursor.down(1);\n      await moveCursor.lineEnd();\n      await newLine();\n      await typeQuick(`W`, { triggerMode: \"firstChar\", waitBeforeAccept: 500 });\n      await typeQuick(` opt`);\n      await typeAuto(` the`, { waitAccept: 1e3 });\n      await typeQuick(` `);\n      await typeAuto(` '`, {\n        msPerChar: 55,\n        waitAccept: 500,\n        triggerMode: \"firstChar\",\n        waitBeforeAccept: 500,\n      });\n      testResult(\n        `/* Data aware suggestions, jsonb support */\\nSELECT options, age(created)\\nFROM users\\nWHERE options ->>'theme' = 'dark'\\nLIMIT 200`,\n      );\n    },\n  );\n\n  /** Current statement execution */\n  await showScript(\n    `Current statement execution`,\n    fixIndent(`\n      SELECT * \n      FROM orders\n      \n      SELECT * \n      FROM`),\n    async () => {\n      // await actions.selectCodeBlock();\n      await moveCursor.up(4, 30);\n      await tout(500);\n      await actions.selectCodeBlock();\n      await runSQL();\n      focusEditor();\n      await moveCursor.down(4, 30);\n      await moveCursor.lineEnd();\n      await typeAuto(\" use\");\n      await actions.selectCodeBlock();\n      await tout(500);\n      await runSQL();\n      await newLine(2);\n      await typeAuto(`SEL`);\n      await actions.selectCodeBlock();\n      await tout(500);\n      await runSQL();\n    },\n  );\n\n  /** Selection expansion */\n  // await showScript(`Selection/statement expansion`, async () => {\n  //   const script = fixIndent(`\n  //     /* Selection/statement expansion */\n  //     SELECT *\n  //     FROM users u\n  //     LEFT JOIN (\n  //       SELECT *\n  //       FROM orders o\n  //       LEFT JOIN (\n  //         SELECT *\n  //         FROM order_items\n  //       ) oi\n  //       ON oi.order_id = o.id\n  //       GROUP BY user_id\n  //     ) o\n  //     ON o.user_id = u.id\n  //   `);\n  //   fromBeginning(false, script);\n  //   await moveCursor.up(4, 100);\n  //   await tout(500);\n  //   for (let i = 0; i < 3; i++) {\n  //     actions.selectCodeBlock();\n  //     await tout(500);\n  //   }\n  // });\n\n  await showScript(\"Documentation and schema extracts\", \"\", async () => {\n    await typeQuick(`cr`, { msPerChar: 40, waitAccept, waitBeforeAccept });\n    await typeQuick(` us`, { msPerChar: 40, waitBeforeAccept });\n    await typeQuick(` mynewuser`, { msPerChar: 4, dontAccept: true });\n    await newLine();\n    await typeQuick(`pass`, { msPerChar: 140, waitAccept, waitBeforeAccept });\n\n    await newLine(2);\n    await typeQuick(`gr`);\n    await typeQuick(` sel`);\n    await typeQuick(` usern`);\n    await typeQuick(` `);\n    await typeQuick(` myne`);\n    await runSQL();\n    await newLine(2);\n\n    await typeAuto(`cre`);\n    await typeAuto(` pol`);\n    await typeAuto(` read_own_data`, { dontAccept: true });\n    await typeAuto(`\\n`);\n    await typeAuto(` us`);\n    await typeAuto(` f`);\n    await typeAuto(` se`);\n    await typeAuto(`\\n`);\n    await typeAuto(` myn`);\n    await typeAuto(`\\n`);\n    await typeAuto(` id`);\n    await typeAuto(`= userid`);\n    await tout(1e3);\n    await runSQL();\n  });\n\n  await showScript(\"Schema extracts with access details\", \"\", async () => {\n    await typeQuick(`?user `, { nth: 1, dontAccept: true });\n    await moveCursor.left();\n    await typeQuick(` mynew`, { waitBeforeAccept: 2e3 });\n    testResult(\n      [\"/* Schema extracts with access details */\", \"?user mynewuser\"].join(\n        \"\\n\",\n      ),\n    );\n  });\n\n  await showScript(`Schema extracts with related objects`, \"\", async () => {\n    await typeQuick(`al`, { msPerChar: 40, waitAccept, waitBeforeAccept });\n    await typeQuick(` ta`, { msPerChar: 40 });\n    await typeQuick(` us`, { msPerChar: 40, waitAccept, waitBeforeAccept });\n    await typeQuick(`\\nalt`);\n    await typeQuick(` padm`, { waitAccept: 1e3 });\n    await newLine();\n    await typeQuick(`def`, { waitAccept });\n    await typeQuick(` FALSE`, { dontAccept: true, nth: -1 });\n    testResult(\n      fixIndent(`\n      /* Schema extracts with related objects */\n      ALTER TABLE users\n      ALTER COLUMN passwordless_admin\n      SET DEFAULT FALSE`),\n    );\n  });\n\n  /** Insert value suggestions */\n  await showScript(`Argument hints`, \"\", async () => {\n    await typeQuick(`INSE`);\n    await typeQuick(` users`, { nth: 1 });\n    await typeQuick(`(`, { nth: -1 });\n    await typeQuick(`DEFAULT)`, { nth: -1 });\n    await moveCursor.left(1);\n    await typeAuto(`,`, { waitAccept: 1e3, nth: -1 });\n    await typeAuto(`1,`, { waitAccept: 1e3, nth: -1 });\n    testResult(\n      fixIndent(`\n        /* Argument hints */\n        INSERT INTO users (id, status, username, password, type, passwordless_admin, created, last_updated, options, \"2fa\", has_2fa_enabled)\n        VALUES(DEFAULT,1,)`),\n    );\n  });\n\n  await showScript(`Settings details`, \"\", async () => {\n    await typeQuick(`st`);\n    await typeQuick(` wm`, { waitBeforeAccept: 2e3 });\n    await typeQuick(` `);\n    await typeQuick(` `, { waitBeforeAccept: 1e3, nth: 2 });\n    testResult(\n      fixIndent(`\n      /* Settings details */\n      SET work_mem TO DEFAULT`),\n    );\n  });\n\n  // await tout(1e3);\n  // fromBeginning(false);\n  // await typeQuick(`GRANT SE`, { nth: 1 });\n  // await typeQuick(`\\nall`);\n  // await typeQuick(` pub`);\n  // await typeQuick(`\\n`);\n  // await typeQuick(` myn`);\n  // testResult(fixIndent(`\n  //   GRANT SELECT ON\n  //   ALL TABLES IN SCHEMA public\n  //   TO mynewuser`)\n  // );\n\n  // await runSQL();\n};\n\n// /**\n//  * Ensure that multi-line strings are indented correctly\n//  */\n// export const fixIndent = (_str: string | TemplateStringsArray): string => {\n//   const str = typeof _str === \"string\" ? _str : (_str[0] ?? \"\");\n//   const lines = str.split(\"\\n\");\n//   if (!lines.some((l) => l.trim())) return str;\n//   let minIdentOffset = lines.reduce(\n//     (a, line) => {\n//       if (!line.trim()) return a;\n//       const indent = line.length - line.trimStart().length;\n//       return Math.min(a ?? indent, indent);\n//     },\n//     undefined as number | undefined,\n//   );\n//   minIdentOffset = Math.max(minIdentOffset ?? 0, 0);\n\n//   return lines\n//     .map((l, i) => (i === 0 ? l : l.slice(minIdentOffset)))\n//     .join(\"\\n\")\n//     .trim();\n// };\n\nexport const sqlDemo = async () => {\n  await click(\"dashboard.goToConnections\");\n  await tout(500);\n  await openConnection(\"prostgles_video_demo\");\n  await tout(500);\n  await closeAllViews();\n  await click(\"dashboard.menu\");\n  await click(\"dashboard.menu.sqlEditor\");\n  await tout(1500);\n  await movePointer(-20, -20);\n  const getSqlWindow = () =>\n    Array.from(\n      document.querySelectorAll<HTMLDivElement>(\n        `[data-box-id][data-box-type=item]`,\n      ),\n    ).find((n) => n.querySelector(\".ProstglesSQL\"));\n  if (!getSqlWindow()) {\n    click(\"dashboard.menu.sqlEditor\");\n    await tout(1500);\n  }\n  const sqlWindow = getSqlWindow();\n  const id = sqlWindow?.dataset!.boxId;\n  if (!sqlWindow || !id) throw \"not ok\";\n  await (window as any).dbs.windows.update(\n    { id },\n    { sql_options: { $merge: [{ executeOptions: \"smallest-block\" }] } },\n  );\n  await tout(1500);\n  const testUtils = getDemoUtils({ id });\n\n  const currDbName = await testUtils.runDbSQL(\n    `SELECT current_database() as db_name`,\n    {},\n    { returnType: \"value\" },\n  );\n  if (currDbName === VIDEO_DEMO_DB_NAME) {\n    return sqlVideoDemo(testUtils);\n  }\n};\n\nconst timeChartDemo: DemoScript = async ({\n  fromBeginning,\n  newLine,\n  moveCursor,\n  getEditor,\n  runSQL,\n}) => {\n  await fromBeginning(\n    false,\n    fixIndent(`\n    SELECT *, random() as rval\n    FROM generate_series(\n      '2021-01-01'::timestamp, \n      '2021-01-31'::timestamp, \n      '1 day'::interval\n    ) as date \n  `),\n  );\n  const addTChartBtn = await waitForElement<HTMLButtonElement>(\n    \"AddChartMenu.Timechart\",\n  );\n  await tout(1500);\n  await clickWhenReady(addTChartBtn);\n\n  const layer = await waitForElement<HTMLButtonElement>(\n    \"TimeChartLayerOptions.aggFunc\",\n  );\n\n  /** Shows numeric col avg by default */\n  shouldBeEqual(layer.innerText, \"Avg(\\nrval\\n),\\ndate\");\n\n  /** Cannot add the same layer */\n  shouldBeEqual(addTChartBtn.disabled, true);\n\n  const setLayerFunc = async (func: \"$avg\" | \"$countAll\", layerNumber = 0) => {\n    await click(\"TimeChartLayerOptions.aggFunc\", \"\", {\n      nth: layerNumber,\n      whenReady: true,\n    });\n    await click(\"TimeChartLayerOptions.aggFunc.select\");\n    await click(\n      \"TimeChartLayerOptions.aggFunc.select\",\n      `[data-key=${JSON.stringify(func)}]`,\n    );\n    await click(\"Popup.close\");\n    await tout(222);\n  };\n\n  /** Count all works */\n  await setLayerFunc(\"$countAll\");\n  shouldBeEqual(layer.innerText, \"count(*), date\");\n\n  /** Switch back */\n  await setLayerFunc(\"$avg\");\n  shouldBeEqual(layer.innerText, \"Avg(\\nrval\\n),\\ndate\");\n\n  const addSqlLayer = async (sql: string) => {\n    await moveCursor.pageDown();\n    await moveCursor.lineEnd();\n    await newLine(3);\n    const newQ = fixIndent(sql);\n    getEditor().e.setValue(getEditor().e.getValue() + newQ);\n\n    moveCursor.pageDown();\n    moveCursor.up(3, 50);\n    moveCursor.down(2, 50);\n\n    await tout(2000);\n    await click(\"AddChartMenu.Timechart\");\n  };\n\n  /** Add another layer */\n  await addSqlLayer(`\n    SELECT *, 10 * random() as rvalx10\n    FROM generate_series(\n      '2021-01-01'::timestamp, \n      '2021-01-31'::timestamp, \n      '1 day'::interval\n    ) as date \n  `);\n\n  const secondLayer = await waitForElement<HTMLButtonElement>(\n    \"TimeChartLayerOptions.aggFunc\",\n    \"\",\n    { nth: 1 },\n  );\n  shouldBeEqual(\"Avg(\\nrvalx10\\n),\\ndate\", secondLayer.innerText);\n\n  /** Add group by layer */\n  await addSqlLayer(`\n    SELECT *\n      , 10 * random() as rvalx11\n      , CASE WHEN random() > .3 then 'g1' when random() > .6 then 'g2' else 'g3' END as groupbyval\n    FROM generate_series(\n      '2021-01-01'::timestamp, \n      '2021-01-31'::timestamp, \n      '1 day'::interval\n    ) as date \n  `);\n  const thirdLayer = await waitForElement<HTMLButtonElement>(\n    \"TimeChartLayerOptions.aggFunc\",\n    \"\",\n    { nth: 2 },\n  );\n  shouldBeEqual(\"Avg(\\nrvalx11\\n),\\ndate\", thirdLayer.innerText);\n  thirdLayer.click();\n\n  /**\n   * Group by column\n   */\n  await click(\"TimeChartLayerOptions.groupBy\");\n  await click(\"TimeChartLayerOptions.groupBy\", `[data-key=\"groupbyval\"]`);\n  await click(\"Popup.close\");\n\n  await click(\"dashboard.window.closeChart\");\n};\n\nexport const shouldBeEqual = (a: any, b: any) => {\n  if (a !== b) {\n    throw new Error(`Expected ${a} to equal ${b}`);\n  }\n};\n"
  },
  {
    "path": "client/src/demo/scripts/videoDemoAccessControlScripts.ts",
    "content": "const startCreatingAccessRule = [\n  {\n    selector: '[data-command=\"config.ac.create\"]',\n    timestamp: 1731,\n  },\n  {\n    selector: '[data-command=\"config.ac.edit.user\"]',\n    timestamp: 2531,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"SearchList.List\"] [data-key=\"default\"]',\n    timestamp: 3443,\n  },\n  {\n    selector: '[data-command=\"SmartSelect.Done\"]',\n    timestamp: 4087,\n  },\n  {\n    selector: '[data-command=\"config.ac.edit.type\"] [data-key=\"Custom\"]',\n    timestamp: 4844,\n  },\n];\n\nconst setChatsRules = [\n  {\n    selector:\n      '[data-command=\"config.ac.edit.dataAccess\"] [data-command=\"SearchList.List\"] [data-key=\"chats\"] [data-command=\"selectRule\"]',\n    timestamp: 7601,\n  },\n  {\n    selector:\n      '[data-command=\"config.ac.edit.dataAccess\"] [data-command=\"SearchList.List\"] [data-key=\"chats\"] [data-command=\"selectRuleAdvanced\"]',\n    timestamp: 8536,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ForcedFilterControl\"] [data-command=\"ForcedFilterControl.type\"]',\n    timestamp: 9597,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ForcedFilterControl.type\"] [data-command=\"SearchList.List\"] [data-key=\"enabled\"]',\n    timestamp: 10174,\n  },\n  {\n    selector: '[data-command=\"Popup.content\"] [data-command=\"SmartAddFilter\"]',\n    timestamp: 11826,\n  },\n  {\n    selector: `[data-command=\"SmartAddFilter\"] [data-command=\"SmartAddFilter.toggleIncludeLinkedColumns\"]`,\n    timestamp: 19411,\n  },\n  {\n    selector:\n      '[data-command=\"SmartAddFilter\"] [data-command=\"Popup.content\"] [data-command=\"SearchList.List\"] [data-key=\"chat_members,id,chat_id.user_id\"]',\n    timestamp: 35371,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-key=\"user_id\"] [data-command=\"ContextDataSelector\"]',\n    timestamp: 40420,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ContextDataSelector\"] [data-command=\"SearchList.List\"] [data-key=\"user.id\"]',\n    timestamp: 41292,\n  },\n  {\n    selector: '[data-command=\"TablePermissionControls.done\"]',\n    timestamp: 6731,\n  },\n];\n\nconst setMessagesRules = [\n  {\n    selector:\n      '[data-command=\"config.ac.edit.dataAccess\"] [data-command=\"SearchList.List\"] [data-key=\"messages\"] [data-command=\"selectRule\"]',\n    timestamp: 1715023273451,\n  },\n  {\n    selector:\n      '[data-command=\"config.ac.edit.dataAccess\"] [data-command=\"SearchList.List\"] [data-key=\"messages\"] [data-command=\"selectRuleAdvanced\"]',\n    timestamp: 1280,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ForcedFilterControl\"] [data-command=\"ForcedFilterControl.type\"]',\n    timestamp: 6896,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ForcedFilterControl.type\"] [data-command=\"SearchList.List\"] [data-key=\"enabled\"]',\n    timestamp: 7813,\n  },\n  {\n    selector: '[data-command=\"Popup.content\"] [data-command=\"SmartAddFilter\"]',\n    timestamp: 20550,\n  },\n  {\n    selector: `[data-command=\"SmartAddFilter\"] [data-command=\"SmartAddFilter.toggleIncludeLinkedColumns\"]`,\n    timestamp: 19411,\n  },\n  {\n    selector: `[data-command=\"SmartAddFilter\"] [data-command=\"Popup.content\"] [data-command=\"SearchList.List\"] [data-key=\"chats,chat_id,id,chat_members,id,chat_id.user_id\"]`,\n    timestamp: 31371,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-key=\"user_id\"] [data-command=\"ContextDataSelector\"]',\n    timestamp: 33040,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ContextDataSelector\"] [data-command=\"SearchList.List\"] [data-key=\"user.id\"]',\n    timestamp: 43397,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"MenuList\"] [data-key=\"insert\"]',\n    timestamp: 64875,\n  },\n  {\n    selector: '[data-command=\"Popup.content\"] [data-command=\"RuleToggle\"]',\n    timestamp: 66111,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"CheckFilterControl\"] [data-command=\"CheckFilterControl.type\"]',\n    timestamp: 67208,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"CheckFilterControl.type\"] [data-command=\"SearchList.List\"] [data-key=\"enabled\"]',\n    timestamp: 68264,\n  },\n  {\n    selector: '[data-command=\"Popup.content\"] [data-command=\"SmartAddFilter\"]',\n    timestamp: 74361,\n  },\n  {\n    selector: `[data-command=\"SmartAddFilter\"] [data-command=\"SmartAddFilter.toggleIncludeLinkedColumns\"]`,\n    timestamp: 19411,\n  },\n  {\n    selector: `[data-command=\"SmartAddFilter\"] [data-command=\"Popup.content\"] [data-command=\"SearchList.List\"] [data-key=\"chats,chat_id,id,chat_members,id,chat_id.user_id\"]`,\n    timestamp: 31371,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-key=\"user_id\"] [data-command=\"ContextDataSelector\"]',\n    timestamp: 101828,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ContextDataSelector\"] [data-command=\"SearchList.List\"] [data-key=\"user.id\"]',\n    timestamp: 102512,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"MenuList\"] [data-key=\"update\"]',\n    timestamp: 106736,\n  },\n  {\n    selector: '[data-command=\"Popup.content\"] [data-command=\"RuleToggle\"]',\n    timestamp: 108675,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"FieldFilterControl\"] [data-command=\"FieldFilterControl.type\"]',\n    timestamp: 109779,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"FieldFilterControl.type\"] [data-command=\"SearchList.List\"] [data-key=\"only these fields\"]',\n    timestamp: 112080,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"FieldFilterControl\"] [data-command=\"FieldFilterControl.select\"]',\n    timestamp: 113606,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"SearchList.toggleAll\"] DIV',\n    timestamp: 114490,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"FieldFilterControl.select\"] [data-command=\"SearchList.List\"] [data-key=\"seen_at\"]',\n    timestamp: 115561,\n  },\n  {\n    selector: '[data-command=\"ClickCatchOverlay\"]',\n    timestamp: 116327,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ForcedFilterControl\"] [data-command=\"ForcedFilterControl.type\"]',\n    timestamp: 117700,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ForcedFilterControl.type\"] [data-command=\"SearchList.List\"] [data-key=\"enabled\"]',\n    timestamp: 118364,\n  },\n  {\n    selector: '[data-command=\"Popup.content\"] [data-command=\"SmartAddFilter\"]',\n    timestamp: 120753,\n  },\n  {\n    selector:\n      '[data-command=\"SmartAddFilter\"] [data-command=\"Popup.content\"] [data-command=\"SearchList.List\"] [data-key=\"sender_id\"]',\n    timestamp: 123185,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-key=\"sender_id\"] [data-command=\"ContextDataSelector\"]',\n    timestamp: 124205,\n  },\n  {\n    selector:\n      '[data-command=\"Popup.content\"] [data-command=\"ContextDataSelector\"] [data-command=\"SearchList.List\"] [data-key=\"user.id\"]',\n    timestamp: 127003,\n  },\n  {\n    selector: '[data-command=\"TablePermissionControls.done\"]',\n    timestamp: 144286,\n  },\n];\n\nexport const videoDemoAccessControlScripts = [\n  ...startCreatingAccessRule,\n  ...setChatsRules,\n  ...setMessagesRules,\n  {\n    selector: '[data-command=\"config.ac.save\"]',\n    timestamp: 10290,\n  },\n\n  // {\n  //   \"selector\": \"[data-command=\\\"dashboard.goToConnConfig\\\"]\",\n  //   \"timestamp\": 10292\n  // }\n];\n"
  },
  {
    "path": "client/src/exports.ts",
    "content": "// export { default as FormField } from \"./components/FormField\";\n// export { default as SmartForm } from \"./dashboard/SmartForm/SmartForm\";\n// export { default as Checkbox } from \"./components/Checkbox\";\n// export { default as SmartFormField } from \"./dashboard/SmartForm/SmartFormField\";\nexport { default as List } from \"./components/List\";\n// export { default as Btn } from \"./components/Btn\";\n// export { default as Popup } from \"./components/Popup\";\n// export { default as SmartCard, nFormatter } from \"./dashboard/SmartCard/SmartCard\";\n// export { default as SmartCardView } from \"./dashboard/SmartCard/SmartCardView\";\n// export { default as ConfirmationDialog } from \"./components/ConfirmationDialog\";\n// export { default as ErrorComponent } from \"./components/ErrorComponent\";\n// export { default as Dashboard } from \"./dashboard/Dashboard\";\n// export { default as DeckGLMap } from \"./dashboard/DeckGLMap\";\n// export { default as QueryFilter, FilterComp } from \"./dashboard/QueryFilter\";\n// export { SuccessSVG } from \"./components/Animations\";\n"
  },
  {
    "path": "client/src/hooks/useDebouncedCallback.ts",
    "content": "import { useCallback, useRef, useEffect } from \"react\";\n\nexport const useDebouncedCallback = <\n  T extends (...args: Parameters<T>) => void,\n>(\n  callback: T,\n  deps: React.DependencyList,\n  delay = 300,\n): ((...args: Parameters<T>) => void) => {\n  const timeoutRef = useRef<NodeJS.Timeout>();\n\n  useEffect(() => {\n    return () => clearTimeout(timeoutRef.current);\n  }, []);\n\n  return useCallback(\n    (...args: Parameters<T>) => {\n      clearTimeout(timeoutRef.current);\n      timeoutRef.current = setTimeout(() => {\n        callback(...args);\n      }, delay);\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [delay, ...deps],\n  );\n};\n"
  },
  {
    "path": "client/src/hooks/useThrottledCallback.ts",
    "content": "import { useCallback, useEffect, useRef } from \"react\";\n\nexport const useThrottledCallback = <\n  T extends (...args: Parameters<T>) => void,\n>(\n  callback: T,\n  deps: React.DependencyList,\n  delay = 300,\n): ((...args: Parameters<T>) => void) => {\n  const callbackRef = useRef(callback);\n  const lastCallRef = useRef<number>(0);\n  const timeoutRef = useRef<NodeJS.Timeout>();\n\n  useEffect(() => {\n    callbackRef.current = callback;\n  }, [callback, deps]);\n\n  useEffect(() => {\n    return () => clearTimeout(timeoutRef.current);\n  }, []);\n\n  return useCallback(\n    (...args: Parameters<T>) => {\n      const now = Date.now();\n      const timeSinceLastCall = now - lastCallRef.current;\n\n      if (timeSinceLastCall >= delay) {\n        lastCallRef.current = now;\n        callbackRef.current(...args);\n      } else {\n        clearTimeout(timeoutRef.current);\n        timeoutRef.current = setTimeout(() => {\n          lastCallRef.current = Date.now();\n          callbackRef.current(...args);\n        }, delay - timeSinceLastCall);\n      }\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [delay, ...deps],\n  );\n};\n"
  },
  {
    "path": "client/src/hooks/useTypedSearchParams.ts",
    "content": "import { useMemoDeep } from \"prostgles-client\";\nimport {\n  getJSONBObjectSchemaValidationError,\n  type JSONB,\n} from \"prostgles-types\";\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { getKeys } from \"src/utils/utils\";\n\nexport const useTypedSearchParams = <\n  JSONBType extends Record<\n    string,\n    JSONB.EnumType | JSONB.BasicType | Extract<JSONB.FieldType, \"string\">\n  >,\n>(\n  jsonbType: JSONBType,\n): [\n  JSONB.GetObjectType<JSONBType>,\n  (newValue: JSONB.GetObjectType<JSONBType>) => void,\n] => {\n  const type = useMemoDeep(() => jsonbType, [jsonbType]);\n  const [searchParams, setSearchParamsUnstable] = useSearchParams();\n  const setSearchParamsRef = useRef(setSearchParamsUnstable);\n  const value = useMemo(() => {\n    const rawValue = getKeys(type).reduce(\n      (acc, key) => {\n        const paramValue = searchParams.get(key);\n        if (paramValue !== null) {\n          acc[key] = paramValue;\n        }\n        return acc;\n      },\n      {} as Record<string, string>,\n    );\n\n    const validation = getJSONBObjectSchemaValidationError(type, rawValue, \"\");\n\n    if (validation.error) {\n      console.error(\n        \"Invalid search params:\",\n        rawValue,\n        \"Errors:\",\n        validation.error,\n      );\n      return {} as JSONB.GetObjectType<JSONBType>;\n    }\n\n    return rawValue as JSONB.GetObjectType<JSONBType>;\n  }, [searchParams, type]);\n\n  const setParams = useCallback((newValue: JSONB.GetObjectType<JSONBType>) => {\n    setSearchParamsRef.current((prev) => {\n      const newSearchParams = new URLSearchParams(prev.toString());\n      Object.entries(newValue).forEach(([key, value]) => {\n        if (value == null || value === \"\") {\n          newSearchParams.delete(key);\n        } else {\n          if (\n            typeof value !== \"string\" &&\n            typeof value !== \"number\" &&\n            typeof value !== \"boolean\"\n          ) {\n            throw new Error(\n              `useTypedSearchParams Key \"${key}\" has illegal value`,\n            );\n          }\n          newSearchParams.set(key, String(value));\n        }\n      });\n      return newSearchParams;\n    });\n  }, []);\n\n  return [value, setParams];\n};\n"
  },
  {
    "path": "client/src/i18n/LanguageSelector.tsx",
    "content": "import { mdiEarth, mdiTranslate } from \"@mdi/js\";\nimport React from \"react\";\nimport { Select } from \"@components/Select/Select\";\nimport { getLanguage, t } from \"./i18nUtils\";\nimport { type Language, LANGUAGES } from \"./translations/translations\";\n\nexport const LanguageSelector = ({ isElectron }: { isElectron: boolean }) => {\n  const lang = getLanguage();\n  const title = t.common.Language;\n  return (\n    <Select\n      title={title}\n      btnProps={{\n        variant: \"default\",\n        iconPath: mdiEarth,\n        children: window.isLowWidthScreen || isElectron ? title : \"\",\n      }}\n      data-command=\"App.LanguageSelector\"\n      fullOptions={LANGUAGES}\n      value={lang}\n      iconPath={mdiTranslate}\n      onChange={(lang) => {\n        setLanguage(lang);\n        window.location.reload();\n      }}\n    />\n  );\n};\n\nconst setLanguage = (lang: Language) => {\n  document.documentElement.lang = lang;\n  localStorage.setItem(\"lang\", lang);\n};\n"
  },
  {
    "path": "client/src/i18n/i18nUtils.ts",
    "content": "import { isObject, getObjectEntries } from \"prostgles-types\";\nimport {\n  type Language,\n  LANGUAGES,\n  translations,\n} from \"./translations/translations\";\nimport es from \"./translations/es.json\";\nimport de from \"./translations/de.json\";\nimport zh from \"./translations/zh.json\";\nimport hi from \"./translations/hi.json\";\nimport ru from \"./translations/ru.json\";\nimport fr from \"./translations/fr.json\";\nexport const isPlaywrightTest =\n  navigator.userAgent.includes(\"Playwright\") || navigator.webdriver;\n\nconst getMatchingLanguage = (lang: string): Language | undefined => {\n  return LANGUAGES.find((l) => l.key === lang)?.key;\n};\n\nlet cachedLang: Language | undefined;\nexport const getLanguage = (): Language => {\n  if (cachedLang) {\n    return cachedLang;\n  }\n  const storedLang = localStorage.getItem(\"lang\");\n  const result =\n    getMatchingLanguage(storedLang ?? \"\") ||\n    getMatchingLanguage(navigator.language.slice(0, 2)) ||\n    \"en\";\n\n  document.documentElement.lang = result;\n  cachedLang = result;\n  return result;\n};\n\ntype TranslationsType<T> = {\n  [K1 in keyof T]: {\n    [K2 in keyof T[K1]]: string;\n  };\n};\n\ntype TranslationFile = TranslationsType<typeof translations>;\n\nconst translationFiles: Record<LanguageWithoutEn, TranslationFile> = {\n  es,\n  de,\n  zh,\n  ru,\n  hi,\n  fr,\n};\n\ntype LanguageWithoutEn = Exclude<Language, \"en\">;\nexport type TemplatedTranslation = {\n  text: string;\n  argNames: string[];\n};\nexport type Translation = undefined | TemplatedTranslation;\nexport type TranslationGroup = Record<string, Translation>;\n\n/** Ensure argument names are valid */\nconst validateTranslationFiles = () => {\n  getObjectEntries(translations).forEach(\n    ([componentName, componentTranslations]) => {\n      const checkArgs = (\n        translationKey: string,\n        translation: Translation,\n        lang: string,\n      ) => {\n        let argNames: string[] | undefined;\n        let text = translationKey;\n        if (isObject(translation) && Array.isArray(translation.argNames)) {\n          ({ argNames, text } = translation);\n          argNames.forEach((argName) => {\n            if (!text.includes(`{{${argName}}}`)) {\n              console.log(componentTranslations);\n              throw new Error(\n                `${lang} Translation \"${componentName}.${text}\" has invalid argName: ${argName}`,\n              );\n            }\n          });\n        }\n        const textArgCount = text.split(\"{{\").length - 1;\n        if (textArgCount !== (argNames?.length ?? 0)) {\n          throw new Error(\n            `${lang} Translation \"${componentName}.${translationKey}\" has incorrect number of argNames`,\n          );\n        }\n      };\n      getObjectEntries(componentTranslations).forEach(\n        ([_enTranslationKey, translation]) => {\n          const tr = translation as Translation;\n          // checkArgs(_enTranslationKey as string, argNamesOrText, \"en\");\n          getObjectEntries(translationFiles).forEach(([lang, _translation]) => {\n            // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n            const v = _translation[componentName]?.[_enTranslationKey];\n            checkArgs(_enTranslationKey, tr, lang);\n          });\n        },\n      );\n    },\n  );\n};\n\nexport const t = new Proxy(\n  {},\n  {\n    get: (_, firstKey: string) => {\n      if (firstKey in translations) {\n        return new Proxy(\n          {},\n          {\n            get(_, secondKey: string) {\n              const lang = getLanguage();\n              const engTranslation = (translations[\n                firstKey as keyof typeof translations\n              ][secondKey] ?? secondKey) as Translation;\n              const translation: Translation | string =\n                lang === \"en\" ? engTranslation : (\n                  /** This is needed during dev when keys are missing */\n                  ((translationFiles as any)?.[lang]?.[firstKey]?.[secondKey] ??\n                  secondKey + \"?\")\n                );\n\n              if (lang === \"en\" && !engTranslation) {\n                console.warn(\n                  `Missing translation for ${firstKey}.${secondKey}`,\n                );\n              }\n              if (!isObject(translation)) {\n                return translation;\n              }\n              return (args: Record<string, any>) => {\n                let result = translation.text;\n                translation.argNames.forEach((argName) => {\n                  result = result.replace(`{{${argName}}}`, args[argName]);\n                });\n                return result;\n              };\n            },\n          },\n        );\n      }\n    },\n  },\n) as TranslationHandler;\n\ntype BaseTranslation = typeof translations;\n\ntype TranslationHandler = {\n  [CompKey in keyof BaseTranslation]: {\n    [TranslationKey in keyof BaseTranslation[CompKey]]: BaseTranslation[CompKey][TranslationKey] extends (\n      { argNames: string[] }\n    ) ?\n      (\n        opts: Record<\n          BaseTranslation[CompKey][TranslationKey][\"argNames\"][number],\n          string | number\n        >,\n      ) => string\n    : string;\n  };\n};\n\nif (isPlaywrightTest) {\n  try {\n    validateTranslationFiles();\n  } catch (e) {\n    console.error(e);\n    throw new Error(\"Failed to validate translation files\");\n  }\n  const enFile = Object.entries(translations).reduce(\n    (acc, [componentName, componentTranslations]) => {\n      return {\n        ...acc,\n        [componentName]: Object.fromEntries(\n          Object.keys(componentTranslations).map((translationKey) => [\n            translationKey,\n            translationKey,\n          ]),\n        ),\n      };\n    },\n    {},\n  );\n  // console.log(\"en.json\", JSON.stringify(enFile));\n}\n"
  },
  {
    "path": "client/src/i18n/translations/de.json",
    "content": "{\n  \"common\": {\n    \"Remove\": \"Entfernen\",\n    \"Add\": \"Hinzufügen\",\n    \"Language\": \"Sprache\",\n    \"Generate\": \"Generieren\",\n    \"Next\": \"Weiter\",\n    \"Login\": \"Anmelden\",\n    \"Logout\": \"Abmelden\",\n    \"Confirm\": \"Bestätigen\",\n    \"Register\": \"Registrieren\",\n    \"Close\": \"Schließen\",\n    \"Options\": \"Optionen\",\n    \"Not permitted\": \"Nicht erlaubt\",\n    \"Something went wrong\": \"Etwas ist schiefgelaufen\",\n    \"Configure\": \"Konfigurieren\",\n    \"Workspaces\": \"Arbeitsbereiche\",\n    \"Theme\": \"Design\",\n    \"Enabled\": \"Aktiviert\",\n    \"Cancel\": \"Abbrechen\",\n    \"Save\": \"Speichern\",\n    \"Copied!\": \"Kopiert!\",\n    \"Test and Save\": \"Testen und Speichern\",\n    \"No changes\": \"Keine Änderungen\",\n    \"Run\": \"Ausführen\",\n    \"Delete\": \"Löschen\",\n    \"Clone\": \"Klonen\",\n    \"Update\": \"Aktualisieren\",\n    \"Related data\": \"Verwandte Daten\",\n    \"Create\": \"Erstellen\",\n    \"Already exists. Chose another name\": \"Existiert bereits. Wähle einen anderen Namen\",\n    \"Nothing to update\": \"Nichts zu aktualisieren\",\n    \"You do not have sufficient privileges to access this\": \"Du hast nicht die erforderlichen Berechtigungen, um darauf zuzugreifen\",\n    \"Copy to clipboard\": \"In die Zwischenablage kopieren\",\n    \"Send\": \"Senden\",\n    \"Stop\": \"Stopp\",\n    \"Toggle fullscreen\": \"Vollbild umschalten\",\n    \"experimental\": \"experimentell\",\n    \"Stop recording\": \"Aufnahme stoppen\",\n    \"Speech to text\": \"Sprache zu Text\",\n    \"Record audio\": \"Audio aufnehmen\"\n  },\n  \"App\": {\n    \"Connections\": \"Verbindungen\",\n    \"Users\": \"Benutzer\",\n    \"Server settings\": \"Servereinstellungen\",\n    \"Security issue\": \"Sicherheitsproblem\",\n    \"Settings\": \"Einstellungen\",\n    \"Reconnecting...\": \"Verbindung wird wiederhergestellt...\"\n  },\n  \"Users\": { \"Prostgles UI users\": \"Prostgles UI-Benutzer\" },\n  \"AccountMenu\": { \"Account\": \"Konto\" },\n  \"ServerSettings\": {\n    \"Security\": \"Sicherheit\",\n    \"Validate a CIDR\": \"CIDR validieren\",\n    \"Enter a value to see the allowed IP ranges\": \"Gib einen Wert ein, um die zulässigen IP-Bereiche zu sehen\",\n    \"Add your current IP\": \"Deine aktuelle IP hinzufügen\",\n    \"Allowed IP\": \"Zulässige IP\",\n    \"From IP\": \"Von IP\",\n    \"To IP\": \"Bis IP\",\n    \"Authentication\": \"Authentifizierung\",\n    \"Cloud credentials\": \"Cloud-Anmeldeinformationen\",\n    \"No cloud credentials. Credentials can be added for file storage\": \"Keine Cloud-Anmeldeinformationen. Anmeldeinformationen können für die Dateispeicherung hinzugefügt werden\",\n    \"The default user type assigned to new users. Defaults to 'default'\": \"Der Standardbenutzertyp, der neuen Benutzern zugewiesen wird. Standardmäßig 'default'\",\n    \"Warning: The default user type is set to 'admin'. This will allow new users to access all resources.\": \"Warnung: Der Standardbenutzertyp ist auf 'admin' eingestellt. Dies ermöglicht neuen Benutzern den Zugriff auf alle Ressourcen.\"\n  },\n  \"AuthProviderSetup\": {\n    \"Default user type\": \"Standardbenutzertyp\",\n    \"Website URL\": \"Webseiten-URL\",\n    \"Used for redirect uri\": \"Wird für die Weiterleitungs-URI verwendet\",\n    \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\": \"Warnung: Du setzt den Standardbenutzertyp auf 'admin'. Dies bedeutet, dass neuen Benutzern die höchste Zugriffsebene gewährt wird!\"\n  },\n  \"OAuthProviderSetup\": {\n    \"Must configure the provider first\": \"Muss zuerst den Anbieter konfigurieren\",\n    \"Must provide a client ID and secret\": \"Muss eine Client-ID und ein Client-Secret angeben\",\n    \"Display Name\": \"Anzeigename\",\n    \"Display Icon\": \"Anzeigesymbol\",\n    \"Authorization URL\": \"Autorisierungs-URL\",\n    \"Client ID\": \"Client-ID\",\n    \"Client Secret\": \"Client-Secret\",\n    \"Scopes\": \"Bereiche\",\n    \"Prompt\": \"Eingabeaufforderung\",\n    \"Indicates the type of user interaction that is required.\": \"Gibt die Art der erforderlichen Benutzerinteraktion an.\",\n    \"Return URL\": \"Rückgabe-URL\"\n  },\n  \"EmailAuthSetup\": {\n    \"Email signup\": \"E-Mail-Anmeldung\",\n    \"Must configure the email provider first\": \"Muss zuerst den E-Mail-Anbieter konfigurieren\",\n    \"Signup type\": \"Anmeldetyp\",\n    \"With password\": \"Mit Passwort\",\n    \"Email and password will be required to login. User will have to confirm email\": \"E-Mail und Passwort werden für die Anmeldung benötigt. Der Benutzer muss die E-Mail-Adresse bestätigen\",\n    \"With magic link\": \"Mit Magic Link\",\n    \"Only email is required to login. A magic link will be sent to the user's email\": \"Nur die E-Mail ist für die Anmeldung erforderlich. Ein Magic Link wird an die E-Mail-Adresse des Benutzers gesendet\",\n    \"Minimum password length\": \"Mindestpasswortlänge\",\n    \"Minimum password length. Defaults to 8\": \"Mindestpasswortlänge. Standardmäßig 8\",\n    \"Magic link email configuration\": \"Magic Link E-Mail-Konfiguration\",\n    \"Email verification\": \"E-Mail-Verifizierung\"\n  },\n  \"EmailSMTPAndTemplateSetup\": {\n    \"Configured\": \"Konfiguriert\",\n    \"Not configured\": \"Nicht konfiguriert\",\n    \"Users will receive an email with a link/code to verify their email address.\": \"Benutzer erhalten eine E-Mail mit einem Link/Code, um ihre E-Mail-Adresse zu verifizieren.\",\n    \"Email Provider\": \"E-Mail-Anbieter\",\n    \"Template\": \"Vorlage\"\n  },\n  \"Connections\": {\n    \"Access granted to \": \"Zugriff gewährt auf \",\n    \"Create new connection\": \"Neue Verbindung erstellen\",\n    \"New connection\": \"Neue Verbindung\",\n    \"Editing state data directly may break functionality. Proceed at your own risk!\": \"Das direkte Bearbeiten von Zustandsdaten kann die Funktionalität beeinträchtigen. Fortfahren auf eigene Gefahr!\",\n    \"No connections available/permitted\": \"Keine Verbindungen verfügbar/erlaubt\",\n    \"Show database names\": \"Datenbanknamen anzeigen\",\n    \"Show state connection\": \"Statusverbindung anzeigen\"\n  },\n  \"ConnectionActionBar\": {\n    \"Connected. Click to disconnect\": \"Verbunden. Zum Trennen klicken\",\n    \"Not connected\": \"Nicht verbunden\",\n    \"Close all windows\": \"Alle Fenster schließen\",\n    \"Windows have been closed\": \"Fenster wurden geschlossen\",\n    \"Could not close windows: workspace not found\": \"Fenster konnten nicht geschlossen werden: Arbeitsbereich nicht gefunden\",\n    \"Edit connection\": \"Verbindung bearbeiten\"\n  },\n  \"ConnectionServer\": {\n    \"Server info\": \"Serverinformationen\",\n    \"Add or create a database\": \"Datenbank hinzufügen oder erstellen\",\n    \"Create a database\": \"Datenbank erstellen\",\n    \"Not allowed to create databases with this user\": \"Mit diesem Benutzer ist das Erstellen von Datenbanken nicht erlaubt\",\n    \"Create a database in this server\": \"Erstelle eine Datenbank auf diesem Server\",\n    \"Select a database from this server\": \"Wähle eine Datenbank von diesem Server aus\",\n    \"Save and connect\": \"Speichern und verbinden\",\n    \"Create and connect\": \"Erstellen und verbinden\",\n    \"Some data is missing\": \"Einige Daten fehlen\",\n    \"Must fix connection name error\": \"Muss den Fehler beim Verbindungsnamen beheben\",\n    \"New database name\": \"Neuer Datenbankname\",\n    \"Name already in use\": \"Name bereits vergeben\",\n    \"Create demo schema (optional)\": \"Demoschema erstellen (optional)\",\n    \"Database\": \"Datenbank\",\n    \"Already added to connections\": \"Bereits zu den Verbindungen hinzugefügt\"\n  },\n  \"Connection\": {\n    \"Access granted to \": \"Zugriff gewährt auf \",\n    \"All Prostgles connection and dashboard data is stored here. Edit at your own risk\": \"Alle Prostgles-Verbindungs- und Dashboard-Daten werden hier gespeichert. Bearbeitung auf eigene Gefahr\",\n    \"Realtime requires table triggers to be created as and when needed.\": \"Realtime erfordert, dass Tabellentriggers nach Bedarf erstellt werden.\",\n    \"A \\\"prostgles\\\" schema with necessary metadata will also be created\": \"Ein \\\"prostgles\\\"-Schema mit notwendigen Metadaten wird ebenfalls erstellt\",\n    \"Realtime\": \"Echtzeit\",\n    \"Needed to allow realtime data view. Requires superuser\": \"Wird benötigt, um die Echtzeitdatenansicht zu ermöglichen. Erfordert Superuser\",\n    \"Reload schema\": \"Schema neu laden\",\n    \"Watch schema\": \"Schema überwachen\",\n    \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\": \"Aktualisiert das Dashboard und die API bei Schemaänderungen. Erfordert Superuser für die beste Erfahrung\",\n    \"Reject unauthorized\": \"Nicht autorisiert ablehnen\",\n    \"Client Key\": \"Clientschlüssel\",\n    \"Client Certificate\": \"Clientzertifikat\",\n    \"CA Certificate\": \"CA-Zertifikat\",\n    \"SSL Mode\": \"SSL-Modus\",\n    \"Connection timeout (ms)\": \"Verbindungszeitüberschreitung (ms)\",\n    \"Schemas\": \"Schemaliste\",\n    \"More options\": \"Weitere Optionen\",\n    \"Must connect to see schemas\": \"Muss verbunden sein, um Schemas zu sehen\",\n    \"You are about to create a new database\": \"Du bist dabei, eine neue Datenbank zu erstellen\",\n    \"Create database\": \"Datenbank erstellen\",\n    \"Connection type\": \"Verbindungstyp\",\n    \"Connection name\": \"Verbindungsname\",\n    \"Socket URL\": \"Socket-URL\",\n    \"Socket params (JSON)\": \"Socket-Parameter (JSON)\",\n    \"Connection URI\": \"Verbindungs-URI\",\n    \"Host\": \"Host\",\n    \"Port\": \"Port\",\n    \"User\": \"Benutzer\",\n    \"Password\": \"Passwort\",\n    \"Optional\": \"Optional\",\n    \"Database\": \"Datenbank\",\n    \"Test connection\": \"Verbindung testen\"\n  },\n  \"NewConnection\": {\n    \"Delete connection\": \"Verbindung löschen\",\n    \"Any related dashboard content will also be deleted\": \"Alle zugehörigen Dashboard-Inhalte werden ebenfalls gelöscht\",\n    \"Drop database as well\": \"Datenbank ebenfalls löschen\",\n    \"You are about to drop\": \"Du bist dabei, zu löschen\",\n    \"database\": \"Datenbank\",\n    \"Ensure data is backed up. This action is not reversible\": \"Stelle sicher, dass die Daten gesichert sind. Diese Aktion ist nicht umkehrbar\",\n    \"Clone connection\": \"Verbindung klonen\",\n    \"Cloning connection\": \"Verbindung wird geklont\",\n    \"Connections\": \"Verbindungen\",\n    \"Connection not found\": \"Verbindung nicht gefunden\",\n    \"Go back to connections\": \"Zurück zu den Verbindungen\"\n  },\n  \"ConnectionConfig\": {\n    \"Must be admin to access this\": \"Muss Administrator sein, um darauf zuzugreifen\",\n    \"Connection details\": \"Verbindungsdetails\",\n    \"Status monitor\": \"Statusmonitor\",\n    \"Access control\": \"Zugriffskontrolle\",\n    \"File storage\": \"Dateispeicher\",\n    \"Backup/Restore\": \"Sicherung/Wiederherstellung\",\n    \"API\": \"API\",\n    \"Table config\": \"Tabellenkonfiguration\",\n    \"experimental\": \"experimentell\",\n    \"Server-side functions\": \"Serverseitige Funktionen\"\n  },\n  \"ChangePassword\": {\n    \"Change password\": \"Passwort ändern\",\n    \"Old password\": \"Altes Passwort\",\n    \"New password\": \"Neues Passwort\",\n    \"Confirm new password\": \"Neues Passwort bestätigen\",\n    \"Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter\": \"Stelle sicher, dass es mindestens 15 Zeichen ODER mindestens 8 Zeichen einschließlich einer Zahl und eines Kleinbuchstabens hat\",\n    \"Enter your old password\": \"Gib dein altes Passwort ein\",\n    \"Enter your new password\": \"Gib dein neues Passwort ein\",\n    \"Confirm your new password\": \"Bestätige dein neues Passwort\",\n    \"Passwords do not match\": \"Passwörter stimmen nicht überein\"\n  },\n  \"Setup2FA\": {\n    \"Enable 2FA\": \"2FA aktivieren\",\n    \"Confirm code\": \"Bestätigungscode\",\n    \"2FA Enabled\": \"2FA aktiviert\",\n    \"Along with your username and password, you will be asked to verify your identity using the code from authenticator app.\": \"Zusammen mit deinem Benutzernamen und Passwort wirst du aufgefordert, deine Identität mit dem Code aus der Authenticator-App zu verifizieren.\",\n    \"Two-factor authentication\": \"Zwei-Faktor-Authentifizierung\",\n    \"Scan\": \"Scanne\",\n    \" or tap \": \" oder tippe auf \",\n    \"the image below with the two-factor authentication app on your phone.\": \"das untere Bild mit der Zwei-Faktor-Authentifizierungs-App auf deinem Telefon.\",\n    \"If you can't use a QR code you can enter this information manually:\": \"Wenn du keinen QR-Code verwenden kannst, kannst du diese Informationen manuell eingeben:\",\n    \"Base64 secret\": \"Base64-Secret\",\n    \"Type\": \"Typ\",\n    \"Name\": \"Name\",\n    \"Save the Recovery code below. It will be used in case you lose access to your authenticator app:\": \"Speichere den unten stehenden Wiederherstellungscode. Er wird verwendet, falls du den Zugriff auf deine Authenticator-App verlierst:\",\n    \"Generate QR Code\": \"QR-Code generieren\",\n    \"I can't scan the QR Code\": \"Ich kann den QR-Code nicht scannen\"\n  },\n  \"Account\": {\n    \"Account details\": \"Kontodetails\",\n    \"Security\": \"Sicherheit\",\n    \"API\": \"API\"\n  },\n  \"APIDetailsWs\": {\n    \"Allowed origin specifies which domains can access this app in a cross-origin manner. Sets the Access-Control-Allow-Origin header. Use '*' or a specific URL to allow API access\": \"Zulässiger Ursprung gibt an, welche Domänen auf diese App in einer Cross-Origin-Weise zugreifen können. Setzt den Access-Control-Allow-Origin-Header. Verwende '*' oder eine spezifische URL, um API-Zugriff zu erlauben\",\n    \"For testing it is recommended to use \\\"*\\\" as the allowed origin value\": \"Für Tests wird empfohlen, \\\"*\\\" als Wert für den zulässigen Ursprung zu verwenden\",\n    \"Allowed origin\": \"Zulässiger Ursprung\",\n    \"Allowed origin is required\": \"Zulässiger Ursprung ist erforderlich\",\n    \"Allowed origin not set\": \"Zulässiger Ursprung nicht gesetzt\",\n    \"Websocket API (recommended)\": \"Websocket-API (empfohlen)\",\n    \"Realtime Isomorphic API using\": \"Isomorphe Echtzeit-API mit\",\n    \"library. End-to-end type-safety between client & server using the database Typescript schema provided below:\": \"Bibliothek. Ende-zu-Ende-Typsicherheit zwischen Client und Server unter Verwendung des unten angegebenen Datenbank-Typescript-Schemas:\",\n    \"Examples\": \"Beispiele\",\n    \"Download typescript schema\": \"Typescript-Schema herunterladen\",\n    \"Database schema types\": \"Datenbankschematypen\"\n  },\n  \"APIDetailsTokens\": {\n    \"Access tokens (\": \"Zugriffstoken (\",\n    \"Provide the same level of access as the current account\": \"Bieten die gleiche Zugriffsebene wie das aktuelle Konto\",\n    \"Create access token\": \"Zugriffstoken erstellen\",\n    \"Create token\": \"Token erstellen\",\n    \"Expires in\": \"Läuft ab in\",\n    \"Days\": \"Tagen\",\n    \"These token values will not be shown again\": \"Diese Token-Werte werden nicht mehr angezeigt\",\n    \"Websocket API\": \"Websocket-API\",\n    \"HTTP API (base64)\": \"HTTP-API (base64)\",\n    \"Already generated. Close and re-open the popup\": \"Bereits generiert. Schließe und öffne das Popup erneut\"\n  },\n  \"APIDetailsHttp\": {\n    \"Provides similar level of access to the Websocket API with the following limitations: no subscriptions, no sync, no file upload\": \"Bietet eine ähnliche Zugriffsebene wie die Websocket-API mit den folgenden Einschränkungen: keine Abonnements, keine Synchronisierung, kein Dateiupload\"\n  },\n  \"Sessions\": {\n    \"API tokens\": \"API-Token\",\n    \"Sessions\": \"Sitzungen\",\n    \"Last used\": \"Zuletzt verwendet\",\n    \"Created\": \"Erstellt\",\n    \"Expires\": \"Läuft ab\",\n    \"Disable\": \"Deaktivieren\",\n    \"User agent\": \"User-Agent\",\n    \"Active\": \"Aktiv\",\n    \"Inactive\": \"Inaktiv\",\n    \"No active \": \"Keine aktiven \"\n  },\n  \"TopControls\": {\n    \"Go to Connections\": \"Zu Verbindungen gehen\",\n    \"Connections\": \"Verbindungen\",\n    \"Go to workspace\": \"Zum Arbeitsbereich gehen\",\n    \"Menu is pinned\": \"Menü ist angeheftet\",\n    \"pinned\": \"angeheftet\",\n    \"Configure database connection\": \"Datenbankverbindung konfigurieren\",\n    \"Go back to connection workspace\": \"Zurück zum Verbindungsarbeitsbereich\",\n    \"Not allowed for state database\": \"Nicht erlaubt für Statusdatenbank\",\n    \"Connection configuration\": \"Verbindungskonfiguration\"\n  },\n  \"Feedback\": {\n    \"Send feedback\": \"Feedback senden\",\n    \"Leave feedback\": \"Feedback hinterlassen\",\n    \"Feedback\": \"Feedback\",\n    \"Already sent\": \"Bereits gesendet\",\n    \"Must provide some details\": \"Muss einige Details angeben\",\n    \"Feedback was sent! Thanks a lot!\": \"Feedback wurde gesendet! Vielen Dank!\",\n    \"Email (optional)\": \"E-Mail (optional)\",\n    \"Details\": \"Details\",\n    \"Other options\": \"Weitere Optionen\",\n    \"open an issue\": \"ein Problem melden\",\n    \"or\": \"oder\",\n    \"email us\": \"sende uns eine E-Mail\"\n  },\n  \"AskLLM\": {\n    \"Chat to an AI Assistant to get help with your queries\": \"Chatte mit einem KI-Assistenten, um Hilfe bei deinen Anfragen zu erhalten\",\n    \"AI assistant not available. Talk to the admin\": \"KI-Assistent nicht verfügbar. Wende dich an den Administrator\",\n    \"AI Assistant\": \"KI fragen\"\n  },\n  \"AskLLMChatHeader\": {\n    \"Chat\": \"Chat\",\n    \"New chat\": \"Neuer Chat\",\n    \"No prompt found\": \"Keine Eingabeaufforderung gefunden\",\n    \"Prompt\": \"Eingabeaufforderung\",\n    \"Chat settings\": \"Chat-Einstellungen\"\n  },\n  \"ConnectionSelector\": { \"Switch database\": \"Datenbank wechseln\" },\n  \"W_SQLMenu\": {\n    \"SQL Editor settings\": \"SQL-Editor-Einstellungen\",\n    \"Press\": \"Drücke\",\n    \"to get a list of possible options\": \"um eine Liste möglicher Optionen zu erhalten\",\n    \"Update options\": \"Optionen aktualisieren\",\n    \"Nothing to update\": \"Nichts zu aktualisieren\",\n    \"Cannot save due to error\": \"Kann aufgrund eines Fehlers nicht gespeichert werden\",\n    \"Delete query\": \"Abfrage löschen\",\n    \"Open SQL file\": \"SQL-Datei öffnen\",\n    \"Open query from file\": \"Abfrage aus Datei öffnen\",\n    \"Download query\": \"Abfrage herunterladen\",\n    \"Save query as file\": \"Abfrage als Datei speichern\",\n    \"Query name\": \"Abfragename\",\n    \"Result display mode\": \"Ergebnisanzeigemodus\",\n    \"General\": \"Allgemein\",\n    \"Editor options\": \"Editoroptionen\",\n    \"Hotkeys\": \"Hotkeys\"\n  },\n  \"SQLHotkeys\": {\n    \"Show autocomplete suggestions\": \"Autovervollständigungsvorschläge anzeigen\",\n    \"Execute current statement\": \"Aktuelle Anweisung ausführen\",\n    \"Select current statement\": \"Aktuelle Anweisung auswählen\",\n    \"Show all suggestions\": \"Alle Vorschläge anzeigen\",\n    \"Show PSQL command queries\": \"PSQL-Befehlsabfragen anzeigen\"\n  },\n  \"W_SQLBottomBar\": {\n    \"Execution mode\": \"Ausführungsmodus\",\n    \"Loop query execution\": \"Abfrageausführung wiederholen\",\n    \"Cancel this query (Esc)\": \"Diese Abfrage abbrechen (Esc)\",\n    \"Terminate this query\": \"Diese Abfrage beenden\",\n    \"Query running time\": \"Abfragelaufzeit\",\n    \"Stop LISTEN\": \"LISTEN beenden\",\n    \"repeat every\": \"wiederhole alle\",\n    \"seconds\": \"Sekunden\",\n    \"Run query (CTRL+E, ALT+E)\": \"Abfrage ausführen (STRG+E, ALT+E)\",\n    \"Run\": \"Ausführen\",\n    \"Query running time: \": \"Abfragelaufzeit: \",\n    \"Show/Hide table\": \"Tabelle anzeigen/ausblenden\",\n    \"Show/Hide code editor\": \"Code-Editor anzeigen/ausblenden\",\n    \"Show/Hide notices\": \"Hinweise anzeigen/ausblenden\",\n    \"Clear value to show all rows\": \"Wert löschen, um alle Zeilen anzuzeigen\",\n    \"Limit\": \"Limit\"\n  },\n  \"Window\": {\n    \"Open menu\": \"Menü öffnen\",\n    \"Menu\": \"Menü\",\n    \"Collapse chart\": \"Diagramm einklappen\",\n    \"Detach chart\": \"Diagramm lösen\",\n    \"Close chart\": \"Diagramm schließen\",\n    \"Chart options\": \"Diagrammoptionen\"\n  },\n  \"AddChartMenu\": {\n    \"Layer already added\": \"Ebene bereits hinzugefügt\",\n    \"No\": \"Nein\",\n    \"No {{chartColumnDataType}} columns available\": \"Keine {{chartColumnDataType}} Spalten verfügbar\"\n  },\n  \"DashboardMenuHeader\": {\n    \"Opens SQL Query editor\": \"Öffnet den SQL-Abfrageeditor\",\n    \"SQL Editor\": \"SQL-Editor\",\n    \"Show quick search menu (CTRL + P)\": \"Schnellsuchmenü anzeigen (STRG + P)\",\n    \"Cannot be used in a low width device\": \"Kann nicht auf Geräten mit geringer Breite verwendet werden\",\n    \"Pin/Unpin\": \"Anheften/Lösen\"\n  },\n  \"W_QuickMenu\": {\n    \"Show/Hide filtering\": \"Filterung ein-/ausblenden\",\n    \"Restore minimised charts\": \"Minimierte Diagramme wiederherstellen\",\n    \"Cross filter tables\": \"Tabellenübergreifende Filterung\",\n    \"Add chart\": \"Diagramm hinzufügen\"\n  },\n  \"AddColumnMenu\": {\n    \"Add Computed Field\": \"Berechnetes Feld hinzufügen\",\n    \"Show a computed column\": \"Eine berechnete Spalte anzeigen\",\n    \"Add Linked Data\": \"Verknüpfte Daten hinzufügen\",\n    \"Show data from a related table\": \"Daten aus einer verknüpften Tabelle anzeigen\",\n    \"Create New Column\": \"Neue Spalte erstellen\",\n    \"Create a new column in this table\": \"Eine neue Spalte in dieser Tabelle erstellen\",\n    \"Create New File Column\": \"Neue Dateispalte erstellen\",\n    \"Create a new file column in this table\": \"Eine neue Dateispalte in dieser Tabelle erstellen\",\n    \"Not enough privileges\": \"Nicht genügend Berechtigungen\",\n    \"This is a view. Cannot create columns, must recreate\": \"Dies ist eine Ansicht. Es können keine Spalten erstellt werden, muss neu erstellt werden\",\n    \"Add column\": \"Spalte hinzufügen\",\n    \"New Field\": \"Neues Feld\",\n    \"No foreign keys to/from this table\": \"Keine Fremdschlüssel zu/von dieser Tabelle\",\n    \"Not allowed for nested columns\": \"Nicht erlaubt für verschachtelte Spalten\",\n    \"Aggregates and/or Count not allowed with linked \": \"Aggregate und/oder Anzahl nicht erlaubt mit verknüpften \",\n    \"Create new column\": \"Neue Spalte erstellen\",\n    \"Add Referenced/Linked Fields\": \"Referenzierte/Verknüpfte Felder hinzufügen\"\n  },\n  \"CreateColumn\": {\n    \"Create column query\": \"Spalte erstellen Abfrage\",\n    \"Show create column query\": \"Spalte erstellen Abfrage anzeigen\",\n    \"New column name missing\": \"Neuer Spaltenname fehlt\",\n    \"Data type missing\": \"Datentyp fehlt\"\n  },\n  \"W_Table\": { \"Insert row\": \"Zeile einfügen\" },\n  \"LinkedColumn\": {\n    \"Row\": \"Zeile\",\n    \"Column\": \"Spalte\",\n    \"No headers\": \"Keine Kopfzeilen\",\n    \"Column name already used. Change to another\": \"Spaltenname bereits verwendet. Ändere ihn zu einem anderen\",\n    \"Must select columns\": \"Muss Spalten auswählen\",\n    \"Must select a table\": \"Muss eine Tabelle auswählen\",\n    \"Join to and show data from tables that are related through a\": \"Verbinde und zeige Daten aus Tabellen, die durch ein\",\n    \"Column label\": \"Spaltenbezeichnung\",\n    \"More options\": \"Weitere Optionen\",\n    \"Layout\": \"Layout\",\n    \"Must disable chart first\": \"Muss zuerst das Diagramm deaktivieren\",\n    \"Join type\": \"Verbindungstyp\",\n    \"Added!\": \"Hinzugefügt!\"\n  },\n  \"NewConnectionForm\": {\n    \"Realtime requires table triggers to be created as and when needed.\": \"Echtzeit erfordert, dass Tabellentrigger bei Bedarf erstellt werden.\",\n    \"A \\\"prostgles\\\" schema with necessary metadata will also be created\": \"Ein 'prostgles'-Schema mit den erforderlichen Metadaten wird ebenfalls erstellt.\",\n    \"Realtime\": \"Echtzeit\",\n    \"Needed to allow realtime data view. Requires superuser\": \"Erforderlich, um die Echtzeit-Datenansicht zu ermöglichen. Benötigt Superuser.\",\n    \"Reload schema\": \"Schema neu laden\",\n    \"Watch schema\": \"Schema überwachen\",\n    \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\": \"Aktualisiert das Dashboard und die API bei Schemaänderungen. Für die beste Erfahrung wird ein Superuser benötigt.\",\n    \"Reject unauthorized\": \"Unbefugte ablehnen\",\n    \"Client Key\": \"Client-Schlüssel\",\n    \"Client Certificate\": \"Client-Zertifikat\",\n    \"CA Certificate\": \"CA-Zertifikat\",\n    \"SSL Mode\": \"SSL-Modus\",\n    \"Connection timeout (ms)\": \"Verbindungszeitüberschreitung (ms)\",\n    \"Schemas\": \"Schemaliste\",\n    \"More options\": \"Weitere Optionen\",\n    \"Must connect to see schemas\": \"Muss verbunden sein, um Schemas zu sehen\",\n    \"You are about to create a new database\": \"Sie sind dabei, eine neue Datenbank zu erstellen\",\n    \"You are about to clone the current database\": \"Sie sind dabei, die aktuelle Datenbank {{currDb}} in {{newDb}} zu klonen. Dies wird alle bestehenden Verbindungen zur aktuellen Datenbank schließen!\",\n    \"Create database\": \"Datenbank erstellen\",\n    \"Connection type\": \"Verbindungstyp\",\n    \"Connection name\": \"Verbindungsname\",\n    \"Socket URL\": \"Socket-URL\",\n    \"Socket params (JSON)\": \"Socket-Parameter (JSON)\",\n    \"Connection URI\": \"Verbindungs-URI\",\n    \"Host\": \"Host\",\n    \"Port\": \"Port\",\n    \"User\": \"Benutzer\",\n    \"Password\": \"Passwort\",\n    \"Optional\": \"Optional\",\n    \"Database\": \"Datenbank\",\n    \"Test connection\": \"Verbindung testen\"\n  }\n}\n"
  },
  {
    "path": "client/src/i18n/translations/es.json",
    "content": "{\n  \"common\": {\n    \"Remove\": \"Eliminar\",\n    \"Add\": \"Añadir\",\n    \"Language\": \"Idioma\",\n    \"Generate\": \"Generar\",\n    \"Next\": \"Siguiente\",\n    \"Login\": \"Iniciar sesión\",\n    \"Logout\": \"Cerrar sesión\",\n    \"Confirm\": \"Confirmar\",\n    \"Register\": \"Registrarse\",\n    \"Close\": \"Cerrar\",\n    \"Options\": \"Opciones\",\n    \"Not permitted\": \"No permitido\",\n    \"Something went wrong\": \"Algo salió mal\",\n    \"Configure\": \"Configurar\",\n    \"Workspaces\": \"Espacios de trabajo\",\n    \"Theme\": \"Tema\",\n    \"Enabled\": \"Activado\",\n    \"Cancel\": \"Cancelar\",\n    \"Save\": \"Guardar\",\n    \"Copied!\": \"¡Copiado!\",\n    \"Test and Save\": \"Probar y guardar\",\n    \"No changes\": \"Sin cambios\",\n    \"Run\": \"Ejecutar\",\n    \"Delete\": \"Eliminar\",\n    \"Clone\": \"Clonar\",\n    \"Update\": \"Actualizar\",\n    \"Related data\": \"Datos relacionados\",\n    \"Create\": \"Crear\",\n    \"Already exists. Chose another name\": \"Ya existe. Elige otro nombre\",\n    \"Nothing to update\": \"Nada que actualizar\",\n    \"You do not have sufficient privileges to access this\": \"No tienes suficientes privilegios para acceder a esto\",\n    \"Copy to clipboard\": \"Copiar al portapapeles\",\n    \"Send\": \"Enviar\",\n    \"Stop\": \"Detener\",\n    \"Toggle fullscreen\": \"Alternar pantalla completa\",\n    \"experimental\": \"experimental\",\n    \"Stop recording\": \"Detener grabación\",\n    \"Speech to text\": \"Conversión de voz a texto\",\n    \"Record audio\": \"Grabar audio\"\n  },\n  \"App\": {\n    \"Connections\": \"Conexiones\",\n    \"Users\": \"Usuarios\",\n    \"Server settings\": \"Configuración del servidor\",\n    \"Security issue\": \"Problema de seguridad\",\n    \"Settings\": \"Configuración\",\n    \"Reconnecting...\": \"Reconectando...\"\n  },\n  \"Users\": {\n    \"Prostgles UI users\": \"Usuarios de la interfaz de usuario de Prostgles\"\n  },\n  \"AccountMenu\": { \"Account\": \"Cuenta\" },\n  \"ServerSettings\": {\n    \"Security\": \"Seguridad\",\n    \"Validate a CIDR\": \"Validar un CIDR\",\n    \"Enter a value to see the allowed IP ranges\": \"Ingresa un valor para ver los rangos de IP permitidos\",\n    \"Add your current IP\": \"Agregar tu IP actual\",\n    \"Allowed IP\": \"IP permitida\",\n    \"From IP\": \"Desde IP\",\n    \"To IP\": \"Hasta IP\",\n    \"Authentication\": \"Autenticación\",\n    \"Cloud credentials\": \"Credenciales de la nube\",\n    \"No cloud credentials. Credentials can be added for file storage\": \"No hay credenciales de la nube. Se pueden agregar credenciales para el almacenamiento de archivos\",\n    \"The default user type assigned to new users. Defaults to 'default'\": \"El tipo de usuario predeterminado asignado a los nuevos usuarios. El valor predeterminado es 'default'\",\n    \"Warning: The default user type is set to 'admin'. This will allow new users to access all resources.\": \"Advertencia: el tipo de usuario predeterminado está configurado como 'admin'. Esto permitirá a los nuevos usuarios acceder a todos los recursos.\"\n  },\n  \"AuthProviderSetup\": {\n    \"Default user type\": \"Tipo de usuario predeterminado\",\n    \"Website URL\": \"URL del sitio web\",\n    \"Used for redirect uri\": \"Se utiliza para la URI de redireccionamiento\",\n    \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\": \"Advertencia: estás configurando el tipo de usuario predeterminado como 'admin'. ¡Esto significa que a los nuevos usuarios se les otorgará el nivel más alto de acceso!\"\n  },\n  \"OAuthProviderSetup\": {\n    \"Must configure the provider first\": \"Primero debes configurar el proveedor\",\n    \"Must provide a client ID and secret\": \"Debes proporcionar un ID de cliente y un secreto\",\n    \"Display Name\": \"Nombre para mostrar\",\n    \"Display Icon\": \"Icono para mostrar\",\n    \"Authorization URL\": \"URL de autorización\",\n    \"Client ID\": \"ID de cliente\",\n    \"Client Secret\": \"Secreto de cliente\",\n    \"Scopes\": \"Ámbitos\",\n    \"Prompt\": \"Prompt\",\n    \"Indicates the type of user interaction that is required.\": \"Indica el tipo de interacción del usuario que se requiere.\",\n    \"Return URL\": \"URL de retorno\"\n  },\n  \"EmailAuthSetup\": {\n    \"Email signup\": \"Registro por correo electrónico\",\n    \"Must configure the email provider first\": \"Primero debes configurar el proveedor de correo electrónico\",\n    \"Signup type\": \"Tipo de registro\",\n    \"With password\": \"Con contraseña\",\n    \"Email and password will be required to login. User will have to confirm email\": \"Se requerirá correo electrónico y contraseña para iniciar sesión. El usuario tendrá que confirmar el correo electrónico\",\n    \"With magic link\": \"Con enlace mágico\",\n    \"Only email is required to login. A magic link will be sent to the user's email\": \"Solo se requiere correo electrónico para iniciar sesión. Se enviará un enlace mágico al correo electrónico del usuario\",\n    \"Minimum password length\": \"Longitud mínima de la contraseña\",\n    \"Minimum password length. Defaults to 8\": \"Longitud mínima de la contraseña. El valor predeterminado es 8\",\n    \"Magic link email configuration\": \"Configuración de correo electrónico de enlace mágico\",\n    \"Email verification\": \"Verificación de correo electrónico\"\n  },\n  \"EmailSMTPAndTemplateSetup\": {\n    \"Configured\": \"Configurado\",\n    \"Not configured\": \"No configurado\",\n    \"Users will receive an email with a link/code to verify their email address.\": \"Los usuarios recibirán un correo electrónico con un enlace/código para verificar su dirección de correo electrónico.\",\n    \"Email Provider\": \"Proveedor de correo electrónico\",\n    \"Template\": \"Plantilla\"\n  },\n  \"Connections\": {\n    \"Access granted to \": \"Acceso concedido a \",\n    \"Create new connection\": \"Crear nueva conexión\",\n    \"New connection\": \"Nueva conexión\",\n    \"Editing state data directly may break functionality. Proceed at your own risk!\": \"¡Editar los datos de estado directamente puede romper la funcionalidad. ¡Continúa bajo tu propio riesgo!\",\n    \"No connections available/permitted\": \"No hay conexiones disponibles/permitidas\",\n    \"Show database names\": \"Mostrar nombres de bases de datos\",\n    \"Show state connection\": \"Mostrar conexión de estado\"\n  },\n  \"ConnectionActionBar\": {\n    \"Connected. Click to disconnect\": \"Conectado. Haz clic para desconectar\",\n    \"Not connected\": \"No conectado\",\n    \"Close all windows\": \"Cerrar todas las ventanas\",\n    \"Windows have been closed\": \"Se han cerrado las ventanas\",\n    \"Could not close windows: workspace not found\": \"No se pudieron cerrar las ventanas: no se encontró el espacio de trabajo\",\n    \"Edit connection\": \"Editar conexión\"\n  },\n  \"ConnectionServer\": {\n    \"Server info\": \"Información del servidor\",\n    \"Add or create a database\": \"Agregar o crear una base de datos\",\n    \"Create a database\": \"Crear una base de datos\",\n    \"Not allowed to create databases with this user\": \"No se permite crear bases de datos con este usuario\",\n    \"Create a database in this server\": \"Crear una base de datos en este servidor\",\n    \"Select a database from this server\": \"Seleccionar una base de datos de este servidor\",\n    \"Save and connect\": \"Guardar y conectar\",\n    \"Create and connect\": \"Crear y conectar\",\n    \"Some data is missing\": \"Faltan algunos datos\",\n    \"Must fix connection name error\": \"Debes corregir el error del nombre de la conexión\",\n    \"New database name\": \"Nombre de la nueva base de datos\",\n    \"Name already in use\": \"Nombre ya en uso\",\n    \"Create demo schema (optional)\": \"Crear esquema de demostración (opcional)\",\n    \"Database\": \"Base de datos\",\n    \"Already added to connections\": \"Ya agregado a las conexiones\"\n  },\n  \"Connection\": {\n    \"Access granted to \": \"Acceso concedido a \",\n    \"All Prostgles connection and dashboard data is stored here. Edit at your own risk\": \"Todos los datos de conexión y del panel de Prostgles se almacenan aquí. Edita bajo tu propio riesgo\"\n  },\n  \"NewConnectionForm\": {\n    \"Realtime requires table triggers to be created as and when needed.\": \"Realtime requiere que se creen triggers de tabla según sea necesario.\",\n    \"A \\\"prostgles\\\" schema with necessary metadata will also be created\": \"También se creará un esquema \\\"prostgles\\\" con los metadatos necesarios\",\n    \"Realtime\": \"Tiempo real\",\n    \"Needed to allow realtime data view. Requires superuser\": \"Necesario para permitir la visualización de datos en tiempo real. Requiere superusuario\",\n    \"Reload schema\": \"Recargar esquema\",\n    \"Watch schema\": \"Observar esquema\",\n    \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\": \"Actualizará el panel y la API en caso de cambio de esquema. Requiere superusuario para la mejor experiencia\",\n    \"Reject unauthorized\": \"Rechazar no autorizado\",\n    \"Client Key\": \"Clave de cliente\",\n    \"Client Certificate\": \"Certificado de cliente\",\n    \"CA Certificate\": \"Certificado CA\",\n    \"SSL Mode\": \"Modo SSL\",\n    \"Connection timeout (ms)\": \"Tiempo de espera de conexión (ms)\",\n    \"Schemas\": \"Lista de esquemas\",\n    \"More options\": \"Más opciones\",\n    \"Must connect to see schemas\": \"Debes conectarte para ver los esquemas\",\n    \"You are about to create a new database\": \"Estás a punto de crear una nueva base de datos\",\n    \"You are about to clone the current database\": \"Está a punto de clonar la base de datos actual {{currDb}} en {{newDb}}. ¡Esto cerrará todas las conexiones existentes a la base de datos actual!\",\n    \"Create database\": \"Crear base de datos\",\n    \"Connection type\": \"Tipo de conexión\",\n    \"Connection name\": \"Nombre de la conexión\",\n    \"Socket URL\": \"URL del socket\",\n    \"Socket params (JSON)\": \"Parámetros del socket (JSON)\",\n    \"Connection URI\": \"URI de conexión\",\n    \"Host\": \"Host\",\n    \"Port\": \"Puerto\",\n    \"User\": \"Usuario\",\n    \"Password\": \"Contraseña\",\n    \"Optional\": \"Opcional\",\n    \"Database\": \"Base de datos\",\n    \"Test connection\": \"Probar conexión\"\n  },\n  \"NewConnection\": {\n    \"Delete connection\": \"Eliminar conexión\",\n    \"Any related dashboard content will also be deleted\": \"También se eliminará cualquier contenido relacionado del panel\",\n    \"Drop database as well\": \"Eliminar también la base de datos\",\n    \"You are about to drop\": \"Estás a punto de eliminar\",\n    \"database\": \"base de datos\",\n    \"Ensure data is backed up. This action is not reversible\": \"Asegúrate de que los datos estén respaldados. Esta acción no es reversible\",\n    \"Clone connection\": \"Clonar conexión\",\n    \"Cloning connection\": \"Clonando conexión\",\n    \"Connections\": \"Conexiones\",\n    \"Connection not found\": \"Conexión no encontrada\",\n    \"Go back to connections\": \"Volver a las conexiones\"\n  },\n  \"ConnectionConfig\": {\n    \"Must be admin to access this\": \"Debes ser administrador para acceder a esto\",\n    \"Connection details\": \"Detalles de la conexión\",\n    \"Status monitor\": \"Monitor de estado\",\n    \"Access control\": \"Control de acceso\",\n    \"File storage\": \"Almacenamiento de archivos\",\n    \"Backup/Restore\": \"Copia de seguridad/Restaurar\",\n    \"API\": \"API\",\n    \"Table config\": \"Configuración de tabla\",\n    \"experimental\": \"experimental\",\n    \"Server-side functions\": \"Funciones del lado del servidor\"\n  },\n  \"ChangePassword\": {\n    \"Change password\": \"Cambiar contraseña\",\n    \"Old password\": \"Contraseña antigua\",\n    \"New password\": \"Nueva contraseña\",\n    \"Confirm new password\": \"Confirmar nueva contraseña\",\n    \"Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter\": \"Asegúrate de que tenga al menos 15 caracteres O al menos 8 caracteres, incluyendo un número y una letra minúscula\",\n    \"Enter your old password\": \"Ingresa tu contraseña antigua\",\n    \"Enter your new password\": \"Ingresa tu nueva contraseña\",\n    \"Confirm your new password\": \"Confirma tu nueva contraseña\",\n    \"Passwords do not match\": \"Las contraseñas no coinciden\"\n  },\n  \"Setup2FA\": {\n    \"Enable 2FA\": \"Habilitar 2FA\",\n    \"Confirm code\": \"Confirmar código\",\n    \"2FA Enabled\": \"2FA Habilitado\",\n    \"Along with your username and password, you will be asked to verify your identity using the code from authenticator app.\": \"Junto con tu nombre de usuario y contraseña, se te pedirá que verifiques tu identidad usando el código de la aplicación de autenticación.\",\n    \"Two-factor authentication\": \"Autenticación de dos factores\",\n    \"Scan\": \"Escanea\",\n    \" or tap \": \" o toca \",\n    \"the image below with the two-factor authentication app on your phone.\": \"la siguiente imagen con la aplicación de autenticación de dos factores en tu teléfono.\",\n    \"If you can't use a QR code you can enter this information manually:\": \"Si no puedes usar un código QR, puedes ingresar esta información manualmente:\",\n    \"Base64 secret\": \"Secreto Base64\",\n    \"Type\": \"Tipo\",\n    \"Name\": \"Nombre\",\n    \"Save the Recovery code below. It will be used in case you lose access to your authenticator app:\": \"Guarda el código de recuperación a continuación. Se utilizará en caso de que pierdas el acceso a tu aplicación de autenticación:\",\n    \"Generate QR Code\": \"Generar código QR\",\n    \"I can't scan the QR Code\": \"No puedo escanear el código QR\"\n  },\n  \"Account\": {\n    \"Account details\": \"Detalles de la cuenta\",\n    \"Security\": \"Seguridad\",\n    \"API\": \"API\"\n  },\n  \"APIDetailsWs\": {\n    \"Allowed origin specifies which domains can access this app in a cross-origin manner. Sets the Access-Control-Allow-Origin header. Use '*' or a specific URL to allow API access\": \"El origen permitido especifica qué dominios pueden acceder a esta aplicación de forma cruzada. Establece el encabezado Access-Control-Allow-Origin. Usa '*' o una URL específica para permitir el acceso a la API\",\n    \"For testing it is recommended to use \\\"*\\\" as the allowed origin value\": \"Para las pruebas, se recomienda usar \\\"*\\\" como valor de origen permitido\",\n    \"Allowed origin\": \"Origen permitido\",\n    \"Allowed origin is required\": \"Se requiere el origen permitido\",\n    \"Allowed origin not set\": \"Origen permitido no establecido\",\n    \"Websocket API (recommended)\": \"API de Websocket (recomendado)\",\n    \"Realtime Isomorphic API using\": \"API isomórfica en tiempo real usando\",\n    \"library. End-to-end type-safety between client & server using the database Typescript schema provided below:\": \"biblioteca. Seguridad de tipos de extremo a extremo entre el cliente y el servidor utilizando el esquema Typescript de la base de datos que se proporciona a continuación:\",\n    \"Examples\": \"Ejemplos\",\n    \"Download typescript schema\": \"Descargar esquema Typescript\",\n    \"Database schema types\": \"Tipos de esquema de la base de datos\"\n  },\n  \"APIDetailsTokens\": {\n    \"Access tokens (\": \"Tokens de acceso (\",\n    \"Provide the same level of access as the current account\": \"Proporcionan el mismo nivel de acceso que la cuenta actual\",\n    \"Create access token\": \"Crear token de acceso\",\n    \"Create token\": \"Crear token\",\n    \"Expires in\": \"Expira en\",\n    \"Days\": \"Días\",\n    \"These token values will not be shown again\": \"Estos valores de token no se mostrarán de nuevo\",\n    \"Websocket API\": \"API de Websocket\",\n    \"HTTP API (base64)\": \"API HTTP (base64)\",\n    \"Already generated. Close and re-open the popup\": \"Ya generado. Cierra y vuelve a abrir la ventana emergente\"\n  },\n  \"APIDetailsHttp\": {\n    \"Provides similar level of access to the Websocket API with the following limitations: no subscriptions, no sync, no file upload\": \"Proporciona un nivel de acceso similar a la API de Websocket con las siguientes limitaciones: sin suscripciones, sin sincronización, sin carga de archivos\"\n  },\n  \"Sessions\": {\n    \"API tokens\": \"Tokens de API\",\n    \"Sessions\": \"Sesiones\",\n    \"Last used\": \"Último uso\",\n    \"Created\": \"Creado\",\n    \"Expires\": \"Expira\",\n    \"Disable\": \"Desactivar\",\n    \"User agent\": \"Agente de usuario\",\n    \"Active\": \"Activo\",\n    \"Inactive\": \"Inactivo\",\n    \"No active \": \"No activo \"\n  },\n  \"TopControls\": {\n    \"Go to Connections\": \"Ir a Conexiones\",\n    \"Connections\": \"Conexiones\",\n    \"Go to workspace\": \"Ir al espacio de trabajo\",\n    \"Menu is pinned\": \"El menú está fijado\",\n    \"pinned\": \"fijado\",\n    \"Configure database connection\": \"Configurar la conexión de la base de datos\",\n    \"Go back to connection workspace\": \"Volver al espacio de trabajo de la conexión\",\n    \"Not allowed for state database\": \"No permitido para la base de datos de estado\",\n    \"Connection configuration\": \"Configuración de la conexión\"\n  },\n  \"Feedback\": {\n    \"Send feedback\": \"Enviar comentarios\",\n    \"Leave feedback\": \"Dejar comentarios\",\n    \"Feedback\": \"Comentarios\",\n    \"Already sent\": \"Ya enviado\",\n    \"Must provide some details\": \"Debes proporcionar algunos detalles\",\n    \"Feedback was sent! Thanks a lot!\": \"¡Se enviaron los comentarios! ¡Muchas gracias!\",\n    \"Email (optional)\": \"Correo electrónico (opcional)\",\n    \"Details\": \"Detalles\",\n    \"Other options\": \"Otras opciones\",\n    \"open an issue\": \"abrir un problema\",\n    \"or\": \"o\",\n    \"email us\": \"enviarnos un correo electrónico\"\n  },\n  \"AskLLM\": {\n    \"Chat to an AI Assistant to get help with your queries\": \"Chatea con un asistente de IA para obtener ayuda con tus consultas\",\n    \"AI assistant not available. Talk to the admin\": \"Asistente de IA no disponible. Habla con el administrador\",\n    \"AI Assistant\": \"Preguntar a la IA\"\n  },\n  \"AskLLMChatHeader\": {\n    \"Chat\": \"Chat\",\n    \"New chat\": \"Nuevo chat\",\n    \"No prompt found\": \"No se encontró ningún prompt\",\n    \"Prompt\": \"Prompt\",\n    \"Chat settings\": \"Configuración del chat\"\n  },\n  \"ConnectionSelector\": { \"Switch database\": \"Cambiar base de datos\" },\n  \"W_SQLMenu\": {\n    \"SQL Editor settings\": \"Configuración del editor SQL\",\n    \"Press\": \"Presiona\",\n    \"to get a list of possible options\": \"para obtener una lista de posibles opciones\",\n    \"Update options\": \"Actualizar opciones\",\n    \"Nothing to update\": \"Nada que actualizar\",\n    \"Cannot save due to error\": \"No se puede guardar debido a un error\",\n    \"Delete query\": \"Eliminar consulta\",\n    \"Open SQL file\": \"Abrir archivo SQL\",\n    \"Open query from file\": \"Abrir consulta desde archivo\",\n    \"Download query\": \"Descargar consulta\",\n    \"Save query as file\": \"Guardar consulta como archivo\",\n    \"Query name\": \"Nombre de la consulta\",\n    \"Result display mode\": \"Modo de visualización de resultados\",\n    \"General\": \"General\",\n    \"Editor options\": \"Opciones del editor\",\n    \"Hotkeys\": \"Atajos de teclado\"\n  },\n  \"SQLHotkeys\": {\n    \"Show autocomplete suggestions\": \"Mostrar sugerencias de autocompletado\",\n    \"Execute current statement\": \"Ejecutar la declaración actual\",\n    \"Select current statement\": \"Seleccionar la declaración actual\",\n    \"Show all suggestions\": \"Mostrar todas las sugerencias\",\n    \"Show PSQL command queries\": \"Mostrar consultas de comandos PSQL\"\n  },\n  \"W_SQLBottomBar\": {\n    \"Execution mode\": \"Modo de ejecución\",\n    \"Loop query execution\": \"Ejecución de consulta en bucle\",\n    \"Cancel this query (Esc)\": \"Cancelar esta consulta (Esc)\",\n    \"Terminate this query\": \"Terminar esta consulta\",\n    \"Query running time\": \"Tiempo de ejecución de la consulta\",\n    \"Stop LISTEN\": \"Detener LISTEN\",\n    \"repeat every\": \"repetir cada\",\n    \"seconds\": \"segundos\",\n    \"Run query (CTRL+E, ALT+E)\": \"Ejecutar consulta (CTRL+E, ALT+E)\",\n    \"Run\": \"Ejecutar\",\n    \"Query running time: \": \"Tiempo de ejecución de la consulta: \",\n    \"Show/Hide table\": \"Mostrar/Ocultar tabla\",\n    \"Show/Hide code editor\": \"Mostrar/Ocultar editor de código\",\n    \"Show/Hide notices\": \"Mostrar/Ocultar avisos\",\n    \"Clear value to show all rows\": \"Borrar el valor para mostrar todas las filas\",\n    \"Limit\": \"Límite\"\n  },\n  \"Window\": {\n    \"Open menu\": \"Abrir menú\",\n    \"Menu\": \"Menú\",\n    \"Collapse chart\": \"Contraer gráfico\",\n    \"Detach chart\": \"Separar gráfico\",\n    \"Close chart\": \"Cerrar gráfico\",\n    \"Chart options\": \"Opciones del gráfico\"\n  },\n  \"AddChartMenu\": {\n    \"Layer already added\": \"Capa ya añadida\",\n    \"No\": \"No\",\n    \"No {{chartColumnDataType}} columns available\": \"No hay columnas {{chartColumnDataType}} disponibles\"\n  },\n  \"DashboardMenuHeader\": {\n    \"Opens SQL Query editor\": \"Abre el editor de consultas SQL\",\n    \"SQL Editor\": \"Editor SQL\",\n    \"Show quick search menu (CTRL + P)\": \"Mostrar menú de búsqueda rápida (CTRL + P)\",\n    \"Cannot be used in a low width device\": \"No se puede usar en un dispositivo de ancho reducido\",\n    \"Pin/Unpin\": \"Fijar/Desfijar\"\n  },\n  \"W_QuickMenu\": {\n    \"Show/Hide filtering\": \"Mostrar/Ocultar filtrado\",\n    \"Restore minimised charts\": \"Restaurar gráficos minimizados\",\n    \"Cross filter tables\": \"Tablas de filtro cruzado\",\n    \"Add chart\": \"Agregar gráfico\"\n  },\n  \"AddColumnMenu\": {\n    \"Add Computed Field\": \"Agregar campo calculado\",\n    \"Show a computed column\": \"Mostrar una columna calculada\",\n    \"Add Linked Data\": \"Agregar datos vinculados\",\n    \"Show data from a related table\": \"Mostrar datos de una tabla relacionada\",\n    \"Create New Column\": \"Crear nueva columna\",\n    \"Create a new column in this table\": \"Crear una nueva columna en esta tabla\",\n    \"Create New File Column\": \"Crear nueva columna de archivo\",\n    \"Create a new file column in this table\": \"Crear una nueva columna de archivo en esta tabla\",\n    \"Not enough privileges\": \"No hay suficientes privilegios\",\n    \"This is a view. Cannot create columns, must recreate\": \"Esta es una vista. No se pueden crear columnas, se debe recrear\",\n    \"Add column\": \"Agregar columna\",\n    \"New Field\": \"Nuevo campo\",\n    \"No foreign keys to/from this table\": \"No hay claves foráneas hacia/desde esta tabla\",\n    \"Not allowed for nested columns\": \"No permitido para columnas anidadas\",\n    \"Aggregates and/or Count not allowed with linked \": \"No se permiten agregados y/o conteos con \",\n    \"Create new column\": \"Crear nueva columna\",\n    \"Add Referenced/Linked Fields\": \"Agregar campos referenciados/vinculados\"\n  },\n  \"CreateColumn\": {\n    \"Create column query\": \"Crear consulta de columna\",\n    \"Show create column query\": \"Mostrar consulta de creación de columna\",\n    \"New column name missing\": \"Falta el nombre de la nueva columna\",\n    \"Data type missing\": \"Falta el tipo de datos\"\n  },\n  \"W_Table\": { \"Insert row\": \"Insertar fila\" },\n  \"LinkedColumn\": {\n    \"Row\": \"Fila\",\n    \"Column\": \"Columna\",\n    \"No headers\": \"Sin encabezados\",\n    \"Column name already used. Change to another\": \"El nombre de la columna ya está en uso. Cámbialo por otro\",\n    \"Must select columns\": \"Debes seleccionar columnas\",\n    \"Must select a table\": \"Debes seleccionar una tabla\",\n    \"Join to and show data from tables that are related through a\": \"Unir y mostrar datos de tablas que están relacionadas a través de una\",\n    \"Column label\": \"Etiqueta de columna\",\n    \"More options\": \"Más opciones\",\n    \"Layout\": \"Diseño\",\n    \"Must disable chart first\": \"Primero debes desactivar el gráfico\",\n    \"Join type\": \"Tipo de unión\",\n    \"Added!\": \"¡Añadido!\"\n  }\n}\n"
  },
  {
    "path": "client/src/i18n/translations/fr.json",
    "content": "{\n  \"common\": {\n    \"Remove\": \"Supprimer\",\n    \"Add\": \"Ajouter\",\n    \"Language\": \"Langue\",\n    \"Generate\": \"Générer\",\n    \"Next\": \"Suivant\",\n    \"Login\": \"Se connecter\",\n    \"Logout\": \"Se déconnecter\",\n    \"Confirm\": \"Confirmer\",\n    \"Register\": \"S'inscrire\",\n    \"Close\": \"Fermer\",\n    \"Options\": \"Options\",\n    \"Not permitted\": \"Non autorisé\",\n    \"Something went wrong\": \"Une erreur est survenue\",\n    \"Configure\": \"Configurer\",\n    \"Workspaces\": \"Espaces de travail\",\n    \"Theme\": \"Thème\",\n    \"Enabled\": \"Activé\",\n    \"Cancel\": \"Annuler\",\n    \"Save\": \"Enregistrer\",\n    \"Copied!\": \"Copié !\",\n    \"Test and Save\": \"Tester et enregistrer\",\n    \"No changes\": \"Aucune modification\",\n    \"Run\": \"Exécuter\",\n    \"Delete\": \"Supprimer\",\n    \"Clone\": \"Cloner\",\n    \"Update\": \"Mettre à jour\",\n    \"Related data\": \"Données associées\",\n    \"Create\": \"Créer\",\n    \"Already exists. Chose another name\": \"Existe déjà. Choisissez un autre nom\",\n    \"Nothing to update\": \"Rien à mettre à jour\",\n    \"You do not have sufficient privileges to access this\": \"Vous n'avez pas les privilèges suffisants pour y accéder\",\n    \"Copy to clipboard\": \"Copier dans le presse-papiers\",\n    \"Send\": \"Envoyer\",\n    \"Stop\": \"Arrêter\",\n    \"Toggle fullscreen\": \"Activer/désactiver le plein écran\",\n    \"experimental\": \"expérimental\",\n    \"Stop recording\": \"Arrêter l'enregistrement\",\n    \"Speech to text\": \"Parole en texte\",\n    \"Record audio\": \"Enregistrer l'audio\"\n  },\n  \"App\": {\n    \"Connections\": \"Connexions\",\n    \"Users\": \"Utilisateurs\",\n    \"Server settings\": \"Paramètres du serveur\",\n    \"Security issue\": \"Problème de sécurité\",\n    \"Settings\": \"Paramètres\",\n    \"Reconnecting...\": \"Reconnexion...\"\n  },\n  \"Users\": {\n    \"Prostgles UI users\": \"Utilisateurs de l'interface utilisateur Prostgles\"\n  },\n  \"AccountMenu\": { \"Account\": \"Compte\" },\n  \"ServerSettings\": {\n    \"Security\": \"Sécurité\",\n    \"Validate a CIDR\": \"Valider un CIDR\",\n    \"Enter a value to see the allowed IP ranges\": \"Saisissez une valeur pour voir les plages d'adresses IP autorisées\",\n    \"Add your current IP\": \"Ajouter votre adresse IP actuelle\",\n    \"Allowed IP\": \"IP autorisée\",\n    \"From IP\": \"De l'IP\",\n    \"To IP\": \"À l'IP\",\n    \"Authentication\": \"Authentification\",\n    \"Cloud credentials\": \"Identifiants Cloud\",\n    \"No cloud credentials. Credentials can be added for file storage\": \"Aucun identifiant Cloud. Des identifiants peuvent être ajoutés pour le stockage de fichiers\",\n    \"The default user type assigned to new users. Defaults to 'default'\": \"Le type d'utilisateur par défaut attribué aux nouveaux utilisateurs. La valeur par défaut est 'default'\",\n    \"Warning: The default user type is set to 'admin'. This will allow new users to access all resources.\": \"Avertissement : Le type d'utilisateur par défaut est défini sur 'admin'. Cela permettra aux nouveaux utilisateurs d'accéder à toutes les ressources.\"\n  },\n  \"AuthProviderSetup\": {\n    \"Default user type\": \"Type d'utilisateur par défaut\",\n    \"Website URL\": \"URL du site Web\",\n    \"Used for redirect uri\": \"Utilisé pour l'URI de redirection\",\n    \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\": \"Avertissement : vous définissez le type d'utilisateur par défaut sur 'admin'. Cela signifie que les nouveaux utilisateurs bénéficieront du plus haut niveau d'accès !\"\n  },\n  \"OAuthProviderSetup\": {\n    \"Must configure the provider first\": \"Le fournisseur doit d'abord être configuré\",\n    \"Must provide a client ID and secret\": \"Un ID client et un secret doivent être fournis\",\n    \"Display Name\": \"Nom d'affichage\",\n    \"Display Icon\": \"Icône d'affichage\",\n    \"Authorization URL\": \"URL d'autorisation\",\n    \"Client ID\": \"ID client\",\n    \"Client Secret\": \"Secret client\",\n    \"Scopes\": \"Portées\",\n    \"Prompt\": \"Invite\",\n    \"Indicates the type of user interaction that is required.\": \"Indique le type d'interaction utilisateur requis.\",\n    \"Return URL\": \"URL de retour\"\n  },\n  \"EmailAuthSetup\": {\n    \"Email signup\": \"Inscription par e-mail\",\n    \"Must configure the email provider first\": \"Le fournisseur de messagerie doit d'abord être configuré\",\n    \"Signup type\": \"Type d'inscription\",\n    \"With password\": \"Avec mot de passe\",\n    \"Email and password will be required to login. User will have to confirm email\": \"Un e-mail et un mot de passe seront requis pour se connecter. L'utilisateur devra confirmer son e-mail\",\n    \"With magic link\": \"Avec lien magique\",\n    \"Only email is required to login. A magic link will be sent to the user's email\": \"Seul l'e-mail est requis pour se connecter. Un lien magique sera envoyé à l'e-mail de l'utilisateur\",\n    \"Minimum password length\": \"Longueur minimale du mot de passe\",\n    \"Minimum password length. Defaults to 8\": \"Longueur minimale du mot de passe. 8 par défaut\",\n    \"Magic link email configuration\": \"Configuration de l'e-mail du lien magique\",\n    \"Email verification\": \"Vérification de l'e-mail\"\n  },\n  \"EmailSMTPAndTemplateSetup\": {\n    \"Configured\": \"Configuré\",\n    \"Not configured\": \"Non configuré\",\n    \"Users will receive an email with a link/code to verify their email address.\": \"Les utilisateurs recevront un e-mail avec un lien/code pour vérifier leur adresse e-mail.\",\n    \"Email Provider\": \"Fournisseur de messagerie\",\n    \"Template\": \"Modèle\"\n  },\n  \"Connections\": {\n    \"Access granted to \": \"Accès accordé à \",\n    \"Create new connection\": \"Créer une nouvelle connexion\",\n    \"New connection\": \"Nouvelle connexion\",\n    \"Editing state data directly may break functionality. Proceed at your own risk!\": \"La modification directe des données d'état peut altérer les fonctionnalités. Procédez à vos risques et périls !\",\n    \"No connections available/permitted\": \"Aucune connexion disponible/autorisée\",\n    \"Show database names\": \"Afficher les noms des bases de données\",\n    \"Show state connection\": \"Afficher la connexion d'état\"\n  },\n  \"ConnectionActionBar\": {\n    \"Connected. Click to disconnect\": \"Connecté. Cliquez pour vous déconnecter\",\n    \"Not connected\": \"Non connecté\",\n    \"Close all windows\": \"Fermer toutes les fenêtres\",\n    \"Windows have been closed\": \"Les fenêtres ont été fermées\",\n    \"Could not close windows: workspace not found\": \"Impossible de fermer les fenêtres : espace de travail introuvable\",\n    \"Edit connection\": \"Modifier la connexion\"\n  },\n  \"ConnectionServer\": {\n    \"Server info\": \"Informations sur le serveur\",\n    \"Add or create a database\": \"Ajouter ou créer une base de données\",\n    \"Create a database\": \"Créer une base de données\",\n    \"Not allowed to create databases with this user\": \"Non autorisé à créer des bases de données avec cet utilisateur\",\n    \"Create a database in this server\": \"Créer une base de données sur ce serveur\",\n    \"Select a database from this server\": \"Sélectionner une base de données sur ce serveur\",\n    \"Save and connect\": \"Enregistrer et se connecter\",\n    \"Create and connect\": \"Créer et se connecter\",\n    \"Some data is missing\": \"Certaines données sont manquantes\",\n    \"Must fix connection name error\": \"L'erreur de nom de connexion doit être corrigée\",\n    \"New database name\": \"Nouveau nom de la base de données\",\n    \"Name already in use\": \"Nom déjà utilisé\",\n    \"Create demo schema (optional)\": \"Créer un schéma de démonstration (facultatif)\",\n    \"Database\": \"Base de données\",\n    \"Already added to connections\": \"Déjà ajouté aux connexions\"\n  },\n  \"Connection\": {\n    \"Access granted to \": \"Accès accordé à \",\n    \"All Prostgles connection and dashboard data is stored here. Edit at your own risk\": \"Toutes les données de connexion et de tableau de bord de Prostgles sont stockées ici. Modifiez à vos risques et périls\"\n  },\n  \"NewConnectionForm\": {\n    \"Realtime requires table triggers to be created as and when needed.\": \"Le temps réel nécessite que des déclencheurs de table soient créés au besoin.\",\n    \"A \\\"prostgles\\\" schema with necessary metadata will also be created\": \"Un schéma \\\"prostgles\\\" avec les métadonnées nécessaires sera également créé\",\n    \"Realtime\": \"Temps réel\",\n    \"Needed to allow realtime data view. Requires superuser\": \"Nécessaire pour permettre la vue des données en temps réel. Requiert un superutilisateur\",\n    \"Reload schema\": \"Recharger le schéma\",\n    \"Watch schema\": \"Surveiller le schéma\",\n    \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\": \"Actualisera le tableau de bord et l'API en cas de changement de schéma. Requiert un superutilisateur pour une meilleure expérience\",\n    \"Reject unauthorized\": \"Rejeter les non autorisés\",\n    \"Client Key\": \"Clé client\",\n    \"Client Certificate\": \"Certificat client\",\n    \"CA Certificate\": \"Certificat AC\",\n    \"SSL Mode\": \"Mode SSL\",\n    \"Connection timeout (ms)\": \"Délai de connexion (ms)\",\n    \"Schemas\": \"Schémas\",\n    \"More options\": \"Plus d'options\",\n    \"Must connect to see schemas\": \"Doit être connecté pour voir les schémas\",\n    \"You are about to create a new database\": \"Vous êtes sur le point de créer une nouvelle base de données\",\n    \"You are about to clone the current database\": \"Vous êtes sur le point de cloner la base de données actuelle {{currDb}} dans {{newDb}}. Cela fermera toutes les connexions existantes à la base de données actuelle !\",\n    \"Create database\": \"Créer la base de données\",\n    \"Connection type\": \"Type de connexion\",\n    \"Connection name\": \"Nom de la connexion\",\n    \"Socket URL\": \"URL du socket\",\n    \"Socket params (JSON)\": \"Paramètres du socket (JSON)\",\n    \"Connection URI\": \"URI de connexion\",\n    \"Host\": \"Hôte\",\n    \"Port\": \"Port\",\n    \"User\": \"Utilisateur\",\n    \"Password\": \"Mot de passe\",\n    \"Optional\": \"Facultatif\",\n    \"Database\": \"Base de données\",\n    \"Test connection\": \"Tester la connexion\"\n  },\n  \"NewConnection\": {\n    \"Delete connection\": \"Supprimer la connexion\",\n    \"Any related dashboard content will also be deleted\": \"Tout contenu de tableau de bord associé sera également supprimé\",\n    \"Drop database as well\": \"Supprimer également la base de données\",\n    \"You are about to drop\": \"Vous êtes sur le point de supprimer\",\n    \"database\": \"base de données\",\n    \"Ensure data is backed up. This action is not reversible\": \"Assurez-vous que les données sont sauvegardées. Cette action est irréversible\",\n    \"Clone connection\": \"Cloner la connexion\",\n    \"Cloning connection\": \"Clonage de la connexion\",\n    \"Connections\": \"Connexions\",\n    \"Connection not found\": \"Connexion introuvable\",\n    \"Go back to connections\": \"Retour aux connexions\"\n  },\n  \"ConnectionConfig\": {\n    \"Must be admin to access this\": \"Doit être administrateur pour y accéder\",\n    \"Connection details\": \"Détails de la connexion\",\n    \"Status monitor\": \"Moniteur d'état\",\n    \"Access control\": \"Contrôle d'accès\",\n    \"File storage\": \"Stockage de fichiers\",\n    \"Backup/Restore\": \"Sauvegarde/Restauration\",\n    \"API\": \"API\",\n    \"Table config\": \"Configuration de la table\",\n    \"experimental\": \"expérimental\",\n    \"Server-side functions\": \"Fonctions côté serveur\"\n  },\n  \"ChangePassword\": {\n    \"Change password\": \"Changer le mot de passe\",\n    \"Old password\": \"Ancien mot de passe\",\n    \"New password\": \"Nouveau mot de passe\",\n    \"Confirm new password\": \"Confirmer le nouveau mot de passe\",\n    \"Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter\": \"Assurez-vous qu'il comporte au moins 15 caractères OU au moins 8 caractères, dont un chiffre et une lettre minuscule\",\n    \"Enter your old password\": \"Saisissez votre ancien mot de passe\",\n    \"Enter your new password\": \"Saisissez votre nouveau mot de passe\",\n    \"Confirm your new password\": \"Confirmez votre nouveau mot de passe\",\n    \"Passwords do not match\": \"Les mots de passe ne correspondent pas\"\n  },\n  \"Setup2FA\": {\n    \"Enable 2FA\": \"Activer 2FA\",\n    \"Confirm code\": \"Confirmer le code\",\n    \"2FA Enabled\": \"2FA activé\",\n    \"Along with your username and password, you will be asked to verify your identity using the code from authenticator app.\": \"En plus de votre nom d'utilisateur et de votre mot de passe, il vous sera demandé de vérifier votre identité à l'aide du code de l'application d'authentification.\",\n    \"Two-factor authentication\": \"Authentification à deux facteurs\",\n    \"Scan\": \"Scannez\",\n    \" or tap \": \" ou appuyez sur \",\n    \"the image below with the two-factor authentication app on your phone.\": \"l'image ci-dessous avec l'application d'authentification à deux facteurs sur votre téléphone.\",\n    \"If you can't use a QR code you can enter this information manually:\": \"Si vous ne pouvez pas utiliser un code QR, vous pouvez saisir ces informations manuellement :\",\n    \"Base64 secret\": \"Secret Base64\",\n    \"Type\": \"Type\",\n    \"Name\": \"Nom\",\n    \"Save the Recovery code below. It will be used in case you lose access to your authenticator app:\": \"Enregistrez le code de récupération ci-dessous. Il sera utilisé si vous perdez l'accès à votre application d'authentification :\",\n    \"Generate QR Code\": \"Générer un code QR\",\n    \"I can't scan the QR Code\": \"Je ne peux pas scanner le code QR\"\n  },\n  \"Account\": {\n    \"Account details\": \"Détails du compte\",\n    \"Security\": \"Sécurité\",\n    \"API\": \"API\"\n  },\n  \"APIDetailsWs\": {\n    \"Allowed origin specifies which domains can access this app in a cross-origin manner. Sets the Access-Control-Allow-Origin header. Use '*' or a specific URL to allow API access\": \"L'origine autorisée spécifie les domaines qui peuvent accéder à cette application de manière inter-origines. Définit l'en-tête Access-Control-Allow-Origin. Utilisez '*' ou une URL spécifique pour autoriser l'accès à l'API\",\n    \"For testing it is recommended to use \\\"*\\\" as the allowed origin value\": \"Pour les tests, il est recommandé d'utiliser \\\"*\\\" comme valeur d'origine autorisée\",\n    \"Allowed origin\": \"Origine autorisée\",\n    \"Allowed origin is required\": \"L'origine autorisée est requise\",\n    \"Allowed origin not set\": \"Origine autorisée non définie\",\n    \"Websocket API (recommended)\": \"API WebSocket (recommandé)\",\n    \"Realtime Isomorphic API using\": \"API isomorphe en temps réel utilisant\",\n    \"library. End-to-end type-safety between client & server using the database Typescript schema provided below:\": \"bibliothèque. Sécurité des types de bout en bout entre le client et le serveur à l'aide du schéma TypeScript de la base de données fourni ci-dessous :\",\n    \"Examples\": \"Exemples\",\n    \"Download typescript schema\": \"Télécharger le schéma TypeScript\",\n    \"Database schema types\": \"Types de schéma de base de données\"\n  },\n  \"APIDetailsTokens\": {\n    \"Access tokens (\": \"Jetons d'accès (\",\n    \"Provide the same level of access as the current account\": \"Fournir le même niveau d'accès que le compte actuel\",\n    \"Create access token\": \"Créer un jeton d'accès\",\n    \"Create token\": \"Créer un jeton\",\n    \"Expires in\": \"Expire dans\",\n    \"Days\": \"Jours\",\n    \"These token values will not be shown again\": \"Ces valeurs de jeton ne seront plus affichées\",\n    \"Websocket API\": \"API WebSocket\",\n    \"HTTP API (base64)\": \"API HTTP (base64)\",\n    \"Already generated. Close and re-open the popup\": \"Déjà généré. Fermez et rouvrez la fenêtre contextuelle\"\n  },\n  \"APIDetailsHttp\": {\n    \"Provides similar level of access to the Websocket API with the following limitations: no subscriptions, no sync, no file upload\": \"Fournit un niveau d'accès similaire à l'API WebSocket avec les limitations suivantes : pas d'abonnements, pas de synchronisation, pas de téléversement de fichiers\"\n  },\n  \"Sessions\": {\n    \"API tokens\": \"Jetons API\",\n    \"Sessions\": \"Sessions\",\n    \"Last used\": \"Dernière utilisation\",\n    \"Created\": \"Créé\",\n    \"Expires\": \"Expire\",\n    \"Disable\": \"Désactiver\",\n    \"User agent\": \"Agent utilisateur\",\n    \"Active\": \"Actif\",\n    \"Inactive\": \"Inactif\",\n    \"No active \": \"Aucun actif \"\n  },\n  \"TopControls\": {\n    \"Go to Connections\": \"Aller aux connexions\",\n    \"Connections\": \"Connexions\",\n    \"Go to workspace\": \"Aller à l'espace de travail\",\n    \"Menu is pinned\": \"Le menu est épinglé\",\n    \"pinned\": \"épinglé\",\n    \"Configure database connection\": \"Configurer la connexion à la base de données\",\n    \"Go back to connection workspace\": \"Retour à l'espace de travail de la connexion\",\n    \"Not allowed for state database\": \"Non autorisé pour la base de données d'état\",\n    \"Connection configuration\": \"Configuration de la connexion\"\n  },\n  \"Feedback\": {\n    \"Send feedback\": \"Envoyer des commentaires\",\n    \"Leave feedback\": \"Laisser des commentaires\",\n    \"Feedback\": \"Commentaires\",\n    \"Already sent\": \"Déjà envoyé\",\n    \"Must provide some details\": \"Quelques détails doivent être fournis\",\n    \"Feedback was sent! Thanks a lot!\": \"Les commentaires ont été envoyés ! Merci beaucoup !\",\n    \"Email (optional)\": \"E-mail (facultatif)\",\n    \"Details\": \"Détails\",\n    \"Other options\": \"Autres options\",\n    \"open an issue\": \"ouvrir un ticket\",\n    \"or\": \"ou\",\n    \"email us\": \"nous envoyer un e-mail\"\n  },\n  \"AskLLM\": {\n    \"Chat to an AI Assistant to get help with your queries\": \"Discutez avec un assistant IA pour obtenir de l'aide sur vos requêtes\",\n    \"AI assistant not available. Talk to the admin\": \"Assistant IA non disponible. Parlez à l'administrateur\",\n    \"AI Assistant\": \"Demander à l'IA\"\n  },\n  \"AskLLMChatHeader\": {\n    \"Chat\": \"Chat\",\n    \"New chat\": \"Nouveau chat\",\n    \"No prompt found\": \"Aucune invite trouvée\",\n    \"Prompt\": \"Invite\",\n    \"Chat settings\": \"Paramètres du chat\"\n  },\n  \"ConnectionSelector\": { \"Switch database\": \"Changer de base de données\" },\n  \"W_SQLMenu\": {\n    \"SQL Editor settings\": \"Paramètres de l'éditeur SQL\",\n    \"Press\": \"Appuyez sur\",\n    \"to get a list of possible options\": \"pour obtenir une liste d'options possibles\",\n    \"Update options\": \"Mettre à jour les options\",\n    \"Nothing to update\": \"Rien à mettre à jour\",\n    \"Cannot save due to error\": \"Impossible d'enregistrer en raison d'une erreur\",\n    \"Delete query\": \"Supprimer la requête\",\n    \"Open SQL file\": \"Ouvrir un fichier SQL\",\n    \"Open query from file\": \"Ouvrir une requête à partir d'un fichier\",\n    \"Download query\": \"Télécharger la requête\",\n    \"Save query as file\": \"Enregistrer la requête en tant que fichier\",\n    \"Query name\": \"Nom de la requête\",\n    \"Result display mode\": \"Mode d'affichage des résultats\",\n    \"General\": \"Général\",\n    \"Editor options\": \"Options de l'éditeur\",\n    \"Hotkeys\": \"Raccourcis\"\n  },\n  \"SQLHotkeys\": {\n    \"Show autocomplete suggestions\": \"Afficher les suggestions de saisie semi-automatique\",\n    \"Execute current statement\": \"Exécuter l'instruction actuelle\",\n    \"Select current statement\": \"Sélectionner l'instruction actuelle\",\n    \"Show all suggestions\": \"Afficher toutes les suggestions\",\n    \"Show PSQL command queries\": \"Afficher les requêtes de commande PSQL\"\n  },\n  \"W_SQLBottomBar\": {\n    \"Execution mode\": \"Mode d'exécution\",\n    \"Loop query execution\": \"Exécution de la requête en boucle\",\n    \"Cancel this query (Esc)\": \"Annuler cette requête (Échap)\",\n    \"Terminate this query\": \"Terminer cette requête\",\n    \"Query running time\": \"Temps d'exécution de la requête\",\n    \"Stop LISTEN\": \"Arrêter LISTEN\",\n    \"repeat every\": \"répéter toutes les\",\n    \"seconds\": \"secondes\",\n    \"Run query (CTRL+E, ALT+E)\": \"Exécuter la requête (CTRL+E, ALT+E)\",\n    \"Run\": \"Exécuter\",\n    \"Query running time: \": \"Temps d'exécution de la requête : \",\n    \"Show/Hide table\": \"Afficher/Masquer la table\",\n    \"Show/Hide code editor\": \"Afficher/Masquer l'éditeur de code\",\n    \"Show/Hide notices\": \"Afficher/Masquer les avis\",\n    \"Clear value to show all rows\": \"Effacer la valeur pour afficher toutes les lignes\",\n    \"Limit\": \"Limite\"\n  },\n  \"Window\": {\n    \"Open menu\": \"Ouvrir le menu\",\n    \"Menu\": \"Menu\",\n    \"Collapse chart\": \"Réduire le graphique\",\n    \"Detach chart\": \"Détacher le graphique\",\n    \"Close chart\": \"Fermer le graphique\",\n    \"Chart options\": \"Options du graphique\"\n  },\n  \"AddChartMenu\": {\n    \"Layer already added\": \"Couche déjà ajoutée\",\n    \"No\": \"Non\",\n    \"No {{chartColumnDataType}} columns available\": \"Aucune colonne {{chartColumnDataType}} disponible\"\n  },\n  \"DashboardMenuHeader\": {\n    \"Opens SQL Query editor\": \"Ouvre l'éditeur de requêtes SQL\",\n    \"SQL Editor\": \"Éditeur SQL\",\n    \"Show quick search menu (CTRL + P)\": \"Afficher le menu de recherche rapide (CTRL + P)\",\n    \"Cannot be used in a low width device\": \"Ne peut pas être utilisé sur un appareil de faible largeur\",\n    \"Pin/Unpin\": \"Épingler/Désépingler\"\n  },\n  \"W_QuickMenu\": {\n    \"Show/Hide filtering\": \"Afficher/Masquer le filtrage\",\n    \"Restore minimised charts\": \"Restaurer les graphiques minimisés\",\n    \"Cross filter tables\": \"Tables de filtres croisés\",\n    \"Add chart\": \"Ajouter un graphique\"\n  },\n  \"AddColumnMenu\": {\n    \"Add Computed Field\": \"Ajouter un champ calculé\",\n    \"Show a computed column\": \"Afficher une colonne calculée\",\n    \"Add Linked Data\": \"Ajouter des données liées\",\n    \"Show data from a related table\": \"Afficher les données d'une table associée\",\n    \"Create New Column\": \"Créer une nouvelle colonne\",\n    \"Create a new column in this table\": \"Créer une nouvelle colonne dans cette table\",\n    \"Create New File Column\": \"Créer une nouvelle colonne de fichier\",\n    \"Create a new file column in this table\": \"Créer une nouvelle colonne de fichier dans cette table\",\n    \"Not enough privileges\": \"Privilèges insuffisants\",\n    \"This is a view. Cannot create columns, must recreate\": \"Ceci est une vue. Impossible de créer des colonnes, doit être recréé\",\n    \"Add column\": \"Ajouter une colonne\",\n    \"New Field\": \"Nouveau champ\",\n    \"No foreign keys to/from this table\": \"Aucune clé étrangère vers/depuis cette table\",\n    \"Not allowed for nested columns\": \"Non autorisé pour les colonnes imbriquées\",\n    \"Aggregates and/or Count not allowed with linked \": \"Les agrégats et/ou le comptage ne sont pas autorisés avec \",\n    \"Create new column\": \"Créer une nouvelle colonne\",\n    \"Add Referenced/Linked Fields\": \"Ajouter des champs référencés/liés\"\n  },\n  \"CreateColumn\": {\n    \"Create column query\": \"Créer une requête de colonne\",\n    \"Show create column query\": \"Afficher la requête de création de colonne\",\n    \"New column name missing\": \"Nom de la nouvelle colonne manquant\",\n    \"Data type missing\": \"Type de données manquant\"\n  },\n  \"W_Table\": { \"Insert row\": \"Insérer une ligne\" },\n  \"LinkedColumn\": {\n    \"Row\": \"Ligne\",\n    \"Column\": \"Colonne\",\n    \"No headers\": \"Aucun en-tête\",\n    \"Column name already used. Change to another\": \"Nom de colonne déjà utilisé. Changez-le\",\n    \"Must select columns\": \"Les colonnes doivent être sélectionnées\",\n    \"Must select a table\": \"Une table doit être sélectionnée\",\n    \"Join to and show data from tables that are related through a\": \"Joindre et afficher les données des tables qui sont liées par un(e)\",\n    \"Column label\": \"Étiquette de colonne\",\n    \"More options\": \"Plus d'options\",\n    \"Layout\": \"Mise en page\",\n    \"Must disable chart first\": \"Le graphique doit d'abord être désactivé\",\n    \"Join type\": \"Type de jointure\",\n    \"Added!\": \"Ajouté !\"\n  }\n}\n"
  },
  {
    "path": "client/src/i18n/translations/hi.json",
    "content": "{\n  \"common\": {\n    \"Remove\": \"हटाएं\",\n    \"Add\": \"जोड़ें\",\n    \"Language\": \"भाषा\",\n    \"Generate\": \"उत्पन्न करें\",\n    \"Next\": \"अगला\",\n    \"Login\": \"लॉग इन करें\",\n    \"Logout\": \"लॉग आउट करें\",\n    \"Confirm\": \"पुष्टि करें\",\n    \"Register\": \"रजिस्टर करें\",\n    \"Close\": \"बंद करें\",\n    \"Options\": \"विकल्प\",\n    \"Not permitted\": \"अनुमति नहीं है\",\n    \"Something went wrong\": \"कुछ गलत हो गया\",\n    \"Configure\": \"कॉन्फ़िगर करें\",\n    \"Workspaces\": \"कार्यक्षेत्र\",\n    \"Theme\": \"थीम\",\n    \"Enabled\": \"सक्षम\",\n    \"Cancel\": \"रद्द करें\",\n    \"Save\": \"सहेजें\",\n    \"Copied!\": \"कॉपी किया गया!\",\n    \"Test and Save\": \"परीक्षण करें और सहेजें\",\n    \"No changes\": \"कोई बदलाव नहीं\",\n    \"Run\": \"चलाएं\",\n    \"Delete\": \"हटाएं\",\n    \"Clone\": \"क्लोन\",\n    \"Update\": \"अपडेट करें\",\n    \"Related data\": \"संबंधित डेटा\",\n    \"Create\": \"बनाएं\",\n    \"Already exists. Chose another name\": \"पहले से मौजूद है। दूसरा नाम चुनें\",\n    \"Nothing to update\": \"अपडेट करने के लिए कुछ भी नहीं है\",\n    \"You do not have sufficient privileges to access this\": \"आपके पास इस तक पहुंचने के लिए पर्याप्त विशेषाधिकार नहीं हैं\",\n    \"Copy to clipboard\": \"क्लिपबोर्ड पर कॉपी करें\",\n    \"Send\": \"भेजें\",\n    \"Stop\": \"रोकें\",\n    \"Toggle fullscreen\": \"पूर्ण स्क्रीन टॉगल करें\",\n    \"experimental\": \"प्रायोगिक\",\n    \"Stop recording\": \"रिकॉर्डिंग रोकें\",\n    \"Speech to text\": \"वाणी से पाठ\",\n    \"Record audio\": \"ऑडियो रिकॉर्ड करें\"\n  },\n  \"App\": {\n    \"Connections\": \"कनेक्शन\",\n    \"Users\": \"उपयोगकर्ता\",\n    \"Server settings\": \"सर्वर सेटिंग्स\",\n    \"Security issue\": \"सुरक्षा समस्या\",\n    \"Settings\": \"सेटिंग्स\",\n    \"Reconnecting...\": \"पुनः कनेक्ट हो रहा है...\"\n  },\n  \"Users\": {\n    \"Prostgles UI users\": \"Prostgles UI उपयोगकर्ता\"\n  },\n  \"AccountMenu\": {\n    \"Account\": \"खाता\"\n  },\n  \"ServerSettings\": {\n    \"Security\": \"सुरक्षा\",\n    \"Validate a CIDR\": \"CIDR को मान्य करें\",\n    \"Enter a value to see the allowed IP ranges\": \"अनुमत IP श्रेणियां देखने के लिए एक मान दर्ज करें\",\n    \"Add your current IP\": \"अपना वर्तमान IP जोड़ें\",\n    \"Allowed IP\": \"अनुमत IP\",\n    \"From IP\": \"IP से\",\n    \"To IP\": \"IP तक\",\n    \"Authentication\": \"प्रमाणीकरण\",\n    \"Cloud credentials\": \"क्लाउड क्रेडेंशियल\",\n    \"No cloud credentials. Credentials can be added for file storage\": \"कोई क्लाउड क्रेडेंशियल नहीं। फ़ाइल संग्रहण के लिए क्रेडेंशियल जोड़े जा सकते हैं\",\n    \"The default user type assigned to new users. Defaults to 'default'\": \"नए उपयोगकर्ताओं को सौंपा गया डिफ़ॉल्ट उपयोगकर्ता प्रकार। 'डिफ़ॉल्ट' पर सेट है\",\n    \"Warning: The default user type is set to 'admin'. This will allow new users to access all resources.\": \"चेतावनी: डिफ़ॉल्ट उपयोगकर्ता प्रकार 'व्यवस्थापक' पर सेट है। यह नए उपयोगकर्ताओं को सभी संसाधनों तक पहुंचने की अनुमति देगा।\"\n  },\n  \"AuthProviderSetup\": {\n    \"Default user type\": \"डिफ़ॉल्ट उपयोगकर्ता प्रकार\",\n    \"Website URL\": \"वेबसाइट URL\",\n    \"Used for redirect uri\": \"रीडायरेक्ट यूआरआई के लिए प्रयुक्त\",\n    \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\": \"चेतावनी: आप डिफ़ॉल्ट उपयोगकर्ता प्रकार को 'व्यवस्थापक' पर सेट कर रहे हैं। इसका मतलब है कि नए उपयोगकर्ताओं को उच्चतम स्तर की पहुंच प्रदान की जाएगी!\"\n  },\n  \"OAuthProviderSetup\": {\n    \"Must configure the provider first\": \"पहले प्रदाता को कॉन्फ़िगर करना होगा\",\n    \"Must provide a client ID and secret\": \"एक क्लाइंट आईडी और सीक्रेट प्रदान करना होगा\",\n    \"Display Name\": \"प्रदर्शित नाम\",\n    \"Display Icon\": \"प्रदर्शन आइकन\",\n    \"Authorization URL\": \"प्राधिकरण URL\",\n    \"Client ID\": \"क्लाइंट आईडी\",\n    \"Client Secret\": \"क्लाइंट सीक्रेट\",\n    \"Scopes\": \"स्कोप\",\n    \"Prompt\": \"प्रॉम्प्ट\",\n    \"Indicates the type of user interaction that is required.\": \"इंगित करता है कि किस प्रकार की उपयोगकर्ता सहभागिता आवश्यक है।\",\n    \"Return URL\": \"रिटर्न URL\"\n  },\n  \"EmailAuthSetup\": {\n    \"Email signup\": \"ईमेल साइनअप\",\n    \"Must configure the email provider first\": \"पहले ईमेल प्रदाता को कॉन्फ़िगर करना होगा\",\n    \"Signup type\": \"साइनअप प्रकार\",\n    \"With password\": \"पासवर्ड के साथ\",\n    \"Email and password will be required to login. User will have to confirm email\": \"लॉग इन करने के लिए ईमेल और पासवर्ड की आवश्यकता होगी। उपयोगकर्ता को ईमेल की पुष्टि करनी होगी\",\n    \"With magic link\": \"मैजिक लिंक के साथ\",\n    \"Only email is required to login. A magic link will be sent to the user's email\": \"लॉग इन करने के लिए केवल ईमेल की आवश्यकता है। उपयोगकर्ता के ईमेल पर एक मैजिक लिंक भेजा जाएगा\",\n    \"Minimum password length\": \"न्यूनतम पासवर्ड लंबाई\",\n    \"Minimum password length. Defaults to 8\": \"न्यूनतम पासवर्ड लंबाई। डिफ़ॉल्ट रूप से 8\",\n    \"Magic link email configuration\": \"मैजिक लिंक ईमेल कॉन्फ़िगरेशन\",\n    \"Email verification\": \"ईमेल सत्यापन\"\n  },\n  \"EmailSMTPAndTemplateSetup\": {\n    \"Configured\": \"कॉन्फ़िगर किया गया\",\n    \"Not configured\": \"कॉन्फ़िगर नहीं किया गया\",\n    \"Users will receive an email with a link/code to verify their email address.\": \"उपयोगकर्ताओं को उनके ईमेल पते को सत्यापित करने के लिए एक लिंक/कोड वाला ईमेल प्राप्त होगा।\",\n    \"Email Provider\": \"ईमेल प्रदाता\",\n    \"Template\": \"टेम्पलेट\"\n  },\n  \"Connections\": {\n    \"Access granted to \": \"पहुंच स्वीकृत \",\n    \"Create new connection\": \"नया कनेक्शन बनाएँ\",\n    \"New connection\": \"नया कनेक्शन\",\n    \"Editing state data directly may break functionality. Proceed at your own risk!\": \"राज्य डेटा को सीधे संपादित करने से कार्यक्षमता टूट सकती है। अपने जोखिम पर आगे बढ़ें!\",\n    \"No connections available/permitted\": \"कोई कनेक्शन उपलब्ध/अनुमति नहीं है\",\n    \"Show database names\": \"डेटाबेस नाम दिखाएं\",\n    \"Show state connection\": \"राज्य कनेक्शन दिखाएं\"\n  },\n  \"ConnectionActionBar\": {\n    \"Connected. Click to disconnect\": \"जुड़ा हुआ। डिस्कनेक्ट करने के लिए क्लिक करें\",\n    \"Not connected\": \"जुड़े नहीं\",\n    \"Close all windows\": \"सभी विंडो बंद करें\",\n    \"Windows have been closed\": \"विंडो बंद कर दी गई हैं\",\n    \"Could not close windows: workspace not found\": \"विंडो बंद नहीं कर सका: कार्यक्षेत्र नहीं मिला\",\n    \"Edit connection\": \"कनेक्शन संपादित करें\"\n  },\n  \"ConnectionServer\": {\n    \"Server info\": \"सर्वर जानकारी\",\n    \"Add or create a database\": \"डेटाबेस जोड़ें या बनाएं\",\n    \"Create a database\": \"डेटाबेस बनाएं\",\n    \"Not allowed to create databases with this user\": \"इस उपयोगकर्ता के साथ डेटाबेस बनाने की अनुमति नहीं है\",\n    \"Create a database in this server\": \"इस सर्वर में एक डेटाबेस बनाएं\",\n    \"Select a database from this server\": \"इस सर्वर से एक डेटाबेस चुनें\",\n    \"Save and connect\": \"सहेजें और कनेक्ट करें\",\n    \"Create and connect\": \"बनाएं और कनेक्ट करें\",\n    \"Some data is missing\": \"कुछ डेटा गुम है\",\n    \"Must fix connection name error\": \"कनेक्शन नाम त्रुटि को ठीक करना होगा\",\n    \"New database name\": \"नया डेटाबेस नाम\",\n    \"Name already in use\": \"नाम पहले से उपयोग में है\",\n    \"Create demo schema (optional)\": \"डेमो स्कीमा बनाएं (वैकल्पिक)\",\n    \"Database\": \"डेटाबेस\",\n    \"Already added to connections\": \"पहले से ही कनेक्शन में जोड़ा गया\"\n  },\n  \"Connection\": {\n    \"Access granted to \": \"पहुंच स्वीकृत \",\n    \"All Prostgles connection and dashboard data is stored here. Edit at your own risk\": \"सभी Prostgles कनेक्शन और डैशबोर्ड डेटा यहाँ संग्रहीत हैं। अपने जोखिम पर संपादित करें\"\n  },\n  \"NewConnectionForm\": {\n    \"Realtime requires table triggers to be created as and when needed.\": \"रियलटाइम के लिए आवश्यक है कि टेबल ट्रिगर आवश्यकतानुसार बनाए जाएं।\",\n    \"A \\\"prostgles\\\" schema with necessary metadata will also be created\": \"आवश्यक मेटाडेटा के साथ एक \\\"prostgles\\\" स्कीमा भी बनाया जाएगा\",\n    \"Realtime\": \"रियलटाइम\",\n    \"Needed to allow realtime data view. Requires superuser\": \"रियलटाइम डेटा दृश्य की अनुमति देने के लिए आवश्यक। सुपरयूज़र की आवश्यकता है\",\n    \"Reload schema\": \"स्कीमा पुनः लोड करें\",\n    \"Watch schema\": \"स्कीमा देखें\",\n    \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\": \"स्कीमा परिवर्तन पर डैशबोर्ड और API को रीफ़्रेश करेगा। सर्वोत्तम अनुभव के लिए सुपरयूज़र की आवश्यकता है\",\n    \"Reject unauthorized\": \"अनधिकृत को अस्वीकार करें\",\n    \"Client Key\": \"क्लाइंट कुंजी\",\n    \"Client Certificate\": \"क्लाइंट प्रमाणपत्र\",\n    \"CA Certificate\": \"CA प्रमाणपत्र\",\n    \"SSL Mode\": \"SSL मोड\",\n    \"Connection timeout (ms)\": \"कनेक्शन टाइमआउट (ms)\",\n    \"Schemas\": \"स्कीमा सूची\",\n    \"More options\": \"अधिक विकल्प\",\n    \"Must connect to see schemas\": \"स्कीमा देखने के लिए कनेक्ट होना चाहिए\",\n    \"You are about to create a new database\": \"आप एक नया डेटाबेस बनाने जा रहे हैं\",\n    \"You are about to clone the current database\": \"आप वर्तमान डेटाबेस {{currDb}} को {{newDb}} में क्लोन करने वाले हैं। इससे वर्तमान डेटाबेस के सभी मौजूदा कनेक्शन बंद हो जाएंगे!\",\n    \"Create database\": \"डेटाबेस बनाएं\",\n    \"Connection type\": \"कनेक्शन प्रकार\",\n    \"Connection name\": \"कनेक्शन का नाम\",\n    \"Socket URL\": \"सॉकेट URL\",\n    \"Socket params (JSON)\": \"सॉकेट पैरामीटर (JSON)\",\n    \"Connection URI\": \"कनेक्शन URI\",\n    \"Host\": \"होस्ट\",\n    \"Port\": \"पोर्ट\",\n    \"User\": \"उपयोगकर्ता\",\n    \"Password\": \"पासवर्ड\",\n    \"Optional\": \"वैकल्पिक\",\n    \"Database\": \"डेटाबेस\",\n    \"Test connection\": \"कनेक्शन का परीक्षण करें\"\n  },\n  \"NewConnection\": {\n    \"Delete connection\": \"कनेक्शन हटाएं\",\n    \"Any related dashboard content will also be deleted\": \"कोई भी संबंधित डैशबोर्ड सामग्री भी हटा दी जाएगी\",\n    \"Drop database as well\": \"डेटाबेस को भी हटाएं\",\n    \"You are about to drop\": \"आप हटाने वाले हैं\",\n    \"database\": \"डेटाबेस\",\n    \"Ensure data is backed up. This action is not reversible\": \"सुनिश्चित करें कि डेटा का बैकअप लिया गया है। यह क्रिया प्रतिवर्ती नहीं है\",\n    \"Clone connection\": \"कनेक्शन क्लोन करें\",\n    \"Cloning connection\": \"कनेक्शन क्लोनिंग\",\n    \"Connections\": \"कनेक्शन\",\n    \"Connection not found\": \"कनेक्शन नहीं मिला\",\n    \"Go back to connections\": \"कनेक्शन पर वापस जाएं\"\n  },\n  \"ConnectionConfig\": {\n    \"Must be admin to access this\": \"इस तक पहुंचने के लिए व्यवस्थापक होना चाहिए\",\n    \"Connection details\": \"कनेक्शन विवरण\",\n    \"Status monitor\": \"स्थिति मॉनिटर\",\n    \"Access control\": \"पहुंच नियंत्रण\",\n    \"File storage\": \"फ़ाइल संग्रहण\",\n    \"Backup/Restore\": \"बैकअप/पुनर्स्थापना\",\n    \"API\": \"API\",\n    \"Table config\": \"टेबल कॉन्फ़िगरेशन\",\n    \"experimental\": \"प्रायोगिक\",\n    \"Server-side functions\": \"सर्वर-साइड फ़ंक्शन\"\n  },\n  \"ChangePassword\": {\n    \"Change password\": \"पासवर्ड बदलें\",\n    \"Old password\": \"पुराना पासवर्ड\",\n    \"New password\": \"नया पासवर्ड\",\n    \"Confirm new password\": \"नए पासवर्ड की पुष्टि करें\",\n    \"Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter\": \"सुनिश्चित करें कि यह कम से कम 15 वर्णों का है या कम से कम 8 वर्णों का है जिसमें एक संख्या और एक छोटा अक्षर शामिल है\",\n    \"Enter your old password\": \"अपना पुराना पासवर्ड दर्ज करें\",\n    \"Enter your new password\": \"अपना नया पासवर्ड दर्ज करें\",\n    \"Confirm your new password\": \"अपने नए पासवर्ड की पुष्टि करें\",\n    \"Passwords do not match\": \"पासवर्ड मेल नहीं खाते\"\n  },\n  \"Setup2FA\": {\n    \"Enable 2FA\": \"2FA सक्षम करें\",\n    \"Confirm code\": \"कोड की पुष्टि करें\",\n    \"2FA Enabled\": \"2FA सक्षम\",\n    \"Along with your username and password, you will be asked to verify your identity using the code from authenticator app.\": \"आपके उपयोगकर्ता नाम और पासवर्ड के साथ, आपको प्रमाणक ऐप से कोड का उपयोग करके अपनी पहचान सत्यापित करने के लिए कहा जाएगा।\",\n    \"Two-factor authentication\": \"दो-कारक प्रमाणीकरण\",\n    \"Scan\": \"स्कैन करें\",\n    \" or tap \": \" या टैप करें \",\n    \"the image below with the two-factor authentication app on your phone.\": \"अपने फ़ोन पर दो-कारक प्रमाणीकरण ऐप के साथ नीचे दी गई छवि।\",\n    \"If you can't use a QR code you can enter this information manually:\": \"यदि आप QR कोड का उपयोग नहीं कर सकते हैं तो आप इस जानकारी को मैन्युअल रूप से दर्ज कर सकते हैं:\",\n    \"Base64 secret\": \"Base64 सीक्रेट\",\n    \"Type\": \"प्रकार\",\n    \"Name\": \"नाम\",\n    \"Save the Recovery code below. It will be used in case you lose access to your authenticator app:\": \"नीचे दिए गए रिकवरी कोड को सेव करें। यदि आप अपने प्रमाणक ऐप तक पहुंच खो देते हैं तो इसका उपयोग किया जाएगा:\",\n    \"Generate QR Code\": \"QR कोड जनरेट करें\",\n    \"I can't scan the QR Code\": \"मैं QR कोड स्कैन नहीं कर सकता\"\n  },\n  \"Account\": {\n    \"Account details\": \"खाते का विवरण\",\n    \"Security\": \"सुरक्षा\",\n    \"API\": \"API\"\n  },\n  \"APIDetailsWs\": {\n    \"Allowed origin specifies which domains can access this app in a cross-origin manner. Sets the Access-Control-Allow-Origin header. Use '' or a specific URL to allow API access\": \"अनुमत मूल निर्दिष्ट करता है कि कौन से डोमेन इस ऐप को क्रॉस-ऑरिजिन तरीके से एक्सेस कर सकते हैं। Access-Control-Allow-Origin हेडर सेट करता है। API एक्सेस की अनुमति देने के लिए '' या एक विशिष्ट URL का उपयोग करें\",\n    \"For testing it is recommended to use \\\"*\\\" as the allowed origin value\": \"परीक्षण के लिए यह अनुशंसा की जाती है कि अनुमत मूल मान के रूप में \\\"\\\" का उपयोग करें\",\n    \"Allowed origin\": \"अनुमत मूल\",\n    \"Allowed origin is required\": \"अनुमत मूल आवश्यक है\",\n    \"Allowed origin not set\": \"अनुमत मूल सेट नहीं है\",\n    \"Websocket API (recommended)\": \"वेबसॉकेट API (अनुशंसित)\",\n    \"Realtime Isomorphic API using\": \"रियलटाइम आइसोमॉर्फिक API का उपयोग करके\",\n    \"library. End-to-end type-safety between client & server using the database Typescript schema provided below:\": \"लाइब्रेरी। नीचे दिए गए डेटाबेस टाइपस्क्रिप्ट स्कीमा का उपयोग करके क्लाइंट और सर्वर के बीच एंड-टू-एंड टाइप-सेफ्टी:\",\n    \"Examples\": \"उदाहरण\",\n    \"Download typescript schema\": \"टाइपस्क्रिप्ट स्कीमा डाउनलोड करें\",\n    \"Database schema types\": \"डेटाबेस स्कीमा प्रकार\"\n  },\n  \"APIDetailsTokens\": {\n    \"Access tokens (\": \"एक्सेस टोकन (\",\n    \"Provide the same level of access as the current account\": \"वर्तमान खाते के समान स्तर की पहुंच प्रदान करें\",\n    \"Create access token\": \"एक्सेस टोकन बनाएं\",\n    \"Create token\": \"टोकन बनाएं\",\n    \"Expires in\": \"समाप्त होने का समय\",\n    \"Days\": \"दिन\",\n    \"These token values will not be shown again\": \"ये टोकन मान दोबारा नहीं दिखाए जाएंगे\",\n    \"Websocket API\": \"वेबसॉकेट API\",\n    \"HTTP API (base64)\": \"HTTP API (base64)\",\n    \"Already generated. Close and re-open the popup\": \"पहले ही जेनरेट हो चुका है। पॉपअप बंद करें और दोबारा खोलें\"\n  },\n  \"APIDetailsHttp\": {\n    \"Provides similar level of access to the Websocket API with the following limitations: no subscriptions, no sync, no file upload\": \"वेबसॉकेट API के समान स्तर की पहुंच प्रदान करता है, निम्नलिखित सीमाओं के साथ: कोई सदस्यता नहीं, कोई सिंक नहीं, कोई फ़ाइल अपलोड नहीं\"\n  },\n  \"Sessions\": {\n    \"API tokens\": \"API टोकन\",\n    \"Sessions\": \"सत्र\",\n    \"Last used\": \"अंतिम बार उपयोग किया गया\",\n    \"Created\": \"बनाया गया\",\n    \"Expires\": \"समाप्त होता है\",\n    \"Disable\": \"अक्षम करें\",\n    \"User agent\": \"उपयोगकर्ता एजेंट\",\n    \"Active\": \"सक्रिय\",\n    \"Inactive\": \"निष्क्रिय\",\n    \"No active \": \"कोई सक्रिय नहीं \"\n  },\n  \"TopControls\": {\n    \"Go to Connections\": \"कनेक्शन पर जाएं\",\n    \"Connections\": \"कनेक्शन\",\n    \"Go to workspace\": \"कार्यक्षेत्र पर जाएं\",\n    \"Menu is pinned\": \"मेनू पिन किया गया है\",\n    \"pinned\": \"पिन किया हुआ\",\n    \"Configure database connection\": \"डेटाबेस कनेक्शन कॉन्फ़िगर करें\",\n    \"Go back to connection workspace\": \"कनेक्शन कार्यक्षेत्र पर वापस जाएं\",\n    \"Not allowed for state database\": \"राज्य डेटाबेस के लिए अनुमति नहीं है\",\n    \"Connection configuration\": \"कनेक्शन कॉन्फ़िगरेशन\"\n  },\n  \"Feedback\": {\n    \"Send feedback\": \"प्रतिक्रिया भेजें\",\n    \"Leave feedback\": \"प्रतिक्रिया छोड़ें\",\n    \"Feedback\": \"प्रतिक्रिया\",\n    \"Already sent\": \"पहले ही भेजा जा चुका है\",\n    \"Must provide some details\": \"कुछ विवरण प्रदान करना होगा\",\n    \"Feedback was sent! Thanks a lot!\": \"प्रतिक्रिया भेजी गई! बहुत-बहुत धन्यवाद!\",\n    \"Email (optional)\": \"ईमेल (वैकल्पिक)\",\n    \"Details\": \"विवरण\",\n    \"Other options\": \"अन्य विकल्प\",\n    \"open an issue\": \"एक मुद्दा खोलें\",\n    \"or\": \"या\",\n    \"email us\": \"हमें ईमेल करें\"\n  },\n  \"AskLLM\": {\n    \"Chat to an AI Assistant to get help with your queries\": \"अपनी क्वेरी में सहायता प्राप्त करने के लिए AI सहायक से चैट करें\",\n    \"AI assistant not available. Talk to the admin\": \"AI सहायक उपलब्ध नहीं है। व्यवस्थापक से बात करें\",\n    \"AI Assistant\": \"AI से पूछें\"\n  },\n  \"AskLLMChatHeader\": {\n    \"Chat\": \"बात करना\",\n    \"New chat\": \"नई चैट\",\n    \"No prompt found\": \"कोई संकेत नहीं मिला\",\n    \"Prompt\": \"संकेत\",\n    \"Chat settings\": \"चैट सेटिंग्स\"\n  },\n  \"ConnectionSelector\": {\n    \"Switch database\": \"डेटाबेस स्विच करें\"\n  },\n  \"W_SQLMenu\": {\n    \"SQL Editor settings\": \"SQL संपादक सेटिंग्स\",\n    \"Press\": \"दबाएँ\",\n    \"to get a list of possible options\": \"संभावित विकल्पों की सूची प्राप्त करने के लिए\",\n    \"Update options\": \"विकल्प अपडेट करें\",\n    \"Nothing to update\": \"अपडेट करने के लिए कुछ भी नहीं\",\n    \"Cannot save due to error\": \"त्रुटि के कारण सहेज नहीं सकता\",\n    \"Delete query\": \"क्वेरी हटाएं\",\n    \"Open SQL file\": \"SQL फ़ाइल खोलें\",\n    \"Open query from file\": \"फ़ाइल से क्वेरी खोलें\",\n    \"Download query\": \"क्वेरी डाउनलोड करें\",\n    \"Save query as file\": \"क्वेरी को फ़ाइल के रूप में सहेजें\",\n    \"Query name\": \"क्वेरी का नाम\",\n    \"Result display mode\": \"परिणाम प्रदर्शन मोड\",\n    \"General\": \"सामान्य\",\n    \"Editor options\": \"संपादक विकल्प\",\n    \"Hotkeys\": \"हॉटकी\"\n  },\n  \"SQLHotkeys\": {\n    \"Show autocomplete suggestions\": \"स्वतः पूर्ण सुझाव दिखाएं\",\n    \"Execute current statement\": \"वर्तमान कथन निष्पादित करें\",\n    \"Select current statement\": \"वर्तमान कथन का चयन करें\",\n    \"Show all suggestions\": \"सभी सुझाव दिखाएं\",\n    \"Show PSQL command queries\": \"PSQL कमांड क्वेरी दिखाएं\"\n  },\n  \"W_SQLBottomBar\": {\n    \"Execution mode\": \"निष्पादन मोड\",\n    \"Loop query execution\": \"लूप क्वेरी निष्पादन\",\n    \"Cancel this query (Esc)\": \"इस क्वेरी को रद्द करें (Esc)\",\n    \"Terminate this query\": \"इस क्वेरी को समाप्त करें\",\n    \"Query running time\": \"क्वेरी चलने का समय\",\n    \"Stop LISTEN\": \"सुनना बंद करें\",\n    \"repeat every\": \"हर बार दोहराएं\",\n    \"seconds\": \"सेकंड\",\n    \"Run query (CTRL+E, ALT+E)\": \"क्वेरी चलाएं (CTRL+E, ALT+E)\",\n    \"Run\": \"चलाएं\",\n    \"Query running time: \": \"क्वेरी चलने का समय: \",\n    \"Show/Hide table\": \"टेबल दिखाएं/छिपाएं\",\n    \"Show/Hide code editor\": \"कोड संपादक दिखाएं/छिपाएं\",\n    \"Show/Hide notices\": \"नोटिस दिखाएं/छिपाएं\",\n    \"Clear value to show all rows\": \"सभी पंक्तियाँ दिखाने के लिए मान साफ़ करें\",\n    \"Limit\": \"सीमा\"\n  },\n  \"Window\": {\n    \"Open menu\": \"मेनू खोलें\",\n    \"Menu\": \"मेनू\",\n    \"Collapse chart\": \"चार्ट संक्षिप्त करें\",\n    \"Detach chart\": \"चार्ट अलग करें\",\n    \"Close chart\": \"चार्ट बंद करें\",\n    \"Chart options\": \"चार्ट विकल्प\"\n  },\n  \"AddChartMenu\": {\n    \"Layer already added\": \"लेयर पहले ही जोड़ी जा चुकी है\",\n    \"No\": \"नहीं\",\n    \"No {{chartColumnDataType}} columns available\": \"कोई {{chartColumnDataType}} कॉलम उपलब्ध नहीं है\"\n  },\n  \"DashboardMenuHeader\": {\n    \"Opens SQL Query editor\": \"SQL क्वेरी संपादक खोलता है\",\n    \"SQL Editor\": \"SQL संपादक\",\n    \"Show quick search menu (CTRL + P)\": \"त्वरित खोज मेनू दिखाएं (CTRL + P)\",\n    \"Cannot be used in a low width device\": \"कम चौड़ाई वाले उपकरण में उपयोग नहीं किया जा सकता\",\n    \"Pin/Unpin\": \"पिन/अनपिन\"\n  },\n  \"W_QuickMenu\": {\n    \"Show/Hide filtering\": \"फ़िल्टरिंग दिखाएं/छिपाएं\",\n    \"Restore minimised charts\": \"न्यूनतम चार्ट पुनर्स्थापित करें\",\n    \"Cross filter tables\": \"क्रॉस फ़िल्टर टेबल\",\n    \"Add chart\": \"चार्ट जोड़ें\"\n  },\n  \"AddColumnMenu\": {\n    \"Add Computed Field\": \"परिकलित फ़ील्ड जोड़ें\",\n    \"Show a computed column\": \"एक परिकलित कॉलम दिखाएं\",\n    \"Add Linked Data\": \"लिंक्ड डेटा जोड़ें\",\n    \"Show data from a related table\": \"संबंधित टेबल से डेटा दिखाएं\",\n    \"Create New Column\": \"नया कॉलम बनाएं\",\n    \"Create a new column in this table\": \"इस टेबल में एक नया कॉलम बनाएं\",\n    \"Create New File Column\": \"नया फ़ाइल कॉलम बनाएं\",\n    \"Create a new file column in this table\": \"इस टेबल में एक नया फ़ाइल कॉलम बनाएं\",\n    \"Not enough privileges\": \"पर्याप्त विशेषाधिकार नहीं\",\n    \"This is a view. Cannot create columns, must recreate\": \"यह एक दृश्य है। कॉलम नहीं बना सकता, पुनः बनाना होगा\",\n    \"Add column\": \"कॉलम जोड़ें\",\n    \"New Field\": \"नया फ़ील्ड\",\n    \"No foreign keys to/from this table\": \"इस टेबल से/तक कोई विदेशी कुंजी नहीं\",\n    \"Not allowed for nested columns\": \"नेस्टेड कॉलम के लिए अनुमति नहीं है\",\n    \"Aggregates and/or Count not allowed with linked \": \"लिंक्ड के साथ एग्रीगेट्स और/या गणना की अनुमति नहीं है \",\n    \"Create new column\": \"नया कॉलम बनाएं\",\n    \"Add Referenced/Linked Fields\": \"संदर्भित/लिंक्ड फ़ील्ड जोड़ें\"\n  },\n  \"CreateColumn\": {\n    \"Create column query\": \"कॉलम क्वेरी बनाएं\",\n    \"Show create column query\": \"कॉलम क्वेरी बनाएं दिखाएं\",\n    \"New column name missing\": \"नया कॉलम नाम गुम है\",\n    \"Data type missing\": \"डेटा प्रकार गुम है\"\n  },\n  \"W_Table\": {\n    \"Insert row\": \"पंक्ति डालें\"\n  },\n  \"LinkedColumn\": {\n    \"Row\": \"पंक्ति\",\n    \"Column\": \"कॉलम\",\n    \"No headers\": \"कोई हेडर नहीं\",\n    \"Column name already used. Change to another\": \"कॉलम नाम पहले से उपयोग में है। दूसरे में बदलें\",\n    \"Must select columns\": \"कॉलम का चयन करना होगा\",\n    \"Must select a table\": \"एक टेबल का चयन करना होगा\",\n    \"Join to and show data from tables that are related through a\": \"से जुड़ें और उन तालिकाओं से डेटा दिखाएं जो a . के माध्यम से संबंधित हैं\",\n    \"Column label\": \"कॉलम लेबल\",\n    \"More options\": \"अधिक विकल्प\",\n    \"Layout\": \"ख़ाका\",\n    \"Must disable chart first\": \"पहले चार्ट को अक्षम करना होगा\",\n    \"Join type\": \"जॉइन टाइप\",\n    \"Added!\": \"जोड़ा गया!\"\n  }\n}\n"
  },
  {
    "path": "client/src/i18n/translations/ru.json",
    "content": "{\n  \"common\": {\n    \"Remove\": \"Удалить\",\n    \"Add\": \"Добавить\",\n    \"Language\": \"Язык\",\n    \"Generate\": \"Сгенерировать\",\n    \"Next\": \"Далее\",\n    \"Login\": \"Войти\",\n    \"Logout\": \"Выйти\",\n    \"Confirm\": \"Подтвердить\",\n    \"Register\": \"Зарегистрироваться\",\n    \"Close\": \"Закрыть\",\n    \"Options\": \"Настройки\",\n    \"Not permitted\": \"Не разрешено\",\n    \"Something went wrong\": \"Что-то пошло не так\",\n    \"Configure\": \"Настроить\",\n    \"Workspaces\": \"Рабочие пространства\",\n    \"Theme\": \"Тема\",\n    \"Enabled\": \"Включено\",\n    \"Cancel\": \"Отмена\",\n    \"Save\": \"Сохранить\",\n    \"Copied!\": \"Скопировано!\",\n    \"Test and Save\": \"Проверить и сохранить\",\n    \"No changes\": \"Нет изменений\",\n    \"Run\": \"Запустить\",\n    \"Delete\": \"Удалить\",\n    \"Clone\": \"Клонировать\",\n    \"Update\": \"Обновить\",\n    \"Related data\": \"Связанные данные\",\n    \"Create\": \"Создать\",\n    \"Already exists. Chose another name\": \"Уже существует. Выберите другое имя\",\n    \"Nothing to update\": \"Нечего обновлять\",\n    \"You do not have sufficient privileges to access this\": \"У вас недостаточно прав для доступа к этому\",\n    \"Copy to clipboard\": \"Скопировать в буфер обмена\",\n    \"Send\": \"Отправить\",\n    \"Stop\": \"Остановить\",\n    \"Toggle fullscreen\": \"Переключить полноэкранный режим\",\n    \"experimental\": \"экспериментальный\",\n    \"Stop recording\": \"Остановить запись\",\n    \"Speech to text\": \"Речь в текст\",\n    \"Record audio\": \"Записать аудио\"\n  },\n  \"App\": {\n    \"Connections\": \"Подключения\",\n    \"Users\": \"Пользователи\",\n    \"Server settings\": \"Настройки сервера\",\n    \"Security issue\": \"Проблема безопасности\",\n    \"Settings\": \"Настройки\",\n    \"Reconnecting...\": \"Переподключение...\"\n  },\n  \"Users\": {\n    \"Prostgles UI users\": \"Пользователи Prostgles UI\"\n  },\n  \"AccountMenu\": {\n    \"Account\": \"Аккаунт\"\n  },\n  \"ServerSettings\": {\n    \"Security\": \"Безопасность\",\n    \"Validate a CIDR\": \"Проверить CIDR\",\n    \"Enter a value to see the allowed IP ranges\": \"Введите значение, чтобы увидеть разрешенные диапазоны IP\",\n    \"Add your current IP\": \"Добавить ваш текущий IP\",\n    \"Allowed IP\": \"Разрешенный IP\",\n    \"From IP\": \"От IP\",\n    \"To IP\": \"До IP\",\n    \"Authentication\": \"Аутентификация\",\n    \"Cloud credentials\": \"Облачные учетные данные\",\n    \"No cloud credentials. Credentials can be added for file storage\": \"Нет облачных учетных данных. Учетные данные могут быть добавлены для хранения файлов\",\n    \"The default user type assigned to new users. Defaults to 'default'\": \"Тип пользователя по умолчанию, назначаемый новым пользователям. По умолчанию 'default'\",\n    \"Warning: The default user type is set to 'admin'. This will allow new users to access all resources.\": \"Внимание: Тип пользователя по умолчанию установлен на 'admin'. Это позволит новым пользователям получать доступ ко всем ресурсам.\"\n  },\n  \"AuthProviderSetup\": {\n    \"Default user type\": \"Тип пользователя по умолчанию\",\n    \"Website URL\": \"URL веб-сайта\",\n    \"Used for redirect uri\": \"Используется для URI перенаправления\",\n    \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\": \"Внимание: Вы устанавливаете тип пользователя по умолчанию на 'admin'. Это означает, что новым пользователям будет предоставлен самый высокий уровень доступа!\"\n  },\n  \"OAuthProviderSetup\": {\n    \"Must configure the provider first\": \"Сначала необходимо настроить провайдера\",\n    \"Must provide a client ID and secret\": \"Необходимо указать идентификатор клиента и секретный ключ\",\n    \"Display Name\": \"Отображаемое имя\",\n    \"Display Icon\": \"Иконка\",\n    \"Authorization URL\": \"URL авторизации\",\n    \"Client ID\": \"Идентификатор клиента\",\n    \"Client Secret\": \"Секретный ключ клиента\",\n    \"Scopes\": \"Области действия\",\n    \"Prompt\": \"Подсказка\",\n    \"Indicates the type of user interaction that is required.\": \"Указывает тип взаимодействия с пользователем, который требуется.\",\n    \"Return URL\": \"URL возврата\"\n  },\n  \"EmailAuthSetup\": {\n    \"Email signup\": \"Регистрация по электронной почте\",\n    \"Must configure the email provider first\": \"Сначала необходимо настроить почтового провайдера\",\n    \"Signup type\": \"Тип регистрации\",\n    \"With password\": \"С паролем\",\n    \"Email and password will be required to login. User will have to confirm email\": \"Для входа потребуется адрес электронной почты и пароль. Пользователю нужно будет подтвердить адрес электронной почты\",\n    \"With magic link\": \"С магической ссылкой\",\n    \"Only email is required to login. A magic link will be sent to the user's email\": \"Для входа потребуется только адрес электронной почты. На электронную почту пользователя будет отправлена магическая ссылка\",\n    \"Minimum password length\": \"Минимальная длина пароля\",\n    \"Minimum password length. Defaults to 8\": \"Минимальная длина пароля. По умолчанию 8\",\n    \"Magic link email configuration\": \"Настройка электронной почты с магической ссылкой\",\n    \"Email verification\": \"Подтверждение электронной почты\"\n  },\n  \"EmailSMTPAndTemplateSetup\": {\n    \"Configured\": \"Настроено\",\n    \"Not configured\": \"Не настроено\",\n    \"Users will receive an email with a link/code to verify their email address.\": \"Пользователи получат электронное письмо со ссылкой/кодом для подтверждения своего адреса электронной почты.\",\n    \"Email Provider\": \"Почтовый провайдер\",\n    \"Template\": \"Шаблон\"\n  },\n  \"Connections\": {\n    \"Access granted to \": \"Доступ предоставлен \",\n    \"Create new connection\": \"Создать новое подключение\",\n    \"New connection\": \"Новое подключение\",\n    \"Editing state data directly may break functionality. Proceed at your own risk!\": \"Редактирование данных состояния напрямую может нарушить функциональность. Действуйте на свой страх и риск!\",\n    \"No connections available/permitted\": \"Нет доступных/разрешенных подключений\",\n    \"Show database names\": \"Показывать имена баз данных\",\n    \"Show state connection\": \"Показывать подключение к состоянию\"\n  },\n  \"ConnectionActionBar\": {\n    \"Connected. Click to disconnect\": \"Подключено. Нажмите, чтобы отключиться\",\n    \"Not connected\": \"Не подключено\",\n    \"Close all windows\": \"Закрыть все окна\",\n    \"Windows have been closed\": \"Окна были закрыты\",\n    \"Could not close windows: workspace not found\": \"Не удалось закрыть окна: рабочее пространство не найдено\",\n    \"Edit connection\": \"Редактировать подключение\"\n  },\n  \"ConnectionServer\": {\n    \"Server info\": \"Информация о сервере\",\n    \"Add or create a database\": \"Добавить или создать базу данных\",\n    \"Create a database\": \"Создать базу данных\",\n    \"Not allowed to create databases with this user\": \"Не разрешено создавать базы данных этим пользователем\",\n    \"Create a database in this server\": \"Создать базу данных на этом сервере\",\n    \"Select a database from this server\": \"Выбрать базу данных на этом сервере\",\n    \"Save and connect\": \"Сохранить и подключиться\",\n    \"Create and connect\": \"Создать и подключиться\",\n    \"Some data is missing\": \"Некоторые данные отсутствуют\",\n    \"Must fix connection name error\": \"Необходимо исправить ошибку в имени подключения\",\n    \"New database name\": \"Имя новой базы данных\",\n    \"Name already in use\": \"Имя уже используется\",\n    \"Create demo schema (optional)\": \"Создать демонстрационную схему (необязательно)\",\n    \"Database\": \"База данных\",\n    \"Already added to connections\": \"Уже добавлено в подключения\"\n  },\n  \"Connection\": {\n    \"Access granted to \": \"Доступ предоставлен \",\n    \"All Prostgles connection and dashboard data is stored here. Edit at your own risk\": \"Все данные подключения и панели управления Prostgles хранятся здесь. Редактируйте на свой страх и риск\"\n  },\n  \"NewConnectionForm\": {\n    \"Realtime requires table triggers to be created as and when needed.\": \"Realtime требует создания триггеров таблиц по мере необходимости.\",\n    \"A \\\"prostgles\\\" schema with necessary metadata will also be created\": \"Также будет создана схема \\\"prostgles\\\" с необходимыми метаданными\",\n    \"Realtime\": \"Режим реального времени\",\n    \"Needed to allow realtime data view. Requires superuser\": \"Необходимо для просмотра данных в реальном времени. Требуется суперпользователь\",\n    \"Reload schema\": \"Перезагрузить схему\",\n    \"Watch schema\": \"Следить за схемой\",\n    \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\": \"Будет обновлять панель управления и API при изменении схемы. Для лучшего опыта требуется суперпользователь\",\n    \"Reject unauthorized\": \"Отклонять неавторизованные\",\n    \"Client Key\": \"Ключ клиента\",\n    \"Client Certificate\": \"Сертификат клиента\",\n    \"CA Certificate\": \"CA сертификат\",\n    \"SSL Mode\": \"Режим SSL\",\n    \"Connection timeout (ms)\": \"Таймаут подключения (мс)\",\n    \"Schemas\": \"Список схем\",\n    \"More options\": \"Дополнительные параметры\",\n    \"Must connect to see schemas\": \"Необходимо подключиться, чтобы увидеть схемы\",\n    \"You are about to create a new database\": \"Вы собираетесь создать новую базу данных\",\n    \"You are about to clone the current database\": \"Вы собираетесь клонировать текущую базу данных {{currDb}} в {{newDb}}. Это закроет все существующие подключения к текущей базе данных!\",\n    \"Create database\": \"Создать базу данных\",\n    \"Connection type\": \"Тип подключения\",\n    \"Connection name\": \"Имя подключения\",\n    \"Socket URL\": \"URL сокета\",\n    \"Socket params (JSON)\": \"Параметры сокета (JSON)\",\n    \"Connection URI\": \"URI подключения\",\n    \"Host\": \"Хост\",\n    \"Port\": \"Порт\",\n    \"User\": \"Пользователь\",\n    \"Password\": \"Пароль\",\n    \"Optional\": \"Необязательно\",\n    \"Database\": \"База данных\",\n    \"Test connection\": \"Проверить подключение\"\n  },\n  \"NewConnection\": {\n    \"Delete connection\": \"Удалить подключение\",\n    \"Any related dashboard content will also be deleted\": \"Любой связанный контент панели управления также будет удален\",\n    \"Drop database as well\": \"Также удалить базу данных\",\n    \"You are about to drop\": \"Вы собираетесь удалить\",\n    \"database\": \"базу данных\",\n    \"Ensure data is backed up. This action is not reversible\": \"Убедитесь, что данные резервно скопированы. Это действие необратимо\",\n    \"Clone connection\": \"Клонировать подключение\",\n    \"Cloning connection\": \"Клонирование подключения\",\n    \"Connections\": \"Подключения\",\n    \"Connection not found\": \"Подключение не найдено\",\n    \"Go back to connections\": \"Вернуться к подключениям\"\n  },\n  \"ConnectionConfig\": {\n    \"Must be admin to access this\": \"Для доступа требуется учетная запись администратора\",\n    \"Connection details\": \"Детали подключения\",\n    \"Status monitor\": \"Монитор состояния\",\n    \"Access control\": \"Контроль доступа\",\n    \"File storage\": \"Хранилище файлов\",\n    \"Backup/Restore\": \"Резервное копирование/восстановление\",\n    \"API\": \"API\",\n    \"Table config\": \"Конфигурация таблицы\",\n    \"experimental\": \"экспериментальный\",\n    \"Server-side functions\": \"Функции на стороне сервера\"\n  },\n  \"ChangePassword\": {\n    \"Change password\": \"Сменить пароль\",\n    \"Old password\": \"Старый пароль\",\n    \"New password\": \"Новый пароль\",\n    \"Confirm new password\": \"Подтвердите новый пароль\",\n    \"Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter\": \"Убедитесь, что он содержит не менее 15 символов ИЛИ не менее 8 символов, включая цифру и строчную букву\",\n    \"Enter your old password\": \"Введите ваш старый пароль\",\n    \"Enter your new password\": \"Введите ваш новый пароль\",\n    \"Confirm your new password\": \"Подтвердите ваш новый пароль\",\n    \"Passwords do not match\": \"Пароли не совпадают\"\n  },\n  \"Setup2FA\": {\n    \"Enable 2FA\": \"Включить 2FA\",\n    \"Confirm code\": \"Подтвердите код\",\n    \"2FA Enabled\": \"2FA включена\",\n    \"Along with your username and password, you will be asked to verify your identity using the code from authenticator app.\": \"Вместе с вашим именем пользователя и паролем вам будет предложено подтвердить вашу личность, используя код из приложения-аутентификатора.\",\n    \"Two-factor authentication\": \"Двухфакторная аутентификация\",\n    \"Scan\": \"Отсканируйте\",\n    \" or tap \": \" или нажмите \",\n    \"the image below with the two-factor authentication app on your phone.\": \"изображение ниже с помощью приложения двухфакторной аутентификации на вашем телефоне.\",\n    \"If you can't use a QR code you can enter this information manually:\": \"Если вы не можете использовать QR-код, вы можете ввести эту информацию вручную:\",\n    \"Base64 secret\": \"Секрет Base64\",\n    \"Type\": \"Тип\",\n    \"Name\": \"Имя\",\n    \"Save the Recovery code below. It will be used in case you lose access to your authenticator app:\": \"Сохраните код восстановления ниже. Он будет использоваться в случае, если вы потеряете доступ к своему приложению-аутентификатору:\",\n    \"Generate QR Code\": \"Сгенерировать QR-код\",\n    \"I can't scan the QR Code\": \"Я не могу отсканировать QR-код\"\n  },\n  \"Account\": {\n    \"Account details\": \"Детали аккаунта\",\n    \"Security\": \"Безопасность\",\n    \"API\": \"API\"\n  },\n  \"APIDetailsWs\": {\n    \"Allowed origin specifies which domains can access this app in a cross-origin manner. Sets the Access-Control-Allow-Origin header. Use '' or a specific URL to allow API access\": \"Разрешенный источник указывает, какие домены могут получить доступ к этому приложению в режиме кросс-доменного доступа. Устанавливает заголовок Access-Control-Allow-Origin. Используйте '' или конкретный URL, чтобы разрешить доступ к API\",\n    \"For testing it is recommended to use \\\"\\\" as the allowed origin value\": \"Для тестирования рекомендуется использовать \\\"\\\" в качестве значения разрешенного источника\",\n    \"Allowed origin\": \"Разрешенный источник\",\n    \"Allowed origin is required\": \"Разрешенный источник обязателен\",\n    \"Allowed origin not set\": \"Разрешенный источник не установлен\",\n    \"Websocket API (recommended)\": \"Websocket API (рекомендуется)\",\n    \"Realtime Isomorphic API using\": \"Изоморфный API в реальном времени с использованием\",\n    \"library. End-to-end type-safety between client & server using the database Typescript schema provided below:\": \"библиотеки. Сквозная типобезопасность между клиентом и сервером с использованием схемы Typescript базы данных, представленной ниже:\",\n    \"Examples\": \"Примеры\",\n    \"Download typescript schema\": \"Скачать схему Typescript\",\n    \"Database schema types\": \"Типы схемы базы данных\"\n  },\n  \"APIDetailsTokens\": {\n    \"Access tokens (\": \"Токены доступа (\",\n    \"Provide the same level of access as the current account\": \"Предоставляют тот же уровень доступа, что и текущая учетная запись\",\n    \"Create access token\": \"Создать токен доступа\",\n    \"Create token\": \"Создать токен\",\n    \"Expires in\": \"Истекает через\",\n    \"Days\": \"Дней\",\n    \"These token values will not be shown again\": \"Эти значения токенов больше не будут показаны\",\n    \"Websocket API\": \"Websocket API\",\n    \"HTTP API (base64)\": \"HTTP API (base64)\",\n    \"Already generated. Close and re-open the popup\": \"Уже сгенерировано. Закройте и снова откройте всплывающее окно\"\n  },\n  \"APIDetailsHttp\": {\n    \"Provides similar level of access to the Websocket API with the following limitations: no subscriptions, no sync, no file upload\": \"Предоставляет аналогичный уровень доступа к Websocket API со следующими ограничениями: нет подписок, нет синхронизации, нет загрузки файлов\"\n  },\n  \"Sessions\": {\n    \"API tokens\": \"API токены\",\n    \"Sessions\": \"Сессии\",\n    \"Last used\": \"Последнее использование\",\n    \"Created\": \"Создано\",\n    \"Expires\": \"Истекает\",\n    \"Disable\": \"Отключить\",\n    \"User agent\": \"User agent\",\n    \"Active\": \"Активный\",\n    \"Inactive\": \"Неактивный\",\n    \"No active \": \"Нет активных \"\n  },\n  \"TopControls\": {\n    \"Go to Connections\": \"Перейти к подключениям\",\n    \"Connections\": \"Подключения\",\n    \"Go to workspace\": \"Перейти в рабочее пространство\",\n    \"Menu is pinned\": \"Меню закреплено\",\n    \"pinned\": \"закреплено\",\n    \"Configure database connection\": \"Настроить подключение к базе данных\",\n    \"Go back to connection workspace\": \"Вернуться в рабочее пространство подключения\",\n    \"Not allowed for state database\": \"Не разрешено для базы данных состояния\",\n    \"Connection configuration\": \"Конфигурация подключения\"\n  },\n  \"Feedback\": {\n    \"Send feedback\": \"Отправить отзыв\",\n    \"Leave feedback\": \"Оставить отзыв\",\n    \"Feedback\": \"Отзыв\",\n    \"Already sent\": \"Уже отправлено\",\n    \"Must provide some details\": \"Необходимо предоставить некоторые детали\",\n    \"Feedback was sent! Thanks a lot!\": \"Отзыв был отправлен! Большое спасибо!\",\n    \"Email (optional)\": \"Email (необязательно)\",\n    \"Details\": \"Детали\",\n    \"Other options\": \"Другие варианты\",\n    \"open an issue\": \"открыть issue\",\n    \"or\": \"или\",\n    \"email us\": \"напишите нам\"\n  },\n  \"AskLLM\": {\n    \"Chat to an AI Assistant to get help with your queries\": \"Пообщайтесь с ИИ-помощником, чтобы получить помощь с вашими запросами\",\n    \"AI assistant not available. Talk to the admin\": \"ИИ-помощник недоступен. Обратитесь к администратору\",\n    \"AI Assistant\": \"Спросить ИИ\"\n  },\n  \"AskLLMChatHeader\": {\n    \"Chat\": \"Чат\",\n    \"New chat\": \"Новый чат\",\n    \"No prompt found\": \"Подсказка не найдена\",\n    \"Prompt\": \"Подсказка\",\n    \"Chat settings\": \"Настройки чата\"\n  },\n  \"ConnectionSelector\": {\n    \"Switch database\": \"Переключить базу данных\"\n  },\n  \"W_SQLMenu\": {\n    \"SQL Editor settings\": \"Настройки SQL-редактора\",\n    \"Press\": \"Нажмите\",\n    \"to get a list of possible options\": \"чтобы получить список возможных вариантов\",\n    \"Update options\": \"Обновить параметры\",\n    \"Nothing to update\": \"Нечего обновлять\",\n    \"Cannot save due to error\": \"Невозможно сохранить из-за ошибки\",\n    \"Delete query\": \"Удалить запрос\",\n    \"Open SQL file\": \"Открыть SQL файл\",\n    \"Open query from file\": \"Открыть запрос из файла\",\n    \"Download query\": \"Скачать запрос\",\n    \"Save query as file\": \"Сохранить запрос как файл\",\n    \"Query name\": \"Имя запроса\",\n    \"Result display mode\": \"Режим отображения результатов\",\n    \"General\": \"Общие\",\n    \"Editor options\": \"Параметры редактора\",\n    \"Hotkeys\": \"Горячие клавиши\"\n  },\n  \"SQLHotkeys\": {\n    \"Show autocomplete suggestions\": \"Показать автодополнение\",\n    \"Execute current statement\": \"Выполнить текущий оператор\",\n    \"Select current statement\": \"Выбрать текущий оператор\",\n    \"Show all suggestions\": \"Показать все предложения\",\n    \"Show PSQL command queries\": \"Показать запросы команд PSQL\"\n  },\n  \"W_SQLBottomBar\": {\n    \"Execution mode\": \"Режим выполнения\",\n    \"Loop query execution\": \"Циклическое выполнение запроса\",\n    \"Cancel this query (Esc)\": \"Отменить этот запрос (Esc)\",\n    \"Terminate this query\": \"Прервать этот запрос\",\n    \"Query running time\": \"Время выполнения запроса\",\n    \"Stop LISTEN\": \"Остановить LISTEN\",\n    \"repeat every\": \"повторять каждые\",\n    \"seconds\": \"секунд\",\n    \"Run query (CTRL+E, ALT+E)\": \"Запустить запрос (CTRL+E, ALT+E)\",\n    \"Run\": \"Запустить\",\n    \"Query running time: \": \"Время выполнения запроса: \",\n    \"Show/Hide table\": \"Показать/скрыть таблицу\",\n    \"Show/Hide code editor\": \"Показать/скрыть редактор кода\",\n    \"Show/Hide notices\": \"Показать/скрыть уведомления\",\n    \"Clear value to show all rows\": \"Очистите значение, чтобы показать все строки\",\n    \"Limit\": \"Лимит\"\n  },\n  \"Window\": {\n    \"Open menu\": \"Открыть меню\",\n    \"Menu\": \"Меню\",\n    \"Collapse chart\": \"Свернуть диаграмму\",\n    \"Detach chart\": \"Отсоединить диаграмму\",\n    \"Close chart\": \"Закрыть диаграмму\",\n    \"Chart options\": \"Параметры диаграммы\"\n  },\n  \"AddChartMenu\": {\n    \"Layer already added\": \"Слой уже добавлен\",\n    \"No\": \"Нет\",\n    \"No {{chartColumnDataType}} columns available\": \"Нет доступных столбцов типа {{chartColumnDataType}}\"\n  },\n  \"DashboardMenuHeader\": {\n    \"Opens SQL Query editor\": \"Открывает редактор SQL-запросов\",\n    \"SQL Editor\": \"SQL-редактор\",\n    \"Show quick search menu (CTRL + P)\": \"Показать меню быстрого поиска (CTRL + P)\",\n    \"Cannot be used in a low width device\": \"Не может использоваться на устройстве с малой шириной\",\n    \"Pin/Unpin\": \"Закрепить/открепить\"\n  },\n  \"W_QuickMenu\": {\n    \"Show/Hide filtering\": \"Показать/скрыть фильтрацию\",\n    \"Restore minimised charts\": \"Восстановить свернутые диаграммы\",\n    \"Cross filter tables\": \"Кросс-фильтрация таблиц\",\n    \"Add chart\": \"Добавить диаграмму\"\n  },\n  \"AddColumnMenu\": {\n    \"Add Computed Field\": \"Добавить вычисляемое поле\",\n    \"Show a computed column\": \"Показать вычисляемый столбец\",\n    \"Add Linked Data\": \"Добавить связанные данные\",\n    \"Show data from a related table\": \"Показать данные из связанной таблицы\",\n    \"Create New Column\": \"Создать новый столбец\",\n    \"Create a new column in this table\": \"Создать новый столбец в этой таблице\",\n    \"Create New File Column\": \"Создать новый столбец файла\",\n    \"Create a new file column in this table\": \"Создать новый столбец файла в этой таблице\",\n    \"Not enough privileges\": \"Недостаточно прав\",\n    \"This is a view. Cannot create columns, must recreate\": \"Это представление. Невозможно создать столбцы, необходимо пересоздать\",\n    \"Add column\": \"Добавить столбец\",\n    \"New Field\": \"Новое поле\",\n    \"No foreign keys to/from this table\": \"Нет внешних ключей к/из этой таблицы\",\n    \"Not allowed for nested columns\": \"Не разрешено для вложенных столбцов\",\n    \"Aggregates and/or Count not allowed with linked \": \"Агрегаты и/или подсчет не разрешены со связанными \",\n    \"Create new column\": \"Создать новый столбец\",\n    \"Add Referenced/Linked Fields\": \"Добавить ссылочные/связанные поля\"\n  },\n  \"CreateColumn\": {\n    \"Create column query\": \"Создать запрос столбца\",\n    \"Show create column query\": \"Показать запрос на создание столбца\",\n    \"New column name missing\": \"Отсутствует имя нового столбца\",\n    \"Data type missing\": \"Отсутствует тип данных\"\n  },\n  \"W_Table\": {\n    \"Insert row\": \"Вставить строку\"\n  },\n  \"LinkedColumn\": {\n    \"Row\": \"Строка\",\n    \"Column\": \"Столбец\",\n    \"No headers\": \"Нет заголовков\",\n    \"Column name already used. Change to another\": \"Имя столбца уже используется. Измените на другое\",\n    \"Must select columns\": \"Необходимо выбрать столбцы\",\n    \"Must select a table\": \"Необходимо выбрать таблицу\",\n    \"Join to and show data from tables that are related through a\": \"Соединить и показать данные из таблиц, связанных через \",\n    \"Column label\": \"Метка столбца\",\n    \"More options\": \"Дополнительные параметры\",\n    \"Layout\": \"Макет\",\n    \"Must disable chart first\": \"Сначала необходимо отключить диаграмму\",\n    \"Join type\": \"Тип соединения\",\n    \"Added!\": \"Добавлено!\"\n  }\n}\n"
  },
  {
    "path": "client/src/i18n/translations/translations.ts",
    "content": "import type { TranslationGroup } from \"../i18nUtils\";\n\nexport const LANGUAGES = [\n  { key: \"en\", label: \"English\" },\n  { key: \"es\", label: \"Español\" },\n  { key: \"fr\", label: \"Français\" },\n  { key: \"de\", label: \"Deutsch\" },\n  { key: \"ru\", label: \"Русский\" },\n  { key: \"zh\", label: \"简体中文\" },\n  { key: \"hi\", label: \"हिंदी\" },\n] as const;\n\nexport type Language = (typeof LANGUAGES)[number][\"key\"];\n\nexport const translations = {\n  common: {\n    Remove: undefined,\n    Add: undefined,\n    Language: undefined,\n    Generate: undefined,\n    Next: undefined,\n    Login: undefined,\n    Logout: undefined,\n    Confirm: undefined,\n    Register: undefined,\n    Close: undefined,\n    Options: undefined,\n    \"Not permitted\": undefined,\n    \"Something went wrong\": undefined,\n    Configure: undefined,\n    Workspaces: undefined,\n    Theme: undefined,\n    Enabled: undefined,\n    Cancel: undefined,\n    Save: undefined,\n    \"Copied!\": undefined,\n    \"Test and Save\": undefined,\n    \"No changes\": undefined,\n    Run: undefined,\n    Delete: undefined,\n    Clone: undefined,\n    Update: undefined,\n    \"Related data\": undefined,\n    Create: undefined,\n    \"Already exists. Chose another name\": undefined,\n    \"Nothing to update\": undefined,\n    \"You do not have sufficient privileges to access this\": undefined,\n    \"Copy to clipboard\": undefined,\n    Send: undefined,\n    Stop: undefined,\n    \"Stop recording\": undefined,\n    \"Speech to text\": undefined,\n    \"Record audio\": undefined,\n    experimental: undefined,\n    \"Toggle fullscreen\": undefined,\n  },\n  App: {\n    Connections: undefined,\n    Users: undefined,\n    \"Server settings\": undefined,\n    \"Security issue\": undefined,\n    Settings: undefined,\n    \"Reconnecting...\": undefined,\n  },\n  Users: {\n    \"Prostgles UI users\": undefined,\n  },\n  AccountMenu: {\n    Account: undefined,\n  },\n  ServerSettings: {\n    Security: undefined,\n    \"Validate a CIDR\": undefined,\n    \"Enter a value to see the allowed IP ranges\": undefined,\n    \"Add your current IP\": undefined,\n    \"Allowed IP\": undefined,\n    \"From IP\": undefined,\n    \"To IP\": undefined,\n    Authentication: undefined,\n    \"Cloud credentials\": undefined,\n    \"No cloud credentials. Credentials can be added for file storage\":\n      undefined,\n    \"The default user type assigned to new users. Defaults to 'default'\":\n      undefined,\n    \"Warning: The default user type is set to 'admin'. This will allow new users to access all resources.\":\n      undefined,\n  },\n  AuthProviderSetup: {\n    \"Default user type\": undefined,\n    \"Website URL\": undefined,\n    \"Used for redirect uri\": undefined,\n    \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\":\n      undefined,\n  },\n  OAuthProviderSetup: {\n    \"Must configure the provider first\": undefined,\n    \"Must provide a client ID and secret\": undefined,\n    \"Display Name\": undefined,\n    \"Display Icon\": undefined,\n    \"Authorization URL\": undefined,\n    \"Client ID\": undefined,\n    \"Client Secret\": undefined,\n    Scopes: undefined,\n    Prompt: undefined,\n    \"Indicates the type of user interaction that is required.\": undefined,\n    \"Return URL\": undefined,\n  },\n  EmailAuthSetup: {\n    \"Email signup\": undefined,\n    \"Must configure the email provider first\": undefined,\n    \"Signup type\": undefined,\n    \"With password\": undefined,\n    \"Email and password will be required to login. User will have to confirm email\":\n      undefined,\n    \"With magic link\": undefined,\n    \"Only email is required to login. A magic link will be sent to the user's email\":\n      undefined,\n    \"Minimum password length\": undefined,\n    \"Minimum password length. Defaults to 8\": undefined,\n    \"Magic link email configuration\": undefined,\n    \"Email verification\": undefined,\n  },\n  EmailSMTPAndTemplateSetup: {\n    Configured: undefined,\n    \"Not configured\": undefined,\n    \"Users will receive an email with a link/code to verify their email address.\":\n      undefined,\n    \"Email Provider\": undefined,\n    Template: undefined,\n  },\n  Connections: {\n    \"Access granted to \": undefined,\n    \"Create new connection\": undefined,\n    \"New connection\": undefined,\n    \"Editing state data directly may break functionality. Proceed at your own risk!\":\n      undefined,\n    \"No connections available/permitted\": undefined,\n    \"Show database names\": undefined,\n    \"Show state connection\": undefined,\n  },\n  ConnectionActionBar: {\n    \"Connected. Click to disconnect\": undefined,\n    \"Not connected\": undefined,\n    \"Close all windows\": undefined,\n    \"Windows have been closed\": undefined,\n    \"Could not close windows: workspace not found\": undefined,\n    \"Edit connection\": undefined,\n  },\n  ConnectionServer: {\n    \"Server info\": undefined,\n    \"Add or create a database\": undefined,\n    \"Create a database\": undefined,\n    \"Not allowed to create databases with this user\": {\n      text: `Not allowed to create databases with this user ({{rolname}})`,\n      argNames: [\"rolname\"],\n    },\n    \"Create a database in this server\": undefined,\n    \"Select a database from this server\": undefined,\n    \"Save and connect\": undefined,\n    \"Create and connect\": undefined,\n    \"Some data is missing\": undefined,\n    \"Must fix connection name error\": undefined,\n    \"New database name\": undefined,\n    \"Name already in use\": undefined,\n    \"Create demo schema (optional)\": undefined,\n    Database: undefined,\n    \"Already added to connections\": undefined,\n  },\n  Connection: {\n    \"Access granted to \": undefined,\n    \"All Prostgles connection and dashboard data is stored here. Edit at your own risk\":\n      undefined,\n  },\n  NewConnectionForm: {\n    \"Realtime requires table triggers to be created as and when needed.\":\n      undefined,\n    'A \"prostgles\" schema with necessary metadata will also be created':\n      undefined,\n    Realtime: undefined,\n    \"Needed to allow realtime data view. Requires superuser\": undefined,\n    \"Reload schema\": undefined,\n    \"Watch schema\": undefined,\n    \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\":\n      undefined,\n    \"Reject unauthorized\": undefined,\n    \"Client Key\": undefined,\n    \"Client Certificate\": undefined,\n    \"CA Certificate\": undefined,\n    \"SSL Mode\": undefined,\n    \"Connection timeout (ms)\": undefined,\n    Schemas: undefined,\n    \"More options\": undefined,\n    \"Must connect to see schemas\": undefined,\n    \"You are about to create a new database\": undefined,\n    \"You are about to clone the current database\": {\n      text: \"You are about to clone the current database {{currDb}} into {{newDb}}. This will close all existing connections to the current database!\",\n      argNames: [\"currDb\", \"newDb\"],\n    },\n    \"Create database\": undefined,\n    \"Connection type\": undefined,\n    \"Connection name\": undefined,\n    \"Socket URL\": undefined,\n    \"Socket params (JSON)\": undefined,\n    \"Connection URI\": undefined,\n    Host: undefined,\n    Port: undefined,\n    User: undefined,\n    Password: undefined,\n    Optional: undefined,\n    Database: undefined,\n    \"Test connection\": undefined,\n  },\n  NewConnection: {\n    \"Delete connection\": undefined,\n    \"Any related dashboard content will also be deleted\": undefined,\n    \"Drop database as well\": undefined,\n    \"You are about to drop\": undefined,\n    database: undefined,\n    \"Ensure data is backed up. This action is not reversible\": undefined,\n    \"Clone connection\": undefined,\n    \"Cloning connection\": undefined,\n    Connections: undefined,\n    \"Connection not found\": undefined,\n    \"Go back to connections\": undefined,\n  },\n  ConnectionConfig: {\n    \"Must be admin to access this\": undefined,\n    \"Connection details\": undefined,\n    \"Status monitor\": undefined,\n    \"Access control\": undefined,\n    \"File storage\": undefined,\n    \"Backup/Restore\": undefined,\n    API: undefined,\n    \"Table config\": undefined,\n    experimental: undefined,\n    \"Server-side functions\": undefined,\n  },\n  ChangePassword: {\n    \"Change password\": undefined,\n    \"Old password\": undefined,\n    \"New password\": undefined,\n    \"Confirm new password\": undefined,\n    \"Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter\":\n      undefined,\n    \"Enter your old password\": undefined,\n    \"Enter your new password\": undefined,\n    \"Confirm your new password\": undefined,\n    \"Passwords do not match\": undefined,\n  },\n  Setup2FA: {\n    \"Enable 2FA\": undefined,\n    \"Confirm code\": undefined,\n    \"2FA Enabled\": undefined,\n    \"Along with your username and password, you will be asked to verify your identity using the code from authenticator app.\":\n      undefined,\n    \"Two-factor authentication\": undefined,\n    Scan: undefined,\n    \" or tap \": undefined,\n    \"the image below with the two-factor authentication app on your phone.\":\n      undefined,\n    \"If you can't use a QR code you can enter this information manually:\":\n      undefined,\n    \"Base64 secret\": undefined,\n    Type: undefined,\n    Name: undefined,\n    \"Save the Recovery code below. It will be used in case you lose access to your authenticator app:\":\n      undefined,\n    \"Generate QR Code\": undefined,\n    \"I can't scan the QR Code\": undefined,\n  },\n  Account: {\n    \"Account details\": undefined,\n    Security: undefined,\n    API: undefined,\n  },\n  APIDetailsWs: {\n    \"Allowed origin\": undefined,\n    \"Allowed origin is required\": undefined,\n    \"Allowed origin not set\": undefined,\n    \"Websocket API (recommended)\": undefined,\n    \"Realtime Isomorphic API using\": undefined,\n    \"library. End-to-end type-safety between client & server using the database Typescript schema provided below:\":\n      undefined,\n    Examples: undefined,\n    \"Download typescript schema\": undefined,\n    \"Database schema types\": undefined,\n  },\n  APIDetailsTokens: {\n    \"Access tokens (\": {\n      text: \"Access tokens ({{tokenCount}})\",\n      argNames: [\"tokenCount\"],\n    },\n    \"Provide the same level of access as the current account\": undefined,\n    \"Create access token\": undefined,\n    \"Create token\": undefined,\n    \"Expires in\": undefined,\n    Days: undefined,\n    \"These token values will not be shown again\": undefined,\n    \"Websocket API\": undefined,\n    \"HTTP API (base64)\": undefined,\n    \"Already generated. Close and re-open the popup\": undefined,\n  },\n  APIDetailsHttp: {\n    \"Provides similar level of access to the Websocket API with the following limitations: no subscriptions, no sync, no file upload\":\n      undefined,\n  },\n  Sessions: {\n    \"API tokens\": undefined,\n    Sessions: undefined,\n    \"Last used\": undefined,\n    Created: undefined,\n    Expires: undefined,\n    Disable: undefined,\n    \"User agent\": undefined,\n    Active: undefined,\n    Inactive: undefined,\n    \"No active \": undefined,\n  },\n  TopControls: {\n    \"Go to Connections\": undefined,\n    Connections: undefined,\n    \"Go to workspace\": undefined,\n    \"Menu is pinned\": undefined,\n    pinned: undefined,\n    \"Configure database connection\": undefined,\n    \"Go back to connection workspace\": undefined,\n    \"Not allowed for state database\": undefined,\n    \"Connection configuration\": undefined,\n  },\n  Feedback: {\n    \"Send feedback\": undefined,\n    \"Leave feedback\": undefined,\n    Feedback: undefined,\n    \"Already sent\": undefined,\n    \"Must provide some details\": undefined,\n    \"Feedback was sent! Thanks a lot!\": undefined,\n    \"Email (optional)\": undefined,\n    Details: undefined,\n    \"Other options\": undefined,\n    \"open an issue\": undefined,\n    or: undefined,\n    \"email us\": undefined,\n  },\n  AskLLM: {\n    \"Chat to an AI Assistant to get help with your queries\": undefined,\n    \"AI assistant not available. Talk to the admin\": undefined,\n    \"AI Assistant\": undefined,\n  },\n  AskLLMChatHeader: {\n    Chat: undefined,\n    \"New chat\": undefined,\n    \"No prompt found\": undefined,\n    Prompt: undefined,\n    \"Chat settings\": undefined,\n  },\n  ConnectionSelector: {\n    \"Switch database\": undefined,\n  },\n  W_SQLMenu: {\n    \"SQL Editor settings\": undefined,\n    Press: undefined,\n    \"to get a list of possible options\": undefined,\n    \"Update options\": undefined,\n    \"Nothing to update\": undefined,\n    \"Cannot save due to error\": undefined,\n    \"Delete query\": undefined,\n    \"Open SQL file\": undefined,\n    \"Open query from file\": undefined,\n    \"Download query\": undefined,\n    \"Save query as file\": undefined,\n    \"Query name\": undefined,\n    \"Result display mode\": undefined,\n    General: undefined,\n    \"Editor options\": undefined,\n    Hotkeys: undefined,\n  },\n  SQLHotkeys: {\n    \"Show autocomplete suggestions\": undefined,\n    \"Execute current statement\": undefined,\n    \"Select current statement\": undefined,\n    \"Show all suggestions\": undefined,\n    \"Show PSQL command queries\": undefined,\n  },\n  W_SQLBottomBar: {\n    \"Execution mode\": undefined,\n    \"Loop query execution\": undefined,\n    \"Cancel this query (Esc)\": undefined,\n    \"Terminate this query\": undefined,\n    \"Query running time\": undefined,\n    \"Stop LISTEN\": undefined,\n    \"repeat every\": undefined,\n    seconds: undefined,\n    \"Run query (CTRL+E, ALT+E)\": undefined,\n    Run: undefined,\n    \"Query running time: \": {\n      text: \"Query running time: ({{duration}})\",\n      argNames: [\"duration\"],\n    },\n    \"Show/Hide table\": undefined,\n    \"Show/Hide code editor\": undefined,\n    \"Show/Hide notices\": undefined,\n    \"Clear value to show all rows\": undefined,\n    Limit: undefined,\n  },\n  Window: {\n    \"Open menu\": undefined,\n    Menu: undefined,\n    \"Collapse chart\": undefined,\n    \"Detach chart\": undefined,\n    \"Close chart\": undefined,\n    \"Chart options\": undefined,\n  },\n  AddChartMenu: {\n    \"Layer already added\": undefined,\n    No: undefined,\n    \"No {{chartColumnDataType}} columns available\": {\n      text: \"No {{chartColumnDataType}} columns available\",\n      argNames: [\"chartColumnDataType\"],\n    },\n  },\n  DashboardMenuHeader: {\n    \"Opens SQL Query editor\": undefined,\n    \"SQL Editor\": undefined,\n    \"Show quick search menu (CTRL + P)\": undefined,\n    \"Cannot be used in a low width device\": undefined,\n    \"Pin/Unpin\": undefined,\n  },\n  W_QuickMenu: {\n    \"Show/Hide filtering\": undefined,\n    \"Restore minimised charts\": undefined,\n    \"Cross filter tables\": undefined,\n    \"Add chart\": undefined,\n  },\n  AddColumnMenu: {\n    \"Add Computed Field\": undefined,\n    \"Show a computed column\": undefined,\n    \"Add Linked Data\": undefined,\n    \"Show data from a related table\": undefined,\n    \"Create New Column\": undefined,\n    \"Create a new column in this table\": undefined,\n    \"Create New File Column\": undefined,\n    \"Create a new file column in this table\": undefined,\n    \"Not enough privileges\": undefined,\n    \"This is a view. Cannot create columns, must recreate\": undefined,\n    \"Add column\": undefined,\n    \"New Field\": undefined,\n    \"No foreign keys to/from this table\": undefined,\n    \"Not allowed for nested columns\": undefined,\n    \"Aggregates and/or Count not allowed with linked \": undefined,\n    \"Create new column\": undefined,\n    \"Add Referenced/Linked Fields\": undefined,\n  },\n  CreateColumn: {\n    \"Create column query\": undefined,\n    \"Show create column query\": undefined,\n    \"New column name missing\": undefined,\n    \"Data type missing\": undefined,\n  },\n  W_Table: {\n    \"Insert row\": undefined,\n  },\n  LinkedColumn: {\n    Row: undefined,\n    Column: undefined,\n    \"No headers\": undefined,\n    \"Column name already used. Change to another\": undefined,\n    \"Must select columns\": undefined,\n    \"Must select a table\": undefined,\n    \"Join to and show data from tables that are related through a\": undefined,\n    \"Column label\": undefined,\n    \"More options\": undefined,\n    Layout: undefined,\n    \"Must disable chart first\": undefined,\n    \"Join type\": undefined,\n    \"Added!\": undefined,\n  },\n} as const satisfies Record<string, TranslationGroup>;\n"
  },
  {
    "path": "client/src/i18n/translations/zh.json",
    "content": "{\n  \"common\": {\n    \"Remove\": \"移除\",\n    \"Add\": \"添加\",\n    \"Language\": \"语言\",\n    \"Generate\": \"生成\",\n    \"Next\": \"下一步\",\n    \"Login\": \"登录\",\n    \"Logout\": \"登出\",\n    \"Confirm\": \"确认\",\n    \"Register\": \"注册\",\n    \"Close\": \"关闭\",\n    \"Options\": \"选项\",\n    \"Not permitted\": \"不允许\",\n    \"Something went wrong\": \"出错了\",\n    \"Configure\": \"配置\",\n    \"Workspaces\": \"工作区\",\n    \"Theme\": \"主题\",\n    \"Enabled\": \"已启用\",\n    \"Cancel\": \"取消\",\n    \"Save\": \"保存\",\n    \"Copied!\": \"已复制！\",\n    \"Test and Save\": \"测试并保存\",\n    \"No changes\": \"无更改\",\n    \"Run\": \"运行\",\n    \"Delete\": \"删除\",\n    \"Clone\": \"克隆\",\n    \"Update\": \"更新\",\n    \"Related data\": \"相关数据\",\n    \"Create\": \"创建\",\n    \"Already exists. Chose another name\": \"已存在。请选择另一个名称\",\n    \"Nothing to update\": \"没有需要更新的内容\",\n    \"You do not have sufficient privileges to access this\": \"您没有足够的权限访问此内容\",\n    \"Copy to clipboard\": \"复制到剪贴板\",\n    \"Send\": \"发送\",\n    \"Stop\": \"停止\",\n    \"Toggle fullscreen\": \"切换全屏\",\n    \"experimental\": \"实验性功能\",\n    \"Stop recording\": \"停止录音\",\n    \"Speech to text\": \"语音转文本\",\n    \"Record audio\": \"录音\"\n  },\n  \"App\": {\n    \"Connections\": \"连接\",\n    \"Users\": \"用户\",\n    \"Server settings\": \"服务器设置\",\n    \"Security issue\": \"安全问题\",\n    \"Settings\": \"设置\",\n    \"Reconnecting...\": \"重新连接中...\"\n  },\n  \"Users\": { \"Prostgles UI users\": \"Prostgles UI 用户\" },\n  \"AccountMenu\": { \"Account\": \"账户\" },\n  \"ServerSettings\": {\n    \"Security\": \"安全\",\n    \"Validate a CIDR\": \"验证 CIDR\",\n    \"Enter a value to see the allowed IP ranges\": \"输入一个值以查看允许的 IP 范围\",\n    \"Add your current IP\": \"添加您的当前 IP\",\n    \"Allowed IP\": \"允许的 IP\",\n    \"From IP\": \"起始 IP\",\n    \"To IP\": \"结束 IP\",\n    \"Authentication\": \"认证方式\",\n    \"Cloud credentials\": \"云凭证\",\n    \"No cloud credentials. Credentials can be added for file storage\": \"没有云凭证。可以添加用于文件存储的凭证\",\n    \"The default user type assigned to new users. Defaults to 'default'\": \"分配给新用户的默认用户类型。默认为“default”\",\n    \"Warning: The default user type is set to 'admin'. This will allow new users to access all resources.\": \"警告：默认用户类型设置为“admin”。这将允许新用户访问所有资源。\"\n  },\n  \"AuthProviderSetup\": {\n    \"Default user type\": \"默认用户类型\",\n    \"Website URL\": \"网站 URL\",\n    \"Used for redirect uri\": \"用于重定向 URI\",\n    \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\": \"警告：您正在将默认用户类型设置为“admin”。这意味着新用户将被授予最高级别的访问权限！\"\n  },\n  \"OAuthProviderSetup\": {\n    \"Must configure the provider first\": \"必须先配置提供商\",\n    \"Must provide a client ID and secret\": \"必须提供客户端 ID 和密钥\",\n    \"Display Name\": \"显示名称\",\n    \"Display Icon\": \"显示图标\",\n    \"Authorization URL\": \"授权 URL\",\n    \"Client ID\": \"客户端 ID\",\n    \"Client Secret\": \"客户端密钥\",\n    \"Scopes\": \"范围\",\n    \"Prompt\": \"提示\",\n    \"Indicates the type of user interaction that is required.\": \"指示所需的用户交互类型。\",\n    \"Return URL\": \"返回 URL\"\n  },\n  \"EmailAuthSetup\": {\n    \"Email signup\": \"邮箱注册\",\n    \"Must configure the email provider first\": \"必须先配置电子邮件提供商\",\n    \"Signup type\": \"注册类型\",\n    \"With password\": \"使用密码\",\n    \"Email and password will be required to login. User will have to confirm email\": \"登录时需要电子邮件和密码。用户必须确认电子邮件\",\n    \"With magic link\": \"使用魔术链接\",\n    \"Only email is required to login. A magic link will be sent to the user's email\": \"只需电子邮件即可登录。魔术链接将发送到用户的电子邮件\",\n    \"Minimum password length\": \"最小密码长度\",\n    \"Minimum password length. Defaults to 8\": \"最小密码长度。默认为 8\",\n    \"Magic link email configuration\": \"魔术链接电子邮件配置\",\n    \"Email verification\": \"邮箱验证\"\n  },\n  \"EmailSMTPAndTemplateSetup\": {\n    \"Configured\": \"已配置\",\n    \"Not configured\": \"未配置\",\n    \"Users will receive an email with a link/code to verify their email address.\": \"用户将收到一封包含链接/代码的电子邮件，以验证其电子邮件地址。\",\n    \"Email Provider\": \"电子邮件提供商\",\n    \"Template\": \"模板\"\n  },\n  \"Connections\": {\n    \"Access granted to \": \"授予访问权限给\",\n    \"Create new connection\": \"创建新连接\",\n    \"New connection\": \"新连接\",\n    \"Editing state data directly may break functionality. Proceed at your own risk!\": \"直接编辑状态数据可能会破坏功能。请自行承担风险！\",\n    \"No connections available/permitted\": \"没有可用/允许的连接\",\n    \"Show database names\": \"显示数据库名称\",\n    \"Show state connection\": \"显示状态连接\"\n  },\n  \"ConnectionActionBar\": {\n    \"Connected. Click to disconnect\": \"已连接。单击以断开连接\",\n    \"Not connected\": \"未连接\",\n    \"Close all windows\": \"关闭所有窗口\",\n    \"Windows have been closed\": \"窗口已关闭\",\n    \"Could not close windows: workspace not found\": \"无法关闭窗口：找不到工作区\",\n    \"Edit connection\": \"编辑连接\"\n  },\n  \"ConnectionServer\": {\n    \"Server info\": \"服务器信息\",\n    \"Add or create a database\": \"添加或创建数据库\",\n    \"Create a database\": \"创建数据库\",\n    \"Not allowed to create databases with this user\": \"不允许使用此用户创建数据库\",\n    \"Create a database in this server\": \"在此服务器中创建数据库\",\n    \"Select a database from this server\": \"从此服务器中选择数据库\",\n    \"Save and connect\": \"保存并连接\",\n    \"Create and connect\": \"创建并连接\",\n    \"Some data is missing\": \"缺少一些数据\",\n    \"Must fix connection name error\": \"必须修复连接名称错误\",\n    \"New database name\": \"新数据库名称\",\n    \"Name already in use\": \"名称已被使用\",\n    \"Create demo schema (optional)\": \"创建演示模式（可选）\",\n    \"Database\": \"数据库\",\n    \"Already added to connections\": \"已添加到连接\"\n  },\n  \"Connection\": {\n    \"Access granted to \": \"授予访问权限给\",\n    \"All Prostgles connection and dashboard data is stored here. Edit at your own risk\": \"所有 Prostgles 连接和仪表板数据都存储在此处。编辑风险自负\"\n  },\n  \"NewConnectionForm\": {\n    \"Realtime requires table triggers to be created as and when needed.\": \"实时功能需要在需要时创建表触发器。\",\n    \"A \\\"prostgles\\\" schema with necessary metadata will also be created\": \"还将创建一个具有必要元数据的“prostgles”模式\",\n    \"Realtime\": \"实时\",\n    \"Needed to allow realtime data view. Requires superuser\": \"需要允许实时数据视图。需要超级用户\",\n    \"Reload schema\": \"重新加载模式\",\n    \"Watch schema\": \"监视模式\",\n    \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\": \"将在模式更改时刷新仪表板和 API。需要超级用户以获得最佳体验\",\n    \"Reject unauthorized\": \"拒绝未经授权的访问\",\n    \"Client Key\": \"客户端密钥\",\n    \"Client Certificate\": \"客户端证书\",\n    \"CA Certificate\": \"CA 证书\",\n    \"SSL Mode\": \"SSL 模式\",\n    \"Connection timeout (ms)\": \"连接超时（毫秒）\",\n    \"Schemas\": \"模式列表\",\n    \"More options\": \"更多选项\",\n    \"Must connect to see schemas\": \"必须连接才能查看模式\",\n    \"You are about to create a new database\": \"您将要创建一个新的数据库\",\n    \"You are about to clone the current database\": \"您即将把当前数据库 {{currDb}} 克隆到 {{newDb}}。这将关闭与当前数据库的所有现有连接！\",\n    \"Create database\": \"创建数据库\",\n    \"Connection type\": \"连接类型\",\n    \"Connection name\": \"连接名称\",\n    \"Socket URL\": \"Socket URL\",\n    \"Socket params (JSON)\": \"Socket 参数 (JSON)\",\n    \"Connection URI\": \"连接 URI\",\n    \"Host\": \"主机\",\n    \"Port\": \"端口\",\n    \"User\": \"用户\",\n    \"Password\": \"密码\",\n    \"Optional\": \"可选\",\n    \"Database\": \"数据库\",\n    \"Test connection\": \"测试连接\"\n  },\n  \"NewConnection\": {\n    \"Delete connection\": \"删除连接\",\n    \"Any related dashboard content will also be deleted\": \"任何相关的仪表板内容也将被删除\",\n    \"Drop database as well\": \"同时删除数据库\",\n    \"You are about to drop\": \"您将要删除\",\n    \"database\": \"数据库\",\n    \"Ensure data is backed up. This action is not reversible\": \"确保数据已备份。此操作不可逆\",\n    \"Clone connection\": \"克隆连接\",\n    \"Cloning connection\": \"正在克隆连接\",\n    \"Connections\": \"连接\",\n    \"Connection not found\": \"未找到连接\",\n    \"Go back to connections\": \"返回连接\"\n  },\n  \"ConnectionConfig\": {\n    \"Must be admin to access this\": \"必须是管理员才能访问此内容\",\n    \"Connection details\": \"连接详情\",\n    \"Status monitor\": \"状态监控\",\n    \"Access control\": \"访问控制\",\n    \"File storage\": \"文件存储\",\n    \"Backup/Restore\": \"备份/恢复\",\n    \"API\": \"API\",\n    \"Table config\": \"表格配置\",\n    \"experimental\": \"实验性功能\",\n    \"Server-side functions\": \"服务器端函数\"\n  },\n  \"ChangePassword\": {\n    \"Change password\": \"修改密码\",\n    \"Old password\": \"旧密码\",\n    \"New password\": \"新密码\",\n    \"Confirm new password\": \"确认新密码\",\n    \"Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter\": \"确保至少 15 个字符或至少 8 个字符，包括一个数字和一个小写字母\",\n    \"Enter your old password\": \"输入您的旧密码\",\n    \"Enter your new password\": \"输入您的新密码\",\n    \"Confirm your new password\": \"确认您的新密码\",\n    \"Passwords do not match\": \"密码不匹配\"\n  },\n  \"Setup2FA\": {\n    \"Enable 2FA\": \"启用 2FA\",\n    \"Confirm code\": \"确认代码\",\n    \"2FA Enabled\": \"已启用 2FA\",\n    \"Along with your username and password, you will be asked to verify your identity using the code from authenticator app.\": \"除了您的用户名和密码之外，您还需要使用身份验证器应用程序中的代码来验证您的身份。\",\n    \"Two-factor authentication\": \"双因素身份验证\",\n    \"Scan\": \"扫描\",\n    \" or tap \": \" 或点击 \",\n    \"the image below with the two-factor authentication app on your phone.\": \"手机上双因素身份验证应用程序中的以下图像。\",\n    \"If you can't use a QR code you can enter this information manually:\": \"如果您无法使用二维码，您可以手动输入以下信息：\",\n    \"Base64 secret\": \"Base64 密钥\",\n    \"Type\": \"类型\",\n    \"Name\": \"名称\",\n    \"Save the Recovery code below. It will be used in case you lose access to your authenticator app:\": \"保存下面的恢复代码。如果您无法访问您的身份验证器应用程序，它将被使用：\",\n    \"Generate QR Code\": \"生成二维码\",\n    \"I can't scan the QR Code\": \"我无法扫描二维码\"\n  },\n  \"Account\": {\n    \"Account details\": \"账户详情\",\n    \"Security\": \"安全\",\n    \"API\": \"API\"\n  },\n  \"APIDetailsWs\": {\n    \"Allowed origin specifies which domains can access this app in a cross-origin manner. Sets the Access-Control-Allow-Origin header. Use '*' or a specific URL to allow API access\": \"允许的来源指定哪些域可以跨域访问此应用程序。设置 Access-Control-Allow-Origin 标头。使用“*”或特定 URL 以允许 API 访问\",\n    \"For testing it is recommended to use \\\"*\\\" as the allowed origin value\": \"对于测试，建议使用“*”作为允许的来源值\",\n    \"Allowed origin\": \"允许的来源\",\n    \"Allowed origin is required\": \"需要允许的来源\",\n    \"Allowed origin not set\": \"未设置允许的来源\",\n    \"Websocket API (recommended)\": \"Websocket API（推荐）\",\n    \"Realtime Isomorphic API using\": \"使用实时同构 API\",\n    \"library. End-to-end type-safety between client & server using the database Typescript schema provided below:\": \"库。使用下面提供的数据库 Typescript 模式在客户端和服务器之间实现端到端类型安全：\",\n    \"Examples\": \"示例\",\n    \"Download typescript schema\": \"下载 Typescript 模式\",\n    \"Database schema types\": \"数据库模式类型\"\n  },\n  \"APIDetailsTokens\": {\n    \"Access tokens (\": \"访问令牌 (\",\n    \"Provide the same level of access as the current account\": \"提供与当前帐户相同的访问级别\",\n    \"Create access token\": \"创建访问令牌\",\n    \"Create token\": \"创建令牌\",\n    \"Expires in\": \"过期时间\",\n    \"Days\": \"天\",\n    \"These token values will not be shown again\": \"这些令牌值将不再显示\",\n    \"Websocket API\": \"Websocket API\",\n    \"HTTP API (base64)\": \"HTTP API (base64)\",\n    \"Already generated. Close and re-open the popup\": \"已生成。关闭并重新打开弹出窗口\"\n  },\n  \"APIDetailsHttp\": {\n    \"Provides similar level of access to the Websocket API with the following limitations: no subscriptions, no sync, no file upload\": \"提供与 Websocket API 类似的访问级别，但具有以下限制：无订阅、无同步、无文件上传\"\n  },\n  \"Sessions\": {\n    \"API tokens\": \"API 令牌\",\n    \"Sessions\": \"会话\",\n    \"Last used\": \"最后使用时间\",\n    \"Created\": \"创建时间\",\n    \"Expires\": \"过期时间\",\n    \"Disable\": \"禁用\",\n    \"User agent\": \"用户代理\",\n    \"Active\": \"活动\",\n    \"Inactive\": \"非活动\",\n    \"No active \": \"无活动 \"\n  },\n  \"TopControls\": {\n    \"Go to Connections\": \"转到连接\",\n    \"Connections\": \"连接\",\n    \"Go to workspace\": \"转到工作区\",\n    \"Menu is pinned\": \"菜单已固定\",\n    \"pinned\": \"已固定\",\n    \"Configure database connection\": \"配置数据库连接\",\n    \"Go back to connection workspace\": \"返回连接工作区\",\n    \"Not allowed for state database\": \"不允许用于状态数据库\",\n    \"Connection configuration\": \"连接配置\"\n  },\n  \"Feedback\": {\n    \"Send feedback\": \"发送反馈\",\n    \"Leave feedback\": \"留下反馈\",\n    \"Feedback\": \"反馈\",\n    \"Already sent\": \"已发送\",\n    \"Must provide some details\": \"必须提供一些详细信息\",\n    \"Feedback was sent! Thanks a lot!\": \"反馈已发送！非常感谢！\",\n    \"Email (optional)\": \"电子邮件（可选）\",\n    \"Details\": \"详细信息\",\n    \"Other options\": \"其他选项\",\n    \"open an issue\": \"提出问题\",\n    \"or\": \"或\",\n    \"email us\": \"给我们发送电子邮件\"\n  },\n  \"AskLLM\": {\n    \"Chat to an AI Assistant to get help with your queries\": \"与 AI 助手聊天以获取查询帮助\",\n    \"AI assistant not available. Talk to the admin\": \"AI 助手不可用。请联系管理员\",\n    \"AI Assistant\": \"询问 AI\"\n  },\n  \"AskLLMChatHeader\": {\n    \"Chat\": \"聊天\",\n    \"New chat\": \"新聊天\",\n    \"No prompt found\": \"未找到提示\",\n    \"Prompt\": \"提示\",\n    \"Chat settings\": \"聊天设置\"\n  },\n  \"ConnectionSelector\": { \"Switch database\": \"切换数据库\" },\n  \"W_SQLMenu\": {\n    \"SQL Editor settings\": \"SQL 编辑器设置\",\n    \"Press\": \"按\",\n    \"to get a list of possible options\": \"获取可能的选项列表\",\n    \"Update options\": \"更新选项\",\n    \"Nothing to update\": \"没有需要更新的内容\",\n    \"Cannot save due to error\": \"由于错误无法保存\",\n    \"Delete query\": \"删除查询\",\n    \"Open SQL file\": \"打开 SQL 文件\",\n    \"Open query from file\": \"从文件中打开查询\",\n    \"Download query\": \"下载查询\",\n    \"Save query as file\": \"将查询保存为文件\",\n    \"Query name\": \"查询名称\",\n    \"Result display mode\": \"结果显示模式\",\n    \"General\": \"常规\",\n    \"Editor options\": \"编辑器选项\",\n    \"Hotkeys\": \"快捷键\"\n  },\n  \"SQLHotkeys\": {\n    \"Show autocomplete suggestions\": \"显示自动完成建议\",\n    \"Execute current statement\": \"执行当前语句\",\n    \"Select current statement\": \"选择当前语句\",\n    \"Show all suggestions\": \"显示所有建议\",\n    \"Show PSQL command queries\": \"显示 PSQL 命令查询\"\n  },\n  \"W_SQLBottomBar\": {\n    \"Execution mode\": \"执行模式\",\n    \"Loop query execution\": \"循环执行查询\",\n    \"Cancel this query (Esc)\": \"取消此查询 (Esc)\",\n    \"Terminate this query\": \"终止此查询\",\n    \"Query running time\": \"查询运行时间\",\n    \"Stop LISTEN\": \"停止监听\",\n    \"repeat every\": \"每\",\n    \"seconds\": \"秒\",\n    \"Run query (CTRL+E, ALT+E)\": \"运行查询 (CTRL+E, ALT+E)\",\n    \"Run\": \"运行\",\n    \"Query running time: \": \"查询运行时间：\",\n    \"Show/Hide table\": \"显示/隐藏表格\",\n    \"Show/Hide code editor\": \"显示/隐藏代码编辑器\",\n    \"Show/Hide notices\": \"显示/隐藏通知\",\n    \"Clear value to show all rows\": \"清除值以显示所有行\",\n    \"Limit\": \"限制\"\n  },\n  \"Window\": {\n    \"Open menu\": \"打开菜单\",\n    \"Menu\": \"菜单\",\n    \"Collapse chart\": \"折叠图表\",\n    \"Detach chart\": \"分离图表\",\n    \"Close chart\": \"关闭图表\",\n    \"Chart options\": \"图表选项\"\n  },\n  \"AddChartMenu\": {\n    \"Layer already added\": \"已添加图层\",\n    \"No\": \"否\",\n    \"No {{chartColumnDataType}} columns available\": \"没有 {{chartColumnDataType}} 列可用\"\n  },\n  \"DashboardMenuHeader\": {\n    \"Opens SQL Query editor\": \"打开 SQL 查询编辑器\",\n    \"SQL Editor\": \"SQL 编辑器\",\n    \"Show quick search menu (CTRL + P)\": \"显示快速搜索菜单 (CTRL + P)\",\n    \"Cannot be used in a low width device\": \"无法在低宽度设备中使用\",\n    \"Pin/Unpin\": \"固定/取消固定\"\n  },\n  \"W_QuickMenu\": {\n    \"Show/Hide filtering\": \"显示/隐藏过滤\",\n    \"Restore minimised charts\": \"恢复最小化的图表\",\n    \"Cross filter tables\": \"交叉过滤表格\",\n    \"Add chart\": \"添加图表\"\n  },\n  \"AddColumnMenu\": {\n    \"Add Computed Field\": \"添加计算字段\",\n    \"Show a computed column\": \"显示计算列\",\n    \"Add Linked Data\": \"添加链接数据\",\n    \"Show data from a related table\": \"显示来自相关表格的数据\",\n    \"Create New Column\": \"创建新列\",\n    \"Create a new column in this table\": \"在此表中创建新列\",\n    \"Create New File Column\": \"创建新文件列\",\n    \"Create a new file column in this table\": \"在此表中创建新文件列\",\n    \"Not enough privileges\": \"权限不足\",\n    \"This is a view. Cannot create columns, must recreate\": \"这是一个视图。无法创建列，必须重新创建\",\n    \"Add column\": \"添加列\",\n    \"New Field\": \"新字段\",\n    \"No foreign keys to/from this table\": \"没有到此表或从此表的外键\",\n    \"Not allowed for nested columns\": \"不允许用于嵌套列\",\n    \"Aggregates and/or Count not allowed with linked \": \"聚合和/或计数不允许与链接的 \",\n    \"Create new column\": \"创建新列\",\n    \"Add Referenced/Linked Fields\": \"添加引用/链接字段\"\n  },\n  \"CreateColumn\": {\n    \"Create column query\": \"创建列查询\",\n    \"Show create column query\": \"显示创建列查询\",\n    \"New column name missing\": \"缺少新列名\",\n    \"Data type missing\": \"缺少数据类型\"\n  },\n  \"W_Table\": { \"Insert row\": \"插入行\" },\n  \"LinkedColumn\": {\n    \"Row\": \"行\",\n    \"Column\": \"列\",\n    \"No headers\": \"无标题\",\n    \"Column name already used. Change to another\": \"列名已被使用。更改为另一个\",\n    \"Must select columns\": \"必须选择列\",\n    \"Must select a table\": \"必须选择表格\",\n    \"Join to and show data from tables that are related through a\": \"连接到并通过以下方式显示来自相关表格的数据\",\n    \"Column label\": \"列标签\",\n    \"More options\": \"更多选项\",\n    \"Layout\": \"布局\",\n    \"Must disable chart first\": \"必须先禁用图表\",\n    \"Join type\": \"连接类型\",\n    \"Added!\": \"已添加！\"\n  }\n}\n"
  },
  {
    "path": "client/src/index.css",
    "content": "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\",\n    \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\",\n    \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n    monospace;\n}\n\nhtml,\nbody,\nbody > #root {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\nbody > #root {\n  display: flex;\n  flex-direction: column;\n}\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  margin: 0;\n}\n\n.media-onclick-cover:hover {\n  background: #cee3f59e;\n}\n\n.sqleditor > section {\n  flex: 1;\n}\n\n.monaco-editor:not(:focus-within) .view-overlays .current-line {\n  border: unset !important;\n}\n\n/* Dark mode */\n/* @media (prefers-color-scheme: dark) {\n  body {\n    background-color: black;\n  }\n}\n\nhtml.dark-theme body {\n  background-color: black;\n} */\n"
  },
  {
    "path": "client/src/index.html.ejs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" dir=\"ltr\">\n\n    <head data-version=\"<%= htmlWebpackPlugin.options.v%>\">\n        <meta charset=\"UTF-8\" />\n        <meta name=\"viewport\"\n            content=\"width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1\">\n        <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\">\n        <link rel=\"icon\" type=\"image/x-icon\" href=\"/prostgles-logo.svg\">\n\n        <link href=\"/util.css\" rel=\"stylesheet\" />\n        <title>Prostgles UI</title>\n    </head>\n\n    <body>\n        <main id=\"root\">\n            <!-- Loader until js bundle loads -->\n            <div class=\"spinner-loader ws-nowrap flex-row gap-1 ai-center m-auto\"\n                style=\"width: 30px; height: 30px; cursor: wait;\"\n            >\n                <svg class=\"spinner f-0\" width=\"30px\" height=\"30px\"\n                    viewBox=\"0 0 66 66\" xmlns=\"http://www.w3.org/2000/svg\">\n                    <circle class=\"path  color-animation \" fill=\"none\" stroke-width=\"6\" stroke-linecap=\"round\" cx=\"33\"\n                        cy=\"33\" r=\"30\"></circle>\n                </svg>\n            </div>\n        </main>\n\n        <!-- Dependencies -->\n        <% if (webpackConfig.mode=='production' ) { %>\n\n            <% } else { %>\n\n                <% } %>\n    </body>\n\n</html>"
  },
  {
    "path": "client/src/index.tsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\nimport { App } from \"./App\";\nimport \"./index.css\";\n\nconst rootNode = document.getElementById(\"root\");\nif (!rootNode) throw new Error(\"Root node not found\");\nconst root = createRoot(rootNode);\nroot.render(\n  <BrowserRouter>\n    <App />\n  </BrowserRouter>,\n);\n"
  },
  {
    "path": "client/src/pages/Account/Account.tsx",
    "content": "import { mdiAccount, mdiApplicationBracesOutline, mdiSecurity } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { getKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { API_ENDPOINTS } from \"@common/utils\";\nimport type { ExtraProps } from \"../../App\";\nimport { FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Tabs from \"@components/Tabs\";\nimport { PasswordlessSetup } from \"../../dashboard/AccessControl/PasswordlessSetup\";\nimport { APIDetails } from \"../../dashboard/ConnectionConfig/APIDetails/APIDetails\";\nimport { SmartForm } from \"../../dashboard/SmartForm/SmartForm\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { ChangePassword } from \"./ChangePassword\";\nimport { Sessions } from \"./Sessions\";\nimport { Setup2FA } from \"./Setup2FA\";\n\ntype AccountProps = ExtraProps;\n\nexport const Account = (props: AccountProps) => {\n  const { dbs, dbsTables, dbsMethods, user } = props;\n\n  const [searchParams, setSearchParams] = useSearchParams();\n  const { data: dbsConnection } = dbs.connections.useFindOne({\n    is_state_db: true,\n  });\n\n  const notAllowedBanner = (\n    <InfoRow>\n      <h2>Not allowed</h2>\n      <p>You are not allowed to access</p>\n    </InfoRow>\n  );\n  if (!user || user.type === \"public\") {\n    return notAllowedBanner;\n  }\n\n  if (user.passwordless_admin) {\n    return (\n      <div\n        className=\" f-1 flex-col w-full gap-1 p-p5 o-auto\"\n        style={{ maxWidth: \"700px\" }}\n      >\n        <PasswordlessSetup {...props} />\n      </div>\n    );\n  }\n\n  const allowedColumns = [\n    \"id\",\n    \"username\",\n    \"email\",\n    \"name\",\n    \"type\",\n    \"status\",\n    \"options\",\n    \"created_at\",\n    \"auth_provider\",\n  ];\n  const sectionItems = {\n    details: {\n      label: t.Account[\"Account details\"],\n      leftIconPath: mdiAccount,\n      content: (\n        <SmartForm\n          label=\"\"\n          db={dbs as DBHandlerClient}\n          methods={dbsMethods}\n          tableName=\"users\"\n          tables={dbsTables}\n          rowFilter={[{ fieldName: \"id\", value: user.id }]}\n          confirmUpdates={true}\n          columnFilter={(c) => allowedColumns.includes(c.name)}\n          disabledActions={[\"clone\", \"delete\"]}\n        />\n      ),\n    },\n    security: {\n      label: t.Account.Security,\n      leftIconPath: mdiSecurity,\n      content: (\n        <div className=\"flex-col gap-1 px-1 f-1\">\n          <FlexRow>\n            <Setup2FA\n              user={user}\n              dbsMethods={dbsMethods}\n              onChange={console.log}\n            />\n            <ChangePassword dbsMethods={dbsMethods} />\n          </FlexRow>\n\n          <Sessions displayType=\"web_session\" {...props} />\n        </div>\n      ),\n    },\n    api: {\n      label: t.Account.API,\n      leftIconPath: mdiApplicationBracesOutline,\n      content: (\n        <div className=\"flex-col gap-1 px-1 f-1\">\n          {dbsConnection ?\n            <APIDetails\n              {...props}\n              projectPath={API_ENDPOINTS.WS_DBS}\n              connection={dbsConnection}\n            />\n          : notAllowedBanner}\n        </div>\n      ),\n    },\n  };\n\n  const sectionItemKeys = getKeys(sectionItems);\n\n  return (\n    <div className=\"Account f-1 flex-col w-full o-auto ai-center \">\n      <div\n        className=\"flex-col f-1 min-h-0 pt-1 w-full\"\n        style={{ maxWidth: \"800px\" }}\n      >\n        <Tabs\n          variant={{\n            controlsBreakpoint: 200,\n            contentBreakpoint: 500,\n            controlsCollapseWidth: 350,\n          }}\n          className=\"f-1 shadow\"\n          activeKey={\n            sectionItemKeys.find((s) => s === searchParams.get(\"section\")) ??\n            sectionItemKeys[0]\n          }\n          onChange={(section) => {\n            setSearchParams({ section: section as string });\n          }}\n          items={sectionItems}\n          contentClass=\"f-1 o-autdo flex-row jc-center bg-color-2 \"\n          onRender={(item) => (\n            <div className=\"flex-col f-1 max-w-800 min-w-0 bg-color-0 shadow w-full\">\n              <h2 style={{ paddingLeft: \"18px\" }} className=\" max-h-fit\">\n                {item.label}\n              </h2>\n              <div\n                className={\" f-1 o-auto flex-row \"}\n                style={{ alignSelf: \"stretch\" }}\n              >\n                {item.content}\n              </div>\n            </div>\n          )}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Account/ChangePassword.tsx",
    "content": "import React, { useState } from \"react\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport Btn from \"@components/Btn\";\nimport FormField from \"@components/FormField/FormField\";\nimport type { Prgl } from \"../../App\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { Success } from \"@components/Animations\";\nimport { t } from \"../../i18n/i18nUtils\";\n\nexport const ChangePassword = ({ dbsMethods }: Pick<Prgl, \"dbsMethods\">) => {\n  const [password, setPassword] = useState(\"\");\n  const [newPassword, setNewPassword] = useState(\"\");\n  const [confirmPassword, setConfirmPassword] = useState(\"\");\n  const issue =\n    !password ? t.ChangePassword[\"Enter your old password\"]\n    : !newPassword ? t.ChangePassword[\"Enter your new password\"]\n    : !confirmPassword ? t.ChangePassword[\"Confirm your new password\"]\n    : newPassword !== confirmPassword ?\n      t.ChangePassword[\"Passwords do not match\"]\n    : undefined;\n  const [success, setSuccess] = useState(false);\n  return (\n    <PopupMenu\n      title={t.ChangePassword[\"Change password\"]}\n      data-command=\"Account.ChangePassword\"\n      positioning=\"center\"\n      button={\n        <Btn color=\"action\" variant=\"filled\">\n          {t.ChangePassword[\"Change password\"]}\n        </Btn>\n      }\n      contentClassName=\"flex-col p-1 gap-1\"\n      clickCatchStyle={{ opacity: 0.5 }}\n      render={(pClose) => {\n        if (success) return <Success />;\n        return (\n          <>\n            <FormField\n              label={t.ChangePassword[\"Old password\"]}\n              type=\"password\"\n              value={password}\n              onChange={setPassword}\n            />\n            <FormField\n              label={t.ChangePassword[\"New password\"]}\n              autoComplete=\"new-password\"\n              type=\"password\"\n              value={newPassword}\n              onChange={setNewPassword}\n            />\n            <FormField\n              label={t.ChangePassword[\"Confirm new password\"]}\n              autoComplete=\"new-password\"\n              type=\"password\"\n              value={confirmPassword}\n              onChange={setConfirmPassword}\n            />\n            <InfoRow variant=\"naked\" className=\"mt-1\" iconPath=\"\">\n              {\n                t.ChangePassword[\n                  \"Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter\"\n                ]\n              }\n            </InfoRow>\n          </>\n        );\n      }}\n      footerButtons={(pClose) => [\n        {\n          label: t.common.Cancel,\n          onClickClose: true,\n        },\n        {\n          label: t.ChangePassword[\"Change password\"],\n          color: \"action\",\n          variant: \"filled\",\n          disabledInfo: issue,\n          onClickPromise: async (e) => {\n            await dbsMethods.changePassword!(password, newPassword);\n            setSuccess(true);\n            setTimeout(() => {\n              pClose!(e);\n              setPassword(\"\");\n              setNewPassword(\"\");\n              setConfirmPassword(\"\");\n              setSuccess(false);\n            }, 2000);\n          },\n        },\n      ]}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Account/Sessions.tsx",
    "content": "import {\n  mdiApple,\n  mdiAppleSafari,\n  mdiCellphone,\n  mdiDelete,\n  mdiFirefox,\n  mdiGoogleChrome,\n  mdiLaptop,\n  mdiLinux,\n  mdiMicrosoftEdge,\n  mdiMicrosoftWindows,\n} from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport type { DivProps } from \"@components/Flex\";\nimport { classOverride, FlexRow } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SmartCardList } from \"../../dashboard/SmartCardList/SmartCardList\";\nimport {\n  getPGIntervalAsText,\n  StyledInterval,\n} from \"../../dashboard/W_SQL/customRenderers\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport type { FieldConfig } from \"../../dashboard/SmartCard/SmartCard\";\n\ntype SessionsProps = Pick<Prgl, \"dbs\" | \"dbsTables\" | \"user\" | \"dbsMethods\"> & {\n  displayType: \"web_session\" | \"api_token\";\n  className?: string;\n};\n\nexport const getActiveTokensFilter = (\n  type: SessionsProps[\"displayType\"],\n  user_id: string | undefined,\n) =>\n  ({\n    user_id,\n    type: type === \"api_token\" ? \"api_token\" : \"web\",\n    $filter: [{ $ageNow: [\"expires\"] }, \"<\", \"0\"],\n    active: true,\n  }) as AnyObject;\n\nexport const Sessions = ({\n  dbs,\n  dbsTables,\n  user,\n  displayType,\n  className = \"\",\n  dbsMethods,\n}: SessionsProps) => {\n  const tokenMode = displayType === \"api_token\";\n  const sessionLabel =\n    tokenMode ? t.Sessions[\"API tokens\"] : t.Sessions[\"Sessions\"];\n\n  const listProps = useMemo(\n    () => ({\n      title: tokenMode ? undefined : ({ count }) => `${sessionLabel} ${count}`,\n      filter: getActiveTokensFilter(displayType, user?.id),\n      style: {\n        maxHeight: \"40vh\",\n      },\n      orderBy: {\n        key: \"id_num\",\n        asc: false,\n      },\n      fieldConfigs: [\n        { name: \"is_connected\", hide: true },\n        {\n          name: \"active\",\n          label: \" \",\n          renderMode: \"valueNode\",\n          render: (v) => (\n            <StatusDotCircleIcon\n              className=\"my-p5\"\n              title={v ? t.Sessions.Active : t.Sessions.Inactive}\n              color={v ? \"green\" : \"red\"}\n            />\n          ),\n        },\n        {\n          name: \"user_agent\",\n          hide: displayType === \"api_token\",\n          renderMode: \"valueNode\",\n          render: (v, row) => {\n            const os = (\n              v.match(/Windows|Linux|Mac|Android|iOS/i)?.[0] ?? \"\"\n            ).toLowerCase();\n            const browser = (\n              v.match(/Chrome|Firefox|Safari|Edge|Opera/i)?.[0] ?? \"\"\n            ).toLowerCase();\n            return (\n              <PopupMenu\n                title={t.Sessions[\"User agent\"]}\n                button={\n                  <FlexRow className=\"gap-0 pointer\">\n                    <Btn\n                      title={\n                        row.is_connected ? \"User is connected\" : (\n                          \"User is offline\"\n                        )\n                      }\n                      style={{ color: row.is_connected ? \"green\" : undefined }}\n                      iconPath={isMobileUserAgent(v) ? mdiCellphone : mdiLaptop}\n                    />\n                    {!!os && (\n                      <Icon\n                        title={os}\n                        path={\n                          os === \"linux\" ? mdiLinux\n                          : os === \"windows\" ?\n                            mdiMicrosoftWindows\n                          : os === \"mac\" || os === \"ios\" ?\n                            mdiApple\n                          : \"\"\n                        }\n                      />\n                    )}\n                    {!!browser && (\n                      <Icon\n                        title={browser}\n                        path={\n                          browser === \"chrome\" ? mdiGoogleChrome\n                          : browser === \"firefox\" ?\n                            mdiFirefox\n                          : browser === \"safari\" ?\n                            mdiAppleSafari\n                          : browser === \"edge\" ?\n                            mdiMicrosoftEdge\n                          : \"\"\n                        }\n                      />\n                    )}\n                  </FlexRow>\n                }\n                render={() => <div className=\"\">{v}</div>}\n              />\n            );\n          },\n        },\n        {\n          name: \"last_usedd\",\n          select: { $ageNow: [\"last_used\", null, \"second\"] },\n          label: t.Sessions[\"Last used\"],\n          render: (value) => <StyledInterval value={value} />,\n        },\n        { name: \"ip_address\", hide: displayType === \"api_token\" },\n        {\n          name: \"createdd\",\n          select: { $ageNow: [\"created\", null, \"second\"] },\n          label: t.Sessions.Created,\n          render: (v) => getPGIntervalAsText(v, true, true),\n        },\n        {\n          name: \"expiress\",\n          select: { $ageNow: [\"expires\", null, \"hour\"] },\n          label: t.Sessions.Expires,\n          hideIf: (_, row) => !row.active,\n          render: (v) => {\n            return getPGIntervalAsText(v, true, true);\n          },\n        },\n        {\n          name: \"id_num\",\n          label: \" \",\n          style: { marginLeft: \"auto\" },\n          renderMode: \"valueNode\",\n          render: (v) =>\n            !!v && (\n              <Btn\n                title={t.Sessions.Disable}\n                variant=\"faded\"\n                color=\"danger\"\n                iconPath={mdiDelete}\n                onClickPromise={async () => {\n                  // return dbs.sessions.update({ id_num: v }, { active: false })\n                  await dbs.sessions.delete({ id_num: v });\n                }}\n              />\n            ),\n        },\n      ] satisfies FieldConfig[],\n    }),\n    [dbs.sessions, displayType, sessionLabel, tokenMode, user?.id],\n  );\n  if (!user) return null;\n\n  return (\n    <SmartCardList\n      className={\"min-h-0 f-1 \" + className}\n      db={dbs as DBHandlerClient}\n      methods={dbsMethods}\n      tableName=\"sessions\"\n      tables={dbsTables}\n      realtime={true}\n      showEdit={false}\n      limit={10}\n      noDataComponentMode=\"hide-all\"\n      noDataComponent={\n        <InfoRow color=\"info\" style={{ alignItems: \"center\" }}>\n          {t.Sessions[\"No active \"]}\n          {sessionLabel}\n        </InfoRow>\n      }\n      {...listProps}\n    />\n  );\n};\n\nfunction isMobileUserAgent(v: string) {\n  const toMatch = [\n    /Android/i,\n    /webOS/i,\n    /iPhone/i,\n    /iPad/i,\n    /iPod/i,\n    /BlackBerry/i,\n    /Windows Phone/i,\n  ];\n\n  return toMatch.some((toMatchItem) => {\n    return v.match(toMatchItem);\n  });\n}\n\ntype StatusDotCircleIconProps = {\n  title?: string;\n  color: \"green\" | \"red\" | \"gray\";\n} & Pick<DivProps, \"className\" | \"style\">;\nexport const StatusDotCircleIcon = ({\n  title,\n  color,\n  style = {},\n  className = \"\",\n}: StatusDotCircleIconProps) => {\n  return (\n    <div\n      title={title}\n      className={classOverride(\"shadow\", className)}\n      style={{\n        borderRadius: \"100%\",\n        width: \"12px\",\n        height: \"12px\",\n        background: `var(--${color}-500)`,\n        ...style,\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Account/Setup2FA.tsx",
    "content": "import React, { useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport { SuccessMessage } from \"@components/Animations\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { ExpandSection } from \"@components/ExpandSection\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { QRCodeImage } from \"@components/QRCodeImage\";\nimport type { UserData } from \"../../dashboard/Dashboard/dashboardUtils\";\nimport { t } from \"../../i18n/i18nUtils\";\n\nexport const Setup2FA = (\n  props: Pick<Prgl, \"dbsMethods\"> & { user: UserData; onChange: VoidFunction },\n) => {\n  const { user, dbsMethods, onChange } = props;\n  const [OTP, setOTP] = useState<{\n    url: string;\n    secret: string;\n    recoveryCode: string;\n  }>();\n  const [err, setErr] = useState<any>();\n  const [codeConfirm, setCodeConfirm] = useState<number>();\n  const [enabled, setEnabled] = useState(false);\n\n  const reset = () => {\n    setCodeConfirm(undefined);\n    setOTP(undefined);\n    setErr(null);\n  };\n\n  const enable2FA = async (closePopup: VoidFunction) => {\n    try {\n      if (!dbsMethods.enable2FA) throw \"Something went wrong\";\n      await dbsMethods.enable2FA(codeConfirm! + \"\");\n\n      setTimeout(() => {\n        onChange();\n        closePopup();\n        reset();\n      }, 1500);\n      setEnabled(true);\n    } catch (err) {\n      setErr(err);\n    }\n  };\n  const imageSize = 250;\n  return user.has_2fa_enabled ?\n      <Btn\n        color=\"warn\"\n        variant=\"faded\"\n        data-command=\"Setup2FA.Disable\"\n        onClickMessage={async (_, setMsg) => {\n          await dbsMethods.disable2FA?.();\n          setMsg({ ok: \"Disabled!\" }, onChange);\n        }}\n      >\n        Disable 2FA\n      </Btn>\n    : <PopupMenu\n        title={\n          <div className=\"bold\">{t.Setup2FA[\"Two-factor authentication\"]}</div>\n        }\n        data-command=\"Setup2FA.Enable\"\n        button={\n          <Btn variant=\"filled\" color=\"green\">\n            {t.Setup2FA[\"Enable 2FA\"]}\n          </Btn>\n        }\n        clickCatchStyle={{ opacity: 0.5 }}\n        positioning=\"center\"\n        initialState={{\n          enabled: false,\n          canvasNode: null as HTMLCanvasElement | null,\n        }}\n        onClose={reset}\n        contentClassName=\"p-1\"\n        footer={\n          !OTP ? undefined : (\n            (closePopup) => (\n              <div\n                className=\"flex-col gap-1 w-full\"\n                onKeyDown={(e) => {\n                  if (e.key === \"Enter\") enable2FA(closePopup);\n                }}\n              >\n                <FormField\n                  data-command=\"Setup2FA.Enable.ConfirmCode\"\n                  value={codeConfirm}\n                  type=\"number\"\n                  label={t.Setup2FA[\"Confirm code\"]}\n                  onChange={(codeConfirm: number) => {\n                    setCodeConfirm(codeConfirm);\n                  }}\n                  rightContentAlwaysShow={true}\n                  rightContent={\n                    <Btn\n                      variant=\"filled\"\n                      color=\"action\"\n                      className=\"ml-2\"\n                      data-command=\"Setup2FA.Enable.Confirm\"\n                      onClickMessage={() => enable2FA(closePopup)}\n                    >\n                      {t.Setup2FA[\"Enable 2FA\"]}\n                    </Btn>\n                  }\n                />\n              </div>\n            )\n          )\n        }\n        render={() =>\n          enabled ?\n            <SuccessMessage message=\"2FA Enabled\"></SuccessMessage>\n          : <div\n              className=\"flex-col gap-1 ai-center\"\n              style={{ maxWidth: `${imageSize + 100}px` }}\n            >\n              {!OTP && (\n                <div>\n                  {\n                    t.Setup2FA[\n                      \"Along with your username and password, you will be asked to verify your identity using the code from authenticator app.\"\n                    ]\n                  }\n                </div>\n              )}\n\n              {!!OTP && (\n                <>\n                  <div>\n                    {t.Setup2FA.Scan}{\" \"}\n                    <a href={OTP.url} target=\"_blank\" rel=\"noreferrer\">\n                      {t.Setup2FA[\" or tap \"]}\n                    </a>{\" \"}\n                    {\n                      t.Setup2FA[\n                        \"the image below with the two-factor authentication app on your phone.\"\n                      ]\n                    }{\" \"}\n                  </div>\n                  <ExpandSection\n                    label={t.Setup2FA[\"I can't scan the QR Code\"]}\n                    buttonProps={{\n                      variant: \"outline\",\n                      \"data-command\": \"Setup2FA.Enable.CantScanQR\",\n                    }}\n                    iconPath=\"\"\n                  >\n                    <div className=\"flex-col gap-p5 ai-start jc-start\">\n                      <div>\n                        {\n                          t.Setup2FA[\n                            \"If you can't use a QR code you can enter this information manually:\"\n                          ]\n                        }\n                      </div>\n                      <div className=\"flex-col ml-1 pl-2\">\n                        <Chip\n                          variant=\"naked\"\n                          label={t.Setup2FA.Name}\n                          value={user.username}\n                        />\n                        <Chip\n                          variant=\"naked\"\n                          label=\"Issuer\"\n                          value={\"Prostgles UI\"}\n                        />\n                        <Chip\n                          variant=\"naked\"\n                          label={t.Setup2FA[\"Base64 secret\"]}\n                          value={OTP.secret}\n                          data-command=\"Setup2FA.Enable.Base64Secret\"\n                        />\n                        <Chip\n                          variant=\"naked\"\n                          label={t.Setup2FA.Type}\n                          value={\"Time-based OTP\"}\n                        />\n                      </div>\n                    </div>\n                  </ExpandSection>\n                </>\n              )}\n\n              <QRCodeImage\n                url={OTP?.url}\n                size={imageSize}\n                variant=\"href-wrapped\"\n              />\n\n              {OTP?.recoveryCode && (\n                <div\n                  className=\"f-1 flex-col gap-1 p-1\"\n                  style={{ wordBreak: \"break-word\" }}\n                >\n                  <InfoRow variant=\"naked\" className=\"ai-start\">\n                    <div className=\"flex-col gap-1\">\n                      <div>\n                        {\n                          t.Setup2FA[\n                            \"Save the Recovery code below. It will be used in case you lose access to your authenticator app:\"\n                          ]\n                        }\n                      </div>\n                      <div className=\"bold\" id=\"totp_recovery_code\">\n                        {OTP.recoveryCode}\n                      </div>\n                    </div>\n                  </InfoRow>\n                </div>\n              )}\n\n              {!OTP && (\n                <Btn\n                  variant=\"filled\"\n                  color=\"action\"\n                  data-command=\"Setup2FA.Enable.GenerateQR\"\n                  onClickPromise={async () => {\n                    const setup = await dbsMethods.create2FA?.();\n                    if (!setup?.url) {\n                      throw \"Something went wrong. OTP URL not received\";\n                    }\n\n                    setOTP(setup);\n                  }}\n                >\n                  {t.Setup2FA[\"Generate QR Code\"]}\n                </Btn>\n              )}\n\n              {err && (\n                <ErrorComponent\n                  data-command=\"Setup2FA.error\"\n                  error={err}\n                  findMsg={true}\n                />\n              )}\n            </div>\n        }\n      />;\n};\n"
  },
  {
    "path": "client/src/pages/AccountMenu.tsx",
    "content": "import { useNavigate } from \"react-router-dom\";\nimport React from \"react\";\nimport { NavLink } from \"react-router-dom\";\nimport type { ClientUser } from \"../App\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport Btn from \"@components/Btn\";\nimport {\n  mdiAccountOutline,\n  mdiAccountStarOutline,\n  mdiLogin,\n  mdiLogout,\n} from \"@mdi/js\";\nimport { MenuList } from \"@components/MenuList\";\nimport { isDefined } from \"prostgles-types\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { t } from \"../i18n/i18nUtils\";\nimport { ROUTES } from \"@common/utils\";\n\ntype P = {\n  forNavBar?: boolean;\n  user: ClientUser | undefined;\n};\n\nexport const AccountMenu = ({ user, forNavBar }: P) => {\n  const navigate = useNavigate();\n\n  if (!user || user.type === \"public\") {\n    return (\n      <NavLink\n        key={\"account\"}\n        data-key={\"/account\"}\n        className={\n          \"text-0 ml-auto flex-row ai-center gap-p5  bb font-16 min-w-0\"\n        }\n        to={ROUTES.LOGIN}\n      >\n        <Icon className=\"f-0\" path={mdiLogin} size={1} />\n        <div className=\"f-1 min-w-0 text-ellipsis ws-no-wrap\">\n          {t.common[\"Login\"]}\n        </div>\n      </NavLink>\n    );\n  }\n\n  const iconPath =\n    user.type === \"admin\" ? mdiAccountStarOutline : mdiAccountOutline;\n\n  const cannotLogout = user.passwordless_admin;\n\n  if (forNavBar) {\n    return (\n      <>\n        <NavLink\n          key={\"account\"}\n          data-key={\"/account\"}\n          className={\n            \"text-0 ml-auto flex-row ai-center gap-p5  bb font-16 min-w-0\"\n          }\n          to={ROUTES.ACCOUNT}\n        >\n          <Icon className=\"f-0\" path={iconPath} size={1} />\n          <div className=\"f-1 min-w-0 text-ellipsis ws-no-wrap\">\n            {user.name || user.username || \"??\"}\n          </div>\n        </NavLink>\n\n        {!cannotLogout && (\n          <>\n            <form\n              id=\"logout-form\"\n              action={ROUTES.LOGOUT}\n              method=\"POST\"\n              style={{ display: \"none\" }}\n            ></form>\n            <a\n              key={\"logout\"}\n              data-command=\"NavBar.logout\"\n              href=\"#\"\n              onClick={() => {\n                (\n                  document.getElementById(\n                    \"logout-form\",\n                  ) as HTMLFormElement | null\n                )?.submit();\n              }}\n              className=\"ws-nowrap text-0 font-16 flex-row ai-center gap-p5\"\n            >\n              <Icon className=\"f-0\" path={mdiLogout} size={1} />\n              <div className=\"f-1 min-w-0 text-ellipsis ws-no-wrap\">\n                {t.common[\"Logout\"]}\n              </div>\n            </a>\n          </>\n        )}\n      </>\n    );\n  }\n  const { isLowWidthScreen } = window;\n  return (\n    <PopupMenu\n      positioning=\"beneath-right\"\n      contentStyle={{ padding: 0, borderRadius: 0 }}\n      rootStyle={{ borderRadius: 0 }}\n      button={\n        <Btn\n          style={{\n            paddingBottom: 0,\n            color: \"var(--gray-100)\",\n            background: \"var(--gray-700)\",\n          }}\n          size=\"default\"\n          title={user.username || \"Account\"}\n          className=\" flex-col text-white b g-gray-700\"\n        >\n          <Icon path={iconPath} size={!isLowWidthScreen ? 0.75 : 1} />\n          {!isLowWidthScreen && (\n            <div className=\" text-2 font-12 ws-nowrap\">{user.username}</div>\n          )}\n        </Btn>\n      }\n    >\n      <MenuList\n        style={{ borderRadius: 0 }}\n        items={[\n          {\n            label: t.AccountMenu[\"Account\"],\n            leftIconPath:\n              user.type === \"admin\" ? mdiAccountStarOutline : mdiAccountOutline,\n            onPress: () => {\n              navigate(ROUTES.ACCOUNT);\n            },\n          },\n          cannotLogout ? undefined : (\n            {\n              label: t.common[\"Logout\"],\n              leftIconPath: mdiLogout,\n              onPress: () => {\n                window.location.href = ROUTES.LOGOUT;\n              },\n            }\n          ),\n        ].filter(isDefined)}\n      />\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Alerts.tsx",
    "content": "import { mdiBellBadgeOutline, mdiDelete } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React, { useMemo } from \"react\";\nimport { NavLink } from \"react-router-dom\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { ROUTES } from \"@common/utils\";\nimport type { Prgl } from \"../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SmartCardList } from \"../dashboard/SmartCardList/SmartCardList\";\nimport { StyledInterval } from \"../dashboard/W_SQL/customRenderers\";\nimport type { FieldConfig } from \"../dashboard/SmartCard/SmartCard\";\n\nexport const Alerts = (prgl: Prgl) => {\n  const { connectionId, dbs, user } = prgl;\n  const user_id = user?.id;\n  const alertsFilter = useMemo(() => {\n    return {\n      $existsJoined: { \"database_configs.connections\": { id: connectionId } },\n      $notExistsJoined: { alert_viewed_by: { user_id } },\n    } as Record<string, any>;\n  }, [connectionId, user_id]);\n  const { data: alerts } = dbs.alerts.useSubscribe(alertsFilter);\n\n  const listProps = useMemo(() => {\n    const fieldConfigs = [\n      {\n        name: \"severity\",\n        hide: true,\n      },\n      {\n        name: \"age\",\n        select: {\n          $ageNow: [\"created\"],\n        },\n        hide: true,\n      },\n      {\n        name: \"section\",\n        hide: true,\n      },\n      {\n        name: \"connection_id\",\n        hide: true,\n      },\n      {\n        name: \"message\",\n        renderMode: \"valueNode\",\n        render: (message, row) => {\n          const {\n            severity,\n            title,\n            age,\n            id: alert_id,\n            connection_id,\n            section,\n          } = row as DBSSchema[\"alerts\"] & { age: any };\n          return (\n            <FlexRow className=\"ai-start\">\n              <InfoRow\n                variant=\"naked\"\n                color={\n                  severity === \"error\" ? \"danger\"\n                  : severity === \"warning\" ?\n                    \"warning\"\n                  : \"info\"\n                }\n              >\n                <FlexCol>\n                  <StyledInterval\n                    value={age}\n                    style={{ color: \"var(--text-0)\" }}\n                  />\n                  {title && <div className=\"bold\">{title}</div>}\n                  <div>{message}</div>\n                  {connection_id && section && (\n                    <NavLink\n                      to={`${ROUTES.CONFIG}/${connection_id}?section=${section}`}\n                    >\n                      Go to issue\n                    </NavLink>\n                  )}\n                </FlexCol>\n              </InfoRow>\n              <Btn\n                iconPath={mdiDelete}\n                onClickPromise={() =>\n                  dbs.alert_viewed_by.insert({\n                    alert_id,\n                    user_id,\n                  })\n                }\n              />\n            </FlexRow>\n          );\n        },\n      },\n      {\n        name: \"id\",\n        hide: true,\n      },\n    ] satisfies FieldConfig[];\n    const rowProps = {\n      className: \"ai-center\",\n    };\n    return {\n      fieldConfigs,\n      rowProps,\n    };\n  }, [dbs, user_id]);\n\n  if (!alerts?.length) return null;\n\n  return (\n    <PopupMenu\n      button={\n        <div>\n          <Btn\n            variant=\"faded\"\n            color={alerts.length ? \"action\" : undefined}\n            iconPath={alerts.length ? mdiBellBadgeOutline : mdiBellBadgeOutline}\n            disabledInfo={alerts.length ? undefined : \"No alerts\"}\n          />\n        </div>\n      }\n      positioning=\"beneath-center\"\n      contentStyle={{\n        background: \"transparent\",\n      }}\n      onClickClose={false}\n      rootStyle={{\n        border: \"unset\",\n        boxShadow: \"unset\",\n        background: \"transparent\",\n      }}\n    >\n      {!!alerts.length && (\n        <SmartCardList\n          db={dbs as DBHandlerClient}\n          methods={prgl.dbsMethods}\n          tables={prgl.dbsTables}\n          tableName={\"alerts\"}\n          realtime={true}\n          filter={alertsFilter}\n          showEdit={false}\n          {...listProps}\n        />\n      )}\n    </PopupMenu>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ComponentList.tsx",
    "content": "import { mdiFullscreen } from \"@mdi/js\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { isEmpty } from \"../utils/utils\";\nimport { useLocation } from \"react-router-dom\";\nimport Loading from \"@components/Loader/Loading\";\n\nconst buttonHeights: Record<string, number> = {};\nconst buttonLoadingHeights: Record<string, number> = {};\n\nexport const ComponentList = () => {\n  const { hash } = useLocation();\n  const [isLoading, setIsLoading] = useState(false);\n  const [checkMessage, setCheckMessage] = useState(\n    \"Checking button heights...\",\n  );\n  const isLoaderTest = hash === \"#loader-test\";\n\n  const ref = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    setTimeout(() => {\n      if (ref.current) {\n        ref.current.querySelectorAll(\"[data-size]\").forEach((container) => {\n          const size = container.getAttribute(\"data-size\");\n          if (!size) return;\n          const buttons = container.querySelectorAll(\"button\");\n          const firstButtonRect = buttons[0]?.getBoundingClientRect();\n          if (!firstButtonRect)\n            throw new Error(\"No buttons found in container\");\n          const height = firstButtonRect.height;\n          if (!isLoading) {\n            buttonHeights[size] = height;\n          } else {\n            buttonLoadingHeights[size] = height;\n          }\n        });\n        if (!isLoading && isEmpty(buttonLoadingHeights)) {\n          setIsLoading(true);\n        } else if (isLoading) {\n          if (isLoaderTest) return;\n          let allHeightsMatch = true as boolean;\n          // If we are loading, we want to ensure the heights are set for loading state\n          Object.keys(buttonHeights).forEach((size) => {\n            if (buttonLoadingHeights[size] !== buttonHeights[size]) {\n              allHeightsMatch = false;\n              const msg = `Button heights mismatch for size ${size}. Loading height: ${buttonLoadingHeights[size]}, Normal height: ${buttonHeights[size]}`;\n              setCheckMessage(msg);\n            }\n          });\n          if (allHeightsMatch) {\n            setCheckMessage(\"All button heights match for loading state.\");\n          }\n          setIsLoading(false);\n        }\n      }\n    }, 1000);\n  }, [isLoading, isLoaderTest]);\n\n  if (hash === \"#loader\") {\n    return <Loading />;\n  }\n\n  return (\n    <FlexCol ref={ref} className=\"ComponentList f-1 min-w-0 p-2 o-auto\">\n      {checkMessage}\n      {([\"micro\", \"small\", \"default\", \"large\"] as const).map((size) => (\n        <FlexCol key={size} data-size={size} className=\"mb-2\">\n          Buttons - {size} - {buttonHeights[size]}px\n          {[0, 1, 2, 3, 4].map((n) => (\n            <FlexRow key={n + size} className={`bg-color-${n}`}>\n              {(\n                [\"icon\", undefined, \"faded\", \"outline\", \"no-icon\"] as const\n              ).map((variant, i) => (\n                <FlexCol key={variant + size} className=\"p-2\">\n                  {([undefined, \"action\", \"danger\"] as const).map((color) => (\n                    <Btn\n                      key={(color ?? \"c\") + (variant ?? \"v\") + i}\n                      loading={isLoading}\n                      iconPath={\n                        variant === \"no-icon\" ? undefined : mdiFullscreen\n                      }\n                      variant={variant === \"no-icon\" ? \"outline\" : variant}\n                      size={size}\n                      color={color}\n                      children={\n                        (variant && variant !== \"icon\") || color ?\n                          `${variant} ${color}`\n                        : undefined\n                      }\n                    />\n                  ))}\n                </FlexCol>\n              ))}\n            </FlexRow>\n          ))}\n        </FlexCol>\n      ))}\n      {Array(isLoaderTest ? 5000 : 0)\n        .fill(0)\n        .map((_, i) => (\n          <div key={i} style={{ top: 0, left: 0, position: \"absolute\" }}>\n            Increasing node count\n          </div>\n        ))}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Connections/Connection.tsx",
    "content": "import { mdiAccountMultiple } from \"@mdi/js\";\nimport React from \"react\";\nimport { NavLink } from \"react-router-dom\";\nimport { ROUTES } from \"@common/utils\";\nimport type { ExtraProps } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRowWrap } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { WspIconPath } from \"../../dashboard/AccessControl/ExistingAccessRules\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { ConnectionActionBar } from \"./ConnectionActionBar\";\nimport type {\n  AdminConnectionModel,\n  BasicConnectionModel,\n} from \"./useConnections\";\n\nexport type ConnectionProps = (\n  | {\n      connection: AdminConnectionModel;\n      isAdmin: true;\n    }\n  | {\n      connection: BasicConnectionModel;\n      isAdmin: false;\n    }\n) &\n  Pick<ExtraProps, \"dbs\" | \"dbsMethods\" | \"dbsTables\" | \"theme\"> & {\n    showDbName: boolean;\n  };\n\nconst getConnectionPath = (connectionId: string, wid?: string) =>\n  `${ROUTES.CONNECTIONS}/${connectionId}` + (wid ? `?workspaceId=${wid}` : \"\");\n\nexport const Connection = (props: ConnectionProps) => {\n  const { connection, isAdmin } = props;\n  const noWorkspaceAndCannotCreateOne =\n    !connection.workspaces.length && !(props.dbs.workspaces as any).insert;\n\n  if (noWorkspaceAndCannotCreateOne) {\n    return (\n      <InfoRow className=\"shadow bg-color-0\">\n        <strong>{connection.id}</strong>\n        <div>\n          Issue with connection permissions: No published workspace and not\n          allowed to create workspaces\n        </div>\n      </InfoRow>\n    );\n  }\n\n  const showWorkspaces =\n    !!connection.workspaces.length &&\n    connection.workspaces.map((w) => w.name).join(\"\") !== \"default\";\n\n  const showAccessInfo = isAdmin && connection.access_control.length > 0;\n\n  /** Remove published workspaces that have been cloned\n   * TODO: remove workspaces from other users\n   */\n  const workspaces = connection.workspaces.filter((w) => {\n    return !connection.workspaces.some(\n      (pw) => pw.id === w.parent_workspace_id && pw.name === w.name,\n    );\n  });\n\n  return (\n    <FlexCol\n      key={connection.id}\n      className={\"Connection gap-0 bg-color-0 text-black shadow trigger-hover \"}\n      style={{ minWidth: \"250px\" }}\n      data-key={connection.name}\n    >\n      <div className=\"Connection_TOP-CONNECTION-INFO_ACTIONS flex-row\">\n        <NavLink\n          key={connection.id}\n          className={\n            \"no-decor flex-col min-w-0 text-ellipsis f-1 text-active-hover \"\n          }\n          data-command=\"Connection.openConnection\"\n          to={getConnectionPath(connection.id)}\n        >\n          <div className=\"flex-col gap-p5 p-1 h-full\">\n            <FlexRowWrap className=\"gap-1\">\n              <div\n                className=\"text-ellipsis font-20 text-0\"\n                title={(isAdmin ? connection.db_name : connection.name) || \"\"}\n              >\n                {isAdmin ? connection.name : connection.name || connection.id}\n              </div>\n              {isAdmin && !!props.showDbName && (\n                <div\n                  title=\"Database name\"\n                  className=\"text-2 text-ellipsis font-16\"\n                >\n                  {connection.db_name}\n                </div>\n              )}\n            </FlexRowWrap>\n            {isAdmin && connection.is_state_db && (\n              <InfoRow variant=\"naked\" iconPath=\"\" color=\"warning\">\n                {\n                  t.Connection[\n                    \"All Prostgles connection and dashboard data is stored here. Edit at your own risk\"\n                  ]\n                }\n              </InfoRow>\n            )}\n          </div>\n        </NavLink>\n\n        <ConnectionActionBar {...props} />\n      </div>\n\n      {(showWorkspaces || showAccessInfo) && (\n        <FlexRowWrap\n          title={t.common[\"Workspaces\"]}\n          data-command=\"Connection.workspaceList\"\n          className=\"ConnectionWorkspaceList  pl-1 p-p25 pt-0 ai-center gap-0\"\n        >\n          {showWorkspaces && (\n            <>\n              <Icon\n                path={WspIconPath}\n                size={0.75}\n                className=\"text-action mr-p5\"\n              />\n              {workspaces.map((w) => (\n                <Btn\n                  key={w.id}\n                  className=\"w-fit\"\n                  data-key={w.name}\n                  color=\"action\"\n                  asNavLink={true}\n                  href={getConnectionPath(connection.id, w.id)}\n                >\n                  {w.name || <em>Workspace</em>}\n                </Btn>\n              ))}\n            </>\n          )}\n\n          {showAccessInfo && (\n            <Btn\n              className=\"as-end ml-auto\"\n              title={`${t.Connections[\"Access granted to \"]}${pluralisePreffixed(connection.allowedUsers, \"user\")} `}\n              iconPath={mdiAccountMultiple}\n              iconPosition=\"right\"\n              color=\"action\"\n              asNavLink={true}\n              href={`${ROUTES.CONFIG}/${connection.id}?section=access_control`}\n            >\n              {connection.allowedUsers}\n            </Btn>\n          )}\n        </FlexRowWrap>\n      )}\n    </FlexCol>\n  );\n};\n\nconst pluralisePreffixed = (n: number, s: string) => {\n  return `${n} ${pluralise(n, s)}`;\n};\n\nexport const pluralise = (n: number, s: string) => {\n  if ((n > 1 || n === 0) && !s.toLowerCase().endsWith(\"s\")) {\n    return s + \"s\";\n  }\n\n  return s;\n};\n"
  },
  {
    "path": "client/src/pages/Connections/ConnectionActionBar.tsx",
    "content": "import {\n  mdiChartLine,\n  mdiCog,\n  mdiDotsHorizontal,\n  mdiDotsVertical,\n  mdiLadybug,\n  mdiPencil,\n} from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { StatusMonitor } from \"../../dashboard/StatusMonitor/StatusMonitor\";\nimport { StatusDotCircleIcon } from \"../Account/Sessions\";\nimport type { ConnectionProps } from \"./Connection\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { ROUTES } from \"@common/utils\";\n\nexport const ConnectionActionBar = (props: ConnectionProps) => {\n  const { dbsMethods, connection, dbs, isAdmin } = props;\n\n  const { isMobile } = window;\n\n  const disconectButton = dbsMethods.disconnect && !connection.is_state_db && (\n    <Btn\n      title={t.ConnectionActionBar[\"Connected. Click to disconnect\"]}\n      disabledInfo={\n        !connection.isConnected ?\n          t.ConnectionActionBar[\"Not connected\"]\n        : undefined\n      }\n      data-command=\"Connection.disconnect\"\n      disabledVariant=\"no-fade\"\n      color={connection.isConnected ? \"green\" : undefined}\n      className={connection.isConnected ? \"\" : \"show-on-trigger-hover\"}\n      onClickPromise={async () => {\n        await dbsMethods.disconnect!(connection.id);\n      }}\n      style={{\n        padding: \"14px\",\n      }}\n    >\n      <StatusDotCircleIcon color={connection.isConnected ? \"green\" : \"gray\"} />\n    </Btn>\n  );\n\n  const buttons = (\n    <>\n      <Btn\n        title={t.ConnectionActionBar[\"Close all windows\"]}\n        data-command=\"Connection.closeAllWindows\"\n        titleAsLabel={isMobile}\n        iconPath={mdiLadybug}\n        onClickPromise={async () => {\n          // const wsp = await dbs.workspaces.findOne({ connection_id: c.id });\n          const closed = await dbs.windows.update(\n            { $existsJoined: { workspaces: { connection_id: connection.id } } },\n            { closed: true },\n            { returning: \"*\" },\n          );\n          if (closed) {\n            alert(t.ConnectionActionBar[\"Windows have been closed\"]);\n          } else {\n            alert(\n              t.ConnectionActionBar[\n                \"Could not close windows: workspace not found\"\n              ],\n            );\n          }\n        }}\n      />\n      {dbsMethods.getStatus && dbsMethods.runConnectionQuery && (\n        <PopupMenu\n          positioning=\"fullscreen\"\n          onClickClose={false}\n          title={t.ConnectionConfig[\"Status monitor\"]}\n          data-command=\"Connection.statusMonitor\"\n          button={\n            <Btn\n              title={t.ConnectionConfig[\"Status monitor\"]}\n              titleAsLabel={isMobile}\n              color={\"action\"}\n              iconPath={mdiChartLine}\n            />\n          }\n        >\n          <StatusMonitor\n            {...props}\n            connectionId={connection.id}\n            getStatus={dbsMethods.getStatus}\n            runConnectionQuery={dbsMethods.runConnectionQuery}\n          />\n        </PopupMenu>\n      )}\n\n      {isAdmin && (\n        <Btn\n          href={ROUTES.CONFIG + \"/\" + connection.id}\n          title={t.common[\"Configure\"]}\n          data-command=\"Connection.configure\"\n          titleAsLabel={isMobile}\n          className=\" \"\n          iconPath={mdiCog}\n          asNavLink={true}\n          color=\"action\"\n        />\n      )}\n\n      {isAdmin && (\n        <Btn\n          data-command=\"Connection.edit\"\n          href={ROUTES.EDIT_CONNECTION + \"/\" + connection.id}\n          title={t.ConnectionActionBar[\"Edit connection\"]}\n          titleAsLabel={isMobile}\n          className=\"  \"\n          iconPath={mdiPencil}\n          asNavLink={true}\n          color=\"action\"\n        />\n      )}\n    </>\n  );\n\n  const buttonBar =\n    isMobile ?\n      <PopupMenu\n        button={\n          <Btn\n            size=\"default\"\n            iconPath={mdiDotsHorizontal}\n            style={{\n              padding: \"11px\",\n            }}\n          />\n        }\n      >\n        {buttons}\n        {disconectButton}\n      </PopupMenu>\n    : <>\n        <FlexRow className=\"flex-row ai-center c--fit  show-on-trigger-hover\">\n          {buttons}\n        </FlexRow>\n        {disconectButton}\n      </>;\n\n  return (\n    <FlexRow className=\"ActionsContainer gap-p5 p-p25 ai-start\">\n      {buttonBar}\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Connections/Connections.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport React from \"react\";\nimport { ROUTES } from \"@common/utils\";\nimport type { PrglState } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Loading from \"@components/Loader/Loading\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { Connection } from \"./Connection\";\nimport { ConnectionsOptions } from \"./ConnectionsOptions\";\nimport { CreateConnection } from \"./CreateConnection/CreateConnection\";\nimport { useConnections } from \"./useConnections\";\nimport { useConnectionServersList } from \"./useConnectionServersList\";\n\nexport const Connections = (props: PrglState) => {\n  const { user, dbs, dbsMethods } = props;\n  const state = useConnections(props);\n  const { connections, isAdmin, showDbNames } = state;\n  const { serverUserGroupings } = useConnectionServersList(state);\n  if (!user || !connections || !serverUserGroupings) return <Loading />;\n\n  const {\n    createConnection,\n    getSampleSchemas,\n    runConnectionQuery,\n    validateConnection,\n  } = dbsMethods;\n\n  return (\n    <FlexCol\n      data-command=\"Connections\"\n      className=\"Connections gap-0 f-1 w-full min-h-0\"\n    >\n      <div className=\"flex-row as-center w-full gap-p5 mt-1 p-p5  max-w-800\">\n        {!connections.length && (\n          <InfoRow color=\"info\" className=\" f-1 w-full\">\n            {t.Connections[\"No connections available/permitted\"]}\n          </InfoRow>\n        )}\n        {isAdmin && (\n          <Btn\n            href={ROUTES.NEW_CONNECTION}\n            asNavLink={true}\n            title={t.Connections[\"Create new connection\"]}\n            iconPath={mdiPlus}\n            variant=\"filled\"\n            color=\"action\"\n            data-command=\"Connections.new\"\n          >\n            {t.Connections[\"New connection\"]}\n          </Btn>\n        )}\n\n        <ConnectionsOptions {...props} {...state} />\n      </div>\n      <div className=\"Connections_list flex-col o-auto min-h-0 p-p5 pb-1 mt-1 gap-2 ai-center\">\n        {serverUserGroupings.map(({ name, conns }, i) => {\n          const firstConn = conns[0];\n          return (\n            <div key={i} className=\" max-w-800 w-full\">\n              <FlexRow\n                className=\"gap-p25 jc-end p-p5\"\n                style={{ fontWeight: 400 }}\n              >\n                <h4\n                  title={t.ConnectionServer[\"Server info\"]}\n                  className=\"m-0 flex-row gap-p5 p-p5 ai-center text-1p5 jc-end text-ellipsis\"\n                >\n                  <div className=\"text-ellipsis\">{name}</div>\n                </h4>\n                {firstConn &&\n                  createConnection &&\n                  getSampleSchemas &&\n                  runConnectionQuery &&\n                  validateConnection && (\n                    <CreateConnection\n                      connId={firstConn.id}\n                      createConnection={createConnection}\n                      getSampleSchemas={getSampleSchemas}\n                      runConnectionQuery={runConnectionQuery}\n                      validateConnection={validateConnection}\n                      connections={conns}\n                      connectionGroupKey={name}\n                      dbs={dbs}\n                      showCreateText={Boolean(\n                        isAdmin &&\n                          serverUserGroupings.length <= 1 &&\n                          !conns.filter((c) => !c.is_state_db).length,\n                      )}\n                    />\n                  )}\n              </FlexRow>\n              <div className=\"flex-col gap-p5 \">\n                {conns.map((c) => (\n                  //@ts-ignore\n                  <Connection\n                    key={c.id}\n                    {...props}\n                    connection={c}\n                    showDbName={showDbNames}\n                    isAdmin={isAdmin}\n                  />\n                ))}\n              </div>\n            </div>\n          );\n        })}\n      </div>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Connections/ConnectionsOptions.tsx",
    "content": "import { mdiAlert, mdiCogOutline } from \"@mdi/js\";\nimport React from \"react\";\nimport type { PrglState } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport ConfirmationDialog from \"@components/ConfirmationDialog\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport type { useConnections } from \"./useConnections\";\n\ntype P = Pick<PrglState, \"dbs\" | \"user\"> & ReturnType<typeof useConnections>;\n\nexport const ConnectionsOptions = ({\n  dbs,\n  connections,\n  isAdmin,\n  user,\n  setShowDbNames,\n  showDbNames,\n  setShowStateConfirm,\n  showStateConfirm,\n  showStateConn,\n}: P) => {\n  if (!user || !connections) return null;\n\n  const canViewStateDB =\n    !!connections.length &&\n    connections.some((c) => \"is_state_db\" in c && c.is_state_db);\n\n  if (!isAdmin) return null;\n  return (\n    <>\n      <PopupMenu\n        className=\"ml-auto\"\n        clickCatchStyle={{ opacity: 0 }}\n        positioning=\"beneath-right\"\n        onClickClose={false}\n        contentClassName=\"p-p5\"\n        data-command=\"ConnectionsOptions\"\n        button={<Btn title={t.common.Options} iconPath={mdiCogOutline} />}\n      >\n        {canViewStateDB && (\n          <SwitchToggle\n            label={t.Connections[\"Show state connection\"]}\n            data-command=\"ConnectionsOptions.showStateDatabase\"\n            checked={!!showStateConn}\n            onChange={(checked, e) => {\n              if (checked) {\n                setShowStateConfirm(e.currentTarget);\n              } else {\n                dbs.users.update(\n                  { id: user.id },\n                  { options: { $merge: [{ showStateDB: false }] } },\n                );\n              }\n            }}\n          />\n        )}\n        <SwitchToggle\n          label={t.Connections[\"Show database names\"]}\n          data-command=\"ConnectionsOptions.showDatabaseNames\"\n          checked={showDbNames}\n          onChange={(showDbNames) => {\n            setShowDbNames(showDbNames);\n          }}\n        />\n      </PopupMenu>\n      {showStateConfirm && (\n        <ConfirmationDialog\n          positioning={\"beneath-center\"}\n          anchorEl={showStateConfirm}\n          iconPath={mdiAlert}\n          message={\n            t.Connections[\n              \"Editing state data directly may break functionality. Proceed at your own risk!\"\n            ]\n          }\n          onClose={() => {\n            setShowStateConfirm(undefined);\n          }}\n          onAccept={async () => {\n            await dbs.users.update(\n              { id: user.id },\n              { options: { $merge: [{ showStateDB: true }] } },\n            );\n            setShowStateConfirm(undefined);\n          }}\n          acceptBtn={{\n            color: \"action\",\n            text: \"OK\",\n            dataCommand: \"Connections.add\",\n          }}\n          asPopup={true}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Connections/CreateConnection/CreateConnection.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport React, { useState } from \"react\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport Popup from \"@components/Popup/Popup\";\nimport { Select } from \"@components/Select/Select\";\nimport type { DBS, DBSMethods } from \"../../../dashboard/Dashboard/DBS\";\nimport { SampleSchemas } from \"../../../dashboard/SampleSchemas\";\nimport { t } from \"../../../i18n/i18nUtils\";\nimport { CreatePostgresUser } from \"../CreatePostgresUser\";\nimport type { IConnection } from \"../useConnections\";\nimport {\n  useCreateConnection,\n  type CreateConnectionType,\n} from \"./useCreateConnection\";\n\nexport type CreateConnectionProps = Required<\n  Pick<\n    DBSMethods,\n    | \"runConnectionQuery\"\n    | \"getSampleSchemas\"\n    | \"createConnection\"\n    | \"validateConnection\"\n     \n  >\n> & {\n  connId: string;\n  dbs: DBS;\n  connections: IConnection[];\n  showCreateText: boolean;\n  connectionGroupKey: string;\n};\n\nexport const CreateConnection = (props: CreateConnectionProps) => {\n  const { showCreateText, getSampleSchemas, connectionGroupKey } = props;\n  const {\n    error,\n    onCreateConnection,\n    serverInfo,\n    onOpenActions,\n    connectionName,\n    setConnectionName,\n    setError,\n    newUser,\n  } = useCreateConnection(props);\n  const { newUserPasswordError, newUsernameError } = newUser;\n  const cannotCreateDb =\n    error?.toString() || (serverInfo && !serverInfo.canCreateDb);\n\n  const [action, setAction] = useState<CreateConnectionType[number]>();\n  const duplicateDbName =\n    action?.type === \"new\" &&\n    serverInfo?.databases.includes(action.newDatabaseName!);\n\n  const duplicateConnectionName =\n    serverInfo?.existingConnectionNames.includes(connectionName);\n  const ConnectionNameEditor = (\n    <FormField\n      label={\"New connection name\"}\n      value={connectionName}\n      onChange={setConnectionName}\n      error={duplicateConnectionName ? \"Name already in used\" : undefined}\n    />\n  );\n  return (\n    <>\n      <Select\n        title={t.ConnectionServer[\"Add or create a database\"]}\n        btnProps={{\n          iconPath: mdiPlus,\n          children:\n            showCreateText ? t.ConnectionServer[\"Create a database\"] : \"\",\n          size: \"small\",\n          color: \"action\",\n          variant: \"filled\",\n          \"data-command\": \"ConnectionServer.add\",\n          \"data-key\": connectionGroupKey,\n        }}\n        fullOptions={[\n          {\n            key: \"new\",\n            label: t.ConnectionServer[\"Create a database in this server\"],\n            /** This is to ensure serverInfo is loaded before clicking  */\n            \"data-command\":\n              !serverInfo || cannotCreateDb ? undefined : (\n                \"ConnectionServer.add.newDatabase\"\n              ),\n            disabledInfo:\n              error?.toString() ??\n              (cannotCreateDb ?\n                t.ConnectionServer[\n                  \"Not allowed to create databases with this user\"\n                ]({ rolname: serverInfo?.rolname ?? \"\" })\n              : undefined),\n          },\n          {\n            key: \"existing\",\n            label: t.ConnectionServer[\"Select a database from this server\"],\n            disabledInfo: error?.toString(),\n          },\n        ]}\n        onOpen={onOpenActions}\n        onChange={(action) => {\n          setAction({ type: action });\n        }}\n      />\n      {!!action && (\n        <Popup\n          clickCatchStyle={{ opacity: 1 }}\n          positioning=\"center\"\n          title={\n            action.type === \"new\" ?\n              t.ConnectionServer[\"Create a database\"]\n            : t.ConnectionServer[\"Select a database from this server\"]\n          }\n          onClose={() => setAction(undefined)}\n          autoFocusFirst={{ selector: \"input\" }}\n          footerButtons={[\n            { label: \"Cancel\", onClickClose: true },\n            {\n              label:\n                action.type === \"existing\" ?\n                  t.ConnectionServer[\"Save and connect\"]\n                : t.ConnectionServer[\"Create and connect\"],\n              variant: \"filled\",\n              color: \"action\",\n              \"data-command\": \"ConnectionServer.add.confirm\",\n              disabledInfo:\n                (\n                  (action.type === \"new\" &&\n                    !action.newDatabaseName &&\n                    !connectionName) ||\n                  (action.type === \"existing\" &&\n                    !action.existingDatabaseName &&\n                    !connectionName)\n                ) ?\n                  t.ConnectionServer[\"Some data is missing\"]\n                : duplicateConnectionName ?\n                  t.ConnectionServer[\"Must fix connection name error\"]\n                : (newUsernameError ?? newUserPasswordError ?? undefined),\n              onClickMessage: async (e, setMsg) => {\n                setMsg({ loading: 1, delay: 0 });\n                try {\n                  await onCreateConnection(action);\n                } catch (error) {\n                  console.error(error);\n                  setError(error);\n                }\n                setMsg({ loading: 0 });\n              },\n            },\n          ]}\n          contentClassName=\"flex-col gap-1 p-1 mx-p25\"\n        >\n          {action.type === \"new\" ?\n            <>\n              <FormFieldDebounced\n                label={t.ConnectionServer[\"New database name\"]}\n                data-command=\"ConnectionServer.NewDbName\"\n                inputProps={{ autoFocus: true }}\n                error={\n                  duplicateDbName ?\n                    t.ConnectionServer[\"Name already in use\"]\n                  : undefined\n                }\n                onChange={(newDatabaseName) => {\n                  setAction({ ...action, newDatabaseName });\n                  setConnectionName(newDatabaseName);\n                }}\n                value={action.newDatabaseName}\n              />\n              {!action.newDatabaseName && (\n                <FlexCol>\n                  <div>Or</div>\n                  <SampleSchemas\n                    title={t.ConnectionServer[\"Create demo schema (optional)\"]}\n                    name={action.applySchema?.name}\n                    dbsMethods={{ getSampleSchemas }}\n                    onChange={(applySchema) => {\n                      const newDatabaseName = applySchema.name.split(\".\")[0]!;\n                      setConnectionName(newDatabaseName);\n                      setAction({\n                        ...action,\n                        newDatabaseName,\n                        applySchema,\n                      });\n                    }}\n                  />\n                </FlexCol>\n              )}\n              {!!action.newDatabaseName && (\n                <>\n                  {ConnectionNameEditor}\n                  <SampleSchemas\n                    title={t.ConnectionServer[\"Create demo schema (optional)\"]}\n                    name={action.applySchema?.name}\n                    dbsMethods={{ getSampleSchemas }}\n                    onChange={(applySchema) => {\n                      setAction({\n                        ...action,\n                        applySchema,\n                      });\n                    }}\n                  />\n                </>\n              )}\n            </>\n          : serverInfo ?\n            <>\n              <Select\n                label={t.ConnectionServer[\"Database\"]}\n                value={action.existingDatabaseName}\n                data-command=\"ConnectionServer.add.existingDatabase\"\n                fullOptions={serverInfo.databases\n                  .map((key) => ({\n                    key,\n                    subLabel:\n                      serverInfo.usedDatabases.includes(key) ?\n                        t.ConnectionServer[\"Already added to connections\"]\n                      : undefined,\n                  }))\n                  .sort((a, b) => +!!a.subLabel - +!!b.subLabel)}\n                onChange={(existingDatabaseName) => {\n                  setAction({\n                    ...action,\n                    existingDatabaseName,\n                  });\n                  if (!connectionName) {\n                    setConnectionName(existingDatabaseName);\n                  }\n                }}\n              />\n              {action.existingDatabaseName && ConnectionNameEditor}\n            </>\n          : t.common[\"Something went wrong\"]}\n          {(action.type === \"new\" ?\n            action.newDatabaseName\n          : action.existingDatabaseName) && (\n            <CreatePostgresUser {...newUser} connectionName={connectionName} />\n          )}\n          <ErrorComponent error={error} />\n        </Popup>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Connections/CreateConnection/useCreateConnection.ts",
    "content": "import { useCallback, useState } from \"react\";\nimport type { CreateConnectionProps } from \"./CreateConnection\";\nimport { useNavigate } from \"react-router-dom\";\nimport { ROUTES, type SampleSchema } from \"@common/utils\";\nimport type { Connection } from \"../../NewConnection/NewConnnectionForm\";\nimport { asName } from \"prostgles-client/dist/prostgles\";\nimport { isDefined, pickKeys } from \"prostgles-types\";\nimport { useCreatePostgresUser } from \"../CreatePostgresUser\";\n\nexport type CreateConnectionType = [\n  {\n    type: \"new\";\n    applySchema?: SampleSchema;\n    newDatabaseName?: string;\n  },\n  {\n    type: \"existing\";\n    existingDatabaseName?: string;\n  },\n];\n\nexport const useCreateConnection = (props: CreateConnectionProps) => {\n  const {\n    createConnection,\n    getSampleSchemas,\n    runConnectionQuery,\n    validateConnection,\n    connId,\n    dbs,\n    connections,\n  } = props;\n  const [serverInfo, setServerInfo] = useState<\n    | {\n        canCreateDb: boolean;\n        rolname: string;\n        databases: string[];\n        sampleSchemas: SampleSchema[];\n        usedDatabases: string[];\n        mainConnection: Required<Connection>;\n        existingConnectionNames: string[];\n      }\n    | undefined\n  >(undefined);\n  const [connectionName, setConnectionName] = useState(\"\");\n\n  const newUser = useCreatePostgresUser({ connId, runConnectionQuery });\n  const { newPgUser, newUserPasswordError, newUsernameError } = newUser;\n\n  const navigate = useNavigate();\n  const [error, setError] = useState<any>();\n  const onOpenActions = useCallback(async () => {\n    const serverInfo = (\n      await runConnectionQuery(\n        connId,\n        `\n          SELECT rolcreatedb OR rolsuper as \"canCreateDb\", rolname\n          FROM pg_catalog.pg_roles\n          WHERE rolname = \"current_user\"();\n        `,\n      )\n    )[0]! as { canCreateDb: boolean; databases: string[]; rolname: string };\n    const databases = (await runConnectionQuery(\n      connId,\n      `\n          SELECT datname FROM pg_catalog.pg_database\n        `,\n    )) as { datname: string }[];\n    const sampleSchemas = await getSampleSchemas().catch(\n      (gettingSampleSchemasError) => {\n        console.error({ gettingSampleSchemasError });\n        return [];\n      },\n    );\n    const existingConnections = await dbs.connections.find(\n      {},\n      { select: { name: 1 } },\n    );\n    const mainConnection = await dbs.connections.findOne({ id: connId });\n    if (!mainConnection) {\n      throw \"mainConnection not found\";\n    }\n    setServerInfo({\n      ...serverInfo,\n      sampleSchemas,\n      databases: databases.map((d) => d.datname),\n      usedDatabases: connections.map((c) => c.db_name).filter(isDefined),\n      mainConnection,\n      existingConnectionNames: existingConnections\n        .map((c) => c.name)\n        .filter((v) => v),\n    });\n  }, [\n    connId,\n    connections,\n    dbs.connections,\n    getSampleSchemas,\n    runConnectionQuery,\n  ]);\n\n  const onCreateConnection = useCallback(\n    async (action: CreateConnectionType[number]) => {\n      let newDbName = \"\";\n      let newDbOwnerCredentials:\n        | undefined\n        | Pick<Connection, \"db_user\" | \"db_pass\">;\n      if (newPgUser.create) {\n        if (newUsernameError || newUserPasswordError)\n          throw \"User already exists or password is missing\";\n        await runConnectionQuery(\n          connId,\n          `CREATE USER ${asName(newPgUser.name)} WITH ENCRYPTED PASSWORD $1;`,\n          [newPgUser.password],\n        );\n        newDbOwnerCredentials = {\n          db_user: newPgUser.name,\n          db_pass: newPgUser.password,\n        };\n      }\n      if (action.type === \"new\") {\n        const createDbQuery = [\n          `CREATE DATABASE ${asName(action.newDatabaseName!)}`,\n          newDbOwnerCredentials && newPgUser.permissions.type === \"owner\" ?\n            `WITH OWNER ${JSON.stringify(newPgUser.name)}`\n          : \"\",\n        ].join(\"\\n\");\n        await runConnectionQuery(connId, createDbQuery);\n        newDbName = action.newDatabaseName!;\n      } else {\n        if (newDbOwnerCredentials && newPgUser.permissions.type === \"owner\") {\n          await runConnectionQuery(\n            connId,\n            `ALTER DATABASE ${asName(action.existingDatabaseName!)} SET OWNER TO ${asName(newPgUser.name)};`,\n          );\n        }\n        newDbName = action.existingDatabaseName!;\n      }\n\n      if (newDbOwnerCredentials && newPgUser.permissions.type === \"custom\") {\n        const escapedUserName = asName(newUser.newPgUser.name);\n\n        const rulesObj = pickKeys(newPgUser.permissions, [\n          \"select\",\n          \"delete\",\n          \"update\",\n          \"insert\",\n        ]);\n        const allowedActions = Object.entries(rulesObj)\n          .map(([k, v]) => (v ? k : undefined))\n          .filter(isDefined);\n        if (newPgUser.permissions.allow_subscription_triggers) {\n          allowedActions.push(\"TRIGGER\");\n        }\n        const query =\n          `\n        GRANT CONNECT ON DATABASE ${asName(newDbName)} TO ${escapedUserName};\n        GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO ${escapedUserName};\n        GRANT ${allowedActions} ON ALL TABLES IN SCHEMA public TO ${escapedUserName};\n        ` +\n          (newPgUser.permissions.allow_subscription_triggers ?\n            `\n        GRANT USAGE ON SCHEMA prostgles TO ${escapedUserName};\n        GRANT USAGE ON ALL SEQUENCES IN SCHEMA prostgles TO ${escapedUserName};\n        GRANT SELECT, UPDATE, DELETE, INSERT ON ALL TABLES IN SCHEMA prostgles TO ${escapedUserName};\n        `\n          : ``);\n        await runConnectionQuery(connId, query);\n      }\n\n      const validatedConnection = await validateConnection({\n        ...pickKeys(serverInfo!.mainConnection, [\n          \"db_conn\",\n          \"db_host\",\n          \"db_port\",\n          \"db_ssl\",\n          \"db_user\",\n          \"db_pass\",\n          \"db_ssl\",\n          \"ssl_certificate\",\n          \"ssl_client_certificate\",\n          \"ssl_client_certificate_key\",\n          \"ssl_reject_unauthorized\",\n        ]),\n        name: connectionName,\n        db_name: newDbName,\n        type: \"Standard\",\n        db_conn: null,\n        ...newDbOwnerCredentials,\n      });\n      const newConn = await createConnection(\n        validatedConnection.connection,\n        action.type === \"new\" ? action.applySchema?.name : undefined,\n      );\n      const { connection: newConnection } = newConn;\n      if (\n        action.type === \"new\" &&\n        action.applySchema?.type === \"dir\" &&\n        action.applySchema.workspaceConfig\n      ) {\n        for (const workspace of action.applySchema.workspaceConfig.workspaces) {\n          await dbs.sql?.(`DELETE FROM workspaces WHERE name = $1`, [\n            workspace.name,\n          ]);\n          await dbs.workspaces.insert({\n            ...workspace,\n            connection_id: newConnection.id,\n          });\n        }\n      }\n      navigate(`${ROUTES.CONNECTIONS}/${newConnection.id}`);\n    },\n    [\n      connId,\n      connectionName,\n      createConnection,\n      dbs,\n      navigate,\n      newPgUser.create,\n      newPgUser.name,\n      newPgUser.password,\n      newPgUser.permissions,\n      newUser.newPgUser.name,\n      newUserPasswordError,\n      newUsernameError,\n      runConnectionQuery,\n      serverInfo,\n      validateConnection,\n    ],\n  );\n\n  return {\n    serverInfo,\n    error,\n    setError,\n    onOpenActions,\n    onCreateConnection,\n    connectionName,\n    setConnectionName,\n    newUser,\n  };\n};\n"
  },
  {
    "path": "client/src/pages/Connections/CreatePostgresUser.tsx",
    "content": "import { usePromise } from \"prostgles-client\";\nimport React, { useState } from \"react\";\nimport ButtonGroup from \"@components/ButtonGroup\";\nimport { Checkbox } from \"@components/Checkbox\";\nimport { FlexRowWrap } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { DBSMethods } from \"../../dashboard/Dashboard/DBS\";\n\nexport type NewPostgresUser = {\n  name: string;\n  password: string;\n  create: boolean;\n  permissions:\n    | {\n        type: \"owner\";\n      }\n    | {\n        type: \"custom\";\n        allow_subscription_triggers: boolean;\n        select: boolean;\n        insert: boolean;\n        update: boolean;\n        delete: boolean;\n      };\n};\n\ntype P = {\n  connectionName: string;\n  newPgUser: NewPostgresUser;\n  setNewPgUser: (newOwner: NewPostgresUser) => void;\n  newUsernameError: string | undefined;\n  newUserPasswordError: string | undefined;\n};\n\nconst PermissionTypes = [\n  { key: \"owner\", label: \"Owner\" },\n  { key: \"custom\", label: \"Custom\" },\n] as const;\n\nconst CustomPermissions = [\n  { key: \"select\", label: \"Select\" },\n  { key: \"update\", label: \"Update\" },\n  { key: \"insert\", label: \"Insert\" },\n  { key: \"delete\", label: \"Delete\" },\n] as const;\n\nexport const CreatePostgresUser = ({\n  newPgUser,\n  setNewPgUser,\n  connectionName,\n  newUserPasswordError,\n  newUsernameError,\n}: P) => {\n  return (\n    <>\n      <SwitchToggle\n        label=\"Create a user for this database (optional)\"\n        variant=\"col\"\n        data-command=\"ConnectionServer.withNewOwnerToggle\"\n        checked={newPgUser.create}\n        onChange={(create) => {\n          setNewPgUser({\n            ...newPgUser,\n            ...(create &&\n              !newPgUser.name &&\n              connectionName && { name: `${connectionName}_owner` }),\n            create,\n          });\n        }}\n      />\n      {newPgUser.create && (\n        <>\n          <FormFieldDebounced\n            data-command=\"ConnectionServer.NewUserName\"\n            label={\"New username\"}\n            value={newPgUser.name}\n            error={newUsernameError}\n            onChange={(name) => setNewPgUser({ ...newPgUser, name })}\n          />\n          <FormField\n            data-command=\"ConnectionServer.NewUserPassword\"\n            label={\"New username password\"}\n            value={newPgUser.password}\n            error={newUserPasswordError}\n            onChange={(password) => setNewPgUser({ ...newPgUser, password })}\n          />\n          <ButtonGroup\n            data-command=\"ConnectionServer.NewUserPermissionType\"\n            fullOptions={PermissionTypes}\n            value={newPgUser.permissions.type}\n            label={\"Permission type\"}\n            onChange={(type) => {\n              setNewPgUser({\n                ...newPgUser,\n                permissions:\n                  type === \"owner\" ?\n                    { type }\n                  : {\n                      type,\n                      select: true,\n                      insert: true,\n                      update: true,\n                      delete: true,\n                      allow_subscription_triggers: false,\n                    },\n              });\n            }}\n          />\n          {newPgUser.permissions.type === \"custom\" && (\n            <>\n              <div>\n                Ticked commands will be allowed on all tables in the public\n                schema\n              </div>\n              <FlexRowWrap>\n                {CustomPermissions.map((p) => (\n                  <Checkbox\n                    key={p.key}\n                    label={p.label}\n                    checked={newPgUser.permissions[p.key]}\n                    onChange={(e) =>\n                      setNewPgUser({\n                        ...newPgUser,\n                        permissions: {\n                          ...newPgUser.permissions,\n                          [p.key]: e.target.checked,\n                        },\n                      })\n                    }\n                  />\n                ))}\n              </FlexRowWrap>\n              <SwitchToggle\n                label={{\n                  label: \"Allow subscribing to tables\",\n                  info: \"This will allow adding triggers to tables from the public schema and select/update/delete/insert access to tables from the prostgles schema\",\n                }}\n                checked={newPgUser.permissions.allow_subscription_triggers}\n                onChange={(allow_subscription_triggers) => {\n                  if (newPgUser.permissions.type !== \"custom\") return;\n                  setNewPgUser({\n                    ...newPgUser,\n                    permissions: {\n                      ...newPgUser.permissions,\n                      allow_subscription_triggers,\n                    },\n                  });\n                }}\n              />\n            </>\n          )}\n        </>\n      )}\n    </>\n  );\n};\n\ntype Args = {\n  connId: string;\n  runConnectionQuery: DBSMethods[\"runConnectionQuery\"];\n};\nexport const useCreatePostgresUser = ({ connId, runConnectionQuery }: Args) => {\n  const [newPgUser, setNewPgUser] = useState<NewPostgresUser>({\n    name: \"\",\n    password: \"\",\n    create: false,\n    permissions: {\n      type: \"owner\",\n    },\n  });\n  const newUserName = newPgUser.name;\n  const newUsernameError = usePromise(async () => {\n    if (!connId || !runConnectionQuery || !newUserName || !newPgUser.create)\n      return undefined;\n    if (!newUserName) return \"Username is required\";\n    const matchingUserNames = await runConnectionQuery(\n      connId,\n      `SELECT usename FROM pg_catalog.pg_user WHERE usename = $1`,\n      [newUserName],\n    );\n    return matchingUserNames.length > 0 ? \"User already exists\" : undefined;\n  }, [newUserName, connId, runConnectionQuery, newPgUser.create]);\n  const newUserPasswordError =\n    newPgUser.create && !newPgUser.password ?\n      \"Password is required\"\n    : undefined;\n\n  return { newPgUser, setNewPgUser, newUsernameError, newUserPasswordError };\n};\n"
  },
  {
    "path": "client/src/pages/Connections/useConnectionServersList.ts",
    "content": "import { pickKeys, type AnyObject } from \"prostgles-types\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { AdminConnectionModel, useConnections } from \"./useConnections\";\n\ntype ServerUser = Pick<\n  DBSSchema[\"connections\"],\n  \"db_host\" | \"db_port\" | \"db_user\"\n>;\n\ntype P = Pick<\n  ReturnType<typeof useConnections>,\n  \"connections\" | \"isAdmin\" | \"showStateConn\"\n>;\n\nexport const useConnectionServersList = ({\n  connections,\n  isAdmin,\n  showStateConn,\n}: P) => {\n  if (!connections) return { serverUserGroupings: undefined };\n  const renderedConnections = connections.filter(\n    (c) => showStateConn || !c.is_state_db,\n  );\n\n  /** Admins will see db_host and db_port. Group by this */\n  let serverUserGroupings: {\n    name: string;\n    conns: typeof connections;\n    serverStr: string;\n    serverUser: AnyObject;\n  }[] = [];\n  if (isAdmin) {\n    const serverUsers: ServerUser[] = [];\n    const parseServer = (s: ServerUser): ServerUser => ({\n      db_host: s.db_host || \"localhost\",\n      db_port: s.db_port || 5432,\n      db_user: s.db_user,\n    });\n    const sameServer = (\n      _s1: ServerUser,\n      _s2: ServerUser,\n      sameUser = true,\n    ): boolean => {\n      const s1 = parseServer(_s1);\n      const s2 = parseServer(_s2);\n      if (sameUser)\n        return (\n          [s1.db_host, s1.db_port, s1.db_user].join() ===\n          [s2.db_host, s2.db_port, s2.db_user].join()\n        );\n      return (\n        [s1.db_host, s1.db_port].join() === [s2.db_host, s2.db_port].join()\n      );\n    };\n\n    (renderedConnections as AdminConnectionModel[]).forEach((c) => {\n      if (!serverUsers.some((h) => sameServer(h, c))) {\n        serverUsers.push(pickKeys(c, [\"db_host\", \"db_port\", \"db_user\"]));\n      }\n    });\n    serverUserGroupings = serverUsers.map((serverUser) => ({\n      name: getServerCoreInfoStr(serverUser),\n      serverUser,\n      serverStr: (serverUser.db_host || \"\") + (serverUser.db_port || \"\"),\n      conns: renderedConnections.filter((c) =>\n        sameServer(c as AdminConnectionModel, serverUser),\n      ),\n    }));\n\n    serverUserGroupings = serverUserGroupings.sort(\n      (a, b) =>\n        Math.max(...b.conns.map((c) => +new Date(c.created!))) -\n        Math.max(...a.conns.map((c) => +new Date(c.created!))),\n    );\n\n    /** Group same servers together */\n    const serverStrs = Array.from(\n      new Set(serverUserGroupings.map((s) => s.serverStr)),\n    );\n    serverUserGroupings = serverStrs.flatMap((serverStr) =>\n      serverUserGroupings.filter((sg) => sg.serverStr === serverStr),\n    );\n  } else if (renderedConnections.length) {\n    serverUserGroupings.push({\n      name: \"\",\n      serverStr: \"\",\n      serverUser: {},\n      conns: renderedConnections,\n    });\n  }\n\n  return { serverUserGroupings };\n};\n\nexport const getServerCoreInfoStr = <\n  H extends Pick<DBSSchema[\"connections\"], \"db_host\" | \"db_port\" | \"db_user\">,\n>(\n  h: H,\n) => `${h.db_user}@${h.db_host || \"localhost\"}:${h.db_port}`;\n"
  },
  {
    "path": "client/src/pages/Connections/useConnections.ts",
    "content": "import { useState } from \"react\";\nimport type { PrglState } from \"../../App\";\nimport { usePromise } from \"prostgles-client\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Workspace } from \"../../dashboard/Dashboard/dashboardUtils\";\nimport type { FilterItem } from \"prostgles-types\";\n\ntype CommonConnectionInfo = Pick<DBSSchema[\"connections\"], \"created\"> & {\n  access_control: { count: number }[];\n  isConnected?: boolean;\n  allowedUsers: number;\n  workspaces: Workspace[];\n};\n\nexport type BasicConnectionModel = Pick<\n  Required<DBSSchema[\"connections\"]>,\n  \"id\" | \"name\" | \"is_state_db\"\n> &\n  CommonConnectionInfo & { db_name?: undefined };\n\nexport type AdminConnectionModel = Required<DBSSchema[\"connections\"]> &\n  CommonConnectionInfo;\n\nexport type IConnection = BasicConnectionModel | AdminConnectionModel;\nexport const useConnections = (props: PrglState) => {\n  const [showStateConfirm, setShowStateConfirm] = useState<HTMLInputElement>();\n  const [showDbNames, setShowDbNames] = useState(false);\n  const { dbs, user, dbsMethods } = props;\n  const isAdmin = user?.type === \"admin\";\n\n  const showStateConn = user?.options?.showStateDB ?? true;\n  const { data: _connections } = dbs.connections.useSubscribe(\n    {},\n    {\n      orderBy: [{ created: -1 }, { db_conn: 1 }],\n      select: {\n        \"*\": 1,\n        ...(isAdmin ?\n          {\n            access_control: {\n              /** Only include enabled access rules */\n              $leftJoin: [\"access_control_connections\", \"access_control\"],\n              select: {\n                count: {\n                  $countAll: [],\n                },\n              },\n            },\n          }\n        : {}),\n        workspaces: \"*\",\n      },\n    },\n    { skip: !user },\n  );\n\n  const connections = usePromise(async () => {\n    if (!_connections) return;\n\n    const connectedConnectionIds = await dbsMethods.getConnectedIds?.();\n\n    const connections = await Promise.all(\n      (_connections as IConnection[]).map(async (c) => {\n        c.allowedUsers = 0;\n        if ((c.access_control as any)?.[0]?.count && (dbs.users as any).count) {\n          c.allowedUsers = await dbs.users.count({\n            $existsJoined: {\n              \"user_types.access_control_user_types.access_control.database_configs.connections\":\n                { id: c.id },\n            },\n          } as FilterItem);\n        }\n\n        return c;\n      }),\n    );\n\n    return connections.map((c) => ({\n      ...c,\n      isConnected:\n        !connectedConnectionIds || connectedConnectionIds.includes(c.id),\n    }));\n  }, [_connections, dbs.users, dbsMethods]);\n\n  return {\n    connections,\n    showStateConfirm,\n    setShowStateConfirm,\n    showDbNames,\n    setShowDbNames,\n    isAdmin,\n    showStateConn,\n  };\n};\n"
  },
  {
    "path": "client/src/pages/ElectronSetup/ElectronSetup.tsx",
    "content": "import { mdiArrowLeft, mdiArrowRight, mdiConnection } from \"@mdi/js\";\nimport React from \"react\";\nimport type { AppState } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport Loading from \"@components/Loader/Loading\";\nimport type { OS } from \"../../components/PostgresInstallationInstructions\";\nimport { ElectronSetupStateDB } from \"./ElectronSetupStateDB\";\nimport { useElectronSetup } from \"./useElectronSetup\";\n\ntype ElectronSetup = {\n  serverState: AppState[\"serverState\"];\n};\n\nexport const getBrowserOS = () => {\n  const { platform } = window.navigator;\n  const os: OS =\n    platform.startsWith(\"Mac\") ? \"macosx\"\n    : platform.startsWith(\"Linux\") || platform.includes(\"BSD\") ? \"linux\"\n    : \"windows\";\n  return os;\n};\n\nexport const ElectronSetup = ({ serverState }: ElectronSetup) => {\n  const state = useElectronSetup({\n    serverState,\n  });\n  const { step, setStep, loading, validationWarning, error, onPressDone } =\n    state;\n\n  return (\n    <FlexCol className=\"ElectronSetup m-auto min-s-0\">\n      <div\n        className={\"ta-center p-2  flex-col gap-2 max-w-700 p-1 min-h-0 \"}\n        style={{ width: \"500px\" }}\n      >\n        {step === \"1-privacy\" ?\n          <div>\n            <h3>PRIVACY</h3>\n            <section className=\"ta-left  font-18\">\n              The only data we collect is the information you send us through\n              the &quot;Send feedback&quot; button.\n            </section>\n          </div>\n        : <FlexCol className=\"f-1 min-s-0 \">\n            {loading && !validationWarning ?\n              <Loading\n                id=\"main\"\n                className=\"m-auto\"\n                message=\"Connecting to electron state database\"\n              />\n            : <ElectronSetupStateDB state={state} />}\n\n            {Boolean(!loading && error) && (\n              <ErrorComponent\n                className=\"rounded f-0\"\n                style={{ background: \"#fde8e8\" }}\n                withIcon={true}\n                error={\n                  serverState?.initState.state === \"error\" ? error : undefined\n                }\n              />\n            )}\n          </FlexCol>\n        }\n\n        {!loading && (\n          <FlexRow style={{ justifyContent: \"space-between\" }}>\n            <Btn\n              data-command=\"ElectronSetup.Back\"\n              onClick={() => {\n                setStep(\"1-privacy\");\n              }}\n              iconPath={mdiArrowLeft}\n              variant=\"outline\"\n              style={{ opacity: step === \"2-setup\" ? 1 : 0 }}\n            >\n              Back\n            </Btn>\n\n            {step === \"1-privacy\" ?\n              <Btn\n                data-command=\"ElectronSetup.Next\"\n                onClick={() => {\n                  setStep(\"2-setup\");\n                }}\n                iconPosition=\"right\"\n                iconPath={mdiArrowRight}\n                color=\"action\"\n                variant=\"filled\"\n              >\n                Next\n              </Btn>\n            : <Btn\n                data-command=\"ElectronSetup.Done\"\n                color=\"action\"\n                variant=\"filled\"\n                className=\"ml-auto\"\n                iconPath={mdiConnection}\n                onClickMessage={onPressDone}\n              >\n                Connect\n              </Btn>\n            }\n          </FlexRow>\n        )}\n      </div>\n    </FlexCol>\n  );\n};\n\nexport const tout = (timeout: number) => {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(true);\n    }, timeout);\n  });\n};\n"
  },
  {
    "path": "client/src/pages/ElectronSetup/ElectronSetupStateDB.tsx",
    "content": "import React from \"react\";\nimport { DEFAULT_ELECTRON_CONNECTION } from \"@common/electronInitTypes\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport Tabs from \"@components/Tabs\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { NewConnectionForm } from \"../NewConnection/NewConnectionFormFields\";\nimport type { useElectronSetup } from \"./useElectronSetup\";\nimport { PostgresInstallationInstructions } from \"../../components/PostgresInstallationInstructions\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\n\nexport const ElectronSetupStateDB = ({\n  state,\n}: {\n  state: ReturnType<typeof useElectronSetup>;\n}) => {\n  const {\n    c,\n    validationWarning,\n    updateConnection,\n    isQuickMode,\n    setIsQuickMode,\n    os,\n  } = state;\n\n  return (\n    <ScrollFade className=\"px-p25 min-s-0 flex-col f-1 oy-auto no-scroll-bar\">\n      <h2>State database</h2>\n      <section className=\"ta-left font-18\">\n        <strong>Prostgles Desktop</strong> needs a PostgreSQL database to\n        securely store your workspace and connection settings. (This will not\n        affect your existing databases.)\n        <p className=\"m-0 mt-p5\">\n          For best experience we recommend using a locally installed database\n        </p>\n        <div className=\"flex-row-wrap gap-2 f-1 mt-1\">\n          <PostgresInstallationInstructions\n            os={os}\n            placement=\"state-db-quick-setup\"\n          />\n        </div>\n      </section>\n      <Tabs\n        className=\"mt-2\"\n        activeKey={isQuickMode ? \"quick\" : \"manual\"}\n        onChange={(key) => {\n          setIsQuickMode(key === \"quick\");\n        }}\n        contentClass=\"ta-left p-2\"\n        items={{\n          quick: {\n            label: \"Quick setup\",\n            content: (\n              <FlexCol>\n                <div>\n                  Enter the credentials of your local PostgreSQL superuser\n                  (often postgres). These will be used once to create the\n                  Prostgles Desktop database.\n                </div>\n                <div>\n                  Will create a{\" \"}\n                  {c.db_user !== DEFAULT_ELECTRON_CONNECTION.db_user && (\n                    <>\n                      <strong>{DEFAULT_ELECTRON_CONNECTION.db_user}</strong>{\" \"}\n                      superuser and a{\" \"}\n                    </>\n                  )}\n                  <strong>{DEFAULT_ELECTRON_CONNECTION.db_name}</strong> state\n                  database if missing on{\" \"}\n                  <strong>\n                    {c.db_host}:{c.db_port}\n                  </strong>\n                  <br></br>\n                </div>\n                {c.db_user !== DEFAULT_ELECTRON_CONNECTION.db_user && (\n                  <div>\n                    *If <strong>{DEFAULT_ELECTRON_CONNECTION.db_user}</strong>{\" \"}\n                    user exists will overwrite password with a randomly\n                    generated one\n                  </div>\n                )}\n\n                <FormField\n                  id=\"u\"\n                  value={c.db_user}\n                  label={t.NewConnectionForm[\"User\"]}\n                  type=\"text\"\n                  autoComplete=\"off\"\n                  onChange={(db_user) => updateConnection({ db_user })}\n                />\n                <FormField\n                  id=\"pass\"\n                  value={c.db_pass ?? \"\"}\n                  label={t.NewConnectionForm[\"Password\"]}\n                  type=\"text\"\n                  autoComplete=\"off\"\n                  onChange={(db_pass) => updateConnection({ db_pass })}\n                />\n                {validationWarning && (\n                  <ErrorComponent\n                    error={validationWarning}\n                    style={{ minWidth: 0 }}\n                  />\n                )}\n              </FlexCol>\n            ),\n          },\n          manual: {\n            label: \"Manual setup\",\n            content: (\n              <FlexCol className=\"px-p25 min-s-0\">\n                <div>\n                  Provide the connection details to an existing database that\n                  will be used as the state database\n                </div>\n                <FlexCol className=\"min-s-0 o-auto px-p5\">\n                  <NewConnectionForm\n                    mode=\"insert\"\n                    warning={validationWarning}\n                    c={c}\n                    nameErr={\"\"}\n                    isForStateDB={true}\n                    updateConnection={updateConnection}\n                    test={{\n                      status: \"\",\n                      statusOK: false,\n                    }}\n                    dbProps={undefined}\n                  />\n                </FlexCol>\n              </FlexCol>\n            ),\n          },\n        }}\n      />\n    </ScrollFade>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ElectronSetup/useElectronSetup.ts",
    "content": "import { omitKeys, type AnyObject } from \"prostgles-types\";\nimport { useEffect, useState } from \"react\";\nimport { DEFAULT_ELECTRON_CONNECTION } from \"@common/electronInitTypes\";\nimport type { AppState } from \"../../App\";\nimport { pageReload } from \"@components/Loader/Loading\";\nimport type { Connection } from \"../NewConnection/NewConnnectionForm\";\nimport { DEFAULT_CONNECTION } from \"../NewConnection/NewConnnectionForm\";\nimport type { OS } from \"../../components/PostgresInstallationInstructions\";\nimport { tout } from \"./ElectronSetup\";\n\ntype ElectronSetup = {\n  serverState: AppState[\"serverState\"];\n};\n\nexport const getOS = () => {\n  const { platform } = window.navigator;\n  const os: OS =\n    platform.startsWith(\"Mac\") ? \"macosx\"\n    : platform.startsWith(\"Linux\") || platform.includes(\"BSD\") ? \"linux\"\n    : \"windows\";\n  return os;\n};\n\nexport const useElectronSetup = ({ serverState }: ElectronSetup) => {\n  const [c, setConnection] = useState<Connection>({\n    ...DEFAULT_CONNECTION,\n    ...DEFAULT_ELECTRON_CONNECTION,\n    name: \"prostgles_desktop\",\n  });\n  const [validationWarning, setValidationWarning] = useState<any>();\n\n  const [loading, setLoading] = useState(false);\n  const [isQuickMode, setIsQuickMode] = useState(true);\n\n  const updateConnection = async (con: Partial<Connection>) => {\n    setLoading(false);\n    const newData = {\n      ...c,\n      ...con,\n    };\n\n    const res = await postConnection(newData, \"validate\");\n\n    const { connection, warning } = res;\n\n    setValidationWarning(warning);\n    if (connection) {\n      setConnection(connection);\n    }\n  };\n\n  const os = getOS();\n\n  useEffect(() => {\n    setConnection((c) => ({\n      ...c,\n      ...(os === \"windows\" && { db_ssl: \"disable\" }),\n    }));\n  }, [setConnection, os]);\n\n  const [step, setStep] = useState<\"1-privacy\" | \"2-setup\">(\"1-privacy\");\n\n  const { electronCredsProvided, initState, electronCreds } = serverState || {};\n  const error =\n    initState?.state === \"error\" ? initState.error || \"Init error\" : null;\n  useEffect(() => {\n    if (electronCredsProvided) {\n      setStep(\"2-setup\");\n      if (electronCreds) {\n        setConnection((c) => ({\n          ...c,\n          ...omitKeys(electronCreds, [\"db_ssl\"]),\n        }));\n      }\n    }\n  }, [electronCredsProvided, setConnection, error, electronCreds]);\n\n  const onPressDone = async () => {\n    setLoading(true);\n    try {\n      const resp = await postConnection(c, isQuickMode ? \"quick\" : \"manual\");\n      if (resp.warning) {\n        setValidationWarning(resp.warning);\n      } else {\n        await tout(3000);\n        pageReload(\"ElectronSetup.Done\");\n      }\n    } catch (err) {\n      setValidationWarning(err);\n    }\n    setLoading(false);\n  };\n\n  return {\n    c,\n    setConnection,\n    validationWarning,\n    loading,\n    updateConnection,\n    os,\n    step,\n    setStep,\n    error,\n    isQuickMode,\n    setIsQuickMode,\n    onPressDone,\n    setLoading,\n  };\n};\n\nconst postConnection = async (\n  connection: Connection,\n  mode: \"validate\" | \"quick\" | \"manual\",\n): Promise<{ connection?: Connection; warning?: any }> => {\n  const res = await post(\"/dbs\", {\n    connection,\n    mode,\n  });\n  return await res.json();\n};\n\nconst post = async (path: string, data: AnyObject) => {\n  const rawResponse = await fetch(path, {\n    method: \"POST\",\n    headers: {\n      Accept: \"application/json\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify(data),\n  });\n\n  if (!rawResponse.ok) {\n    const error = await rawResponse\n      .json()\n      .catch(() => rawResponse.text())\n      .catch(() => rawResponse.statusText);\n    throw error;\n  }\n\n  return rawResponse;\n};\n"
  },
  {
    "path": "client/src/pages/Login/AuthNotifPopup.tsx",
    "content": "import React from \"react\";\nimport Popup from \"@components/Popup/Popup\";\nimport { SuccessMessage } from \"@components/Animations\";\nimport { InfoRow } from \"@components/InfoRow\";\n\nexport const AuthNotifPopup = ({\n  success,\n  message,\n  onClose,\n}: {\n  success: boolean;\n  message: string;\n  onClose: () => void;\n}) => {\n  return (\n    <Popup\n      data-command=\"AuthNotifPopup\"\n      onClose={onClose}\n      clickCatchStyle={{ opacity: 1 }}\n      footerButtons={[\n        {\n          label: \"OK\",\n          onClickClose: true,\n          variant: \"filled\",\n          color: \"action\",\n        },\n      ]}\n    >\n      {success ?\n        <SuccessMessage variant=\"small\" message={message} />\n      : <InfoRow color=\"danger\" variant=\"naked\">\n          {message}\n        </InfoRow>\n      }\n    </Popup>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Login/Login.tsx",
    "content": "import React from \"react\";\nimport type { Prgl } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { AuthNotifPopup } from \"./AuthNotifPopup\";\nimport { LoginTotpFormFields } from \"./LoginTotpForm\";\nimport { LoginWithProviders } from \"./LoginWithProviders\";\nimport { useLoginState } from \"./useLoginState\";\n\nexport type LoginFormProps = Pick<Prgl, \"auth\">;\n\nexport const Login = ({ auth }: LoginFormProps) => {\n  const authState = useLoginState({ auth });\n  const {\n    formHandlers,\n    isOnLogin,\n    registerTypeAllowed,\n    setState,\n    error,\n    loading,\n    authResponse,\n    clearAuthResponse,\n    onAuthCall,\n  } = authState;\n\n  const headerTitle =\n    !isOnLogin ? \"Sign up\"\n    : !formHandlers?.setPassword ? \"Signup or Login\"\n    : \"Sign in\";\n  return (\n    <form\n      className=\"LoginForm flex-col gap-1 rounded shadow m-auto w-fit bg-color-0\"\n      style={{\n        maxWidth: \"400px\",\n        minWidth: \"380px\",\n      }}\n      onSubmit={(e) => {\n        e.preventDefault();\n      }}\n    >\n      {authResponse && (\n        <AuthNotifPopup {...authResponse} onClose={clearAuthResponse} />\n      )}\n      <FlexCol className=\"p-2\">\n        <h2 className=\"mt-0\">{headerTitle}</h2>\n        {formHandlers?.setUsername && (\n          <FormField\n            id=\"username\"\n            label=\"Email\"\n            value={formHandlers.username}\n            type=\"username\"\n            onChange={formHandlers.setUsername}\n          />\n        )}\n        {formHandlers?.setPassword && (\n          <FormField\n            id=\"password\"\n            label=\"Password\"\n            value={formHandlers.password}\n            type=\"password\"\n            onChange={formHandlers.setPassword}\n          />\n        )}\n        {formHandlers?.setConfirmPassword && (\n          <FormField\n            id=\"new-password\"\n            label=\"Confirm password\"\n            value={formHandlers.confirmPassword}\n            type=\"password\"\n            autoComplete=\"new-password\"\n            onChange={formHandlers.setConfirmPassword}\n          />\n        )}\n        {formHandlers?.setEmailVerificationCode && (\n          <FormField\n            id=\"email-verification-code\"\n            label=\"Email verification code\"\n            value={formHandlers.emailVerificationCode}\n            type=\"text\"\n            onChange={formHandlers.setEmailVerificationCode}\n          />\n        )}\n\n        <LoginTotpFormFields {...authState} />\n\n        {error && <ErrorComponent data-command=\"Login.error\" error={error} />}\n\n        <Btn\n          loading={loading}\n          onClick={onAuthCall}\n          variant=\"filled\"\n          className=\"mt-1 jc-center\"\n          color=\"action\"\n          children={\n            isOnLogin ?\n              auth.loginType === \"email\" ?\n                \"Continue\"\n              : \"Sign in\"\n            : formHandlers?.state === \"registerWithPasswordConfirmationCode\" ?\n              \"Confirm email\"\n            : \"Sign up\"\n          }\n          size=\"large\"\n          style={{ width: \"100%\" }}\n        />\n        <LoginWithProviders auth={auth} />\n      </FlexCol>\n      {!formHandlers && <ErrorComponent error=\"Invalid state\" />}\n      {auth.signupWithEmailAndPassword && (\n        <Btn\n          style={{\n            fontSize: \"14px\",\n            width: \"100%\",\n            borderTopLeftRadius: 0,\n            borderTopRightRadius: 0,\n          }}\n          variant=\"faded\"\n          data-command=\"Login.toggle\"\n          color=\"action\"\n          onClick={() => {\n            setState(isOnLogin ? registerTypeAllowed : \"login\");\n          }}\n        >\n          {isOnLogin ?\n            \"No account? Sign up with email\"\n          : \"Already have an account? Sign in\"}\n        </Btn>\n      )}\n    </form>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Login/LoginTotpForm.tsx",
    "content": "import React from \"react\";\nimport type { useLoginState } from \"./useLoginState\";\nimport FormField from \"@components/FormField/FormField\";\nimport Btn from \"@components/Btn\";\n\nexport const LoginTotpFormFields = ({\n  formHandlers,\n  onAuthCall,\n  setState,\n}: ReturnType<typeof useLoginState>) => {\n  if (\n    !formHandlers ||\n    (formHandlers.state !== \"loginTotp\" &&\n      formHandlers.state !== \"loginTotpRecovery\")\n  )\n    return null;\n\n  return formHandlers.state === \"loginTotp\" ?\n      <>\n        <p>Open your authentication app and enter the code for Prostgles UI</p>\n        <FormField\n          id=\"totp_token\"\n          //@ts-ignore\n          value={formHandlers.totpToken}\n          inputProps={{\n            id: \"totp_token\",\n          }}\n          type=\"number\"\n          label=\"6-digit code\"\n          // error={error}\n          onChange={(v) => {\n            formHandlers.setTotpToken(v.toString());\n            if (formHandlers.totpToken.length > 5) {\n              onAuthCall();\n            }\n          }}\n        />\n        <div className=\"flex-row ai-center mt-p25\">\n          <div className=\"text-1p5\">Or</div>\n          <Btn\n            type=\"button\"\n            color=\"action\"\n            size=\"small\"\n            onClick={() => setState(\"loginTotpRecovery\")}\n          >\n            Enter recovery code\n          </Btn>\n        </div>\n      </>\n    : <>\n        <p>Open your authentication app and enter the code for Prostgles UI</p>\n        <FormField\n          id=\"totp_recovery_code\"\n          inputProps={{\n            id: \"totp_recovery_code\",\n          }}\n          value={formHandlers.totpRecoveryCode}\n          type=\"text\"\n          label=\"2FA Recovery code\"\n          // error={error}\n          onChange={(v) => formHandlers.setTotpRecoveryCode(v)}\n        />\n        <Btn\n          type=\"button\"\n          color=\"action\"\n          size=\"small\"\n          onClick={() => setState(\"loginTotp\")}\n        >\n          Cancel\n        </Btn>\n      </>;\n};\n"
  },
  {
    "path": "client/src/pages/Login/LoginWithProviders.tsx",
    "content": "import { getObjectEntries } from \"prostgles-types\";\nimport React from \"react\";\nimport type { PrglState } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport {\n  FacebookIcon,\n  GithubIcon,\n  GoogleIcon,\n  MicrosoftIcon,\n} from \"./SocialIcons\";\n\nexport const LoginWithProviders = ({ auth }: Pick<PrglState, \"auth\">) => {\n  const { user } = auth;\n  if (user && user.type !== \"public\") return null;\n  const providerConfigs = getObjectEntries(auth.loginWithProvider ?? {});\n  if (!providerConfigs.length) return null;\n  return (\n    <FlexCol className=\"gap-0\">\n      <FlexRow className=\"text-2\">\n        <DividerLine />\n        <div className=\"ws-nowrap\">or</div>\n        <DividerLine />\n      </FlexRow>\n      <FlexRowWrap className=\"LoginWithProviders py-1\">\n        {providerConfigs.map(([providerName, func]) => {\n          const providerIcon =\n            providerName === \"google\" ? <GoogleIcon />\n            : providerName === \"microsoft\" ? <MicrosoftIcon />\n            : providerName === \"github\" ? <GithubIcon />\n            : providerName === \"facebook\" ? <FacebookIcon />\n            : null;\n          return (\n            <Btn\n              key={providerName}\n              onClick={func}\n              title={`Login with ${providerName}`}\n              className=\"shadow\"\n              style={{ width: \"100%\", gap: \"2em\" }}\n              color=\"action\"\n              children={\n                <>\n                  {providerIcon} Continue with {providerName}\n                </>\n              }\n            />\n          );\n        })}\n      </FlexRowWrap>\n    </FlexCol>\n  );\n};\n\nconst DividerLine = () => (\n  <div\n    className=\"DividerLine bg-color-1\"\n    style={{\n      height: \"1px\",\n      width: \"100%\",\n    }}\n  ></div>\n);\n"
  },
  {
    "path": "client/src/pages/Login/SocialIcons.css",
    "content": "/** Tones down the button hover brightness (1.5) */\nbutton:hover .colored-icon {\n  filter: brightness(0.8);\n}\n"
  },
  {
    "path": "client/src/pages/Login/SocialIcons.tsx",
    "content": "import React from \"react\";\nimport \"./SocialIcons.css\";\n\nexport const GoogleIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      height=\"24\"\n      width=\"24\"\n      viewBox=\"1 1 22 22\"\n      className=\"colored-icon\"\n    >\n      <path\n        d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n        fill=\"#4285F4\"\n      />\n      <path\n        d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n        fill=\"#34A853\"\n      />\n      <path\n        d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"\n        fill=\"#FBBC05\"\n      />\n      <path\n        d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n        fill=\"#EA4335\"\n      />\n      <path d=\"M1 1h22v22H1z\" fill=\"none\" />\n    </svg>\n  );\n};\n\nexport const MicrosoftIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      height=\"24\"\n      width=\"24\"\n      viewBox=\"0 0 21 21\"\n      className=\"colored-icon\"\n    >\n      <path fill=\"#f35325\" d=\"M0 0h10v10H0z\" />\n      <path fill=\"#81bc06\" d=\"M11 0h10v10H11z\" />\n      <path fill=\"#05a6f0\" d=\"M0 11h10v10H0z\" />\n      <path fill=\"#ffba08\" d=\"M11 11h10v10H11z\" />\n    </svg>\n  );\n};\n\nexport const GithubIcon = () => {\n  return (\n    <svg\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 98 96\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fill=\"currentColor\"\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z\"\n      />\n    </svg>\n  );\n};\n\nexport const FacebookIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"4 4 39 39\"\n      className=\"colored-icon\"\n    >\n      <path fill=\"#039be5\" d=\"M24 5A19 19 0 1 0 24 43A19 19 0 1 0 24 5Z\"></path>\n      <path\n        fill=\"#fff\"\n        d=\"M26.572,29.036h4.917l0.772-4.995h-5.69v-2.73c0-2.075,0.678-3.915,2.619-3.915h3.119v-4.359c-0.548-0.074-1.707-0.236-3.897-0.236c-4.573,0-7.254,2.415-7.254,7.917v3.323h-4.701v4.995h4.701v13.729C22.089,42.905,23.032,43,24,43c0.875,0,1.729-0.08,2.572-0.194V29.036z\"\n      ></path>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/Login/useLoginState.ts",
    "content": "import { EMAIL_CONFIRMED_SEARCH_PARAM } from \"@common/OAuthUtils\";\nimport { ROUTES } from \"@common/utils\";\nimport {\n  authRequest,\n  type PasswordLogin,\n  type PasswordRegister,\n} from \"prostgles-client/dist/getAuthHandler\";\nimport type { AuthResponse } from \"prostgles-types\";\nimport { useEffect, useState } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport type { LoginFormProps } from \"./Login\";\n\ntype PasswordLoginDataAndFunc = {\n  onCall: PasswordLogin;\n  result: undefined | Awaited<ReturnType<PasswordLogin>>;\n} & Parameters<PasswordLogin>[0];\n\ntype PasswordRegisterDataAndFunc = {\n  onCall: PasswordRegister;\n  result: undefined | Awaited<ReturnType<PasswordRegister>>;\n} & Parameters<PasswordRegister>[0];\n\ntype FormData =\n  | ({ type: \"login\" } & PasswordLoginDataAndFunc)\n  | ({ type: \"loginTotp\" } & PasswordLoginDataAndFunc)\n  | ({ type: \"loginTotpRecovery\" } & PasswordLoginDataAndFunc)\n  | ({\n      type: \"registerWithPassword\";\n    } & PasswordRegisterDataAndFunc)\n  | ({\n      type: \"registerWithPasswordConfirmationCode\";\n    } & PasswordRegisterDataAndFunc);\n\ntype FormStates = FormData[\"type\"];\n\nconst loginStates = [\n  \"login\",\n  \"loginTotp\",\n  \"loginTotpRecovery\",\n] as const satisfies FormStates[];\n\nexport const useLoginState = ({ auth }: LoginFormProps) => {\n  const [loading, setIsLoading] = useState(false);\n  const [state, setState] = useState<FormStates>(\"login\");\n  const [username, setUsername] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n  const [emailVerificationCode, setEmailVerificationCode] = useState(\"\");\n  const [totpToken, setTotpToken] = useState(\"\");\n  const [totpRecoveryCode, setTotpRecoveryCode] = useState(\"\");\n  const [confirmPassword, setConfirmPassword] = useState(\"\");\n  const [usernamesWithPassword, setUsernamesWithPassword] = useState<string[]>(\n    [],\n  );\n  const [error, _setError] = useState(\"\");\n  const [result, setResult] =\n    useState<Extract<FormData, { type: typeof state }>[\"result\"]>();\n  const [authResponse, setAuthResponse] = useState<{\n    success: boolean;\n    message: string;\n  }>();\n  const [searchParams, setSearchParams] = useSearchParams();\n  const verifiedEmail = searchParams.get(EMAIL_CONFIRMED_SEARCH_PARAM);\n  useEffect(() => {\n    if (!verifiedEmail) return;\n    setAuthResponse({\n      success: true,\n      message: SIGNUP_CODE_MESSAGES[\"email-verified\"],\n    });\n  }, [verifiedEmail, setSearchParams]);\n\n  useEffect(() => {\n    _setError(\"\");\n  }, [username, password, totpToken, totpRecoveryCode, confirmPassword]);\n\n  const formHandlers =\n    state === \"login\" && auth.loginType === \"email\" ?\n      {\n        state,\n        username,\n        setUsername,\n        ...(usernamesWithPassword.includes(username) && {\n          password,\n          setPassword,\n        }),\n        onCall: auth.login,\n      }\n    : state === \"login\" && auth.loginType === \"email+password\" ?\n      {\n        state,\n        username,\n        setUsername,\n        password,\n        setPassword,\n        onCall: auth.login,\n      }\n    : state === \"loginTotp\" && auth.login ?\n      {\n        state,\n        totpToken,\n        setTotpToken,\n        onCall: auth.login,\n      }\n    : state === \"loginTotpRecovery\" && auth.login ?\n      {\n        state,\n        totpRecoveryCode,\n        setTotpRecoveryCode,\n        onCall: auth.login,\n      }\n    : state === \"registerWithPassword\" && auth.signupWithEmailAndPassword ?\n      {\n        state,\n        username,\n        setUsername,\n        password,\n        setPassword,\n        confirmPassword,\n        setConfirmPassword,\n        onCall: auth.signupWithEmailAndPassword,\n        result,\n      }\n    : (\n      state === \"registerWithPasswordConfirmationCode\" &&\n      auth.signupWithEmailAndPassword\n    ) ?\n      {\n        state,\n        username,\n        emailVerificationCode,\n        setEmailVerificationCode,\n        onCall: () =>\n          authRequest(ROUTES.MAGIC_LINK, {\n            email: username,\n            code: emailVerificationCode,\n          }),\n        result,\n      }\n    : undefined;\n\n  const isOnLogin = loginStates.some((v) => v === state);\n  const registerTypeAllowed: FormStates = \"registerWithPassword\";\n\n  const onAuthCall = async () => {\n    const formData = {\n      username,\n      password,\n      remember_me: false,\n      totp_token: totpToken,\n      totp_recovery_code: totpRecoveryCode,\n    };\n\n    const errorMap = {\n      \"no match\": \"Invalid credentials\",\n    };\n    const setError = (err: string) => {\n      _setError(err);\n    };\n    const setErrorWithInfo = (err: string) => {\n      return setError(errorMap[err] ?? err);\n    };\n\n    if (!formHandlers?.onCall) {\n      return setError(\"Invalid state\");\n    }\n\n    /**\n     * Validate form data\n     */\n    if (isOnLogin) {\n      if (!username) {\n        return setError(\"Username/email cannot be empty\");\n      }\n      if (!password && formHandlers.setPassword) {\n        return setError(\"Password cannot be empty\");\n      }\n      if (formHandlers.state === \"loginTotp\" && !totpToken) {\n        return setError(\"Token cannot be empty\");\n      }\n      if (formHandlers.state === \"loginTotpRecovery\" && !totpRecoveryCode) {\n        return setError(\"Recovery code cannot be empty\");\n      }\n    } else {\n      if (!username) {\n        return setError(\"Email cannot be empty\");\n      }\n      if (formHandlers.state === \"registerWithPassword\") {\n        if (!password) {\n          return setError(\"Password cannot be empty\");\n        } else if (password !== confirmPassword) {\n          return setError(\"Passwords do not match\");\n        }\n      }\n    }\n\n    const res = await formHandlers.onCall(formData);\n    if (!res.success) {\n      if (\n        state === \"login\" &&\n        res.code === \"password-missing\" &&\n        !usernamesWithPassword.includes(username)\n      ) {\n        setUsernamesWithPassword([...usernamesWithPassword, username]);\n      }\n      if (state === \"login\" && res.code === \"email-not-confirmed\") {\n        setAuthResponse({\n          success: false,\n          message: ERR_CODE_MESSAGES[res.code],\n        });\n        setState(\"registerWithPasswordConfirmationCode\");\n      }\n      if (res.code === \"totp-token-missing\") {\n        setState(\"loginTotp\");\n      } else if (res.code !== \"password-missing\") {\n        const errorMessage = res.message ?? ERR_CODE_MESSAGES[res.code];\n        setErrorWithInfo(errorMessage);\n      }\n    } else {\n      if (\"redirect_url\" in res && res.redirect_url) {\n        window.location.href = res.redirect_url;\n      }\n      if (state === \"registerWithPassword\" || res.code === \"magic-link-sent\") {\n        setState(\"registerWithPasswordConfirmationCode\");\n      }\n\n      let message =\n        (res.code && SIGNUP_CODE_MESSAGES[res.code]) ??\n        res.message ??\n        \"Success\";\n      if (formHandlers.state === \"registerWithPasswordConfirmationCode\") {\n        message = SIGNUP_CODE_MESSAGES[\"email-verified\"];\n        setState(\"login\");\n      }\n\n      !res.redirect_url &&\n        setAuthResponse({\n          ...res,\n          message,\n        });\n    }\n    setResult(res);\n    return res;\n  };\n\n  return {\n    formHandlers,\n    error,\n    setState,\n    state,\n    loading,\n    onAuthCall: () => {\n      setIsLoading(true);\n      return onAuthCall().finally(() => setIsLoading(false));\n    },\n    isOnLogin,\n    registerTypeAllowed,\n    result,\n    authResponse,\n    clearAuthResponse: () => {\n      setAuthResponse(undefined);\n      setSearchParams({});\n    },\n  };\n};\n\nexport const ERR_CODE_MESSAGES = {\n  \"no-match\": \"Invalid credentials\",\n  \"password-missing\": \"Password cannot be empty\",\n  \"totp-token-missing\": \"Token cannot be empty\",\n  \"invalid-totp-recovery-code\": \"Invalid recovery code\",\n  \"rate-limit-exceeded\": \"Too many failed attempts\",\n  \"email-not-confirmed\":\n    \"Email not confirmed. Please check your email and open the confirmation url or enter the provided code\",\n  \"expired-magic-link\": \"Magic link expired\",\n  \"inactive-account\": \"Account is inactive\",\n  \"invalid-password\": \"Invalid or missing password\",\n  \"invalid-totp-code\": \"Invalid or missing totp code\",\n  \"invalid-username\": \"Invalid or missing username\",\n  \"is-from-magic-link\": \"Cannot login with password\",\n  \"is-from-OAuth\": \"Cannot login with password\",\n  \"server-error\": \"Server error\",\n  \"something-went-wrong\": \"Something went wrong\",\n  \"user-already-registered\": \"User already registered\",\n  \"username-missing\": \"Username cannot be empty\",\n  \"weak-password\": \"Password is too weak\",\n  \"expired-email-confirmation-code\": \"Email confirmation code expired\",\n  \"invalid-email-confirmation-code\": \"Invalid email confirmation code\",\n  \"invalid-magic-link\": \"Invalid magic link\",\n  \"used-magic-link\": \"Magic link already used\",\n  \"invalid-email\": \"Invalid or missing email\",\n} satisfies Record<\n  | AuthResponse.MagicLinkAuthFailure[\"code\"]\n  | AuthResponse.PasswordLoginFailure[\"code\"]\n  | AuthResponse.PasswordRegisterFailure[\"code\"],\n  string\n>;\n\nconst SIGNUP_CODE_MESSAGES = {\n  \"already-registered-but-did-not-confirm-email\":\n    \"Email verification re-sent. Open the verification url or enter the code to confirm your email\",\n  \"email-verification-code-sent\":\n    \"Email verification sent. Open the verification url or enter the code to confirm your email\",\n  \"email-verified\": \"Your email has been confirmed. You can now sign in\",\n  \"magic-link-sent\": \"Magic link sent. Open the url from your email to login\",\n} satisfies Record<\n  | AuthResponse.PasswordRegisterSuccess[\"code\"]\n  | AuthResponse.MagicLinkAuthSuccess[\"code\"]\n  | AuthResponse.PasswordRegisterEmailConfirmationSuccess[\"code\"],\n  string\n>;\n"
  },
  {
    "path": "client/src/pages/NewConnection/NewConnectionFormFields.tsx",
    "content": "import { mdiConnection, mdiDotsHorizontal, mdiPlus } from \"@mdi/js\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useEffect, useRef } from \"react\";\nimport Btn from \"@components/Btn\";\nimport ButtonGroup from \"@components/ButtonGroup\";\nimport { ExpandSection } from \"@components/ExpandSection\";\nimport { FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { FormFieldDebounced } from \"@components/FormField/FormFieldDebounced\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport CodeExample from \"../../dashboard/CodeExample\";\nimport type { Connection } from \"./NewConnnectionForm\";\nimport type { FullExtraProps } from \"../ProjectConnection/ProjectConnection\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { getDBCloneQuery, SSL_MODES } from \"./newConnectionUtils\";\nimport { SchemaFilter } from \"./SchemaFilter\";\n\ntype DBProps = {\n  origCon: Partial<Connection>;\n  dbProject: FullExtraProps[\"dbProject\"];\n  dbsTables: FullExtraProps[\"dbsTables\"];\n  dbsMethods: FullExtraProps[\"dbsMethods\"];\n};\n\nexport type NewConnectionFormProps = {\n  c: Connection;\n  nameErr?: string;\n  updateConnection: (con: Partial<Connection>) => Promise<void>;\n  warning?: any;\n  mode: \"clone\" | \"edit\" | \"insert\";\n  isForStateDB?: boolean;\n\n  test: {\n    onTest?: () => Promise<void>;\n    status: string;\n    statusOK: boolean;\n  };\n\n  dbProps: DBProps | undefined;\n};\n\nexport const NewConnectionForm = ({\n  c,\n  updateConnection,\n  nameErr,\n  warning,\n  test,\n  mode,\n  isForStateDB,\n  dbProps,\n}: NewConnectionFormProps) => {\n  const refStatus = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    refStatus.current?.scrollIntoView();\n  }, [test.status]);\n  const sslmode = \"db_ssl\" in c ? c.db_ssl || \"disable\" : \"disabled\";\n\n  const { dbsTables, dbProject, origCon, dbsMethods } = dbProps ?? {};\n\n  const cTable = dbsTables?.find((t) => t.name === \"connections\");\n\n  const { type } = c;\n\n  const suggestions = usePromise(async () => {\n    if (!dbProject?.sql) return {};\n\n    try {\n      const databases = (await dbProject.sql(\n        `\n      SELECT datname\n      FROM pg_catalog.pg_database\n      `,\n        {},\n        { returnType: \"values\" },\n      )) as string[];\n\n      const users = (await dbProject.sql(\n        `\n        SELECT rolname, rolsuper\n        FROM pg_catalog.pg_roles`,\n        {},\n        { returnType: \"rows\" },\n      )) as {\n        rolname: string;\n        rolsuper: boolean;\n      }[];\n\n      const schemas = (await dbProject.sql(\n        `\n        SELECT schema_name, schema_owner\n        FROM information_schema.schemata\n        ORDER BY (\n          CASE WHEN schema_name = 'public' THEN '0' \n            WHEN schema_owner = 'postgres' THEN 'b' \n            ELSE 'a' \n          END\n        ) || schema_name\n        `,\n        {},\n        { returnType: \"rows\" },\n      )) as { schema_name: string; schema_owner: string }[];\n\n      return { users, databases, schemas };\n    } catch (e) {\n      console.error(\"Failed getting user & db suggestions\", e);\n    }\n  }, [dbProject]);\n\n  return (\n    <>\n      {!isForStateDB && (\n        <FormField\n          data-command=\"NewConnectionForm.connectionName\"\n          label={t.NewConnectionForm[\"Connection name\"]}\n          hint={t.NewConnectionForm.Optional}\n          type=\"text\"\n          error={nameErr}\n          value={c.name}\n          onChange={(name: string) => {\n            updateConnection({ name });\n          }}\n        />\n      )}\n\n      <ButtonGroup\n        label={{\n          label: t.NewConnectionForm[\"Connection type\"],\n          variant: \"normal\",\n        }}\n        data-command=\"NewConnectionForm.connectionType\"\n        value={type}\n        options={[\"Standard\", \"Connection URI\"]}\n        onChange={(type) => {\n          updateConnection({ type });\n        }}\n      />\n\n      {type === \"Prostgles\" ?\n        <>\n          <FormField\n            label={t.NewConnectionForm[\"Socket URL\"]}\n            type=\"url\"\n            required={true}\n            value={c.prgl_url || \"\"}\n            onChange={(val: string) => {\n              updateConnection({ prgl_url: val });\n            }}\n          />\n          <FormField\n            label={t.NewConnectionForm[\"Socket params (JSON)\"]}\n            type=\"text\"\n            required={true}\n            value={c.prgl_params}\n            onChange={(val: string) => {\n              updateConnection({ prgl_params: val });\n            }}\n            hint={`{ \"path\": \"/socket\" } `}\n          />\n        </>\n      : type === \"Connection URI\" ?\n        <>\n          <FormFieldDebounced\n            label={t.NewConnectionForm[\"Connection URI\"]}\n            data-command=\"NewConnectionForm.db_conn\"\n            type=\"text\"\n            hint=\"postgres://user:pass@host:port/database?sslmode=require\"\n            required={true}\n            value={c.db_conn || \"\"}\n            onChange={(db_conn: string) => {\n              updateConnection({ db_conn });\n            }}\n          />\n        </>\n      : <>\n          <FormField\n            id=\"h\"\n            value={c.db_host}\n            label={t.NewConnectionForm[\"Host\"]}\n            data-command=\"NewConnectionForm.db_host\"\n            type=\"text\"\n            autoComplete=\"off\"\n            onChange={(db_host) => updateConnection({ db_host })}\n          />\n          <FormField\n            id=\"p\"\n            value={c.db_port}\n            label={t.NewConnectionForm[\"Port\"]}\n            data-command=\"NewConnectionForm.db_port\"\n            type=\"number\"\n            autoComplete=\"off\"\n            onChange={(db_port) => updateConnection({ db_port })}\n          />\n          <FormField\n            id=\"u\"\n            value={c.db_user}\n            fullOptions={suggestions?.users?.map(({ rolname, rolsuper }) => ({\n              key: rolname,\n              subLabel: rolsuper ? \"Superuser\" : \"\",\n            }))}\n            data-command=\"NewConnectionForm.db_user\"\n            label={t.NewConnectionForm[\"User\"]}\n            type=\"text\"\n            autoComplete=\"off\"\n            onChange={(db_user) => updateConnection({ db_user })}\n          />\n          <FormField\n            id=\"pass\"\n            value={c.db_pass ?? \"\"}\n            label={t.NewConnectionForm[\"Password\"]}\n            data-command=\"NewConnectionForm.db_pass\"\n            type=\"text\"\n            autoComplete=\"off\"\n            onChange={(db_pass) => updateConnection({ db_pass })}\n          />\n\n          <FormField\n            id=\"d\"\n            value={c.db_name}\n            label={t.NewConnectionForm.Database}\n            data-command=\"NewConnectionForm.db_name\"\n            type=\"text\"\n            options={suggestions?.databases}\n            autoComplete=\"off\"\n            onChange={(db_name) => {\n              void updateConnection({ db_name });\n            }}\n            rightContentAlwaysShow={true}\n            rightIcons={\n              mode === \"edit\" &&\n              c.db_name &&\n              dbProject?.sql && (\n                <PopupMenu\n                  title={t.NewConnectionForm[\"Create database\"]}\n                  positioning={undefined}\n                  clickCatchStyle={{ opacity: 0.5 }}\n                  button={\n                    <Btn\n                      iconPath={mdiPlus}\n                      title={t.NewConnectionForm[\"Create database\"]}\n                    ></Btn>\n                  }\n                  initialState={\n                    { query: \"\", action: \"create\" } as {\n                      query: string;\n                      action: \"create\" | \"clone\";\n                    }\n                  }\n                  render={(pClose, { query, action }, setState) => {\n                    if (action === \"clone\" && origCon?.db_name) {\n                      getDBCloneQuery(\n                        origCon.db_name,\n                        c.db_name,\n                        dbProject.sql!,\n                      ).then((newQuery) => {\n                        if (newQuery !== query) setState({ query: newQuery });\n                      });\n                    } else {\n                      const newQuery = `CREATE DATABASE ${c.db_name}; `;\n                      if (newQuery !== query) setState({ query: newQuery });\n                    }\n\n                    return (\n                      <div className=\"flex-col gap-1\">\n                        <ButtonGroup\n                          value={action}\n                          options={[\"create\", \"clone\"]}\n                          onChange={(action) => setState({ action })}\n                        />\n                        {action === \"clone\" && (\n                          <InfoRow>\n                            {t.NewConnectionForm[\n                              \"You are about to clone the current database\"\n                            ]({\n                              currDb: origCon?.db_name ?? \"\",\n                              newDb: c.db_name,\n                            })}\n                          </InfoRow>\n                        )}\n                        {action === \"create\" && (\n                          <InfoRow color=\"action\">\n                            {\n                              t.NewConnectionForm[\n                                \"You are about to create a new database\"\n                              ]\n                            }\n                            : {c.db_name}\n                          </InfoRow>\n                        )}\n                        <CodeExample\n                          value={query}\n                          language=\"sql\"\n                          style={{ minHeight: \"400px\" }}\n                        />\n                        <Btn\n                          variant=\"filled\"\n                          color=\"action\"\n                          onClickPromise={() =>\n                            dbProject.sql!(query).then(pClose)\n                          }\n                        >\n                          {t.common.Run}\n                        </Btn>\n                      </div>\n                    );\n                  }}\n                />\n              )\n            }\n          />\n        </>\n      }\n\n      {warning && <ErrorComponent error={warning} style={{ minWidth: 0 }} />}\n\n      {type !== \"Prostgles\" && (\n        <>\n          <ExpandSection\n            label={t.NewConnectionForm[\"More options\"]}\n            buttonProps={{\n              variant: undefined,\n              color: \"action\",\n              iconPath: mdiDotsHorizontal,\n              \"data-command\": \"NewConnectionForm.MoreOptionsToggle\",\n            }}\n          >\n            <SchemaFilter\n              db_schema_filter={c.db_schema_filter}\n              db={dbProject}\n              onChange={(newDbSchemaFilter) => {\n                updateConnection({\n                  type: \"Standard\",\n                  db_schema_filter: newDbSchemaFilter,\n                });\n              }}\n              asSelect={undefined}\n            />\n            <FormField\n              id=\"timeout\"\n              label={t.NewConnectionForm[\"Connection timeout (ms)\"]}\n              data-command=\"NewConnectionForm.connectionTimeout\"\n              optional={true}\n              value={c.db_connection_timeout}\n              onChange={(db_connection_timeout) => {\n                updateConnection({ db_connection_timeout, type: \"Standard\" });\n              }}\n            />\n            <FormField\n              id=\"ssl_mode\"\n              label={t.NewConnectionForm[\"SSL Mode\"]}\n              data-command=\"NewConnectionForm.sslMode\"\n              fullOptions={SSL_MODES}\n              required={true}\n              value={c.db_ssl}\n              onChange={(db_ssl: (typeof SSL_MODES)[number][\"key\"]) => {\n                // if(c.type === \"Connection URI\"){\n                //   const con = await dbsMethods?.validateConnection?.({ ...c, db_ssl, type: \"Standard\" });\n                //   console.log(con);\n                // }\n                /** Switch to standard to ensure the db_conn is updated accordingly */\n                void updateConnection({ db_ssl, type: \"Standard\" });\n              }}\n            />\n            {[\n              \"verify-ca\",\n              \"verify-full\",\n              \"require\",\n              \"prefer\",\n              \"allow\",\n            ].includes(sslmode) && (\n              <>\n                <FormField\n                  id=\"ssl_cert\"\n                  label={t.NewConnectionForm[\"CA Certificate\"]}\n                  type=\"file\"\n                  labelStyle={{ flex: \"unset\" }}\n                  onChange={async (files) => {\n                    const file = files[0];\n                    void updateConnection({\n                      ssl_certificate: (await file?.text()) ?? undefined,\n                    });\n                  }}\n                />\n                <FormField\n                  id=\"ssl_client_cert\"\n                  label=\"Client Certificate\"\n                  type=\"file\"\n                  labelStyle={{ flex: \"unset\" }}\n                  onChange={async (files) => {\n                    const file = files[0];\n                    void updateConnection({\n                      ssl_client_certificate: (await file?.text()) ?? undefined,\n                    });\n                  }}\n                />\n                <FormField\n                  id=\"ssl_client_cert_key\"\n                  label={t.NewConnectionForm[\"Client Key\"]}\n                  type=\"file\"\n                  labelStyle={{ flex: \"unset\" }}\n                  onChange={async (files) => {\n                    const file = files[0];\n                    void updateConnection({\n                      ssl_client_certificate_key:\n                        (await file?.text()) ?? undefined,\n                    });\n                  }}\n                />\n                <FormField\n                  id=\"ssl_rejectUnauthorized\"\n                  label={t.NewConnectionForm[\"Reject unauthorized\"]}\n                  type=\"checkbox\"\n                  labelStyle={{ flex: \"unset\" }}\n                  nullable={true}\n                  value={c.ssl_reject_unauthorized}\n                  onChange={(ssl_reject_unauthorized) => {\n                    updateConnection({ ssl_reject_unauthorized });\n                  }}\n                  hint={\n                    cTable?.columns.find(\n                      (c) => c.name === \"ssl_reject_unauthorized\",\n                    )?.hint\n                  }\n                />\n              </>\n            )}\n          </ExpandSection>\n          {!isForStateDB && (\n            <>\n              <FlexRow className=\"mt-1\">\n                <SwitchToggle\n                  id=\"swatch\"\n                  label={{\n                    label: t.NewConnectionForm[\"Watch schema\"],\n                    info: t.NewConnectionForm[\n                      \"Will refresh the dashboard and API on schema change. Requires superuser for best experience\"\n                    ],\n                  }}\n                  data-command=\"NewConnectionForm.watchSchema\"\n                  checked={!!c.db_watch_shema}\n                  onChange={(db_watch_shema) => {\n                    updateConnection({ db_watch_shema });\n                  }}\n                />\n                {dbsMethods?.reloadSchema && (\n                  <Btn\n                    onClickPromise={() => dbsMethods.reloadSchema!(c.id!)}\n                    color=\"action\"\n                  >\n                    {t.NewConnectionForm[\"Reload schema\"]}\n                  </Btn>\n                )}\n              </FlexRow>\n              <SwitchToggle\n                id=\"realtime\"\n                label={{\n                  label: t.NewConnectionForm.Realtime,\n                  info: t.NewConnectionForm[\n                    \"Needed to allow realtime data view. Requires superuser\"\n                  ],\n                }}\n                data-command=\"NewConnectionForm.realtime\"\n                checked={!c.disable_realtime}\n                onChange={(disable_realtime) => {\n                  updateConnection({ disable_realtime: !disable_realtime });\n                }}\n              />\n              {!c.disable_realtime && (\n                <InfoRow>\n                  {\n                    t.NewConnectionForm[\n                      \"Realtime requires table triggers to be created as and when needed.\"\n                    ]\n                  }\n                  <br></br>\n                  {\n                    t.NewConnectionForm[\n                      'A \"prostgles\" schema with necessary metadata will also be created'\n                    ]\n                  }\n                </InfoRow>\n              )}\n            </>\n          )}\n          <div className=\"flex-col my-1 gap-1\">\n            {test.onTest && (\n              <Btn\n                variant=\"faded\"\n                data-command=\"NewConnectionForm.testConnection\"\n                color=\"default\"\n                iconPath={mdiConnection}\n                onClickPromise={test.onTest}\n              >\n                {t.NewConnectionForm[\"Test connection\"]}\n              </Btn>\n            )}\n\n            {!!test.status && (\n              <div\n                ref={refStatus}\n                style={{ padding: \"1em\", borderRadius: \"8px\" }}\n                className={\n                  \"chip flex-col p-1 \" + (test.statusOK ? \"green\" : \"red\")\n                }\n              >\n                <span className=\"ws-pre\">{test.status}</span>\n              </div>\n            )}\n          </div>\n        </>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/NewConnection/NewConnnectionForm.tsx",
    "content": "import {\n  mdiArrowLeft,\n  mdiCheck,\n  mdiContentDuplicate,\n  mdiDeleteOutline,\n  mdiPlus,\n} from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport React from \"react\";\nimport { NavLink, useNavigate, useParams } from \"react-router-dom\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { isObject } from \"@common/publishUtils\";\nimport type { ExtraProps } from \"../../App\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent, { getErrorMessage } from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Loading from \"@components/Loader/Loading\";\nimport { Section } from \"@components/Section\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { CodeConfirmation } from \"../../dashboard/BackupAndRestore/CodeConfirmation\";\nimport RTComp from \"../../dashboard/RTComp\";\nimport { JoinedRecords } from \"../../dashboard/SmartForm/JoinedRecords/JoinedRecords\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { get } from \"../../utils/utils\";\nimport { getBrowserOS } from \"../ElectronSetup/ElectronSetup\";\nimport { PostgresInstallationInstructions } from \"../../components/PostgresInstallationInstructions\";\nimport type { FullExtraProps } from \"../ProjectConnection/ProjectConnection\";\nimport { NewConnectionForm } from \"./NewConnectionFormFields\";\nimport { ROUTES } from \"@common/utils\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\n\nexport const getSqlErrorText = (e: any) => {\n  let objDetails: [string, any][] = [];\n\n  /** Tablehandler error */\n  if (isObject(e) && \"txt\" in e) {\n    objDetails = [\"table\", \"column\"]\n      .map((k) => [k, e[k]] satisfies [string, any])\n      .filter((d) => typeof d[1] === \"string\");\n  }\n\n  /** SQL handler error */\n  const text =\n    get(e, \"err.err_msg\") ||\n    get(e, \"err.constraint\") ||\n    get(e, \"err.txt\") | get(e, \"err.message\") ||\n    e.err_msg ||\n    e;\n  if (typeof text === \"string\" && isObject(e.err)) {\n    objDetails = [\"schema\", \"table\", \"column\"]\n      .map((k) => [k, e.err[k]] satisfies [string, any])\n      .filter((d) => typeof d[1] === \"string\");\n  }\n  if (objDetails.length) {\n    return `${objDetails\n      .map(([key, val]) => `${key}: ${val}`)\n      .concat([text])\n      .join(\"\\n\")}`;\n  }\n  return text;\n};\n\nexport type Connection = Omit<\n  DBGeneratedSchema[\"connections\"][\"columns\"],\n  \"user_id\"\n>;\n\nexport const DEFAULT_CONNECTION = {\n  id: undefined,\n  name: \"\",\n  type: \"Standard\" as Connection[\"type\"],\n  db_user: \"\",\n  db_pass: \"\",\n  db_name: \"\",\n  db_host: \"\",\n  db_port: 5432,\n  db_ssl: \"prefer\",\n  prgl_url: \"\",\n  prgl_params: \"\",\n  db_conn: \"\",\n  db_watch_shema: true,\n  db_connection_timeout: 10_000,\n} as const;\n\ntype NewConnectionProps = {\n  db: FullExtraProps[\"dbProject\"] | undefined;\n  connectionId: string | undefined;\n  prglState: Pick<\n    ExtraProps,\n    \"dbs\" | \"dbsMethods\" | \"dbsTables\" | \"user\" | \"theme\"\n  >;\n  onDeleted?: () => void;\n  onUpserted?: (connection: Required<Connection>) => void;\n  contentOnly?: boolean;\n  showTitle: boolean;\n};\n\ntype NewConnectionState = {\n  error?: any;\n  nameErr: string;\n  connection: Connection;\n  originalConnection?: DBSSchema[\"connections\"];\n  validationWarning?: string;\n  status: string;\n  statusOK: boolean;\n  conNotFound: boolean;\n  mode: \"clone\" | \"edit\" | \"insert\" | \"loading\";\n  wasEdited: boolean;\n  dropDatabase: boolean;\n  activeTabKey?: string;\n};\n\nclass NewConnection extends RTComp<NewConnectionProps, NewConnectionState> {\n  state: NewConnectionState = {\n    conNotFound: false,\n    nameErr: \"\",\n    connection: DEFAULT_CONNECTION,\n    wasEdited: false,\n    status: \"\",\n    statusOK: false,\n    dropDatabase: false,\n    mode: \"loading\",\n  };\n\n  get conId() {\n    return this.props.connectionId;\n  }\n\n  onMount = async () => {\n    const { prglState: pgrlState } = this.props;\n    if (this.conId) {\n      const { dbs } = pgrlState;\n      const connection = await dbs.connections.findOne({ id: this.conId });\n      if (connection && this.mounted) {\n        this.setState({\n          originalConnection: connection,\n          connection,\n          mode: \"edit\",\n        });\n      } else {\n        this.setState({\n          conNotFound: true,\n          mode: \"edit\",\n        });\n      }\n    } else {\n      this.setState({ mode: \"insert\" });\n    }\n  };\n\n  testConnection = async () => {\n    const { connection } = this.state;\n    const { prglState } = this.props;\n\n    const { dbsMethods } = prglState;\n    this.setState({ status: \"\" });\n    try {\n      const res = await dbsMethods.testDBConnection!(connection);\n      this.setState({\n        status:\n          \"OK\" +\n          ((res as any).isSSLModeFallBack ? \". (sslmode=prefer fallback)\" : \"\"),\n        statusOK: true,\n      });\n    } catch (err: any) {\n      this.setState({\n        status: err.err_msg ?? err.message,\n        statusOK: false,\n      });\n    }\n  };\n\n  onClickDelete = async () => {\n    const { connection: c, dropDatabase } = this.state;\n    const { onDeleted, prglState } = this.props;\n\n    const { dbsMethods } = prglState;\n    try {\n      await dbsMethods.deleteConnection!(c.id!, { dropDatabase });\n      /** Hacky way to prevent reconnections to dropped connection */\n      (window as any).dbSocket?.disconnect();\n      onDeleted?.();\n    } catch (e: any) {\n      this.setState({ error: getSqlErrorText(e) });\n      throw e;\n    }\n  };\n\n  addedName = false;\n  render() {\n    const {\n      nameErr,\n      connection: c,\n      status,\n      statusOK,\n      conNotFound,\n      mode,\n      error,\n      originalConnection: origCon,\n      dropDatabase,\n    } = this.state;\n    const { prglState, onUpserted, contentOnly, showTitle = true } = this.props;\n\n    if (mode === \"loading\") {\n      return <Loading className=\"m-auto\" />;\n    }\n\n    const { dbsMethods, dbs } = prglState;\n    if (!dbsMethods.createConnection || !dbsMethods.deleteConnection) {\n      return (\n        <InfoRow color=\"warning\" className=\"mt-2\">\n          {t.common[\"You do not have sufficient privileges to access this\"]}\n        </InfoRow>\n      );\n    }\n\n    if (conNotFound) {\n      return (\n        <FlexCol className=\"m-auto\">\n          <p>{t.NewConnection[\"Connection not found\"]}</p>\n          <NavLink to=\"/\">{t.NewConnection[\"Go back to connections\"]}</NavLink>\n        </FlexCol>\n      );\n    }\n\n    const updateConnection = async (con: Partial<Connection>) => {\n      if (mode !== \"edit\" && con.db_name !== undefined) {\n        this.addedName = true;\n      }\n\n      const currCon = this.state.connection;\n      const newData = {\n        ...currCon,\n        ...con,\n      };\n\n      /** Validated connection only after some crucial info is provided */\n      let { connection, warning } =\n        (\n          newData.db_host ||\n          newData.db_user ||\n          newData.db_name ||\n          newData.db_conn ||\n          mode === \"edit\" ||\n          mode === \"insert\"\n        ) ?\n          await dbsMethods.validateConnection!(newData).catch((error) => {\n            return { warning: error, connection };\n          })\n        : { connection: newData, warning: undefined };\n\n      if (mode !== \"edit\" && !this.addedName && !connection.name) {\n        connection.name = connection.db_name;\n      }\n\n      /** If just switched connection type then do not prefill */\n      const wasEdited =\n        this.state.wasEdited || Object.keys(con).join(\"\") !== \"type\";\n      if (!wasEdited) {\n        connection = {\n          ...connection,\n          db_conn: \"\",\n        };\n        warning = undefined;\n      }\n\n      this.setState({\n        nameErr: \"\",\n        validationWarning: warning,\n        status: \"\",\n        statusOK: false,\n        connection,\n        wasEdited,\n      });\n    };\n\n    return (\n      <FlexCol\n        className={\"min-h-0 \" + (contentOnly ? \" \" : \" mx-auto \")}\n        data-command=\"NewConnectionForm\"\n        style={{\n          maxWidth: \"100%\",\n          maxHeight: \"100vh\",\n          minWidth: \"450px\",\n        }}\n      >\n        {!contentOnly && (\n          <NavLink\n            className=\"p-1 text-1 round flex-row ai-center\"\n            to={ROUTES.CONNECTIONS}\n          >\n            <Icon path={mdiArrowLeft} size={1} />\n            <div className=\"ml-p5\">{t.NewConnection.Connections}</div>\n          </NavLink>\n        )}\n\n        <div\n          className={\n            \"flex-col bg-color-0  min-h-0 \" + (contentOnly ? \"\" : \" card p-1 \")\n          }\n          style={{ maxWidth: \"100%\" }}\n        >\n          <ScrollFade\n            className=\"flex-col gap-1 f-1 o-auto min-h-0 p-p25 no-scroll-bar\"\n            style={{\n              margin: \"-.25em\" /* Used to ensure focus border is visible */,\n            }}\n          >\n            {mode === \"clone\" && origCon && (\n              <InfoRow color=\"action\">\n                {t.NewConnection[\"Cloning connection\"]}:{\" \"}\n                <strong>{getServerInfo(origCon)}</strong>\n              </InfoRow>\n            )}\n            {showTitle && (\n              <h2 className=\"m-0 p-0 mb-1\">\n                {mode === \"edit\" ? \"Edit connection\" : \"Add connection\"}\n              </h2>\n            )}\n            {mode === \"insert\" && (\n              <PostgresInstallationInstructions\n                os={getBrowserOS()}\n                placement=\"add-connection\"\n              />\n            )}\n            <NewConnectionForm\n              {...this.props}\n              dbProps={\n                this.props.db ?\n                  {\n                    dbProject: this.props.db,\n                    dbsTables: this.props.prglState.dbsTables,\n                    origCon: c,\n                    dbsMethods: this.props.prglState.dbsMethods,\n                  }\n                : undefined\n              }\n              c={c}\n              nameErr={nameErr}\n              updateConnection={updateConnection}\n              mode={mode}\n              test={{\n                onTest: this.testConnection,\n                status,\n                statusOK,\n              }}\n            />\n          </ScrollFade>\n          <ErrorComponent\n            error={error}\n            style={{ background: \"white\", padding: \"1em\" }}\n            withIcon={true}\n          />\n          <div className=\"flex-row-wrap ai-center mt-1 gap-1 \">\n            {mode === \"edit\" && (\n              <CodeConfirmation\n                positioning=\"center\"\n                title={t.NewConnection[\"Delete connection\"]}\n                fixedCode={dropDatabase ? c.db_name : c.name}\n                button={\n                  <Btn\n                    iconPath={mdiDeleteOutline}\n                    color=\"danger\"\n                    variant=\"outline\"\n                    data-command=\"Connection.edit.delete\"\n                  >\n                    {t.common.Delete}...\n                  </Btn>\n                }\n                message={\n                  <FlexCol className=\"w-fit h-fit gap-1\">\n                    <InfoRow className=\"max-w-fit\">\n                      {\n                        t.NewConnection[\n                          \"Any related dashboard content will also be deleted\"\n                        ]\n                      }\n                    </InfoRow>\n                    <Section\n                      title={t.NewConnection[\"Related data\"]}\n                      disableFullScreen={true}\n                      style={{\n                        maxWidth: \"min(100%, 600px)\",\n                      }}\n                    >\n                      <JoinedRecords\n                        activeTabKey={this.state.activeTabKey}\n                        onTabChange={(activeTabKey) =>\n                          this.setState({ activeTabKey })\n                        }\n                        newRowDataHandler={undefined}\n                        newRowData={undefined}\n                        style={{ padding: 0 }}\n                        db={prglState.dbs as DBHandlerClient}\n                        rowFilter={[{ fieldName: \"id\", value: this.conId }]}\n                        showRelated=\"descendants\"\n                        tableName={\"connections\"}\n                        tables={prglState.dbsTables}\n                        methods={prglState.dbsMethods}\n                        errors={{}}\n                      />\n                    </Section>\n                    <SwitchToggle\n                      data-command=\"Connection.edit.delete.dropDatabase\"\n                      label={t.NewConnection[\"Drop database as well\"]}\n                      checked={dropDatabase}\n                      onChange={(dropDatabase) =>\n                        this.setState({ dropDatabase })\n                      }\n                    />\n                    <InfoRow\n                      className=\"ws-pre max-w-fit\"\n                      style={{ opacity: dropDatabase ? 1 : 0 }}\n                    >\n                      {t.NewConnection[\"You are about to drop\"]}{\" \"}\n                      <strong>{c.db_name}</strong> {t.NewConnection.database}.\n                      <br></br>\n                      {\n                        t.NewConnection[\n                          \"Ensure data is backed up. This action is not reversible\"\n                        ]\n                      }\n                    </InfoRow>\n                    <ErrorComponent error={error} />\n                  </FlexCol>\n                }\n                confirmButton={(popupClose) => (\n                  <Btn\n                    color=\"danger\"\n                    variant=\"filled\"\n                    iconPath={mdiDeleteOutline}\n                    className=\"ml-auto\"\n                    data-command=\"Connection.edit.delete.confirm\"\n                    onClickMessage={async (e, setMsg) => {\n                      setMsg({ loading: 1, delay: 0 });\n                      try {\n                        await this.onClickDelete().then(popupClose);\n                      } catch (e) {}\n                      setMsg({ loading: 0 });\n                    }}\n                  >\n                    {t.common.Delete}\n                  </Btn>\n                )}\n              />\n            )}\n\n            {mode === \"edit\" && (\n              <Btn\n                title={t.NewConnection[\"Clone connection\"]}\n                className={\"f-0 mx-1 w-fit \"}\n                variant=\"outline\"\n                color=\"action\"\n                iconPath={mdiContentDuplicate}\n                onClick={(e) => {\n                  if (c.name)\n                    void updateConnection({ name: c.name + \" (copy)\" });\n                  void updateConnection({ created: null, is_state_db: null });\n                  this.setState({ mode: \"clone\" });\n                }}\n              >\n                {t.common.Clone}\n              </Btn>\n            )}\n\n            <Btn\n              className={\"ml-auto w-fit\"}\n              variant=\"filled\"\n              color=\"action\"\n              data-command=\"Connection.edit.updateOrCreateConfirm\"\n              iconPath={mode === \"edit\" ? mdiCheck : mdiPlus}\n              disabledInfo={\n                (\n                  mode === \"edit\" &&\n                  JSON.stringify(c) === JSON.stringify(origCon)\n                ) ?\n                  t.common[\"Nothing to update\"]\n                : undefined\n              }\n              onClickMessage={async (e, setMsg) => {\n                try {\n                  const conn = { ...c };\n                  if (mode !== \"edit\") delete conn.id;\n                  setMsg({ loading: 1 });\n                  if (\n                    c.name &&\n                    (await dbs.connections.findOne({\n                      name: c.name,\n                      \"id.<>\": conn.id,\n                    }))\n                  ) {\n                    this.setState({\n                      nameErr: t.common[\"Already exists. Chose another name\"],\n                    });\n                    setMsg({ loading: 0 });\n                    return;\n                  }\n\n                  const { connection } =\n                    await dbsMethods.createConnection!(conn);\n\n                  onUpserted?.(connection);\n                  setMsg({\n                    ok:\n                      (mode !== \"edit\" ? t.common.Create : t.common.Update) +\n                      \"!\",\n                  });\n                } catch (e: any) {\n                  console.error(e);\n                  setMsg({ loading: 0 });\n                  const status = getErrorMessage(e);\n                  this.setState({ status, statusOK: false });\n                }\n              }}\n            >\n              {mode !== \"edit\" ? t.common.Create : t.common.Update}\n            </Btn>\n          </div>\n        </div>\n      </FlexCol>\n    );\n  }\n}\n\nexport default (props: NewConnectionProps) => {\n  const params = useParams();\n  const navigate = useNavigate();\n  return (\n    <NewConnection\n      {...props}\n      connectionId={props.connectionId ?? params.id}\n      onDeleted={() => {\n        navigate(\"/\");\n      }}\n      onUpserted={({ id }) => {\n        navigate(ROUTES.CONNECTIONS + \"/\" + id);\n      }}\n    />\n  );\n};\n\nconst getServerInfo = (\n  c: Pick<DBSSchema[\"connections\"], \"db_host\" | \"db_port\" | \"db_name\">,\n) => {\n  return (\n    <>\n      <div className=\"shrink-label\">\n        {c.db_host || \"localhost\"}:{c.db_port || \"5432\"}/{c.db_name}\n      </div>\n      {/* <div>User: {c.db_user}</div> */}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/NewConnection/SchemaFilter.tsx",
    "content": "import type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useMemo } from \"react\";\nimport FormField from \"@components/FormField/FormField\";\nimport { Select, type SelectProps } from \"@components/Select/Select\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport type { Connection } from \"./NewConnnectionForm\";\n\ntype P = Pick<Connection, \"db_schema_filter\"> & {\n  db: DBHandlerClient | undefined;\n  onChange: (newSchemaFilter: Connection[\"db_schema_filter\"]) => void;\n  asSelect:\n    | Pick<\n        SelectProps<string, true>,\n        \"asRow\" | \"btnProps\" | \"className\" | \"label\"\n      >\n    | undefined;\n};\n\nexport const SchemaFilter = ({\n  db,\n  db_schema_filter,\n  onChange,\n  asSelect,\n}: P) => {\n  const schemas = usePromise(async () => {\n    if (!db?.sql) return;\n\n    const schemas = (await db.sql(\n      `\n        SELECT schema_name, schema_owner\n        FROM information_schema.schemata\n        ORDER BY (\n          CASE WHEN schema_name = 'public' THEN '0' \n            WHEN schema_owner = 'postgres' THEN 'b' \n            ELSE 'a' \n          END\n        ) || schema_name\n        `,\n      {},\n      { returnType: \"rows\" },\n    )) as { schema_name: string; schema_owner: string }[];\n    return schemas;\n  }, [db]);\n\n  const commonProps = useMemo(() => {\n    const value = db_schema_filter || { public: 1 };\n    const selectedNames = Object.keys(value);\n\n    return {\n      id: \"schema_filter\",\n      label: t.NewConnectionForm[\"Schemas\"],\n      multiSelect: true,\n      \"data-command\": \"NewConnectionForm.schemaFilter\",\n      fullOptions:\n        schemas?.map((s) => ({\n          key: s.schema_name,\n          subLabel: s.schema_owner,\n          disabledInfo:\n            s.schema_name === \"prostgles\" ? \"Not allowed\" : undefined,\n        })) ?? [],\n      value: selectedNames,\n      disabledInfo:\n        !schemas ?\n          t.NewConnectionForm[\"Must connect to see schemas\"]\n        : undefined,\n      onChange: (selectedKeys: string[] | undefined) => {\n        const newSchemaFilter =\n          !selectedKeys ?\n            { public: 1 }\n          : selectedKeys.reduce(\n              (acc, key) => ({\n                ...acc,\n                [key]: Object.values(value).includes(1) ? 1 : 0,\n              }),\n              {},\n            );\n        onChange(newSchemaFilter);\n      },\n    } satisfies SelectProps<string, true>;\n  }, [db_schema_filter, onChange, schemas]);\n\n  if (asSelect) {\n    return <Select {...commonProps} {...asSelect} />;\n  }\n\n  return <FormField {...commonProps} />;\n};\n"
  },
  {
    "path": "client/src/pages/NewConnection/newConnectionUtils.ts",
    "content": "import type { SQLHandler } from \"prostgles-types\";\n\nexport const SSL_MODES = [\n  { key: \"disable\", subLabel: \"only try a non-SSL connection\" },\n  {\n    key: \"allow\",\n    subLabel:\n      \"first try a non-SSL connection; if that fails, try an SSL connection\",\n  },\n  {\n    key: \"prefer\",\n    subLabel:\n      \"(Default) first try an SSL connection; if that fails, try a non-SSL connection\",\n  },\n  {\n    key: \"require\",\n    subLabel:\n      \"only try an SSL connection. If a root CA file is present, verify the certificate in the same way as if verify-ca was specified\",\n  },\n  {\n    key: \"verify-ca\",\n    subLabel:\n      \"only try an SSL connection, and verify that the server certificate is issued by a trusted certificate authority (CA)\",\n  },\n  {\n    key: \"verify-full\",\n    subLabel:\n      \"only try an SSL connection, verify that the server certificate is issued by a trusted CA and that the requested server host name matches that in the certificate\",\n  },\n] as const;\n\nexport const getDBCloneQuery = (\n  oldDb: string,\n  newDb: string,\n  sql: SQLHandler,\n): Promise<string> => {\n  return sql(\n    \"/* originaldb must be idle/not accessed by other users */ \\n \\\n  CREATE DATABASE ${newDb} WITH TEMPLATE ${oldDb} OWNER current_user; \\n \\\n  \\n \\\n  /* To make originaldb idle */ \\n  \\\n  SELECT pg_terminate_backend(pg_stat_activity.pid)   \\n  \\\n  FROM pg_stat_activity  \\n  \\\n  WHERE pg_stat_activity.datname = ${newDb}   \\n  \\\n  AND pid <> pg_backend_pid(); \\n  \\\n  \",\n    { oldDb, newDb },\n    { returnType: \"statement\" },\n  );\n};\n"
  },
  {
    "path": "client/src/pages/NonHTTPSWarning.tsx",
    "content": "import { mdiAlertBox } from \"@mdi/js\";\nimport type { AppState } from \"../App\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Btn from \"@components/Btn\";\nimport React from \"react\";\nimport { pageReload } from \"@components/Loader/Loading\";\n\nexport const NonHTTPSWarning = ({\n  dbs,\n  auth,\n}: Required<AppState>[\"prglState\"]) => {\n  const authUser = auth.user;\n  if (\n    location.protocol !== \"https:\" &&\n    location.hostname !== \"localhost\" &&\n    location.hostname !== \"127.0.0.1\" &&\n    !authUser?.options?.hideNonSSLWarning\n  ) {\n    const canUpdateUsers = !!dbs.users.update as boolean;\n    return (\n      <InfoRow\n        color=\"danger\"\n        iconPath={mdiAlertBox}\n        className=\"m-p5 bg-color-0 ai-center\"\n        contentClassname=\"flex-row-wrap gap-1 ai-center\"\n      >\n        <div>\n          Your are accessing this page over a non-HTTPS connection! You should\n          not enter any sensitive information on this site (passwords, secrets)\n        </div>\n        {canUpdateUsers && (\n          <Btn\n            variant=\"faded\"\n            onClickPromise={async () => {\n              await dbs.users.update(\n                { id: authUser?.id },\n                { options: { $merge: [{ hideNonSSLWarning: true }] } },\n              );\n              await pageReload(\"hideNonSSLWarning toggle\");\n            }}\n          >\n            Do not show again\n          </Btn>\n        )}\n      </InfoRow>\n    );\n  }\n\n  return null;\n};\n"
  },
  {
    "path": "client/src/pages/NotFound.tsx",
    "content": "import { mdiArrowLeft } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\n\nexport const NotFound = () => {\n  return (\n    <FlexCol className=\"bg-color-0 ai-center p-2 f-1\" data-command=\"NotFound\">\n      <div className=\"p-1\">404 page not found</div>\n      <Btn\n        asNavLink={true}\n        href=\"/\"\n        iconPath={mdiArrowLeft}\n        color=\"action\"\n        variant=\"filled\"\n        data-command=\"NotFound.goHome\"\n      >\n        Home\n      </Btn>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ProjectConnection/PrglContextProvider.tsx",
    "content": "import React, { createContext, useContext } from \"react\";\nimport type { Prgl } from \"../../App\";\n\nconst PrglContext = createContext<Prgl | undefined>(undefined);\n\nexport const PrglProvider = ({\n  prgl,\n  children,\n}: {\n  prgl: Prgl;\n  children: React.ReactNode;\n}) => {\n  return <PrglContext.Provider value={prgl}>{children}</PrglContext.Provider>;\n};\n\nexport const usePrgl = () => {\n  const context = useContext(PrglContext);\n  if (context === undefined) {\n    throw new Error(\"usePrgl must be used within a PrglProvider\");\n  }\n  return context;\n};\n"
  },
  {
    "path": "client/src/pages/ProjectConnection/ProjectConnection.tsx",
    "content": "import React from \"react\";\nimport Loading from \"@components/Loader/Loading\";\n\nimport type { CommonWindowProps } from \"../../dashboard/Dashboard/Dashboard\";\nimport { Dashboard } from \"../../dashboard/Dashboard/Dashboard\";\n\nimport type {\n  DBHandlerClient,\n  MethodHandler,\n} from \"prostgles-client/dist/prostgles\";\nimport type { ExtraProps, Prgl, PrglState } from \"../../App\";\n\nimport { useParams, useSearchParams } from \"react-router-dom\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { ConnectionConfig } from \"../../dashboard/ConnectionConfig/ConnectionConfig\";\nimport { ProjectConnectionError } from \"./ProjectConnectionError\";\nimport { useProjectDb } from \"./useProjectDb\";\nimport { PrglProvider } from \"./PrglContextProvider\";\n\nexport type Connections = DBSSchema[\"connections\"];\nexport type ProjectProps = {\n  prglState: PrglState;\n  showConnectionConfig?: boolean;\n};\n\nexport type FullExtraProps = ExtraProps & {\n  projectPath?: string;\n  dbProject: DBHandlerClient;\n  dbMethods: MethodHandler;\n  dbTables: CommonWindowProps[\"tables\"];\n};\n\nexport const ProjectConnection = (props: ProjectProps) => {\n  const [sParams] = useSearchParams();\n  const workspaceId = sParams.get(\"workspaceId\") ?? undefined;\n  const params = useParams();\n  const { prglState, showConnectionConfig } = props;\n  const projectDb = useProjectDb({\n    prglState,\n    connId: params.connectionId,\n  });\n\n  if (projectDb.state === \"loading\") {\n    return (\n      <Loading\n        id=\"main\"\n        delay={0}\n        className=\"m-auto\"\n        message=\"Connecting to database...\"\n        refreshPageTimeout={55000}\n      />\n    );\n  }\n\n  if (projectDb.state === \"error\") {\n    return (\n      <ProjectConnectionError prglState={prglState} projectDb={projectDb} />\n    );\n  }\n\n  const prgl: Prgl = {\n    ...prglState,\n    ...projectDb,\n  };\n\n  const connectionId = params.connectionId;\n  const { connection } = projectDb;\n  if (showConnectionConfig && connectionId) {\n    return (\n      <PrglProvider prgl={prgl}>\n        <ConnectionConfig connection={connection} />\n      </PrglProvider>\n    );\n  }\n\n  return (\n    <div\n      className=\"Project f-1 flex-col h-full w-full min-h-0 min-w-0 relative bg-color-4\"\n      style={{\n        maxWidth: \"100vw\",\n        maxHeight: \"100vh\",\n      }}\n    >\n      <div\n        className=\"f-1 w-full h-full flex-col\"\n        style={prgl.theme === \"dark\" ? { background: \"black\" } : {}}\n      >\n        <PrglProvider prgl={prgl}>\n          <Dashboard\n            key={workspaceId}\n            workspaceId={workspaceId}\n            onLoaded={() => {}}\n          />\n        </PrglProvider>\n      </div>\n    </div>\n  );\n};\n\n// const cached: Record<string, (props: ProjectProps) => JSX.Element> = {};\n// function _Project(props: ProjectProps) {\n//   const compPath = \"./Project\";\n//   if (!cached[compPath]) {\n//     const LazyComponent = React.lazy(() => import(/* webpackChunkName: \"_Project\" */ \"./Project\").then(m => ({ default: m._Project })));\n\n//     cached[compPath] = (props: ProjectProps) => <Suspense fallback={<div>Loading</div>}>\n//       <LazyComponent { ...props } />\n//     </Suspense>\n//   }\n//   return cached[compPath](props);\n// }\n"
  },
  {
    "path": "client/src/pages/ProjectConnection/ProjectConnectionError.tsx",
    "content": "import { mdiArrowLeft, mdiLogin } from \"@mdi/js\";\nimport React from \"react\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport type { PrglState } from \"../../App\";\nimport Btn from \"@components/Btn\";\n\nimport { useParams } from \"react-router-dom\";\nimport { ROUTES } from \"@common/utils\";\nimport type { Command } from \"../../Testing\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { type PrglProjectState } from \"./useProjectDb\";\n\ntype P = {\n  projectDb: Extract<PrglProjectState, { state: \"error\" }>;\n  prglState: PrglState;\n};\nexport const ProjectConnectionError = (props: P) => {\n  const params = useParams();\n  const { prglState, projectDb } = props;\n\n  const canLogin =\n    !prglState.auth.user || prglState.auth.user.type === \"public\";\n  const error = projectDb.error;\n  return (\n    <FlexCol\n      className=\"ProjectConnectionError flex-col w-full h-full ai-center jc-center p-2 gap-1\"\n      data-command={\"ProjectConnection.error\" satisfies Command}\n    >\n      {projectDb.errorType === \"connNotFound\" && (\n        <div className=\"p-1\">\n          This project was not found or you are not allowed to access it\n        </div>\n      )}\n      {!!error && (\n        <>\n          Database connection error:\n          <ErrorComponent error={error} findMsg={true} />\n        </>\n      )}\n\n      <FlexRow>\n        <Btn\n          style={{ fontSize: \"18px\", fontWeight: \"bold\" }}\n          className=\"mt-1\"\n          variant=\"outline\"\n          asNavLink={true}\n          href={`/`}\n          iconPath={mdiArrowLeft}\n          color=\"action\"\n        >\n          {t.App.Connections}\n        </Btn>\n        {canLogin && (\n          <Btn\n            style={{ fontSize: \"18px\", fontWeight: \"bold\" }}\n            className=\"mt-1\"\n            variant=\"filled\"\n            asNavLink={true}\n            href={\n              ROUTES.LOGIN +\n              (!params.connectionId ? \"\" : (\n                `?returnURL=${encodeURIComponent(window.location.pathname + window.location.search)}`\n              ))\n            }\n            iconPath={mdiLogin}\n            color=\"action\"\n          >\n            {t.common.Login}\n          </Btn>\n        )}\n      </FlexRow>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ProjectConnection/useProjectDb.ts",
    "content": "import {\n  useMemoDeep,\n  usePromise,\n  useProstglesClient,\n  type UseProstglesClientProps,\n} from \"prostgles-client\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { useEffect, useMemo } from \"react\";\nimport type { PrglProject, PrglState } from \"../../App\";\nimport { getTables } from \"../../dashboard/Dashboard/getTables\";\nimport { isPlaywrightTest } from \"../../i18n/i18nUtils\";\nimport { prgl_R } from \"../../WithPrgl\";\n\ntype PrglProjectStateError = {\n  error: any;\n  state: \"error\";\n  errorType?: \"connNotFound\" | \"databaseConfigNotFound\" | \"connectionError\";\n};\nexport type PrglProjectState =\n  | {\n      error?: undefined;\n      state: \"loading\";\n    }\n  | PrglProjectStateError\n  | (PrglProject & {\n      /**\n       * Used to re-render dashboard on db reconnect\n       */\n      dbKey?: string;\n      error?: undefined;\n      loading: false;\n      state: \"loaded\";\n    });\n\ntype P = {\n  connId: string | undefined;\n  prglState: PrglState;\n};\n\nconst onDebug: UseProstglesClientProps[\"onDebug\"] = (ev) => {\n  if (\n    ev.type === \"schemaChanged\" ||\n    ev.type === \"onReady\" ||\n    ev.type === \"onReady.notMounted\" ||\n    ev.type === \"onReady.call\"\n  ) {\n    console.log(\n      Date.now(),\n      \"onDebug\",\n      ev.type,\n      Object.keys(ev.type === \"schemaChanged\" ? ev.data.schema : ev.data.dbo),\n    );\n  }\n};\n\nexport const useProjectDb = ({ prglState, connId }: P): PrglProjectState => {\n  const {\n    dbsMethods: { startConnection },\n    dbs,\n    dbsTables,\n  } = prglState;\n  const connectionTableHandler = dbs.connections;\n\n  const conState = connectionTableHandler.useSubscribeOne(\n    {\n      id: connId,\n    },\n    {\n      select: {\n        \"*\": 1,\n        database_configs: {\n          id: 1,\n        },\n      },\n    },\n    { skip: !connId },\n  );\n\n  const connectionInfo = useMemoDeep(() => {\n    if (conState.isLoading) {\n      return {\n        state: \"loading\",\n      } as const;\n    }\n    if (!conState.data) {\n      return {\n        state: \"error\",\n        errorType: \"connNotFound\",\n        error: `Could not find connection with id: ${connId}`,\n      } as const;\n    }\n    const databaseId = conState.data.database_configs?.[0]?.id as\n      | number\n      | undefined;\n    if (!databaseId) {\n      return {\n        state: \"error\",\n        errorType: \"databaseConfigNotFound\",\n        error: `Could not find database config for connection id: ${connId}`,\n      } as const;\n    }\n    return {\n      state: \"loaded\",\n      connectionId: conState.data.id,\n      connection: conState.data,\n      is_state_db: conState.data.is_state_db,\n      table_options: conState.data.table_options,\n      databaseId,\n    } as const;\n  }, [conState]);\n\n  const { connectionId } = connectionInfo;\n  const pathInfo = usePromise(async () => {\n    if (!connectionId) return undefined;\n    try {\n      const path = await startConnection?.(connectionId);\n      if (!path) throw \"No path\";\n      return { path } as const;\n    } catch (error) {\n      return { error, path: undefined, state: \"error\" } as const;\n    }\n  }, [startConnection, connectionId]);\n\n  // const pathInfo = useMemo(() => {\n  //   if (!path?.path || connectionInfo.state === \"loading\") return undefined;\n  //   if (connectionInfo.error) {\n  //     return {\n  //       ...connectionInfo,\n  //       path: undefined,\n  //       connId,\n  //     } as const;\n  //   }\n  //   return { ...path, connId } as const;\n  // }, [path, connectionInfo, connId]);\n\n  const prostglesClientOpts = useMemo(\n    () => ({\n      socketOptions: {\n        path: pathInfo?.path,\n        transports: [\"websocket\"],\n        reconnectionDelay: 1000,\n        reconnection: true,\n      },\n      onDebug: isPlaywrightTest ? onDebug : undefined,\n      skip: !pathInfo?.path,\n    }),\n    [pathInfo?.path],\n  );\n\n  const dbPrgl = useProstglesClient(prostglesClientOpts);\n\n  const dbState = useMemo(() => {\n    try {\n      if (connectionInfo.state === \"error\") {\n        return {\n          ...connectionInfo,\n          state: \"error\",\n          errorType: \"connNotFound\",\n        } satisfies PrglProjectStateError;\n      }\n      if (!pathInfo) return;\n      if (\"error\" in dbPrgl) {\n        return {\n          state: \"error\",\n          error: dbPrgl.error || pathInfo.error || \"ErrorUnknown\",\n          errorType: \"connectionError\",\n        } satisfies PrglProjectStateError;\n      }\n      if (pathInfo.state === \"error\") {\n        return pathInfo;\n      }\n      if (dbPrgl.isLoading) return;\n\n      const { path } = pathInfo;\n      return {\n        path,\n        dbPrgl,\n        state: \"loaded\",\n        loading: false,\n      } as const;\n    } catch (error) {\n      return {\n        error,\n        state: \"error\",\n        errorType: \"connectionError\",\n      } satisfies PrglProjectStateError;\n    }\n  }, [connectionInfo, dbPrgl, pathInfo]);\n\n  const prglProject = useMemo(() => {\n    const con = conState.data;\n    if (\n      !dbState ||\n      dbState.state !== \"loaded\" ||\n      !con ||\n      connectionInfo.state !== \"loaded\"\n    ) {\n      return;\n    }\n    const { dbo: db, methods, tableSchema, socket } = dbState.dbPrgl;\n    const { tables: dbTables = [] } = getTables(\n      tableSchema ?? [],\n      con.table_options,\n      db,\n      con.display_options?.prettyTableAndColumnNames ?? true,\n    );\n\n    const { path } = dbState;\n    const { connectionId, databaseId, is_state_db } = connectionInfo;\n    const prglProject: PrglProject = {\n      dbKey: \"db-onReady-\" + Date.now(),\n      databaseId,\n      db: is_state_db ? (dbs as DBHandlerClient) : db,\n      tables: is_state_db ? dbsTables : dbTables,\n      methods: methods ?? {},\n      projectPath: path,\n      connectionId,\n      connection: con,\n    };\n\n    (window as any).db = db;\n    (window as any).dbSocket = socket;\n    (window as any).dbMethods = methods;\n    return prglProject;\n  }, [conState.data, dbState, connectionInfo, dbs, dbsTables]);\n\n  /** prgl_R.set moved here to prevent theme change to trigger many re-mounts due to dbKey change */\n  useEffect(() => {\n    if (!prglProject) return;\n    prgl_R.set({\n      ...prglProject,\n      ...prglState,\n    });\n  }, [prglProject, prglState]);\n\n  if (!dbState || dbState.state !== \"loaded\") {\n    return dbState ?? { state: \"loading\" };\n  }\n\n  if (!prglProject) {\n    return {\n      state: \"loading\",\n    } as const;\n  }\n  return {\n    ...dbState,\n    ...prglProject,\n  };\n};\n"
  },
  {
    "path": "client/src/pages/ProjectLogs.tsx",
    "content": "// import React from 'react';\n// import FormField from '../components/FormField/FormField';\n// import Checkbox from '../components/Checkbox';\n// import Loading from '../components/Loading';\n// import XTerm from '../components/XTerm';\n\n// import { mdiDatabase, mdiLinkBoxVariantOutline, mdiTableMultiple, mdiAccountCheckOutline, mdiHome, mdiMicrosoftVisualStudioCode, mdiInformationOutline, mdiPulse } from '@mdi/js';\n\n// import { BrowserRouter, Switch, Route, Link, matchPath } from 'react-router-dom';\n\n// import { format, eachDayOfInterval, getHours, getMinutes, startOfDay, differenceInHours } from 'date-fns';\n// import { enGB, eo, de, ro } from 'date-fns/esm/locale'\n\n// import { get } from \"../utils\";\n// import { NavLink } from 'react-router-dom';\n// import RTComp, { DeepPartial } from \"../dashboard/RTComp\";\n// import Dashboard from \"../dashboard/Dashboard\";\n\n// import prostgles from  \"prostgles-client\";\n// import io from \"socket.io-client\";\n// import { DBHandlerClient, SQLResult, Auth } from \"prostgles-client/dist/prostgles\";\n\n// import { TimeChart } from \"../dashboard/Charts\";\n\n// import PopupMenu from '../components/PopupMenu';\n// import Rules from \"./Rules\";\n\n// type P = {\n//   id: any;\n//   db: DBHandlerClient;\n// }\n\n// type S = {\n//   id: string;\n//   url: string;\n//   url_code_server?: string;\n//   dbProject: DBHandlerClient;\n//   dev_mode?: boolean;\n//   iframeLoaded: boolean;\n//   iframeCached: boolean;\n//   status?: \"creating\" | \"error\" | \"live\" | \"\";\n//   timeChartData: any[];\n// }\n\n// type D = {\n\n// }\n\n// export default class ProjectLogs extends RTComp<P, S> {\n\n//   state = {\n//     id: null,\n//     name: \"\",\n//     logs: \"\",\n//     url: \"\",\n//     timeChartData: [],\n//     status: \"\" as S[\"status\"],\n//     notFound: false,\n//     loading: true,\n//     dbProject: undefined,\n//     iframeLoaded: false,\n//     iframeCached: false,\n//     url_code_server: undefined,\n//   }\n\n//   loaded = false;\n//   sub = null;\n//   async onDelta(deltaP: Partial<P>, deltaS: Partial<S>, deltaD?: DeepPartial<D>){\n//     const { dbProject, status } = this.state;\n//     const { db, id } = this.props;\n\n//     const delta = { ...deltaP, ...deltaS, ...deltaD }\n//     console.log(\"project\", delta)\n\n//     if(db && !this.sub){\n//       const proj = await db.projects.findOne({ id });\n//       let ns: any = { loading: false };\n//       this.sub = await db.projects.subscribe({ id }, { select: [\"id\", \"logs\"]}, (project, delta) => {\n//         if(project && project.length){\n//           this.setState(project[0]);\n//           // console.log(get(project, \"0.logs.app.log\"));\n//         }\n//       });\n\n//       if(proj) {\n//       } else {\n//         ns = {\n//           ...ns,\n//           notFound: true\n//         }\n//       }\n//       this.setState(ns)\n//     }\n//   }\n\n//   onUnmount(){\n//     if(this.sub) this.sub.unsubscribe();\n//   }\n\n//   render(){\n//     const {\n//       name, logs, notFound, loading = true,\n//       url_code_server, dbProject,\n//       iframeLoaded = false, iframeCached = false,\n//     } = this.state;\n//     const { id, db } = this.props;\n\n//     if(!this.sub) return <Loading />\n\n//     return (\n//       <div className=\"f-1 flex-col bg-color-0 h-fit min-h-0 \" >\n//         <ProjectResourceMonitor project_id={id} db={db} className=\"f-1 min-h-0 min-w-0\" />\n//         <div className=\"f-0 mt-1\">Console (read-only)</div>\n//         <XTerm className=\"o-auto f-1 w-full mt-p5 noselect pe-none\" style={{ maxHeight: \"200px\", }} value={get(logs, \"app.log\")} />\n//       </div>\n//     )\n//   }\n// }\n\n// export function getStatusColor(status: string){\n\n//   let statusColor = \"red\";\n//   if(status === \"live\"){\n//     statusColor = \"green\"\n//   } else if(status === \"creating\" || status === \"restarting\"){\n//     statusColor = \"yellow\"\n//   }\n//   return statusColor;\n// }\n\n// type Log = {\n//   name: string;\n//   data: any[];\n// }\n\n// export class ProjectResourceMonitor extends RTComp<{\n//   db: DBHandlerClient;\n//   project_id: string;\n//   className?: string;\n// }, {\n//   loading:boolean;\n//   logs: Log[]\n// }> {\n\n//   state = {\n//     loading: true,\n//     logs: []\n//   }\n\n//   loaded = false;\n//   sub = null;\n//   interval = null;\n//   async onDelta(deltaP, deltaS, deltaD?){\n//     const { loading } = this.state;\n//     const { db, project_id } = this.props;\n//     if(db && !this.loaded){\n//       this.loaded = true;\n//       let ns: any = { loading: false };\n\n//       // this.sub = await db.logs.subscribe({ $existsJoined: { \"project_resources\": { project_id } } }, {}, (logs, delta) => {\n//       //   console.log(logs);\n//       // });\n//       const resources = await db.project_resources.find({ project_id });\n//       // this.interval = setInterval(async () => {\n//         let logs = [];\n//         for(const resource of resources){\n//           let data = await db.logs.find({ resource_id: resource.id }, { select: { date: { $date_trunc: [\"second\", \"tstamp\"] }, cpu_mhz: 1 }, orderBy: { tstamp: -1 } });\n//           logs.push({\n//             name: resource.id,\n//             data: data.map(d => ({\n//               ...d,\n//               value: d.cpu_mhz\n//             }))\n//           })\n//           this.setState({ loading: false, logs });\n\n//       // db.logs.find({ resource_id: 1 }, { select: { resource_id: 1, cpu_perc: 1, date: { $date_trunc: [\"second\", \"tstamp\"] } } })\n//       //   .then(data => {\n//       //     this.setState({\n//       //       timeChartData: data.map(d => ({ ...d, color: [0,0,0], value: d.cpu_perc }))\n//       //     })\n//       //   });\n//         }\n//       // }, 1000)\n\n//     }\n//   }\n\n//   onUnmount(){\n//     if(this.sub) this.sub.unsubscribe();\n//     if(this.interval) clearInterval(this.interval);\n//     this.interval = null;\n//   }\n\n//   render(){\n//     const { logs, loading  } = this.state;\n//     const { db, className } = this.props;\n\n//     if(loading) return <Loading />\n\n//     // need three charts with network/cpu/ram + volume size on disk info\n//     const getYLabel = (val) => `${val} MHz`,\n//       chartData = logs.map((l, i)=> [\n//         { getYLabel, data: l.data.reverse().map((d, i)=> ({ ...d, date: !i? new Date(2018, 0, 1) : d.date  })), color: \"black\" },\n//         { getYLabel, data: l.data.reverse().map((d, i) => ({ ...d, value: d.value * Math.random() })), color: \"violet\" },\n//         { getYLabel, data: l.data.reverse().map((d, i) => ({ ...d, value: d.value * Math.random() })), color: \"green\" }\n//       ]\n//     );\n\n//     let content: any = <div>No logs yet</div>;\n//     if(logs.length){\n//       content = chartData.map((d, i)=> (<div key = {i}>\n//         <div className=\"f-0 mt-1\">\n//           CPU\n//         </div>\n//         <TimeChart key={i} className=\"b mt-p5\" layers={d} style={{ maxHeight: \"200px\" }} />\n//       </div>))\n//     }\n\n//     return content;\n//     return (\n//       <div className={\"f-1 flex-col bg-color-0 h-fit \" + className}>\n//         {content}\n//       </div>\n//     )\n//   }\n// }\n"
  },
  {
    "path": "client/src/pages/ServerSettings/AuthProvidersSetup.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport Loading from \"@components/Loader/Loading\";\nimport React, { useCallback, useEffect } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { EmailAuthSetup } from \"./EmailAuthSetup\";\nimport { OAuthProviderSetup } from \"./OAuthProviderSetup\";\n\nexport type AuthProvidersConfig = Extract<\n  DBSSchema[\"global_settings\"][\"auth_providers\"],\n  { website_url: string }\n>;\n\ntype UseProviderPropsArgs = {\n  dbs: Prgl[\"dbs\"];\n  dbsTables: Prgl[\"dbsTables\"];\n  auth_providers: DBSSchema[\"global_settings\"][\"auth_providers\"] | undefined;\n};\nconst useProviderProps = ({\n  dbs,\n  dbsTables,\n  auth_providers,\n}: UseProviderPropsArgs) => {\n  const doUpdate = async (newValue: typeof auth_providers) => {\n    await dbs.global_settings.update(\n      {},\n      {\n        auth_providers: newValue,\n      },\n    );\n  };\n\n  const authProps = {\n    authProviders: auth_providers ?? { website_url: \"\" },\n    dbsTables,\n    disabledInfo:\n      !auth_providers?.website_url ? \"Must setup website URL first\" : undefined,\n    contentClassName: \"flex-col gap-2 p-2\",\n    doUpdate,\n  };\n\n  return authProps;\n};\n\nexport type AuthProviderProps = ReturnType<typeof useProviderProps>;\n\nexport const AuthProviderSetup = ({\n  dbs,\n  dbsTables,\n}: Pick<Prgl, \"dbs\" | \"dbsTables\">) => {\n  const globalSettingsTable = dbsTables.find(\n    (t) => t.name === \"global_settings\",\n  );\n  const authColumn = globalSettingsTable?.columns.find(\n    (c) => c.name === \"auth_providers\",\n  );\n  const { data: global_settings } = dbs.global_settings.useSubscribeOne();\n  const { data: userTypes } = dbs.user_types.useFind();\n  const updateAuth = useCallback(\n    async (auth: Partial<DBSSchema[\"global_settings\"][\"auth_providers\"]>) => {\n      await dbs.global_settings.update(\n        {},\n        {\n          auth_providers:\n            !auth ? undefined : (\n              {\n                website_url:\n                  global_settings?.auth_providers?.website_url ??\n                  window.location.origin,\n                ...global_settings?.auth_providers,\n                ...auth,\n              }\n            ),\n        },\n      );\n    },\n    [dbs.global_settings, global_settings],\n  );\n\n  const settingsLoaded = !!global_settings;\n  const { website_url } = global_settings?.auth_providers ?? {};\n  useEffect(() => {\n    if (!settingsLoaded) return;\n    if (!website_url) {\n      void updateAuth({\n        website_url: window.location.origin,\n      });\n    }\n  }, [updateAuth, settingsLoaded, website_url]);\n\n  const authProps = useProviderProps({\n    auth_providers: global_settings?.auth_providers,\n    dbs,\n    dbsTables,\n  });\n\n  if (!globalSettingsTable || !authColumn) {\n    return (\n      <InfoRow>\n        Could not find global_settings table or authColumn. Make sure you have\n        the correct permissions\n      </InfoRow>\n    );\n  }\n\n  if (!global_settings) {\n    return <Loading />;\n  }\n\n  const { auth_providers, auth_created_user_type } = global_settings;\n\n  return (\n    <FlexCol className=\"AuthProviderSetup f-1\">\n      <InfoRow className=\"mx-1\" variant=\"naked\" color=\"info\" iconPath=\"\">\n        Manage user authentication methods, default user roles, and third-party\n        login providers to control access.\n      </InfoRow>\n      <FlexCol className=\"p-1 gap-2\">\n        <FormField\n          data-command=\"AuthProviderSetup.websiteURL\"\n          label={t.AuthProviderSetup[\"Website URL\"]}\n          hint={t.AuthProviderSetup[\"Used for redirect uri\"]}\n          value={global_settings.auth_providers?.website_url}\n          onChange={(website_url: string) => {\n            void updateAuth({\n              ...auth_providers,\n              website_url,\n            });\n          }}\n        />\n        <FormField\n          label={t.AuthProviderSetup[\"Default user type\"]}\n          data-command=\"AuthProviderSetup.defaultUserType\"\n          value={auth_created_user_type ?? \"default\"}\n          fullOptions={\n            userTypes?.map((ut) => ({\n              key: ut.id,\n              subLabel: ut.description ?? \"\",\n            })) ?? []\n          }\n          onChange={(default_user_type: DBSSchema[\"user_types\"][\"id\"]) => {\n            if (default_user_type === \"admin\") {\n              const result = window.confirm(\n                t.AuthProviderSetup[\n                  \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\"\n                ],\n              );\n              if (!result) return;\n            }\n\n            void dbs.global_settings.update(\n              {},\n              {\n                auth_created_user_type: default_user_type,\n              },\n            );\n          }}\n          hint={\n            t.ServerSettings[\n              \"The default user type assigned to new users. Defaults to 'default'\"\n            ]\n          }\n        />\n        {auth_created_user_type === \"admin\" && (\n          <InfoRow variant=\"filled\" color=\"danger\">\n            {\n              t.AuthProviderSetup[\n                \"Warning: You are setting the default user type to 'admin'. This means that new users will be granted the highest level of access!\"\n              ]\n            }\n          </InfoRow>\n        )}\n      </FlexCol>\n      <FlexCol data-command=\"AuthProviders.list\" className=\"gap-0\">\n        <EmailAuthSetup {...authProps} />\n        <OAuthProviderSetup provider=\"google\" {...authProps} />\n        <OAuthProviderSetup provider=\"github\" {...authProps} />\n        <OAuthProviderSetup provider=\"microsoft\" {...authProps} />\n        <OAuthProviderSetup provider=\"facebook\" {...authProps} />\n        <OAuthProviderSetup provider=\"customOAuth\" {...authProps} />\n      </FlexCol>\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/EmailAuthSetup.tsx",
    "content": "import { mdiEmail } from \"@mdi/js\";\nimport { isEqual } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport FormField from \"@components/FormField/FormField\";\nimport { FooterButtons } from \"@components/Popup/FooterButtons\";\nimport { Section } from \"@components/Section\";\nimport { Select } from \"@components/Select/Select\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { AuthProviderProps } from \"./AuthProvidersSetup\";\nimport {\n  DEFAULT_SMTP_CONFIG,\n  EmailSMTPAndTemplateSetup,\n} from \"./EmailAuthSetupIngredients/EmailSMTPAndTemplateSetup\";\nimport {\n  DEFAULT_EMAIL_VERIFICATION_TEMPLATE,\n  DEFAULT_MAGIC_LINK_TEMPLATE,\n} from \"@common/OAuthUtils\";\nimport { t } from \"../../i18n/i18nUtils\";\n\nexport const EmailAuthSetup = ({\n  authProviders,\n  disabledInfo,\n  contentClassName,\n  doUpdate,\n}: AuthProviderProps) => {\n  const [_localAuth, setLocalAuth] = useState(authProviders.email);\n  const localAuth = _localAuth ?? authProviders.email;\n  const [error, setError] = useState<any>(undefined);\n  const didChange = localAuth && !isEqual(localAuth, authProviders.email);\n\n  const onToggle =\n    !localAuth ? undefined : (\n      async (enabled: boolean) => {\n        await doUpdate({\n          ...authProviders,\n          email: {\n            ...localAuth,\n            enabled,\n          },\n        }).catch(setError);\n        setLocalAuth(undefined);\n      }\n    );\n\n  return (\n    <Section\n      title={t.EmailAuthSetup[\"Email signup\"]}\n      data-command=\"EmailAuthSetup\"\n      titleIconPath={mdiEmail}\n      disabledInfo={disabledInfo}\n      contentClassName={contentClassName}\n      disableFullScreen={true}\n      titleRightContent={\n        <SwitchToggle\n          className=\"ml-auto\"\n          checked={!!authProviders.email?.enabled}\n          data-command=\"EmailAuthSetup.toggle\"\n          disabledInfo={\n            !onToggle &&\n            t.EmailAuthSetup[\"Must configure the email provider first\"]\n          }\n          onChange={onToggle ?? (() => {})}\n        />\n      }\n    >\n      <SwitchToggle\n        label={t.common.Enabled}\n        checked={!!localAuth?.enabled}\n        onChange={(enabled) => {\n          setLocalAuth(\n            !localAuth ?\n              {\n                enabled,\n                signupType: \"withPassword\",\n                emailTemplate: DEFAULT_EMAIL_VERIFICATION_TEMPLATE,\n                smtp: DEFAULT_SMTP_CONFIG,\n                minPasswordLength: 8,\n              }\n            : {\n                ...localAuth,\n                enabled,\n              },\n          );\n        }}\n      />\n      <Select\n        label={\"Signup type\"}\n        data-command=\"EmailAuthSetup.SignupType\"\n        showSelectedSublabel={true}\n        value={localAuth?.signupType}\n        fullOptions={\n          [\n            {\n              key: \"withPassword\",\n              label: t.EmailAuthSetup[\"With password\"],\n              subLabel:\n                t.EmailAuthSetup[\n                  \"Email and password will be required to login. User will have to confirm email\"\n                ],\n            },\n            {\n              key: \"withMagicLink\",\n              label: t.EmailAuthSetup[\"With magic link\"],\n              subLabel:\n                t.EmailAuthSetup[\n                  \"Only email is required to login. A magic link will be sent to the user's email\"\n                ],\n            },\n          ] as const\n        }\n        onChange={(signupType) => {\n          setLocalAuth(\n            signupType === \"withMagicLink\" ?\n              {\n                enabled: localAuth?.enabled ?? false,\n                signupType,\n                emailTemplate: DEFAULT_MAGIC_LINK_TEMPLATE,\n                smtp: localAuth?.smtp ?? DEFAULT_SMTP_CONFIG,\n              }\n            : {\n                signupType,\n                enabled: localAuth?.enabled ?? false,\n                emailTemplate: DEFAULT_EMAIL_VERIFICATION_TEMPLATE,\n                smtp: localAuth?.smtp ?? DEFAULT_SMTP_CONFIG,\n                minPasswordLength:\n                  localAuth?.signupType === \"withPassword\" ?\n                    (localAuth.minPasswordLength ?? 8)\n                  : undefined,\n              },\n          );\n        }}\n      />\n      {localAuth?.signupType === \"withPassword\" && (\n        <FormField\n          label={t.EmailAuthSetup[\"Minimum password length\"]}\n          optional={true}\n          value={localAuth.minPasswordLength ?? 8}\n          onChange={(minPasswordLength: number) => {\n            setLocalAuth({\n              ...localAuth,\n              minPasswordLength,\n            });\n          }}\n          hint={t.EmailAuthSetup[\"Minimum password length. Defaults to 8\"]}\n        />\n      )}\n      <EmailSMTPAndTemplateSetup\n        websiteUrl={authProviders.website_url}\n        label={\n          localAuth?.signupType === \"withMagicLink\" ?\n            t.EmailAuthSetup[\"Magic link email configuration\"]\n          : t.EmailAuthSetup[\"Email verification\"]\n        }\n        value={localAuth}\n        onChange={async (newConfig) => {\n          if (!localAuth) throw \"Local auth not found\";\n          if (localAuth.enabled) {\n            await doUpdate({\n              ...authProviders,\n              email: {\n                ...localAuth,\n                ...newConfig,\n              },\n            });\n            setError(undefined);\n            setLocalAuth(undefined);\n          } else {\n            setLocalAuth({\n              ...localAuth,\n              emailTemplate: newConfig.emailTemplate,\n              smtp: newConfig.smtp,\n              emailConfirmationEnabled: newConfig.emailConfirmationEnabled,\n            });\n          }\n        }}\n      />\n      {error && (\n        <ErrorComponent data-command=\"EmailAuthSetup.error\" error={error} />\n      )}\n      {didChange && (\n        <FooterButtons\n          footerButtons={[\n            {\n              label: \"Save\",\n              color: \"action\",\n              variant: \"filled\",\n              onClickMessage: async (_, setM) => {\n                try {\n                  setM({ loading: 1 });\n                  const newAuth = {\n                    ...authProviders,\n                    email: localAuth,\n                  };\n                  await doUpdate(newAuth);\n                  setError(undefined);\n                  setLocalAuth(undefined);\n                } catch (err) {\n                  setError(err);\n                }\n                setM({ loading: 0 });\n              },\n            },\n          ]}\n        />\n      )}\n    </Section>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/EmailAuthSetupIngredients/EmailSMTPAndTemplateSetup.tsx",
    "content": "import { mdiEmailEdit, mdiMailboxOpenOutline } from \"@mdi/js\";\nimport { pickKeys } from \"prostgles-types\";\nimport React from \"react\";\nimport {\n  DEFAULT_EMAIL_VERIFICATION_TEMPLATE,\n  DEFAULT_MAGIC_LINK_TEMPLATE,\n  getMagicLinkEmailFromTemplate,\n} from \"@common/OAuthUtils\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexCol } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { Section } from \"@components/Section\";\nimport type { AuthProvidersConfig } from \"../AuthProvidersSetup\";\nimport { useEditableData } from \"../useEditableData\";\nimport { EmailSMTPSetup } from \"./EmailSMTPSetup\";\nimport { EmailTemplateSetup } from \"./EmailTemplateSetup\";\nimport { t } from \"../../../i18n/i18nUtils\";\n\ntype EmailConfig = Extract<\n  NonNullable<AuthProvidersConfig[\"email\"]>,\n  { signupType: \"withMagicLink\" }\n>;\nexport type EmailSMTPCofig = EmailConfig[\"smtp\"];\nexport type EmailTemplateCofig = EmailConfig[\"emailTemplate\"];\n\nconst keysToUpdate = [\n  \"emailConfirmationEnabled\",\n  \"smtp\",\n  \"emailTemplate\",\n] as const;\n\ntype P = {\n  websiteUrl: string;\n  value: AuthProvidersConfig[\"email\"];\n  label: string;\n  className?: string;\n  onChange: (\n    newValue: Pick<\n      NonNullable<AuthProvidersConfig[\"email\"]>,\n      \"emailConfirmationEnabled\" | \"smtp\" | \"emailTemplate\"\n    >,\n  ) => Promise<void>;\n};\nexport const EmailSMTPAndTemplateSetup = (props: P) => {\n  const { label, className, onChange, websiteUrl } = props;\n\n  const { didChange, error, onSave, value, setValue, setError } =\n    useEditableData(props.value && pickKeys(props.value, keysToUpdate.slice()));\n  const { signupType } = props.value ?? {};\n  const enabled = props.value?.enabled && !!signupType;\n  return (\n    <PopupMenu\n      positioning=\"top-center\"\n      title={label}\n      data-command=\"EmailSMTPAndTemplateSetup\"\n      button={\n        <Btn\n          variant=\"faded\"\n          color={enabled ? \"action\" : undefined}\n          label={{ label, variant: \"normal\", className: \"mb-p5\" }}\n          disabledInfo={\n            !signupType ? \"Must select a Signup type first\" : undefined\n          }\n        >\n          {enabled ?\n            t.EmailSMTPAndTemplateSetup[\"Configured\"]\n          : t.EmailSMTPAndTemplateSetup[\"Not configured\"]}\n        </Btn>\n      }\n      className={className}\n      clickCatchStyle={{ opacity: 1 }}\n      render={(pClose) => (\n        <FlexCol className=\"ai-start\">\n          <p>\n            {\n              t.EmailSMTPAndTemplateSetup[\n                \"Users will receive an email with a link/code to verify their email address.\"\n              ]\n            }\n          </p>\n          <Section\n            title={t.EmailSMTPAndTemplateSetup[\"Email Provider\"]}\n            titleIconPath={mdiMailboxOpenOutline}\n            className={\"w-full\"}\n            contentClassName=\"p-1\"\n            disableFullScreen={true}\n            data-command=\"EmailSMTPSetup\"\n          >\n            <EmailSMTPSetup\n              value={value?.smtp}\n              onChange={(newSmtpValue) => setValue({ smtp: newSmtpValue })}\n            />\n          </Section>\n          <Section\n            title={t.EmailSMTPAndTemplateSetup[\"Template\"]}\n            titleIconPath={mdiEmailEdit}\n            className={\"w-full\"}\n            contentClassName=\"p-1\"\n            disableFullScreen={true}\n            data-command=\"EmailTemplateSetup\"\n          >\n            <EmailTemplateSetup\n              defaultBody={\n                signupType === \"withMagicLink\" ?\n                  DEFAULT_MAGIC_LINK_TEMPLATE.body\n                : DEFAULT_EMAIL_VERIFICATION_TEMPLATE.body\n              }\n              parseBody={() => {\n                return !value ? \"\" : (\n                    getMagicLinkEmailFromTemplate({\n                      code: \"123456\",\n                      url: websiteUrl || \"https://example.com\",\n                      template: value.emailTemplate,\n                    }).body\n                  );\n              }}\n              value={value?.emailTemplate}\n              onChange={(newEmailTemplate) =>\n                setValue({ emailTemplate: newEmailTemplate })\n              }\n            />\n          </Section>\n          {error && <ErrorComponent error={error} />}\n        </FlexCol>\n      )}\n      footerButtons={(pClose) => [\n        { label: t.common.Cancel, variant: \"faded\", onClickClose: true },\n        {\n          label: enabled ? t.common[\"Test and Save\"] : t.common.Save,\n          \"data-command\": \"EmailSMTPAndTemplateSetup.save\",\n          color: \"action\",\n          variant: \"filled\",\n          disabledInfo: didChange ? undefined : t.common[\"No changes\"],\n          onClickPromiseMessage: \"Error\",\n          onClickPromise: async (e) => {\n            if (!value) return setError(\"No value\");\n            if (!onSave) return setError(\"Nothing to save\");\n            await onSave(async () => {\n              const newValue = { ...props.value, ...value };\n              return onChange(newValue);\n            });\n            pClose?.(e);\n          },\n        },\n      ]}\n    />\n  );\n};\n\nexport const DEFAULT_SMTP_CONFIG = {\n  type: \"smtp\",\n  host: \"\",\n  port: 465,\n  secure: false,\n  user: \"\",\n  pass: \"\",\n} satisfies EmailSMTPCofig;\n"
  },
  {
    "path": "client/src/pages/ServerSettings/EmailAuthSetupIngredients/EmailSMTPSetup.tsx",
    "content": "import React from \"react\";\nimport { type DivProps, FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { Select } from \"@components/Select/Select\";\nimport type { EmailSMTPCofig } from \"./EmailSMTPAndTemplateSetup\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\n\ntype P = Pick<DivProps, \"style\" | \"className\"> & {\n  value: EmailSMTPCofig | undefined;\n  onChange: (newValue: NonNullable<P[\"value\"]>) => void;\n};\n\nconst providerOptions = [\n  {\n    key: \"smtp\",\n    label: \"SMTP Server\",\n    subLabel: \"Standard SMTP configuration\",\n  },\n  { key: \"aws-ses\", label: \"AWS SES\", subLabel: \"Amazon Simple Email Service\" },\n] as const;\n\nexport const EmailSMTPSetup = ({ value, onChange }: P) => {\n  const handleProviderChange = (\n    type: (typeof providerOptions)[number][\"key\"],\n  ) => {\n    if (type === \"smtp\") {\n      onChange({\n        type: \"smtp\",\n        host: \"\",\n        port: 587,\n        user: \"\",\n        pass: \"\",\n      });\n    } else {\n      onChange({\n        type: \"aws-ses\",\n        region: \"\",\n        accessKeyId: \"\",\n        secretAccessKey: \"\",\n        sendingRate: 1,\n      });\n    }\n  };\n\n  return (\n    <FlexCol>\n      <Select\n        label=\"Email provider\"\n        fullOptions={providerOptions}\n        value={value?.type ?? \"None\"}\n        onChange={handleProviderChange}\n      />\n      {!value ?\n        null\n      : value.type === \"smtp\" ?\n        <>\n          <FormField\n            id=\"smtp-Host\"\n            label=\"Host\"\n            value={value.host}\n            onChange={(host) => onChange({ ...value, host })}\n          />\n          <FormField\n            id=\"smtp-Port\"\n            label=\"Port\"\n            type=\"number\"\n            value={value.port}\n            onChange={(port) => onChange({ ...value, port: Number(port) })}\n          />\n          <FormField\n            id=\"smtp-Username\"\n            label=\"Username\"\n            value={value.user}\n            onChange={(user) => onChange({ ...value, user })}\n          />\n          <FormField\n            id=\"smtp-Password\"\n            label=\"Password\"\n            value={value.pass}\n            onChange={(pass) => onChange({ ...value, pass })}\n          />\n          <SwitchToggle\n            label={\"Secure\"}\n            checked={!!value.secure}\n            onChange={(secure) => onChange({ ...value, secure })}\n          />\n          <SwitchToggle\n            label={\"Reject unauthorized\"}\n            checked={!!value.rejectUnauthorized}\n            onChange={(rejectUnauthorized) =>\n              onChange({ ...value, rejectUnauthorized })\n            }\n          />\n        </>\n      : <>\n          <FormField\n            label=\"Region\"\n            type=\"text\"\n            value={value.region}\n            onChange={(region) => onChange({ ...value, region })}\n          />\n          <FormField\n            label=\"Access Key ID\"\n            type=\"text\"\n            value={value.accessKeyId}\n            onChange={(accessKeyId) => onChange({ ...value, accessKeyId })}\n          />\n          <FormField\n            label=\"Secret Access Key\"\n            type=\"password\"\n            value={value.secretAccessKey}\n            onChange={(secretAccessKey) =>\n              onChange({ ...value, secretAccessKey })\n            }\n          />\n          <FormField\n            label=\"Sending Rate (per second)\"\n            type=\"number\"\n            value={value.sendingRate || 14}\n            onChange={(sendingRate) =>\n              onChange({ ...value, sendingRate: Number(sendingRate) })\n            }\n          />\n        </>\n      }\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/EmailAuthSetupIngredients/EmailTemplateSetup.tsx",
    "content": "import { mdiDeleteRestore, mdiEyeCheckOutline } from \"@mdi/js\";\nimport React from \"react\";\nimport sanitize from \"sanitize-html\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport Popup from \"@components/Popup/Popup\";\nimport { CodeEditorWithSaveButton } from \"../../../dashboard/CodeEditor/CodeEditorWithSaveButton\";\n\nexport type EmailTemplateConfig = {\n  from: string;\n  subject: string;\n  body: string;\n};\n\ntype P = {\n  value: EmailTemplateConfig | undefined;\n  onChange: (newValue: EmailTemplateConfig) => void;\n  style?: React.CSSProperties;\n  className?: string;\n  defaultBody: string;\n  parseBody: () => string;\n};\nexport const EmailTemplateSetup = ({\n  value,\n  onChange,\n  style,\n  className,\n  defaultBody,\n  parseBody,\n}: P) => {\n  const onFieldChange = (newValue: Partial<EmailTemplateConfig>) => {\n    const newTemplate = {\n      ...value,\n      ...newValue,\n    };\n    onChange({\n      body: newTemplate.body ?? \"\",\n      from: newTemplate.from ?? \"\",\n      subject: newTemplate.subject ?? \"\",\n    });\n  };\n  const htmlValue = value?.body ?? \"\";\n  const [showPreview, setShowPreview] = React.useState(false);\n  return (\n    <FlexCol\n      style={{\n        ...style,\n        minHeight: \"300px\",\n        minWidth: \"500px\",\n      }}\n      className={className}\n    >\n      <FormField\n        label={\"From\"}\n        type=\"email\"\n        value={value?.from}\n        onChange={(from) => onFieldChange({ from })}\n      />\n      <FormField\n        label={\"Subject\"}\n        type=\"text\"\n        value={value?.subject}\n        onChange={(subject) => onFieldChange({ subject })}\n      />\n      <CodeEditorWithSaveButton\n        language={\"html\"}\n        value={htmlValue}\n        label={\"Body\"}\n        headerButtons={\n          <>\n            <Btn\n              title=\"Reset to default\"\n              iconPath={mdiDeleteRestore}\n              onClick={() => onFieldChange({ body: defaultBody })}\n            />\n            <Btn\n              title=\"Preview\"\n              iconPath={mdiEyeCheckOutline}\n              onClick={() => {\n                setShowPreview(!showPreview);\n              }}\n            />\n          </>\n        }\n        autoSave={true}\n        onSave={(body) => onFieldChange({ body })}\n        options={{\n          lineNumbers: \"off\",\n          padding: {\n            top: 24,\n          },\n          minimap: { enabled: false },\n        }}\n      />\n      {showPreview && (\n        <Popup\n          title=\"Preview\"\n          positioning=\"center\"\n          clickCatchStyle={{ opacity: 1 }}\n          onClose={() => setShowPreview(false)}\n        >\n          <div\n            style={{ maxWidth: \"600px\" }}\n            dangerouslySetInnerHTML={{ __html: sanitize(parseBody()) }}\n          ></div>\n        </Popup>\n      )}\n    </FlexCol>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerConfig/MCPServerConfig.tsx",
    "content": "import Btn from \"@components/Btn\";\nimport { FileBrowser } from \"@components/FileBrowser/FileBrowser\";\nimport { FlexCol, FlexRow, FlexRowWrap } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport Popup from \"@components/Popup/Popup\";\nimport React, { useContext, useState } from \"react\";\nimport type { DBS } from \"../../../../dashboard/Dashboard/DBS\";\nimport { useMCPServerConfigState } from \"./useMCPServerConfigState\";\nimport { mdiDelete } from \"@mdi/js\";\n\nexport type MCPServerEnabledConfig = { configId: number };\n\nexport type MCPServerConfigProps = {\n  dbs: DBS;\n  serverName: string;\n  existingConfig: { id: number; value: Record<string, string> } | undefined;\n  chatId: number | undefined;\n  onDone: (res: void | MCPServerEnabledConfig) => void;\n};\n\nexport const MCPServerConfig = (props: MCPServerConfigProps) => {\n  const { serverName, existingConfig, onDone } = props;\n  const { upsertConfig, canSave, schema, setConfig, config, existingConfigs } =\n    useMCPServerConfigState(props);\n  if (!schema) return null;\n\n  return (\n    <Popup\n      title={`Configure and enable ${JSON.stringify(serverName)} MCP server`}\n      positioning=\"center\"\n      onClose={() => onDone()}\n      data-command=\"MCPServerConfig\"\n      rootStyle={{\n        maxWidth: \"min(600px, 100vw)\",\n      }}\n      clickCatchStyle={{ opacity: 1 }}\n      contentClassName=\"p-1\"\n      footerButtons={[\n        {\n          label: \"Cancel\",\n          onClick: () => onDone(),\n        },\n        {\n          label: existingConfig ? \"Update\" : \"Enable\",\n          \"data-command\": \"MCPServerConfig.save\",\n          disabledInfo: canSave ? undefined : \"No changes\",\n          variant: \"filled\",\n          color: \"action\",\n          className: \"ml-auto\",\n          onClickPromise: upsertConfig,\n        },\n      ]}\n    >\n      <FlexCol className=\"min-h-0\">\n        {Object.entries(schema).map(([key, schema]) => {\n          if (schema.renderWithComponent === \"FileBrowser\") {\n            return (\n              <FileBrowser\n                key={key}\n                title={schema.title ?? key}\n                path={config[key]}\n                onChange={(v) => {\n                  setConfig({\n                    ...config,\n                    [key]: v,\n                  });\n                }}\n              />\n            );\n          }\n          return (\n            <FormField\n              type=\"text\"\n              key={key}\n              label={schema.title ?? key}\n              hint={schema.description}\n              value={config[key]}\n              onChange={(v) =>\n                setConfig({\n                  ...config,\n                  [key]: v,\n                })\n              }\n            />\n          );\n        })}\n        {Boolean(existingConfigs.length) && (\n          <FlexCol className=\"pt-1 pb-2 gap-p5\">\n            <div className=\"ta-start\">\n              Or select from existing configurations:\n            </div>\n            <FlexRowWrap>\n              {existingConfigs.map((existingConfig) => {\n                const renderableTypes = [\"string\", \"number\", \"boolean\"];\n                const values = Object.values(existingConfig.config)\n                  .map((v) =>\n                    renderableTypes.includes(typeof v) ?\n                      String(v)\n                    : JSON.stringify(v),\n                  )\n                  .join(\", \");\n                return (\n                  <FlexRow key={existingConfig.id} className=\"gap-0\">\n                    <Btn\n                      variant=\"faded\"\n                      onClick={() => {\n                        setConfig(existingConfig.config);\n                      }}\n                    >\n                      {values}\n                    </Btn>\n                    <Btn\n                      iconPath={mdiDelete}\n                      title=\"Delete existing config (if not used in other chats)\"\n                      onClickPromise={async () => {\n                        await props.dbs.mcp_server_configs.delete({\n                          id: existingConfig.id,\n                        });\n                      }}\n                    />\n                  </FlexRow>\n                );\n              })}\n            </FlexRowWrap>\n          </FlexCol>\n        )}\n      </FlexCol>\n    </Popup>\n  );\n};\n\nexport type MCPServerConfigContext = {\n  setServerToConfigure: (\n    p: Omit<MCPServerConfigProps, \"onDone\" | \"dbs\">,\n  ) => Promise<void | MCPServerEnabledConfig>;\n};\n\nexport const MCPServerConfigContext = React.createContext<\n  MCPServerConfigContext | undefined\n>(undefined);\n\nexport const MCPServerConfigProvider = ({\n  children,\n  dbs,\n}: {\n  children: React.ReactNode;\n  dbs: DBS;\n}) => {\n  const [serverToConfigure, setServerToConfigure] =\n    useState<MCPServerConfigProps>();\n\n  const value = React.useMemo(() => {\n    return {\n      setServerToConfigure: async (\n        props: Omit<MCPServerConfigProps, \"onDone\" | \"dbs\">,\n      ) => {\n        return new Promise<MCPServerEnabledConfig | void>((resolve) => {\n          setServerToConfigure({\n            ...props,\n            dbs,\n            onDone: (enabled) => {\n              resolve(enabled);\n            },\n          });\n        });\n      },\n    };\n  }, [dbs]);\n\n  return (\n    <MCPServerConfigContext.Provider value={value}>\n      {children}\n      {serverToConfigure && (\n        <MCPServerConfig\n          {...serverToConfigure}\n          onDone={(enabled) => {\n            serverToConfigure.onDone(enabled);\n            setServerToConfigure(undefined);\n          }}\n        />\n      )}\n    </MCPServerConfigContext.Provider>\n  );\n};\n\nexport const useMCPServerConfig = () => {\n  const context = useContext(MCPServerConfigContext);\n  if (!context) {\n    throw new Error(\n      \"useMCPServerConfig must be used within a MCPServerConfigProvider\",\n    );\n  }\n  return context;\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerConfig/MCPServerConfigButton.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport React from \"react\";\nimport {\n  useMCPServerConfig,\n  type MCPServerConfigProps,\n} from \"./MCPServerConfig\";\nimport { mdiCog } from \"@mdi/js\";\n\nexport const MCPServerConfigButton = (\n  props: Omit<MCPServerConfigProps, \"onDone\" | \"variant\"> & {\n    schema: NonNullable<DBSSchema[\"mcp_servers\"][\"config_schema\"]>;\n  },\n) => {\n  const { schema, existingConfig, serverName, chatId } = props;\n  const { setServerToConfigure } = useMCPServerConfig();\n  return (\n    <Btn\n      onClick={() => {\n        void setServerToConfigure({\n          existingConfig,\n          serverName,\n          chatId,\n        });\n      }}\n      style={{ flexShrink: 1 }}\n      iconPath={mdiCog}\n      data-command=\"MCPServerConfigButton\"\n    >\n      {Object.entries(schema).map(([key, schema]) => (\n        <FlexRow\n          key={key}\n          title={schema.title ?? key}\n          className=\"font-12 gap-p5 text-ellipsis ji-start\"\n        >\n          <div className=\"bold\">{existingConfig?.value[key]}</div>\n        </FlexRow>\n      ))}\n    </Btn>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerConfig/useMCPServerConfigState.tsx",
    "content": "import { useOnErrorAlert } from \"@components/AlertProvider\";\nimport { isEqual } from \"prostgles-types\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { MCPServerConfigProps } from \"./MCPServerConfig\";\n\nexport const useMCPServerConfigState = (props: MCPServerConfigProps) => {\n  const { serverName, existingConfig, dbs, onDone, chatId } = props;\n  const [config, setConfig] = useState(existingConfig?.value ?? {});\n  const canSave = useMemo(\n    () => !isEqual(config, existingConfig?.value),\n    [config, existingConfig?.value],\n  );\n\n  const serverInfo = dbs.mcp_servers.useSubscribeOne(\n    {\n      name: serverName,\n    },\n    {},\n    { skip: !serverName },\n  );\n  const existingConfigData = dbs.mcp_server_configs.useSubscribe(\n    {\n      server_name: serverName,\n    },\n    {},\n    { skip: !serverName },\n  );\n  const existingConfigs = useMemo(\n    () => existingConfigData.data ?? [],\n    [existingConfigData.data],\n  );\n  const schema = serverInfo.data?.config_schema;\n  const { onErrorAlert } = useOnErrorAlert();\n\n  const upsertConfig = useCallback(async () => {\n    await onErrorAlert(async () => {\n      const matchingConfig = existingConfigs.find(\n        (ec) => ec.server_name === serverName && isEqual(ec.config, config),\n      );\n\n      const upsertedConfig =\n        matchingConfig ??\n        (await dbs.mcp_server_configs.insert(\n          {\n            server_name: serverName,\n            config,\n          },\n          {\n            returning: \"*\",\n          },\n        ));\n      const configId = upsertedConfig.id;\n\n      if (!configId) {\n        throw new Error(\"Failed to save configuration.\");\n      }\n      await dbs.mcp_servers.update(\n        {\n          name: serverName,\n        },\n        { enabled: true },\n      );\n      if (chatId) {\n        await dbs.llm_chats_allowed_mcp_tools.update(\n          {\n            chat_id: chatId,\n            server_name: serverName,\n          },\n          {\n            server_config_id: configId,\n          },\n        );\n      }\n      onDone({ configId });\n    }).catch((e) => {\n      onDone();\n    });\n  }, [\n    chatId,\n    config,\n    dbs.llm_chats_allowed_mcp_tools,\n    dbs.mcp_server_configs,\n    dbs.mcp_servers,\n    existingConfigs,\n    onDone,\n    onErrorAlert,\n    serverName,\n  ]);\n\n  return {\n    upsertConfig,\n    schema,\n    config,\n    setConfig,\n    canSave,\n    existingConfigs,\n  };\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerConfig/useMCPServerEnable.ts",
    "content": "import { isDefined } from \"@common/filterUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { useCallback } from \"react\";\nimport type { Prgl } from \"../../../../App\";\nimport type { DBS } from \"../../../../dashboard/Dashboard/DBS\";\nimport type { MCPChatAllowedTools } from \"../useMCPChatAllowedTools\";\nimport { useMCPServerConfig } from \"./MCPServerConfig\";\n\nexport type MCPServerChatContext = {\n  chatId: number;\n  llm_chats_allowed_mcp_tools: MCPChatAllowedTools;\n};\n\n/**\n * Enabling an MCP server might require configuration\n */\nexport const useMCPServerEnable = ({\n  mcp_server,\n  dbs,\n  chatContext,\n}: {\n  dbs: DBS;\n  mcp_server: DBSSchema[\"mcp_servers\"] & {\n    mcp_server_configs: DBSSchema[\"mcp_server_configs\"][];\n  };\n  chatContext: undefined | MCPServerChatContext;\n} & Pick<Prgl, \"dbs\">) => {\n  const { enabled, config_schema, mcp_server_configs } = mcp_server;\n  const { setServerToConfigure } = useMCPServerConfig();\n\n  const lastConfigId = mcp_server_configs.at(-1)?.id;\n  const toolsAllowedConfigId = chatContext?.llm_chats_allowed_mcp_tools\n    .map((t) => t.server_config_id || undefined)\n    .filter(isDefined)[0];\n  const chatId = chatContext?.chatId;\n  const onToggle = useCallback(async () => {\n    const newEnabled = !enabled;\n    const mustProvideConfig =\n      newEnabled && config_schema && !mcp_server_configs.length;\n    if (mustProvideConfig) {\n      return setServerToConfigure({\n        existingConfig: undefined,\n        serverName: mcp_server.name,\n        chatId,\n      });\n    } else {\n      /** This ensures we don't re-enable the server through the logic in AskLLMChatActionBarMCPToolsBtn */\n      if (!newEnabled) {\n        await dbs.llm_chats_allowed_mcp_tools.delete({\n          chat_id: chatId,\n          server_name: mcp_server.name,\n        });\n      }\n      await dbs.mcp_servers.update(\n        { name: mcp_server.name },\n        { enabled: newEnabled },\n      );\n      return { configId: lastConfigId };\n    }\n  }, [\n    enabled,\n    config_schema,\n    mcp_server_configs.length,\n    setServerToConfigure,\n    mcp_server.name,\n    chatId,\n    dbs.mcp_servers,\n    dbs.llm_chats_allowed_mcp_tools,\n    lastConfigId,\n  ]);\n\n  const onToggleTools = useCallback(\n    async (toolIds: number[], action: \"approve\" | \"remove\") => {\n      if (!chatId) throw new Error(\"Chat ID is required to toggle tools\");\n      let wasEnabled = enabled;\n      if (action === \"approve\" && !enabled) {\n        wasEnabled = Boolean(await onToggle());\n      }\n      if (action === \"approve\" && wasEnabled) {\n        const data = toolIds.map((tool_id) => ({\n          tool_id,\n          chat_id: chatId,\n          server_name: mcp_server.name,\n          server_config_id: toolsAllowedConfigId || lastConfigId,\n        }));\n        await dbs.llm_chats_allowed_mcp_tools.insert(data);\n      } else {\n        await dbs.llm_chats_allowed_mcp_tools.delete({\n          tool_id: { $in: toolIds },\n          chat_id: chatId,\n        });\n      }\n    },\n    [\n      chatId,\n      enabled,\n      onToggle,\n      dbs.llm_chats_allowed_mcp_tools,\n      mcp_server.name,\n      toolsAllowedConfigId,\n      lastConfigId,\n    ],\n  );\n\n  return {\n    onToggle,\n    onToggleTools,\n  };\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerFooterActions/MCPServerFooterActions.tsx",
    "content": "import { mdiReload } from \"@mdi/js\";\nimport React from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { useAlert } from \"@components/AlertProvider\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { CodeEditor } from \"../../../../dashboard/CodeEditor/CodeEditor\";\nimport type { ServerSettingsProps } from \"../../ServerSettings\";\nimport { MCPServerConfigButton } from \"../MCPServerConfig/MCPServerConfigButton\";\nimport {\n  useMCPServerEnable,\n  type MCPServerChatContext,\n} from \"../MCPServerConfig/useMCPServerEnable\";\nimport type { MCPServerWithToolAndConfigs } from \"../useMCPServersListProps\";\nimport { MCPServersInstall } from \"./MCPServersInstall\";\nimport { pluralise } from \"src/pages/Connections/Connection\";\n\nexport type MCPServerFooterActionsProps = Pick<\n  ServerSettingsProps,\n  \"dbs\" | \"dbsMethods\"\n> & {\n  mcp_server: MCPServerWithToolAndConfigs;\n  envInfo:\n    | {\n        os: string;\n        npmVersion: string;\n        uvxVersion: string;\n      }\n    | undefined;\n  chatContext: MCPServerChatContext | undefined;\n};\nexport const MCPServerFooterActions = ({\n  mcp_server,\n  dbs,\n  dbsMethods,\n  envInfo,\n  chatContext,\n}: MCPServerFooterActionsProps) => {\n  const { reloadMcpServerTools } = dbsMethods;\n  const { mcp_server_configs, config_schema } = mcp_server;\n  const logItem: DBSSchema[\"mcp_server_logs\"] | undefined =\n    mcp_server.mcp_server_logs[0];\n\n  const { onToggle } = useMCPServerEnable({\n    dbs,\n    mcp_server,\n    chatContext,\n  });\n  const { addAlert } = useAlert();\n  const { llm_chats_allowed_mcp_tools, chatId } = chatContext ?? {};\n  return (\n    <FlexRow className=\"jc-end pl-p5\">\n      {mcp_server.source && (\n        <MCPServersInstall\n          mcpServer={mcp_server}\n          dbs={dbs}\n          dbsMethods={dbsMethods}\n        />\n      )}\n      {logItem &&\n        Boolean(\n          logItem.log || logItem.install_log || logItem.install_error,\n        ) && (\n          <PopupMenu\n            title={`MCP Server ${JSON.stringify(mcp_server.name)} stderr logs`}\n            positioning=\"center\"\n            // className=\"mr-auto ml-p25\"\n            data-command=\"MCPServerFooterActions.logs\"\n            showFullscreenToggle={{}}\n            button={\n              <Btn\n                color={logItem.error ? \"danger\" : \"default\"}\n                // variant=\"faded\"\n                size=\"small\"\n              >\n                {logItem.error ? \"Error\" : \"Logs\"}\n              </Btn>\n            }\n            onClickClose={false}\n            clickCatchStyle={{ opacity: 1 }}\n          >\n            <CodeEditor\n              language={\"bash\"}\n              value={logItem.log}\n              style={{\n                minWidth: \"min(900px, 100vw)\",\n                minHeight: \"min(900px, 100vh)\",\n              }}\n            />\n          </PopupMenu>\n        )}\n      {config_schema &&\n        mcp_server_configs.map((config, index) => {\n          const isLast = index === mcp_server_configs.length - 1;\n\n          /** Show active config for this chat. If not active tools then show last config */\n          if (\n            llm_chats_allowed_mcp_tools && llm_chats_allowed_mcp_tools.length ?\n              !llm_chats_allowed_mcp_tools.some(\n                (t) =>\n                  t.server_name === mcp_server.name &&\n                  t.server_config_id === config.id,\n              )\n            : !isLast\n          ) {\n            return null;\n          }\n          return (\n            <MCPServerConfigButton\n              key={config.id}\n              dbs={dbs}\n              schema={config_schema}\n              existingConfig={{ id: config.id, value: config.config }}\n              serverName={mcp_server.name}\n              chatId={chatId}\n            />\n          );\n        })}\n      {reloadMcpServerTools && (\n        <Btn\n          title={\"Refresh tools\"}\n          data-command=\"MCPServerFooterActions.refreshTools\"\n          iconPath={mdiReload}\n          disabledInfo={\n            mcp_server.enabled ? undefined : \"Must enable server first\"\n          }\n          onClickPromise={async () => {\n            const toolCount = await reloadMcpServerTools(mcp_server.name);\n            addAlert(\n              `Reloaded ${toolCount || 0} ${pluralise(toolCount, \"tool\")} for ${JSON.stringify(mcp_server.name)} server`,\n            );\n          }}\n        />\n      )}\n      <SwitchToggle\n        data-command=\"MCPServerFooterActions.enableToggle\"\n        title={!mcp_server.enabled ? \"Press to enable\" : \"Press to disable\"}\n        disabledInfo={\n          (\n            (mcp_server.command === \"npx\" || mcp_server.command === \"npm\") &&\n            !envInfo?.npmVersion\n          ) ?\n            \"Must install npm\"\n          : mcp_server.command === \"uvx\" && !envInfo?.uvxVersion ?\n            \"Must install uvx\"\n          : undefined\n        }\n        checked={!!mcp_server.enabled}\n        onChange={async () => {\n          await onToggle();\n        }}\n      />\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerFooterActions/MCPServersInstall.tsx",
    "content": "import { usePromise } from \"prostgles-client\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport { FlexRow } from \"@components/Flex\";\nimport PopupMenu from \"@components/PopupMenu\";\nimport { CodeEditor } from \"../../../../dashboard/CodeEditor/CodeEditor\";\nimport type { ServerSettingsProps } from \"../../ServerSettings\";\nimport type { MCPServerWithToolAndConfigs } from \"../useMCPServersListProps\";\n\nexport const MCPServersInstall = ({\n  mcpServer,\n  dbs,\n  dbsMethods,\n}: Pick<ServerSettingsProps, \"dbsMethods\" | \"dbs\"> & {\n  mcpServer: MCPServerWithToolAndConfigs;\n}) => {\n  if (!mcpServer.source) {\n    throw new Error(\"MCP Server source is not defined\");\n  }\n  const { name } = mcpServer;\n  const installLogs = dbs.mcp_server_logs.useSubscribeOne({\n    server_name: name,\n  });\n\n  const installLogData = installLogs.data;\n  const { install_log, install_error } = installLogData ?? {};\n  const log = (install_log || \"\") + (install_error || \"\");\n  const mcpServerStatus = usePromise(async () => {\n    mcpServer.installed; // To ensure it refreshes when installed changes\n    return dbsMethods.getMCPServersStatus?.(name);\n  }, [mcpServer.installed, dbsMethods, name]);\n\n  const { installMCPServer } = dbsMethods;\n  if (!installMCPServer) return <>Not allowed</>;\n  return (\n    <FlexRow className=\"f-1 jc-end\">\n      {!!installLogData && (\n        <PopupMenu\n          title=\"MCP Server installation logs\"\n          positioning=\"center\"\n          clickCatchStyle={{ opacity: 1 }}\n          button={\n            <Btn\n              color={installLogData.install_error ? \"danger\" : undefined}\n              variant=\"faded\"\n              size=\"small\"\n            >\n              Installation Logs\n            </Btn>\n          }\n        >\n          <CodeEditor\n            language=\"bash\"\n            style={{\n              minWidth: \"min(600px, 100vw)\",\n              minHeight: \"300px\",\n            }}\n            value={log}\n          />\n          {installLogData.install_error && (\n            <ErrorComponent\n              className=\"mt-1\"\n              error={installLogData.install_error}\n            />\n          )}\n        </PopupMenu>\n      )}\n      <Btn\n        variant=\"faded\"\n        color={\"action\"}\n        size=\"small\"\n        onClickPromise={async () => {\n          return installMCPServer(name);\n        }}\n        data-command=\"MCPServersInstall.install\"\n      >\n        {mcpServerStatus?.ok ? \"Re-Install\" : \"Install\"}\n      </Btn>\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerHeaderCheckbox.tsx",
    "content": "import { useOnErrorAlert } from \"@components/AlertProvider\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport React, { useCallback } from \"react\";\nimport type { DBS } from \"../../../dashboard/Dashboard/DBS\";\nimport {\n  useMCPServerEnable,\n  type MCPServerChatContext,\n} from \"./MCPServerConfig/useMCPServerEnable\";\nimport { MCPServerToolsGroupToggle } from \"./MCPServerTools/MCPServerToolsGroupToggle\";\nimport type { MCPServerWithToolAndConfigs } from \"./useMCPServersListProps\";\nimport { isDefined } from \"src/utils/utils\";\n\nexport const MCPServerHeaderCheckbox = ({\n  mcpServer,\n  dbs,\n  chatContext,\n}: {\n  mcpServer: MCPServerWithToolAndConfigs;\n  chatContext: MCPServerChatContext | undefined;\n  dbs: DBS;\n}) => {\n  const { mcp_server_tools: mcpServerTools, icon_path, enabled } = mcpServer;\n  const { llm_chats_allowed_mcp_tools, chatId } = chatContext ?? {};\n  const toolsAllowed = llm_chats_allowed_mcp_tools?.filter((at) =>\n    mcpServerTools.some((t) => t.id === at.tool_id),\n  );\n  const someToolsAllowed = !!toolsAllowed?.length;\n  const name = mcpServer.name;\n  const { onToggleTools } = useMCPServerEnable({\n    dbs,\n    mcp_server: mcpServer,\n    chatContext,\n  });\n\n  const { onErrorAlert } = useOnErrorAlert();\n\n  const onToggleServer = useCallback(() => {\n    void onErrorAlert(async () => {\n      if (someToolsAllowed) {\n        await onToggleTools(\n          toolsAllowed.map((t) => t.tool_id),\n          \"remove\",\n        );\n      } else {\n        await onToggleTools(\n          mcpServerTools.map((t) => t.id),\n          \"approve\",\n        );\n      }\n    });\n  }, [\n    mcpServerTools,\n    onErrorAlert,\n    onToggleTools,\n    someToolsAllowed,\n    toolsAllowed,\n  ]);\n\n  return (\n    <FlexRow className=\"bold mx-p25 w-full\">\n      <Btn\n        title=\"Toggle all tools\"\n        style={{\n          padding: \"0\",\n          marginRight: \"1em\",\n        }}\n        iconNode={icon_path && <SvgIcon icon={icon_path} />}\n        color={someToolsAllowed ? \"action\" : undefined}\n        disabledInfo={\n          mcpServerTools.length ? undefined : (\n            `No tools available. ${enabled ? \"Press reload\" : \"Enable the server first\"}.`\n          )\n        }\n        onClick={\n          !chatId || !llm_chats_allowed_mcp_tools ?\n            undefined\n          : () => onToggleServer()\n        }\n      >\n        {name}\n      </Btn>\n\n      {isDefined(chatId) && (\n        <MCPServerToolsGroupToggle\n          llm_chats_allowed_mcp_tools={llm_chats_allowed_mcp_tools}\n          onToggleTools={onToggleTools}\n          tools={mcpServerTools}\n        />\n      )}\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerTools/MCPServerTools.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport Chip from \"@components/Chip\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { mdiCheck, mdiCheckAll, mdiCheckboxBlankOutline } from \"@mdi/js\";\nimport React, { useMemo } from \"react\";\nimport type { Prgl } from \"../../../../App\";\nimport {\n  useMCPServerEnable,\n  type MCPServerChatContext,\n} from \"../MCPServerConfig/useMCPServerEnable\";\n\nexport const MCPServerTools = ({\n  server,\n  tools,\n  chatContext,\n  dbs,\n}: {\n  server: DBSSchema[\"mcp_servers\"] & {\n    mcp_server_configs: DBSSchema[\"mcp_server_configs\"][];\n  };\n  tools: DBSSchema[\"mcp_server_tools\"][];\n  chatContext: MCPServerChatContext | undefined;\n  selectedToolName: string | undefined;\n} & Pick<Prgl, \"dbs\">) => {\n  const { onToggleTools } = useMCPServerEnable({\n    dbs,\n    mcp_server: server,\n    chatContext,\n  });\n  const { llm_chats_allowed_mcp_tools, chatId } = chatContext ?? {};\n\n  const toolsSortedByReadonly = useMemo(\n    () =>\n      [...tools].toSorted((a, b) => {\n        const aReadOnly = a.annotations?.readOnlyHint ? 1 : 0;\n        const bReadOnly = b.annotations?.readOnlyHint ? 1 : 0;\n        return bReadOnly - aReadOnly || a.name.localeCompare(b.name);\n      }),\n    [tools],\n  );\n\n  return (\n    <ScrollFade\n      data-command=\"MCPServerTools\"\n      className=\"gap-p25 flex-row-wrap o-auto no-scroll-bar\"\n    >\n      {toolsSortedByReadonly.map((tool, i) => {\n        const allowedTool = llm_chats_allowed_mcp_tools?.find(\n          (at) => at.tool_id === tool.id,\n        );\n        return (\n          <Chip\n            key={`${tool.name}${i}`}\n            title={tool.description}\n            className={\"pointer \" + (allowedTool ? \"noselect\" : \"\")}\n            leftIcon={\n              !chatId ? undefined\n              : allowedTool ?\n                {\n                  path: allowedTool.auto_approve ? mdiCheckAll : mdiCheck,\n                  style: {\n                    marginRight: \"0.25rem\",\n                  },\n                  size: 0.75,\n                }\n              : {\n                  path: mdiCheckboxBlankOutline,\n                  style: {\n                    marginRight: \"0.25rem\",\n                    opacity: 0.25,\n                  },\n                  size: 0.75,\n                }\n\n            }\n            aria-checked={!!allowedTool}\n            color={allowedTool ? \"blue\" : undefined}\n            onClick={\n              !chatId ? undefined : (\n                async () => {\n                  const checked = !allowedTool;\n                  await onToggleTools(\n                    [tool.id],\n                    checked ? \"approve\" : \"remove\",\n                  );\n                }\n              )\n            }\n          >\n            {tool.name}\n          </Chip>\n        );\n      })}\n    </ScrollFade>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServerTools/MCPServerToolsGroupToggle.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { getEntries } from \"@common/utils\";\nimport { useOnErrorAlert } from \"@components/AlertProvider\";\nimport { Checkbox } from \"@components/Checkbox\";\nimport { FlexRow } from \"@components/Flex\";\nimport { pickKeys } from \"prostgles-types\";\nimport React, { useMemo } from \"react\";\nimport { isEmpty } from \"src/utils/utils\";\n\nexport const MCPServerToolsGroupToggle = ({\n  tools,\n  llm_chats_allowed_mcp_tools,\n  onToggleTools,\n}: {\n  llm_chats_allowed_mcp_tools:\n    | {\n        tool_id: number;\n        auto_approve: boolean | null;\n      }[]\n    | undefined;\n\n  tools: DBSSchema[\"mcp_server_tools\"][];\n  onToggleTools: (\n    toolIds: number[],\n    action: \"approve\" | \"remove\",\n  ) => Promise<void>;\n}) => {\n  const toggleableAnnotations = useMemo(() => {\n    return tools.reduce((acc, { id, annotations }) => {\n      if (!annotations || isEmpty(annotations)) return acc;\n      const toggleable = pickKeys(annotations, [\"readOnlyHint\"]);\n      getEntries(toggleable).forEach(([key, yes]) => {\n        const existing = acc.get(key) ?? {\n          no: [],\n          yes: [],\n        };\n        if (yes === true) {\n          existing.yes.push(id);\n        } else if (yes === false) {\n          existing.no.push(id);\n        }\n\n        acc.set(key, existing);\n      });\n      return acc;\n    }, new Map<keyof NonNullable<(typeof tools)[number][\"annotations\"]>, { no: number[]; yes: number[] }>());\n  }, [tools]);\n\n  const { onErrorAlert } = useOnErrorAlert();\n\n  return (\n    <FlexRow>\n      {toggleableAnnotations\n        .entries()\n        .toArray()\n        .map(([annotationKey, annotationIds]) => {\n          if (annotationKey !== \"readOnlyHint\") return null;\n          const info = toggleableAnnotations.get(annotationKey);\n          const allowedToolIds =\n            llm_chats_allowed_mcp_tools?.map((at) => at.tool_id) || [];\n          const yesChecked = allowedToolIds.some((toolId) =>\n            info?.yes.includes(toolId),\n          );\n          const noChecked = allowedToolIds.some((toolId) =>\n            info?.no.includes(toolId),\n          );\n          return (\n            <React.Fragment key={annotationKey}>\n              {[\n                { label: \"Read\", checked: yesChecked, ids: annotationIds.yes },\n                { label: \"Write\", checked: noChecked, ids: annotationIds.no },\n              ].map(({ label, checked, ids }) => {\n                return (\n                  <Checkbox\n                    key={label}\n                    label={label}\n                    title=\"Read Only\"\n                    style={{\n                      fontWeight: 300,\n                    }}\n                    checked={checked}\n                    variant=\"micro\"\n                    onChange={({ currentTarget: { checked: newChecked } }) => {\n                      onErrorAlert(() =>\n                        onToggleTools(ids, newChecked ? \"approve\" : \"remove\"),\n                      );\n                    }}\n                  />\n                );\n              })}\n            </React.Fragment>\n          );\n        })}\n    </FlexRow>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServers.tsx",
    "content": "import { mdiCheck, mdiCheckAll } from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useState } from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexCol } from \"@components/Flex\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { SmartCardList } from \"../../../dashboard/SmartCardList/SmartCardList\";\nimport type { ColumnSort } from \"../../../dashboard/W_Table/ColumnMenu/ColumnMenu\";\nimport type { ServerSettingsProps } from \"../ServerSettings\";\nimport { MCPServerFooterActions } from \"./MCPServerFooterActions/MCPServerFooterActions\";\nimport { MCPServersHeader } from \"./MCPServersHeader\";\nimport { MCPServersToolbar } from \"./MCPServersToolbar/MCPServersToolbar\";\nimport {\n  useMCPServersListProps,\n  type MCPServerWithToolAndConfigs,\n} from \"./useMCPServersListProps\";\nimport { MCPServerConfigProvider } from \"./MCPServerConfig/MCPServerConfig\";\nimport { isDefined } from \"@common/filterUtils\";\n\nexport type MCPServersProps = Omit<ServerSettingsProps, \"auth\"> & {\n  chatId: number | undefined;\n};\n\nexport const MCPServers = (props: MCPServersProps) => {\n  const { dbsMethods, dbs, dbsTables, chatId } = props;\n\n  const { getMcpHostInfo } = dbsMethods;\n  const envInfo = usePromise(async () => getMcpHostInfo?.(), [getMcpHostInfo]);\n  const globalSettings = dbs.global_settings.useSubscribeOne();\n  const { mcp_servers_disabled } = globalSettings.data ?? {};\n\n  const { selectedTool, setSelectedTool, filter, fieldConfigs, chatContext } =\n    useMCPServersListProps(chatId, dbs);\n  const { llm_chats_allowed_mcp_tools } = chatContext || {};\n  const someToolsAutoApproved = llm_chats_allowed_mcp_tools?.some(\n    (t) => t.auto_approve,\n  );\n\n  const [loaded, setLoaded] = useState(false);\n  return (\n    <MCPServerConfigProvider dbs={dbs}>\n      <FlexCol\n        className=\"p-1 pt-0 min-w-0 f-1 max-w-800\"\n        style={{\n          opacity: loaded ? 1 : 0,\n          transition: \"opacity 0.2s ease-in-out\",\n        }}\n      >\n        <MCPServersHeader envInfo={envInfo} />\n        <MCPServersToolbar\n          {...props}\n          selectedTool={selectedTool}\n          setSelectedTool={setSelectedTool}\n        />\n        <FlexCol\n          {...(mcp_servers_disabled && {\n            className: \"disabled\",\n            title: \"MCP Servers are disabled\",\n          })}\n        >\n          {chatId && llm_chats_allowed_mcp_tools && (\n            <Btn\n              variant=\"faded\"\n              data-command=\"MCPServers.toggleAutoApprove\"\n              title=\"Toggle auto-approve for selected tools. When enabled, all selected tools can be called without user approval\"\n              iconPath={someToolsAutoApproved ? mdiCheckAll : mdiCheck}\n              color={\"action\"}\n              onClickPromiseMode=\"noTickIcon\"\n              onClickPromise={async () => {\n                await dbs.llm_chats_allowed_mcp_tools.update(\n                  {\n                    chat_id: chatId,\n                    tool_id: {\n                      $in: llm_chats_allowed_mcp_tools.map((t) => t.tool_id),\n                    },\n                  },\n                  { auto_approve: !someToolsAutoApproved },\n                );\n              }}\n            >\n              Auto-approve: {someToolsAutoApproved ? \"ON\" : \"OFF\"}\n            </Btn>\n          )}\n          <SmartCardList<MCPServerWithToolAndConfigs>\n            db={dbs as DBHandlerClient}\n            methods={dbsMethods}\n            className={mcp_servers_disabled ? \"no-interaction\" : undefined}\n            tableName=\"mcp_servers\"\n            realtime={true}\n            showTopBar={false}\n            noDataComponentMode=\"hide-all\"\n            noDataComponent={\n              <InfoRow color=\"info\" className=\"h-fit\">\n                No MCP servers. MCP servers can be added to allow LLM tool usage\n              </InfoRow>\n            }\n            tables={dbsTables}\n            filter={filter}\n            orderBy={orderByEnabledAndName}\n            fieldConfigs={fieldConfigs}\n            enableListAnimations={true}\n            onSetData={() => {\n              setLoaded(true);\n            }}\n            getRowFooter={(r) => (\n              <MCPServerFooterActions\n                mcp_server={r}\n                dbs={dbs}\n                dbsMethods={dbsMethods}\n                envInfo={envInfo}\n                chatContext={chatContext}\n              />\n            )}\n          />\n        </FlexCol>\n      </FlexCol>\n    </MCPServerConfigProvider>\n  );\n};\n\nconst orderByEnabledAndName = [\n  {\n    key: \"enabled\",\n    asc: false,\n  },\n  {\n    key: \"name\",\n    asc: true,\n  },\n] satisfies ColumnSort[];\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServersHeader.tsx",
    "content": "import { InfoRow } from \"@components/InfoRow\";\nimport React from \"react\";\nimport type { ServerSettingsProps } from \"../ServerSettings\";\n\nexport type MCPServersProps = Pick<ServerSettingsProps, \"dbsMethods\">;\n\nexport const MCPServersHeader = (props: {\n  envInfo:\n    | {\n        os: string;\n        npmVersion: string;\n        uvxVersion: string;\n      }\n    | undefined;\n}) => {\n  const { envInfo } = props;\n\n  const missingDependencies =\n    !envInfo ? undefined\n    : !envInfo.npmVersion ?\n      <>\n        npm not installed. Visit{\" \"}\n        <a href=\"https://nodejs.org/en/download\">\n          https://nodejs.org/en/download\n        </a>\n      </>\n    : !envInfo.uvxVersion ?\n      <>\n        uvx not installed. Visit{\" \"}\n        <a href=\"https://docs.astral.sh/uv/getting-started/installation/\">\n          https://docs.astral.sh/uv/getting-started/installation/\n        </a>\n      </>\n    : \"\";\n\n  return (\n    <>\n      <InfoRow className=\"mb-1\" variant=\"naked\" color=\"info\" iconPath=\"\">\n        Pre-built integrations that can be used through the Ask AI chat and\n        server-side functions. For more information visit{\" \"}\n        <a href=\"https://modelcontextprotocol.io/\">Model Context Protocol</a>.\n        <br></br>\n        <br></br>\n        Enabled MCP servers are available to chats after adding them in the\n        &quot;Allowed MCP Tools&quot; section of the chat settings.\n      </InfoRow>\n      {missingDependencies && <InfoRow>{missingDependencies}</InfoRow>}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServersToolbar/AddMCPServer.tsx",
    "content": "import { mdiPlus } from \"@mdi/js\";\nimport React from \"react\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport Popup from \"@components/Popup/Popup\";\nimport { ScrollFade } from \"@components/ScrollFade/ScrollFade\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { CodeEditorWithSaveButton } from \"../../../../dashboard/CodeEditor/CodeEditorWithSaveButton\";\nimport type { DBS } from \"../../../../dashboard/Dashboard/DBS\";\nimport {\n  useAddMCPServer,\n  type MCPServerConfig as MCPServerJSONConfig,\n} from \"./useAddMCPServer\";\n\nexport const AddMCPServer = ({ dbs }: { dbs: DBS }) => {\n  const [showAddServer, setShowAddServer] = React.useState(false);\n  const state = useAddMCPServer(showAddServer);\n  const { configSchemas, mcpServer, value, setValue, setConfigSchemas } = state;\n\n  return (\n    <>\n      <Btn\n        variant=\"filled\"\n        color=\"action\"\n        iconPath={mdiPlus}\n        data-command=\"AddMCPServer.Open\"\n        onClick={() => setShowAddServer(true)}\n      >\n        Add MCP Server\n      </Btn>\n      {showAddServer && (\n        <Popup\n          data-command=\"AddMCPServer\"\n          positioning=\"top-center\"\n          title=\"Add MCP Server\"\n          clickCatchStyle={{ opacity: 1 }}\n          contentStyle={{\n            minWidth: \"min(600px, 100vw)\",\n            minHeight: \"min(500px, 100vh)\",\n            paddingTop: 0,\n          }}\n          contentClassName=\"f-1 p-2\"\n          onClose={() => setShowAddServer(false)}\n          footerButtons={[\n            {\n              label: \"Close\",\n              onClickClose: true,\n            },\n            {\n              label: \"Add MCP Server\",\n              color: \"action\",\n              variant: \"filled\",\n              className: \"ml-auto\",\n              \"data-command\": \"AddMCPServer.Add\",\n              disabledInfo:\n                !mcpServer ? \"Must provide a config\"\n                : !(dbs as any).mcp_servers?.insert ? \"Must be admin\"\n                : undefined,\n              onClickPromise:\n                !mcpServer ? undefined : (\n                  async () => {\n                    await dbs.mcp_servers.insert(mcpServer);\n                    setShowAddServer(false);\n                  }\n                ),\n            },\n          ]}\n        >\n          <p className=\"ta-start\">\n            Paste the JSON configuration of the MCP server you want to add\n          </p>\n          <CodeEditorWithSaveButton\n            label=\"\"\n            language={\"json\"}\n            options={{\n              minimap: {\n                enabled: false,\n              },\n              lineNumbers: \"off\",\n              automaticLayout: true,\n            }}\n            codePlaceholder={JSON.stringify(exampleConfig, null, 2)}\n            autoSave={true}\n            value={value}\n            onSave={setValue}\n          />\n          {Boolean(configSchemas?.length) && (\n            <ScrollFade\n              className=\"py-1 o-auto flex-col gap-1\"\n              style={{ maxHeight: \"400px\" }}\n            >\n              <p className=\"ta-start\">\n                Specify if any of the arguments or environment variables are\n                configurable\n              </p>\n              {configSchemas?.map((s, schemaIndex) => {\n                const update = (changes: Partial<typeof s>) => {\n                  const newSchemas = configSchemas.map((oldS, oldIndex) => {\n                    if (oldIndex === schemaIndex) {\n                      return {\n                        ...oldS,\n                        ...changes,\n                      } as typeof s;\n                    }\n                    return oldS;\n                  });\n                  setConfigSchemas(newSchemas);\n                };\n\n                return (\n                  <FlexRow key={s.name + schemaIndex}>\n                    <FormField\n                      className=\"f-1\"\n                      label={`${s.name} (${s.type === \"arg\" ? `arg ${s.index}` : \"env\"})`}\n                      value={s.description}\n                      placeholder=\"Description\"\n                      onChange={(description) =>\n                        update({ description, configurable: true })\n                      }\n                    />\n                    <SwitchToggle\n                      data-key={s.name}\n                      label={\"Is configurable\"}\n                      checked={!!s.configurable}\n                      variant=\"col\"\n                      onChange={(configurable) => update({ configurable })}\n                    />\n                  </FlexRow>\n                );\n              })}\n            </ScrollFade>\n          )}\n        </Popup>\n      )}\n    </>\n  );\n};\n\nconst exampleConfig: MCPServerJSONConfig = {\n  mcpServers: {\n    github: {\n      command: \"docker\",\n      args: [\n        \"run\",\n        \"-i\",\n        \"--rm\",\n        \"-e\",\n        \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n        \"ghcr.io/github/github-mcp-server\",\n      ],\n      env: {\n        GITHUB_PERSONAL_ACCESS_TOKEN: \"<YOUR_TOKEN>\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServersToolbar/MCPServersToolbar.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport { Select } from \"@components/Select/Select\";\nimport { mdiFilter, mdiMagnify, mdiPlay, mdiStop } from \"@mdi/js\";\nimport React from \"react\";\nimport type { MCPServersProps } from \"../MCPServers\";\nimport { AddMCPServer } from \"./AddMCPServer\";\n\nexport const MCPServersToolbar = ({\n  dbs,\n  selectedTool,\n  setSelectedTool,\n}: MCPServersProps & {\n  selectedTool: undefined | DBSSchema[\"mcp_server_tools\"];\n  setSelectedTool: (tool: undefined | DBSSchema[\"mcp_server_tools\"]) => void;\n}) => {\n  const { data: tools } = dbs.mcp_server_tools.useFind();\n  const globalSettings = dbs.global_settings.useSubscribeOne();\n\n  return (\n    <>\n      <FlexRow>\n        <AddMCPServer dbs={dbs} />\n        <Btn\n          color=\"action\"\n          variant=\"outline\"\n          data-command=\"MCPServersToolbar.stopAllToggle\"\n          title={\n            globalSettings.data?.mcp_servers_disabled ?\n              \"Start all MCP Servers\"\n            : \"Stop all MCP Servers\"\n          }\n          iconPath={\n            globalSettings.data?.mcp_servers_disabled ? mdiPlay : mdiStop\n          }\n          onClickPromise={async () => {\n            await dbs.global_settings.update(\n              {},\n              {\n                mcp_servers_disabled:\n                  !globalSettings.data?.mcp_servers_disabled,\n              },\n            );\n          }}\n        />\n\n        <Select\n          className=\"min-w-0 ml-auto\"\n          emptyLabel={\"Search tools\"}\n          data-command=\"MCPServersToolbar.searchTools\"\n          btnProps={{\n            iconPath: selectedTool ? mdiFilter : mdiMagnify,\n            color: selectedTool ? \"action\" : \"default\",\n            variant: selectedTool ? \"filled\" : \"faded\",\n            style: {\n              flexShrink: 1,\n            },\n          }}\n          value={selectedTool?.id}\n          fullOptions={(tools ?? []).map((t) => ({\n            key: t.id,\n            label: `${t.server_name} ${t.name}`,\n            subLabel: t.description,\n          }))}\n          onChange={(id) => {\n            setSelectedTool(tools?.find((t) => t.id === id));\n          }}\n        />\n      </FlexRow>\n      {selectedTool && (\n        <FlexRow className=\"jc-end\">\n          <Btn color=\"action\" onClick={() => setSelectedTool(undefined)}>\n            Clear filter\n          </Btn>\n        </FlexRow>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/MCPServersToolbar/useAddMCPServer.ts",
    "content": "import { useEffect, useMemo, useState } from \"react\";\nimport { isObject, type DBSSchema } from \"@common/publishUtils\";\nimport { isEmpty } from \"../../../../utils/utils\";\n\ntype NewMCPServer = Pick<\n  DBSSchema[\"mcp_servers\"],\n  \"name\" | \"command\" | \"args\" | \"env\" | \"config_schema\"\n>;\n\nexport const useAddMCPServer = (showAddServer: boolean) => {\n  const [value, setValue] = useState(\"\");\n  /** Reset on close */\n  useEffect(() => {\n    setValue(\"\");\n  }, [showAddServer]);\n  const [config, setConfig] = useState<MCPServerConfig>();\n  const [mcpServer, setMCPServer] = useState<NewMCPServer>();\n\n  const potentialConfigSchemas = useMemo(\n    () => mcpServer && getPotentialConfigSchemas(mcpServer),\n    [mcpServer],\n  );\n  const [configSchemas, setConfigSchemas] =\n    useState<typeof potentialConfigSchemas>();\n\n  const configSchema = useMemo(() => {\n    if (!configSchemas) return undefined;\n    const schema: NewMCPServer[\"config_schema\"] = Object.fromEntries(\n      configSchemas\n        .filter((s) => s.configurable)\n        .map((schema) => {\n          if (schema.type === \"env\") {\n            return [\n              schema.name,\n              {\n                type: schema.type,\n                title: schema.name,\n                description: schema.description,\n              } satisfies {\n                type: \"env\";\n                title?: string;\n                optional?: boolean;\n                description?: string;\n              },\n            ];\n          }\n          return [\n            schema.name,\n            {\n              type: schema.type,\n              title: schema.name,\n              description: schema.description,\n              index: schema.index,\n            } satisfies {\n              type: \"arg\";\n              title?: string;\n              optional?: boolean;\n              description?: string;\n              index?: number;\n            },\n          ];\n        }),\n    );\n    return schema;\n  }, [configSchemas]);\n\n  useEffect(() => {\n    setConfigSchemas(potentialConfigSchemas);\n  }, [potentialConfigSchemas]);\n\n  useEffect(() => {\n    if (!config) {\n      setMCPServer(undefined);\n    } else {\n      const [mcpServer, ...otherServers] = Object.entries(\n        config.mcpServers,\n      ).map(\n        ([name, config]) =>\n          ({\n            name,\n            ...config,\n            command: config.command as \"docker\",\n            env: config.env ?? null,\n            config_schema: null,\n          }) satisfies NewMCPServer,\n      );\n      if (otherServers.length) {\n        alert(\"Only one MCP server can be added at a time\");\n        return;\n      }\n      setMCPServer(mcpServer);\n    }\n  }, [config]);\n\n  useEffect(() => {\n    try {\n      const parsedConfig = JSON.parse(value);\n      if (\n        !parsedConfig ||\n        !isObject(parsedConfig) ||\n        !parsedConfig.mcpServers ||\n        isEmpty(parsedConfig.mcpServers)\n      ) {\n        throw new Error(\"Invalid config\");\n      }\n      setConfig(parsedConfig as MCPServerConfig);\n    } catch (e) {\n      setConfig(undefined);\n    }\n  }, [value]);\n\n  return {\n    mcpServer: mcpServer && {\n      ...mcpServer,\n      config_schema: isEmpty(configSchema) ? null : configSchema,\n    },\n    configSchemas,\n    setConfigSchemas,\n    value,\n    setValue,\n  };\n};\n\ntype MCPConfig = {\n  command: string;\n  args: string[] | null;\n  env?: Record<string, string> | null;\n};\n\nexport type MCPServerConfig = {\n  mcpServers: Record<string, MCPConfig>;\n};\n\ntype ArgDef =\n  | {\n      type: \"env\";\n      name: string;\n      description?: string;\n      configurable?: boolean;\n    }\n  | {\n      type: \"arg\";\n      name: string;\n      description?: string;\n      index: number;\n      configurable?: boolean;\n    };\nconst getPotentialConfigSchemas = (config: MCPConfig): ArgDef[] => {\n  const envs: ArgDef[] = [];\n  const args: ArgDef[] = [];\n  config.args?.forEach((arg, argIndex) => {\n    /** Ignore first argument because it should just be the tool package name */\n    if (!argIndex) return;\n    args.push({\n      type: \"arg\",\n      name: arg,\n      index: argIndex,\n    });\n  });\n\n  Object.entries(config.env ?? {}).forEach(([key, value]) => {\n    envs.push({\n      type: \"env\",\n      name: key,\n      description: value,\n    });\n  });\n\n  const result = [\n    ...envs,\n    /** Exclude cases where an environment variable is used in the arguments. Keep only the env argument */\n    ...args.filter((a) => !envs.some((e) => e.name === a.name)),\n  ];\n\n  return result.sort((a, b) => {\n    return (\n      /** Env vars first */\n      b.type.localeCompare(a.type) ||\n      b.name.length - a.name.length ||\n      a.name.localeCompare(b.name)\n    );\n  });\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/useMCPChatAllowedTools.ts",
    "content": "import type { DBS } from \"src/dashboard/Dashboard/DBS\";\n\nexport const useMCPChatAllowedTools = (\n  dbs: DBS,\n  chatId: number | undefined,\n) => {\n  const { data: llm_chats_allowed_mcp_tools } =\n    dbs.llm_chats_allowed_mcp_tools.useSubscribe(\n      {\n        chat_id: chatId,\n      },\n      {\n        select: {\n          tool_id: 1,\n          auto_approve: 1,\n          server_config_id: 1,\n          server_name: 1,\n        },\n      },\n    );\n\n  return { llm_chats_allowed_mcp_tools };\n};\nexport type MCPChatAllowedTools = NonNullable<\n  ReturnType<typeof useMCPChatAllowedTools>[\"llm_chats_allowed_mcp_tools\"]\n>;\n"
  },
  {
    "path": "client/src/pages/ServerSettings/MCPServers/useMCPServersListProps.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport React, { useMemo, useState } from \"react\";\nimport type { DBS } from \"../../../dashboard/Dashboard/DBS\";\nimport type { FieldConfig } from \"../../../dashboard/SmartCard/SmartCard\";\nimport { MCPServerHeaderCheckbox } from \"./MCPServerHeaderCheckbox\";\nimport { MCPServerTools } from \"./MCPServerTools/MCPServerTools\";\nimport { useMCPChatAllowedTools } from \"./useMCPChatAllowedTools\";\nimport { isDefined } from \"@common/filterUtils\";\n\nexport type MCPServerWithToolAndConfigs = DBSSchema[\"mcp_servers\"] & {\n  mcp_server_tools: DBSSchema[\"mcp_server_tools\"][];\n  mcp_server_configs: DBSSchema[\"mcp_server_configs\"][];\n  mcp_server_logs: DBSSchema[\"mcp_server_logs\"][];\n};\n\nexport const useMCPServersListProps = (\n  chatId: number | undefined,\n  dbs: DBS,\n) => {\n  const [selectedTool, setSelectedTool] =\n    useState<DBSSchema[\"mcp_server_tools\"]>();\n\n  const { llm_chats_allowed_mcp_tools } = useMCPChatAllowedTools(dbs, chatId);\n  const chatContext = useMemo(() => {\n    if (isDefined(chatId) && llm_chats_allowed_mcp_tools) {\n      return {\n        chatId,\n        llm_chats_allowed_mcp_tools,\n      };\n    }\n    return undefined;\n  }, [chatId, llm_chats_allowed_mcp_tools]);\n\n  const filter = useMemo(() => {\n    return (\n      selectedTool && {\n        name: selectedTool.server_name,\n      }\n    );\n  }, [selectedTool]);\n\n  const fieldConfigs = useMemo(\n    () =>\n      [\n        {\n          name: \"name\",\n          label: \"\",\n          renderMode: \"full\",\n          render: (_, mcpServer) => (\n            <MCPServerHeaderCheckbox\n              mcpServer={mcpServer}\n              chatContext={chatContext}\n              dbs={dbs}\n            />\n          ),\n        },\n        {\n          name: \"mcp_server_configs\",\n          select: \"*\",\n          hide: true,\n        },\n        {\n          name: \"mcp_server_logs\",\n          select: \"*\",\n          hide: true,\n        },\n        {\n          name: \"mcp_server_tools\",\n          select: {\n            id: 1,\n            name: 1,\n            description: 1,\n            annotations: 1,\n            /** TODO: fix nested joins in prostgles-server */\n            // llm_chats_allowed_mcp_tools: \"*\",\n          },\n          renderMode: \"valueNode\",\n          className: \"o-unset\",\n          render: (tools: DBSSchema[\"mcp_server_tools\"][], server) => {\n            return (\n              <MCPServerTools\n                server={server}\n                tools={tools}\n                selectedToolName={selectedTool?.name}\n                chatContext={chatContext}\n                dbs={dbs}\n              />\n            );\n          },\n        },\n        ...(\n          [\n            \"installed\",\n            \"config_schema\",\n            \"enabled\",\n            \"source\",\n            \"command\",\n            \"icon_path\",\n          ] as const\n        ).map((name) => ({\n          name,\n          hide: true,\n        })),\n      ] satisfies FieldConfig<MCPServerWithToolAndConfigs>[],\n    [chatContext, dbs, selectedTool?.name],\n  );\n\n  return {\n    selectedTool,\n    setSelectedTool,\n    filter,\n    fieldConfigs,\n    chatContext,\n  };\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/OAuthProviderSetup.tsx",
    "content": "import { mdiLock } from \"@mdi/js\";\nimport { isEqual } from \"prostgles-types\";\nimport React, { useState } from \"react\";\nimport { OAuthProviderOptions } from \"@common/OAuthUtils\";\nimport { CopyToClipboardBtn } from \"@components/CopyToClipboardBtn\";\nimport ErrorComponent from \"@components/ErrorComponent\";\nimport FormField from \"@components/FormField/FormField\";\nimport { Icon } from \"@components/Icon/Icon\";\nimport { FooterButtons } from \"@components/Popup/FooterButtons\";\nimport { Section } from \"@components/Section\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { tout } from \"../ElectronSetup/ElectronSetup\";\nimport {\n  FacebookIcon,\n  GithubIcon,\n  GoogleIcon,\n  MicrosoftIcon,\n} from \"../Login/SocialIcons\";\nimport type { AuthProviderProps } from \"./AuthProvidersSetup\";\n\ntype P = AuthProviderProps & {\n  provider: keyof Omit<\n    AuthProviderProps[\"authProviders\"],\n    \"email\" | \"website_url\" | \"created_user_type\"\n  >;\n};\nexport const OAuthProviderSetup = ({\n  authProviders,\n  disabledInfo,\n  contentClassName,\n  provider,\n  doUpdate,\n}: P) => {\n  const returnURL = `${authProviders.website_url}/oauth/${provider}/callback`;\n  const auth = authProviders[provider];\n  const [_localAuth, _setLocalAuth] = useState(auth);\n  const localAuth = _localAuth ?? auth;\n  const isCustomOAuth = (\n    localAuth: any,\n  ): localAuth is typeof authProviders.customOAuth =>\n    provider === \"customOAuth\";\n  const setLocalAuth = (update: Partial<typeof auth>) => {\n    if (!update) return _setLocalAuth(undefined);\n    _setLocalAuth({\n      ...(localAuth as any),\n      clientID: localAuth?.clientID ?? \"\",\n      clientSecret: localAuth?.clientSecret ?? \"\",\n      ...update,\n    });\n  };\n  const [error, setError] = useState<any>(undefined);\n  const didChange = localAuth && !isEqual(localAuth, auth);\n  const providerInfo = PROVIDER_INFO[provider];\n  const { name, icon } = providerInfo;\n\n  const msftAuthOpts =\n    localAuth?.authOpts && \"prompt\" in localAuth.authOpts ?\n      localAuth.authOpts\n    : undefined;\n\n  const onSave = async (newProviderConfig = localAuth) => {\n    try {\n      await doUpdate({\n        ...authProviders,\n        [provider]: newProviderConfig,\n      });\n      await tout(500);\n      setLocalAuth(undefined);\n    } catch (err) {\n      setError(err);\n    }\n  };\n\n  return (\n    <Section\n      title={name}\n      titleIcon={icon}\n      disabledInfo={disabledInfo}\n      contentClassName={contentClassName}\n      disableFullScreen={true}\n      titleRightContent={\n        <SwitchToggle\n          className=\"ml-auto\"\n          disabledInfo={\n            !authProviders[provider] ?\n              t.OAuthProviderSetup[\"Must configure the provider first\"]\n            : !localAuth ?\n              t.OAuthProviderSetup[\"Must provide a client ID and secret\"]\n            : undefined\n          }\n          checked={!!authProviders[provider]?.enabled}\n          onChange={(checked) => {\n            if (!localAuth) return;\n            onSave({ ...localAuth, enabled: checked });\n          }}\n        />\n      }\n    >\n      <SwitchToggle\n        label={t.common.Enabled}\n        checked={!!localAuth?.enabled}\n        disabledInfo={\n          !localAuth?.clientID &&\n          !localAuth?.clientSecret &&\n          t.OAuthProviderSetup[\"Must provide a client ID and secret\"]\n        }\n        onChange={(enabled) => {\n          setLocalAuth({\n            enabled,\n          });\n        }}\n      />\n      {isCustomOAuth(localAuth) && (\n        <>\n          <FormField\n            label={t.OAuthProviderSetup[\"Display Name\"]}\n            value={localAuth?.displayName}\n            onChange={(displayName: string) => {\n              void setLocalAuth({\n                displayName,\n              });\n            }}\n          />\n          <FormField\n            label={t.OAuthProviderSetup[\"Display Icon\"]}\n            value={localAuth?.displayIconPath}\n            onChange={(displayIconPath: string) => {\n              void setLocalAuth({\n                displayIconPath,\n              });\n            }}\n          />\n          <FormField\n            label={t.OAuthProviderSetup[\"Authorization URL\"]}\n            value={localAuth?.authorizationURL}\n            onChange={(authorizationURL: string) => {\n              void setLocalAuth({\n                authorizationURL,\n              });\n            }}\n          />\n          <FormField\n            label={t.OAuthProviderSetup[\"Token URL\"]}\n            value={localAuth?.tokenURL}\n            onChange={(tokenURL: string) => {\n              void setLocalAuth({\n                tokenURL,\n              });\n            }}\n          />\n        </>\n      )}\n      <FormField\n        label={t.OAuthProviderSetup[\"Client ID\"]}\n        value={localAuth?.clientID}\n        onChange={(clientID: string) => {\n          setLocalAuth({\n            clientID,\n          });\n        }}\n      />\n      <FormField\n        label={t.OAuthProviderSetup[\"Client Secret\"]}\n        value={localAuth?.clientSecret}\n        onChange={(clientSecret: string) => {\n          void setLocalAuth({\n            clientSecret,\n          });\n        }}\n      />\n      <FormField\n        label={t.OAuthProviderSetup.Scopes}\n        fullOptions={PROVIDER_INFO[provider].scopes}\n        multiSelect={true}\n        value={localAuth?.authOpts?.scope}\n        onChange={(scopes: string[]) => {\n          console.log(\"scopes\", scopes);\n          void setLocalAuth({\n            authOpts: {\n              ...localAuth?.authOpts,\n              scope: scopes,\n            },\n          });\n        }}\n      />\n      {\"prompts\" in providerInfo && (\n        <FormField\n          label={t.OAuthProviderSetup.Prompt}\n          value={msftAuthOpts?.prompt}\n          fullOptions={providerInfo.prompts}\n          onChange={(prompt) =>\n            setLocalAuth({\n              authOpts: {\n                ...msftAuthOpts,\n                prompt,\n              } as typeof msftAuthOpts,\n            })\n          }\n          hint={\n            t.OAuthProviderSetup[\n              \"Indicates the type of user interaction that is required.\"\n            ]\n          }\n        />\n      )}\n      <FormField\n        label={t.OAuthProviderSetup[\"Return URL\"]}\n        readOnly={true}\n        value={returnURL}\n        rightIcons={<CopyToClipboardBtn content={returnURL} />}\n      />\n      {error && <ErrorComponent error={error} />}\n      {didChange && (\n        <FooterButtons\n          footerButtons={[\n            {\n              label: t.common.Save,\n              color: \"action\",\n              variant: \"filled\",\n              onClickPromise: () => onSave(),\n            },\n          ]}\n        />\n      )}\n    </Section>\n  );\n};\n\nconst PROVIDER_INFO = {\n  google: {\n    name: \"Google\",\n    icon: <GoogleIcon />,\n    scopes: OAuthProviderOptions.google.scopes,\n  },\n  facebook: {\n    name: \"Facebook\",\n    icon: <FacebookIcon />,\n    scopes: OAuthProviderOptions.facebook.scopes,\n  },\n  github: {\n    name: \"GitHub\",\n    icon: <GithubIcon />,\n    scopes: OAuthProviderOptions.github.scopes,\n  },\n  microsoft: {\n    name: \"Microsoft\",\n    icon: <MicrosoftIcon />,\n    scopes: OAuthProviderOptions.microsoft.scopes,\n    prompts: OAuthProviderOptions.microsoft.prompts,\n  },\n  customOAuth: {\n    name: \"OAuth2\",\n    icon: <Icon path={mdiLock} />,\n    scopes: OAuthProviderOptions.microsoft.scopes,\n    prompts: OAuthProviderOptions.microsoft.prompts,\n  },\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/ServerSettings.tsx",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { getCIDRRangesQuery } from \"@common/publishUtils\";\nimport Btn from \"@components/Btn\";\nimport Chip from \"@components/Chip\";\nimport { FlexCol } from \"@components/Flex\";\nimport FormField from \"@components/FormField/FormField\";\nimport { InfoRow } from \"@components/InfoRow\";\nimport { TabsWithDefaultStyle } from \"@components/Tabs\";\nimport {\n  mdiAccountKey,\n  mdiAssistant,\n  mdiCloudKeyOutline,\n  mdiDocker,\n  mdiLaptop,\n  mdiSecurity,\n} from \"@mdi/js\";\nimport type { DBHandlerClient } from \"prostgles-client/dist/prostgles\";\nimport { usePromise } from \"prostgles-client\";\nimport React, { useState } from \"react\";\nimport type { Prgl } from \"../../App\";\nimport { LLMProviderSetup } from \"../../dashboard/AskLLM/Setup/LLMProviderSetup\";\nimport { SmartCardList } from \"../../dashboard/SmartCardList/SmartCardList\";\nimport { SmartForm } from \"../../dashboard/SmartForm/SmartForm\";\nimport { t } from \"../../i18n/i18nUtils\";\nimport { AuthProviderSetup } from \"./AuthProvidersSetup\";\nimport { MCPServers } from \"./MCPServers/MCPServers\";\nimport { Services } from \"./Services\";\n\nexport type ServerSettingsProps = Pick<\n  Prgl,\n  \"dbsMethods\" | \"dbs\" | \"dbsTables\" | \"auth\" | \"serverState\"\n>;\nexport const ServerSettings = (props: ServerSettingsProps) => {\n  const { dbsMethods, dbs, dbsTables, serverState } = props;\n\n  const [testCIDR, setCIDR] = useState<string>();\n  const [settingsLoaded, setSettingsLoaded] = useState(false);\n\n  const myIP = usePromise(() => dbsMethods.getMyIP!());\n\n  const ipRanges = usePromise(async () => {\n    try {\n      if (!testCIDR) return;\n      const cidr = testCIDR;\n      const ranges =\n        ((await dbs.sql!(\n          getCIDRRangesQuery({ cidr, returns: [\"from\", \"to\"] }),\n          { cidr },\n          { returnType: \"row\" },\n        )) as { from?: string; to?: string } | undefined) ?? {};\n\n      return {\n        ...ranges,\n        error: undefined,\n      };\n    } catch (error: unknown) {\n      return { error, to: undefined, from: undefined };\n    }\n  }, [testCIDR, dbs.sql]);\n\n  if (!myIP) return null;\n\n  return (\n    <div className=\"ServerSettings w-full o-auto\">\n      <div\n        className=\"flex-row jc-center p-p5\"\n        style={{\n          alignSelf: \"stretch\",\n          paddingBottom: \"4em\",\n        }}\n      >\n        <div\n          className=\"flex-col gap-1 mt-2 max-w-800 min-w-0 f-1\"\n          style={{ alignSelf: \"stretch\" }}\n        >\n          <TabsWithDefaultStyle\n            items={{\n              security: {\n                hide: serverState.isElectron,\n                label: t.ServerSettings[\"Security\"],\n                leftIconPath: mdiSecurity,\n                content: (\n                  <FlexCol\n                    style={{ opacity: settingsLoaded ? 1 : 0 }}\n                    className=\"p-1 pt-0\"\n                  >\n                    <InfoRow\n                      className=\"mb-1\"\n                      variant=\"naked\"\n                      color=\"info\"\n                      iconPath=\"\"\n                    >\n                      Configure domain access, IP restrictions, session\n                      duration, and login rate limits to enhance security.\n                    </InfoRow>\n                    <SmartForm\n                      className=\"bg-color-0 \"\n                      label=\"\"\n                      db={dbs as DBHandlerClient}\n                      methods={dbsMethods}\n                      tableName=\"global_settings\"\n                      contentClassname=\"px-p25  \"\n                      columns={\n                        {\n                          allowed_origin: 1,\n                          allowed_ips: 1,\n                          allowed_ips_enabled: 1,\n                          trust_proxy: 1,\n                          session_max_age_days: 1,\n                          login_rate_limit: 1,\n                          login_rate_limit_enabled: 1,\n                        } satisfies Partial<\n                          Record<\n                            keyof DBGeneratedSchema[\"global_settings\"][\"columns\"],\n                            1\n                          >\n                        >\n                      }\n                      tables={dbsTables}\n                      rowFilter={[{ fieldName: \"id\", type: \"not null\" }]}\n                      confirmUpdates={true}\n                      hideNonUpdateableColumns={true}\n                      onLoaded={() => setSettingsLoaded(true)}\n                    />\n                    <FlexCol className=\"p-1 bg-color-0 shadow \">\n                      <FormField\n                        type=\"text\"\n                        label={t.ServerSettings[\"Validate a CIDR\"]}\n                        value={testCIDR ?? \"\"}\n                        onChange={(cidr) => {\n                          setCIDR(cidr);\n                        }}\n                        placeholder=\"127.1.1.1/32\"\n                        hint={\n                          t.ServerSettings[\n                            \"Enter a value to see the allowed IP ranges\"\n                          ]\n                        }\n                        error={ipRanges?.error}\n                        rightIcons={\n                          <Btn\n                            title={t.ServerSettings[\"Add your current IP\"]}\n                            iconPath={mdiLaptop}\n                            onClick={() => setCIDR(myIP.ip + \"/128\")}\n                          ></Btn>\n                        }\n                      />\n                      {/* {myIP && <InfoRow className=\"\" color=\"info\" variant=\"naked\">\n                    <div className=\"flex-col ai-center w-fit\">\n                      <div> Your current IP Address:</div> \n                      <strong>{myIP?.ip}</strong>\n                    </div>\n                  </InfoRow>} */}\n                      {!!ipRanges?.from && (\n                        <FlexCol>\n                          {ipRanges.from === ipRanges.to ?\n                            <Chip\n                              variant=\"naked\"\n                              label={t.ServerSettings[\"Allowed IP\"]}\n                              value={ipRanges.from}\n                            />\n                          : <>\n                              <Chip\n                                variant=\"naked\"\n                                label={t.ServerSettings[\"From IP\"]}\n                                value={ipRanges.from}\n                              />\n                              <Chip\n                                variant=\"naked\"\n                                label={t.ServerSettings[\"To IP\"]}\n                                value={ipRanges.to}\n                              />\n                            </>\n                          }\n                        </FlexCol>\n                      )}\n                    </FlexCol>\n                  </FlexCol>\n                ),\n              },\n              auth: {\n                hide: serverState.isElectron,\n                leftIconPath: mdiAccountKey,\n                label: t.ServerSettings.Authentication,\n                content: <AuthProviderSetup dbs={dbs} dbsTables={dbsTables} />,\n              },\n              cloud: {\n                hide: serverState.isElectron,\n                leftIconPath: mdiCloudKeyOutline,\n                label: t.ServerSettings[\"Cloud credentials\"],\n                content: (\n                  <FlexCol className=\"p-1\">\n                    {\" \"}\n                    <InfoRow variant=\"naked\" color=\"info\" iconPath=\"\">\n                      Configure AWS S3 cloud credentials for file storage\n                    </InfoRow>\n                    <SmartCardList\n                      db={dbs as DBHandlerClient}\n                      methods={dbsMethods}\n                      tableName=\"credentials\"\n                      tables={dbsTables}\n                      realtime={true}\n                      excludeNulls={true}\n                      noDataComponentMode=\"hide-all\"\n                      noDataComponent={\n                        <InfoRow color=\"info\" className=\"m-1 h-fit\">\n                          {\n                            t.ServerSettings[\n                              \"No cloud credentials. Credentials can be added for file storage\"\n                            ]\n                          }\n                        </InfoRow>\n                      }\n                    />\n                  </FlexCol>\n                ),\n              },\n              mcpServers: {\n                leftIconPath: mdiLaptop,\n                label: \"MCP Servers\",\n                content: <MCPServers {...props} chatId={undefined} />,\n              },\n              llmProviders: {\n                leftIconPath: mdiAssistant,\n                label: \"LLM Providers\",\n                content: (\n                  <FlexCol className=\"p-1 pt-0 min-w-0\">\n                    <InfoRow variant=\"naked\" color=\"info\" iconPath=\"\">\n                      Configure LLM provider credentials used in AI Assistant\n                      chat.\n                    </InfoRow>\n                    <LLMProviderSetup {...props} />\n                  </FlexCol>\n                ),\n              },\n              services: {\n                leftIconPath: mdiDocker,\n                label: \"Services\",\n                content: (\n                  <FlexCol className=\"p-1 pt-0 min-w-0\">\n                    <InfoRow variant=\"naked\" color=\"info\" iconPath=\"\">\n                      Configure services used by AI Assistant.\n                    </InfoRow>\n                    <Services {...props} showSpecificService={undefined} />\n                  </FlexCol>\n                ),\n              },\n            }}\n          />\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/Services.tsx",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { getEntries } from \"@common/utils\";\nimport { useOnErrorAlert } from \"@components/AlertProvider\";\nimport { FlexCol, FlexRow } from \"@components/Flex\";\nimport { Label } from \"@components/Label\";\nimport { MonacoLogRenderer } from \"@components/MonacoLogRenderer/MonacoLogRenderer\";\nimport { Select } from \"@components/Select/Select\";\nimport { StatusChip } from \"@components/StatusChip\";\nimport { SvgIcon } from \"@components/SvgIcon\";\nimport { SwitchToggle } from \"@components/SwitchToggle\";\nimport type { DBHandlerClient } from \"prostgles-client\";\nimport React, { useMemo } from \"react\";\nimport type { Prgl } from \"src/App\";\nimport type { FieldConfig } from \"src/dashboard/SmartCard/SmartCard\";\nimport { SmartCardList } from \"src/dashboard/SmartCardList/SmartCardList\";\n\ntype P = Pick<Prgl, \"dbs\" | \"dbsMethods\" | \"dbsTables\"> & {\n  showSpecificService:\n    | undefined\n    | {\n        title: string;\n        color?: \"red\";\n        serviceName: string;\n      };\n};\n\nexport const Services = ({\n  dbs,\n  dbsMethods,\n  dbsTables,\n  showSpecificService,\n}: P) => {\n  const { servicesFieldConfigs } = useServicesFieldConfigs({\n    dbs,\n    dbsMethods,\n    showSpecificService,\n  });\n  return (\n    <SmartCardList\n      db={dbs as DBHandlerClient}\n      title={\n        showSpecificService && (\n          <Label\n            variant=\"normal\"\n            style={showSpecificService.color && { color: \"var(--red)\" }}\n          >\n            {showSpecificService.title}\n          </Label>\n        )\n      }\n      filter={\n        showSpecificService && {\n          name: showSpecificService.serviceName,\n        }\n      }\n      orderBy={{ key: \"label\" }}\n      tableName={\"services\"}\n      methods={dbsMethods}\n      tables={dbsTables}\n      showTopBar={false}\n      realtime={true}\n      showEdit={false}\n      fieldConfigs={servicesFieldConfigs}\n    />\n  );\n};\n\nconst useServicesFieldConfigs = ({\n  dbs,\n  dbsMethods,\n  showSpecificService,\n}: Pick<P, \"dbsMethods\" | \"dbs\" | \"showSpecificService\">) => {\n  const { toggleService } = dbsMethods;\n  const { onErrorAlert } = useOnErrorAlert();\n  const servicesFieldConfigs = useMemo(() => {\n    return [\n      {\n        name: \"icon\",\n        hide: true,\n      },\n      {\n        name: \"label\",\n        hide: true,\n      },\n      {\n        name: \"configs\",\n        hide: true,\n      },\n      {\n        name: \"selected_config_options\",\n        hide: true,\n      },\n      {\n        name: \"name\",\n        renderMode: \"full\",\n        render: (\n          _,\n          {\n            name,\n            label,\n            icon,\n            status,\n            configs,\n            selected_config_options,\n          }: DBSSchema[\"services\"],\n        ) => {\n          const isRunning =\n            status === \"building\" ||\n            status === \"starting\" ||\n            status === \"running\";\n          return (\n            <FlexCol className=\"w-full gap-p5\">\n              <FlexRow className=\"w-full gap-0\">\n                <SvgIcon className=\"f-0\" icon={icon} size={24} />\n                <Label variant=\"header\" className=\"ta-left mx-1\">\n                  {label}\n                </Label>\n                <StatusChip\n                  data-key=\"service-status\"\n                  color={\n                    status === \"running\" ? \"green\"\n                    : status === \"error\" || status === \"build-error\" ?\n                      \"red\"\n                    : status === \"stopped\" ?\n                      \"gray\"\n                    : \"yellow\"\n                  }\n                  text={status}\n                />\n                {toggleService && (\n                  <SwitchToggle\n                    className=\"ml-auto\"\n                    data-key=\"service-toggle\"\n                    title={isRunning ? \"Stop service\" : \"Start service\"}\n                    checked={isRunning}\n                    onChange={() =>\n                      void onErrorAlert(async () => {\n                        await toggleService(name, !isRunning);\n                      })\n                    }\n                  />\n                )}\n              </FlexRow>\n              <FlexRow>\n                {Object.entries(configs ?? {}).map(([configKey, config]) => {\n                  return (\n                    <Select\n                      key={configKey}\n                      title={config.label || configKey}\n                      data-key={configKey}\n                      btnProps={{\n                        size: \"small\",\n                      }}\n                      disabledInfo={\n                        isRunning ?\n                          \"Stop the service to change this setting.\"\n                        : \"\"\n                      }\n                      value={\n                        selected_config_options?.[configKey] ||\n                        config.defaultOption\n                      }\n                      fullOptions={getEntries(config.options).map(\n                        ([optionKey, option]) => ({\n                          key: optionKey,\n                          label: option.label ?? optionKey,\n                        }),\n                      )}\n                      onChange={(configValue) => {\n                        void dbs.services.update(\n                          {\n                            name,\n                          },\n                          {\n                            selected_config_options: {\n                              $merge: [{ [configKey]: configValue }],\n                            },\n                          },\n                        );\n                      }}\n                    />\n                  );\n                })}\n              </FlexRow>\n            </FlexCol>\n          );\n        },\n      },\n      {\n        name: \"status\",\n        hide: true,\n      },\n      {\n        name: \"description\",\n        hide: !!showSpecificService,\n      },\n      {\n        name: \"default_port\",\n        hide: !!showSpecificService,\n      },\n      {\n        name: \"logs\",\n        hideIf: (v, { status }) =>\n          !v ||\n          Boolean(\n            showSpecificService &&\n            (status === \"running\" || status === \"stopped\"),\n          ),\n        renderMode: \"full\",\n        render: (_, { logs }) => (\n          <FlexCol\n            className=\"relative min-w-full f-1\"\n            style={{\n              maxWidth: showSpecificService ? \"200px\" : undefined,\n            }}\n          >\n            <MonacoLogRenderer logs={logs || \"\"} label=\"Logs\" />\n          </FlexCol>\n        ),\n      },\n    ] satisfies FieldConfig<DBSSchema[\"services\"]>[];\n  }, [dbs.services, onErrorAlert, showSpecificService, toggleService]);\n\n  return { servicesFieldConfigs };\n};\n"
  },
  {
    "path": "client/src/pages/ServerSettings/useEditableData.ts",
    "content": "import { isEqual } from \"prostgles-types\";\nimport { useCallback, useMemo, useState } from \"react\";\n\nexport const useEditableData = <T extends Record<string, any> | undefined>(\n  initialData: T,\n) => {\n  const [error, setError] = useState<any>();\n  const [editedData, setEditedData] = useState<T | undefined>(initialData);\n  const value = editedData ?? initialData;\n\n  const didChange = useMemo(() => {\n    return editedData && !isEqual(editedData, initialData);\n  }, [editedData, initialData]);\n\n  const onSave = useCallback(\n    (onSavePromise: () => Promise<void>) => {\n      return onSavePromise()\n        .then(() => {\n          setEditedData(undefined);\n          setError(undefined);\n        })\n        .catch((err) => {\n          setError(err);\n          throw err;\n        });\n    },\n    [setError, setEditedData],\n  );\n\n  const setValue = useCallback(\n    <K extends keyof NonNullable<T>>(\n      newData: Pick<NonNullable<T>, K> | undefined,\n    ) => {\n      if (!value) return setError(\"Value is undefined. Must pass full data\");\n\n      if (newData === undefined) {\n        setEditedData(undefined);\n      } else {\n        setEditedData({ ...value, ...newData });\n      }\n      setError(undefined);\n    },\n    [value],\n  );\n\n  return {\n    value,\n    originalValue: initialData,\n    didChange,\n    onSave: didChange ? onSave : undefined,\n    setValue,\n    error,\n    setError,\n  };\n};\n"
  },
  {
    "path": "client/src/pages/TopControls.tsx",
    "content": "import { mdiArrowLeft, mdiDatabaseCog } from \"@mdi/js\";\nimport React from \"react\";\nimport type { Prgl } from \"../App\";\nimport { dataCommand } from \"../Testing\";\nimport type { BtnProps } from \"@components/Btn\";\nimport Btn from \"@components/Btn\";\nimport { FlexRow } from \"@components/Flex\";\nimport { ConnectionSelector } from \"../dashboard/ConnectionSelector\";\nimport { getIsPinnedMenu } from \"../dashboard/Dashboard/Dashboard\";\nimport type {\n  LoadedSuggestions,\n  WorkspaceSyncItem,\n} from \"../dashboard/Dashboard/dashboardUtils\";\nimport { Feedback } from \"../dashboard/Feedback\";\nimport { WorkspaceMenu } from \"../dashboard/WorkspaceMenu/WorkspaceMenu\";\nimport { AppVideoDemo } from \"../demo/AppVideoDemo\";\nimport { Alerts } from \"./Alerts\";\nimport type {\n  Connections,\n  FullExtraProps,\n} from \"./ProjectConnection/ProjectConnection\";\nimport { AskLLM } from \"../dashboard/AskLLM/AskLLM\";\nimport { ROUTES } from \"@common/utils\";\nimport { t } from \"../i18n/i18nUtils\";\n\ntype TopControlsProps = {\n  prgl: Prgl;\n  loadedSuggestions: LoadedSuggestions | undefined;\n} & (\n  | {\n      location: \"workspace\";\n      pinned: boolean | undefined;\n      workspace: WorkspaceSyncItem;\n      onClick: Required<BtnProps>[\"onClick\"];\n    }\n  | { location: \"config\" }\n);\nexport const TopControls = (props: TopControlsProps) => {\n  const { prgl, location, loadedSuggestions } = props;\n  const { connectionId } = prgl;\n  const menuBtnProps: DashboardMenuBtnProps<any> =\n    props.location === \"config\" ?\n      {\n        ...dataCommand(\"config.goToConnDashboard\"),\n        title: t.TopControls[\"Go to workspace\"],\n        asNavLink: true,\n        href: `${ROUTES.CONNECTIONS}/${connectionId}`,\n      }\n    : {\n        ...dataCommand(\"dashboard.menu\"),\n        className: getIsPinnedMenu(props.workspace) ? t.TopControls.pinned : \"\",\n        disabledInfo:\n          getIsPinnedMenu(props.workspace) ?\n            t.TopControls[\"Menu is pinned\"]\n          : undefined,\n        onClick: props.onClick,\n      };\n\n  const wrapIfNeeded = (content: React.ReactNode) => {\n    if (props.location !== \"config\") return content;\n    return <FlexRow className=\"gap-0 max-w-1200 f-1\">{content}</FlexRow>;\n  };\n\n  const paddingClass = window.isMobileDevice ? \" p-p25 \" : \" p-p5 \";\n  return (\n    <FlexRow\n      className={`TopControls w-full bg-color-0 jc-center shadow`}\n      style={{ zIndex: 1 }}\n    >\n      {wrapIfNeeded(\n        <>\n          <FlexRow className={`max-w-fit f-1 ai-center ${paddingClass}`}>\n            <DashboardMenuBtn {...menuBtnProps} />\n            <ConnectionConfigBtn {...prgl} location={location} />\n            {!window.isMobileDevice && (\n              <ConnectionSelector {...prgl} location={location} />\n            )}\n          </FlexRow>\n\n          {props.location === \"workspace\" && <WorkspaceMenu {...props} />}\n          <FlexRow\n            className={`ml-auto min-w-0 f-0 ai-start gap-1 text-1p5 w-fit ai-center noselect o-auto no-scroll-bar jc-end ${paddingClass}`}\n            style={{\n              maxWidth: \"100%\",\n            }}\n          >\n            {location === \"workspace\" && <AppVideoDemo {...prgl} />}\n\n            {!!(prgl.dbs.alerts as any)?.subscribe && <Alerts {...prgl} />}\n\n            {prgl.dbsMethods.askLLM && (\n              <AskLLM\n                {...prgl}\n                loadedSuggestions={loadedSuggestions}\n                workspaceId={\n                  props.location === \"workspace\" ?\n                    props.workspace.id\n                  : undefined\n                }\n              />\n            )}\n            <Feedback dbsMethods={prgl.dbsMethods} dbs={prgl.dbs} />\n\n            <Btn\n              data-command=\"dashboard.goToConnections\"\n              title={t.TopControls[\"Go to Connections\"]}\n              href={ROUTES.CONNECTIONS}\n              variant=\"faded\"\n              asNavLink={true}\n              iconPath={mdiArrowLeft}\n            >\n              {/* {window.isMediumWidthScreen ? null : t.TopControls.Connections} */}\n            </Btn>\n          </FlexRow>\n        </>,\n      )}\n    </FlexRow>\n  );\n};\n\ntype ConnectionConfigBtnProps = Pick<FullExtraProps, \"user\"> & {\n  connection: Connections;\n  location: \"workspace\" | \"config\";\n};\nexport const ConnectionConfigBtn = ({\n  user,\n  connection,\n  location,\n}: ConnectionConfigBtnProps) => {\n  const isAdmin = user?.type === \"admin\";\n  if (!isAdmin) return null;\n  const isOnWorkspace = location === \"workspace\";\n  return (\n    <div className=\"h-full flex-col gap-p25 ai-start \">\n      <Btn\n        title={\n          isOnWorkspace ?\n            t.TopControls[\"Configure database connection\"]\n          : t.TopControls[\"Go back to connection workspace\"]\n        }\n        {...dataCommand(\"dashboard.goToConnConfig\")}\n        variant=\"faded\"\n        color={isOnWorkspace ? undefined : \"action\"}\n        className=\"ConnectionConfigBtn\"\n        iconPath={mdiDatabaseCog}\n        href={\n          isOnWorkspace ?\n            `${ROUTES.CONFIG}/${connection.id}`\n          : `${ROUTES.CONNECTIONS}/${connection.id}`\n        }\n        asNavLink={true}\n        children={\n          isOnWorkspace ? null : t.TopControls[\"Connection configuration\"]\n        }\n      />\n    </div>\n  );\n};\n\ntype DashboardMenuBtnProps<HREF extends string | void> = BtnProps<HREF>;\nexport const DashboardMenuBtn = <HREF extends string | void>({\n  ...props\n}: DashboardMenuBtnProps<HREF>) => {\n  return (\n    <Btn\n      title=\"Menu\"\n      {...dataCommand(\"dashboard.menu\")}\n      {...props}\n      id=\"dashboard-menu-button\"\n      style={{\n        borderRadius: \"4px\",\n        padding: \"2px\",\n        borderColor: \"#3ad8e3\",\n        ...props.style,\n      }}\n    >\n      <img src=\"/prostgles-logo.svg\" />\n    </Btn>\n  );\n};\n"
  },
  {
    "path": "client/src/pages/projectUtils.ts",
    "content": ""
  },
  {
    "path": "client/src/theme/ThemeSelector.tsx",
    "content": "import { mdiThemeLightDark } from \"@mdi/js\";\nimport React from \"react\";\nimport type { ExtraProps } from \"../App\";\nimport { Select } from \"@components/Select/Select\";\nimport { t } from \"../i18n/i18nUtils\";\n\nexport type ThemeOption = \"light\" | \"dark\" | \"from-system\";\ntype P = Pick<ExtraProps, \"dbs\"> & {\n  userThemeOption: ThemeOption;\n  serverState: undefined | ExtraProps[\"serverState\"];\n  userId: string | undefined;\n};\nexport const ThemeSelector = ({ dbs, userId, userThemeOption }: P) => {\n  return (\n    <Select\n      title={t.common.Theme}\n      btnProps={{\n        variant: \"default\",\n        iconPath: mdiThemeLightDark,\n        children: \"\",\n      }}\n      data-command=\"App.colorScheme\"\n      value={userThemeOption}\n      fullOptions={[\n        { key: \"light\", label: \"Light\" },\n        { key: \"dark\", label: \"Dark\" },\n        { key: \"from-system\", label: \"System\" },\n      ]}\n      onChange={(theme) => {\n        if (!userId) return;\n\n        dbs.users.update({ id: userId }, { options: { $merge: [{ theme }] } });\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/theme/useAppTheme.ts",
    "content": "import { useEffect } from \"react\";\nimport { appTheme, type AppState, type Theme } from \"../App\";\nimport { useLocalSettings } from \"../dashboard/localSettings\";\nimport { useSystemTheme } from \"./useSystemTheme\";\n\nconst THEMES = [\"light\", \"dark\", \"from-system\"] as const;\nconst THEME_SETTING_NAME = \"theme\" as const;\n\nexport const useAppTheme = (state: Pick<AppState, \"serverState\" | \"user\">) => {\n  const userThemeOption = state.user?.options?.theme;\n\n  const { themeOverride } = useLocalSettings();\n  const systemTheme = useSystemTheme();\n  const userTheme = getTheme(userThemeOption);\n  const theme =\n    themeOverride ?? (userTheme === \"from-system\" ? systemTheme : userTheme);\n\n  useEffect(() => {\n    appTheme.set(theme);\n    if (!userThemeOption || userThemeOption === \"from-system\") {\n      localStorage.removeItem(THEME_SETTING_NAME);\n      return;\n    }\n    /** We persist the theme to localSettings to ensure theme persists after logging out */\n    localStorage.setItem(THEME_SETTING_NAME, userThemeOption);\n  }, [theme, userThemeOption]);\n\n  useEffect(() => {\n    document.documentElement.classList.remove(\"dark-theme\", \"light-theme\");\n    document.documentElement.classList.add(`${theme}-theme`);\n    document.body.classList.add(\"text-0\");\n    document.body.classList.toggle(\"bg-color-2\", theme === \"light\");\n    document.body.classList.toggle(\"bg-color-3\", theme === \"dark\");\n  }, [theme, state.serverState]);\n\n  return { theme, userThemeOption: userThemeOption ?? \"from-system\" };\n};\n\nconst getTheme = (_desired: Theme | \"from-system\" | undefined) => {\n  let savedTheme = _desired;\n  if (!_desired) {\n    const savedThemeSetting = localStorage.getItem(THEME_SETTING_NAME);\n    savedTheme = THEMES.find((t) => t === savedThemeSetting);\n  }\n  const desired = savedTheme ?? \"from-system\";\n  return desired;\n};\n"
  },
  {
    "path": "client/src/theme/useSystemTheme.ts",
    "content": "import { useEffect, useState } from \"react\";\n\nexport const useSystemTheme = () => {\n  const [systemTheme, setSystemTheme] = useState(getTheme());\n  useEffect(() => {\n    const listener = () => {\n      setSystemTheme(getTheme());\n    };\n\n    const matcher = window.matchMedia(\"(prefers-color-scheme: dark)\");\n    matcher.addEventListener(\"change\", listener);\n\n    return () => matcher.removeEventListener(\"change\", listener);\n  }, []);\n\n  return systemTheme;\n};\n\nconst getTheme = (): \"dark\" | \"light\" => {\n  return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ?\n      \"dark\"\n    : \"light\";\n};\n"
  },
  {
    "path": "client/src/useAppState/dbsConnectionOptions.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\n\nexport const dbsConnectionOptions = {\n  table_options: {\n    alerts: {\n      label: \"Alerts\",\n      icon: \"BellBadgeOutline\",\n    },\n    users: {\n      label: \"Users\",\n      icon: \"Account\",\n    },\n    backups: {\n      label: \"Backups\",\n      icon: \"DatabaseSync\",\n    },\n    sessions: {\n      label: \"Sessions\",\n      icon: \"Laptop\",\n    },\n    llm_chats_allowed_mcp_tools: {\n      label: \"Allowed MCP Tools\",\n      icon: \"Tools\",\n    },\n    llm_chats_allowed_functions: {\n      label: \"Allowed Functions\",\n      icon: \"LanguageTypescript\",\n    },\n    mcp_server_tools: {\n      label: \"MCP Tools\",\n      icon: \"Tools\",\n      card: {\n        headerColumn: \"name\",\n      },\n    },\n    mcp_server_tool_calls: {\n      label: \"MCP Tool Calls\",\n      icon: \"WrenchClock\",\n    },\n    llm_chats: {\n      label: \"LLM Chats\",\n      icon: \"Assistant\",\n      columns: {\n        db_schema_permissions: {\n          icon: \"DatabaseEye\",\n        },\n        db_data_permissions: {\n          icon: \"TableEye\",\n        },\n      },\n    },\n    llm_models: {\n      label: \"LLM Models\",\n      icon: \"Atom\",\n    },\n    llm_providers: {\n      label: \"LLM Providers\",\n      icon: \"CloudKeyOutline\",\n      rowIconColumn: \"logo_url\",\n    },\n    llm_prompts: {\n      label: \"LLM Prompts\",\n      icon: \"MessageCogOutline\",\n    },\n    workspaces: {\n      label: \"Workspaces\",\n      icon: \"ViewGrid\",\n    },\n    connections: {\n      label: \"Connections\",\n      icon: \"Database\",\n    },\n    magic_links: {\n      label: \"Magic Links\",\n      icon: \"Link\",\n    },\n    llm_messages: {\n      label: \"LLM Messages\",\n      icon: \"MessageReplyTextOutline\",\n    },\n    access_control: {\n      label: \"Access Control\",\n      icon: \"AccountMultiple\",\n    },\n    global_settings: {\n      label: \"Global Settings\",\n      icon: \"ServerSecurity\",\n    },\n    published_methods: {\n      label: \"Published Methods\",\n      icon: \"LanguageTypescript\",\n    },\n    login_attempts: {\n      icon: \"LockCheck\",\n      label: \"Login Attempts\",\n    },\n    mcp_servers: {\n      icon: \"Server\",\n      label: \"MCP Servers\",\n    },\n  } satisfies Partial<{\n    [tableKey in keyof DBSSchema]: {\n      icon: string;\n      rowIconColumn?: string;\n      label: string;\n      card?: {\n        headerColumn?: string;\n      };\n      columns?: Partial<Record<keyof DBSSchema[tableKey], { icon?: string }>>;\n    };\n  }>, //satisfies DBSSchema[\"connections\"][\"table_options\"]\n};\n"
  },
  {
    "path": "client/src/useAppState/useAppState.ts",
    "content": "import { type DBHandlerClient, useAsyncEffectQueue } from \"prostgles-client\";\nimport { includes } from \"prostgles-types\";\nimport { useMemo, useState } from \"react\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { AppState } from \"../App\";\nimport type { DBS, DBSMethods } from \"../dashboard/Dashboard/DBS\";\nimport { getTables } from \"../dashboard/Dashboard/getTables\";\nimport { dbsConnectionOptions } from \"./dbsConnectionOptions\";\nimport { useDBSClient } from \"./useDBSClient\";\nimport { useServerState } from \"./useServerState\";\n\nexport const useAppState = (\n  onDisconnect: (isDisconnected: boolean) => void,\n) => {\n  const serverState = useServerState();\n  const dbsClient = useDBSClient(onDisconnect, serverState);\n  const [user, setUser] = useState<DBSSchema[\"users\"]>();\n\n  const prglStateWaiting = dbsClient.hasError || dbsClient.isLoading;\n  const prglState: AppState[\"prglState\"] = useMemo(() => {\n    if (prglStateWaiting) return;\n    const { dbo: dbs, methods, auth, tableSchema, socket } = dbsClient;\n\n    const { tables: dbsTables = [] } = getTables(\n      tableSchema ?? [],\n      dbsConnectionOptions.table_options,\n      dbs as DBHandlerClient,\n      true,\n    );\n    (window as any).dbs = dbs;\n    (window as any).dbsSocket = socket;\n    (window as any).dbsMethods = methods;\n    (window as any).auth = auth;\n    return {\n      dbs: dbs as DBS,\n      dbsMethods: methods as DBSMethods,\n      dbsTables,\n      auth,\n      isAdminOrSupport: includes([\"admin\", \"support\"], auth.user?.type),\n      dbsSocket: socket,\n      sid: auth.user?.sid,\n      dbsKey: Date.now() + \"\",\n    };\n  }, [dbsClient, prglStateWaiting]);\n\n  const { dbs, auth } = prglState ?? {};\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useAsyncEffectQueue(async () => {\n    if (!dbs || !auth?.user?.id) return;\n\n    const userSub = await dbs.users.subscribeOne(\n      { id: auth.user.id },\n      {},\n      (user) => {\n        setUser(user);\n      },\n    );\n\n    return userSub.unsubscribe;\n  }, [dbs, auth]);\n\n  const dbsClientError =\n    dbsClient.hasError ? dbsClient.error || \"Unknown error\" : undefined;\n\n  if (dbsClientError) {\n    return {\n      state: \"error\" as const,\n      dbsClientError,\n      prglState: undefined,\n      user: undefined,\n      serverState,\n    };\n  }\n\n  return {\n    state: prglStateWaiting ? (\"loading\" as const) : (\"ok\" as const),\n    dbsClientError: undefined,\n    prglState,\n    user,\n    serverState,\n  };\n};\n"
  },
  {
    "path": "client/src/useAppState/useDBSClient.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { ProstglesState } from \"@common/electronInitTypes\";\nimport { API_ENDPOINTS, ROUTES } from \"@common/utils\";\nimport { pageReload } from \"@components/Loader/Loading\";\nimport {\n  useProstglesClient,\n  type UseProstglesClientProps,\n} from \"prostgles-client/dist/prostgles\";\nimport { useEffect, useMemo } from \"react\";\nimport type { ClientUser } from \"../App\";\nimport { isPlaywrightTest } from \"../i18n/i18nUtils\";\nimport { playwrightTestLogs } from \"../utils/utils\";\n\nexport const useDBSClient = (\n  onDisconnect: (isDisconnected: boolean) => void,\n  serverState: ProstglesState | undefined,\n) => {\n  const clientProps = useMemo(() => {\n    if (serverState?.initState.state !== \"ok\")\n      return { skip: true } satisfies UseProstglesClientProps;\n    const clientProps: UseProstglesClientProps = {\n      socketOptions: {\n        transports: [\"websocket\"],\n        path: API_ENDPOINTS.WS_DBS,\n        reconnection: true,\n        reconnectionDelay: 2000,\n        reconnectionAttempts: 5,\n      },\n      onDisconnect: () => {\n        onDisconnect(true);\n      },\n      onDebug: !isPlaywrightTest ? undefined : playwrightTestLogs,\n      onReconnect: () => {\n        onDisconnect(false);\n        if (window.location.pathname.startsWith(ROUTES.CONNECTIONS + \"/\")) {\n          void pageReload(\"sync reconnect bug\");\n        }\n      },\n    };\n    return clientProps;\n  }, [onDisconnect, serverState?.initState.state]);\n\n  const dbsClient = useProstglesClient<DBGeneratedSchema, ClientUser>(\n    clientProps,\n  );\n\n  const socket =\n    !dbsClient.hasError && !dbsClient.isLoading && dbsClient.socket;\n\n  useEffect(() => {\n    if (!socket) return;\n\n    socket.on(\"infolog\", console.log);\n    socket.on(\"server-restart-request\", (_sure) => {\n      setTimeout(() => {\n        void pageReload(\"server-restart-request\");\n      }, 2000);\n    });\n    socket.on(\"redirect\", (newLocation) => {\n      if (typeof newLocation !== \"string\") return;\n      window.location.href = newLocation;\n    });\n  }, [socket]);\n\n  return dbsClient;\n};\n"
  },
  {
    "path": "client/src/useAppState/useServerState.ts",
    "content": "import { usePromise } from \"prostgles-client\";\nimport { includes } from \"prostgles-types\";\nimport type { ProstglesState } from \"@common/electronInitTypes\";\nimport { SPOOF_TEST_VALUE } from \"@common/utils\";\nimport type { AppState } from \"../App\";\nimport { tout } from \"../utils/utils\";\nimport { MOCK_ELECTRON_WINDOW_ATTR } from \"src/Testing\";\n\n/**\n * Check if state database is setup\n */\nconst fetchServerState = async () => {\n  // window.MOCK_ELECTRON_WINDOW_ATTR = true;\n  const serverState: AppState[\"serverState\"] =\n    window[MOCK_ELECTRON_WINDOW_ATTR] ?\n      {\n        isElectron: true,\n        initState: {\n          state: \"ok\",\n        },\n        electronCredsProvided: false,\n      }\n    : await fetch(\"/dbs\", {\n        headers: { \"x-real-ip\": SPOOF_TEST_VALUE },\n      }).then((r) => r.json());\n  return serverState;\n};\n\nexport const useServerState = () => {\n  const serverState = usePromise(async () => {\n    window.document.title = `Prostgles`;\n    try {\n      let serverState = await fetchServerState();\n      let attemptsLeft = 3;\n      while (\n        attemptsLeft &&\n        !includes([\"error\", \"ok\"], serverState?.initState.state)\n      ) {\n        attemptsLeft--;\n        /** Maybe loading, try again */\n        console.warn(\"Prostgles could not connect. Retrying in 1 second\");\n        await tout(1000);\n        serverState = await fetchServerState();\n      }\n\n      window.document.title = `Prostgles ${serverState?.isElectron ? \"Desktop\" : \"UI\"}`;\n      return serverState;\n    } catch (initError) {\n      console.error(initError);\n      return {\n        initState: {\n          state: \"error\",\n          error: initError as Error,\n          errorType: \"init\",\n        },\n        isElectron: false,\n      } satisfies ProstglesState;\n    }\n  });\n\n  return serverState;\n};\n"
  },
  {
    "path": "client/src/utils/colorUtils.ts",
    "content": "export const asHex = (v: string) => {\n  if (v.startsWith(\"#\")) return v;\n\n  const [r, g, b] = asRGB(v);\n  return rgbToHex(r, g, b);\n};\n\nexport const rgbToHex = (r, g, b) =>\n  \"#\" +\n  [r, g, b]\n    .map((x) => {\n      const hex = x.toString(16);\n      return hex.length === 1 ? \"0\" + hex : hex;\n    })\n    .join(\"\");\n\nexport const asRGB = (color: string, maxOpacity?: \"1\" | \"255\"): RGBA => {\n  if (color.toLowerCase().trim().startsWith(\"rgb\")) {\n    const rgba = color\n      .trim()\n      .split(\"(\")[1]\n      ?.split(\")\")[0]\n      ?.split(\",\")\n      .map((v) => +v.trim());\n    if ((rgba?.length ?? 0) >= 3 && rgba?.every((v) => Number.isFinite(v))) {\n      let opacity = rgba[3] || 1;\n      if (maxOpacity === \"255\" && opacity <= 1) {\n        opacity = Math.max(1, Math.floor((1 / opacity) * 255));\n      }\n      const rgb = rgba.slice(0, 3) as [number, number, number];\n      return [...rgb, opacity] as RGBA;\n    }\n    return [100, 100, 100, maxOpacity === \"255\" ? 255 : 1];\n  }\n\n  const r = parseInt(color.substr(1, 2), 16);\n  const g = parseInt(color.substr(3, 2), 16);\n  const b = parseInt(color.substr(5, 2), 16);\n  let a = parseInt(color.substr(7, 2), 16) || 1;\n\n  if (maxOpacity === \"255\" && a <= 1) {\n    a = Math.max(1, Math.floor((1 / a) * 255));\n  }\n  return [r, g, b, a];\n};\n\nexport type RGBA = [number, number, number, number];\n"
  },
  {
    "path": "client/src/utils/hashCode.ts",
    "content": "// MurmurHash3 (32-bit) — production-grade, handles large inputs safely\nexport const hashCode = (str: string, seed = 0) => {\n  let h = seed >>> 0;\n  let k,\n    i = 0;\n  const len = str.length;\n  const remainder = len & 3;\n  const bytes = len - remainder;\n\n  while (i < bytes) {\n    k =\n      (str.charCodeAt(i) & 0xff) |\n      ((str.charCodeAt(i + 1) & 0xff) << 8) |\n      ((str.charCodeAt(i + 2) & 0xff) << 16) |\n      ((str.charCodeAt(i + 3) & 0xff) << 24);\n\n    k = Math.imul(k, 0xcc9e2d51);\n    k = (k << 15) | (k >>> 17);\n    k = Math.imul(k, 0x1b873593);\n\n    h ^= k;\n    h = (h << 13) | (h >>> 19);\n    h = (Math.imul(h, 5) + 0xe6546b64) >>> 0;\n\n    i += 4;\n  }\n\n  k = 0;\n  switch (remainder) {\n    case 3:\n      k ^= (str.charCodeAt(i + 2) & 0xff) << 16;\n    /* falls through */\n    case 2:\n      k ^= (str.charCodeAt(i + 1) & 0xff) << 8;\n    /* falls through */\n    case 1:\n      k ^= str.charCodeAt(i) & 0xff;\n      k = Math.imul(k, 0xcc9e2d51);\n      k = (k << 15) | (k >>> 17);\n      k = Math.imul(k, 0x1b873593);\n      h ^= k;\n  }\n\n  h ^= len;\n  h ^= h >>> 16;\n  h = Math.imul(h, 0x85ebca6b);\n  h ^= h >>> 13;\n  h = Math.imul(h, 0xc2b2ae35);\n  h ^= h >>> 16;\n\n  return h >>> 0; // unsigned 32-bit integer\n};\n"
  },
  {
    "path": "client/src/utils/utils.ts",
    "content": "import type { InitOptions } from \"prostgles-client/dist/prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { getKeys, isDefined, isEmpty, isObject } from \"prostgles-types\";\nexport { getKeys, isDefined, isEmpty };\nexport const get = (nestedObj: any, pathArr: string | (string | number)[]) => {\n  if (typeof pathArr === \"string\") pathArr = pathArr.split(\".\");\n  return pathArr.reduce(\n    (obj, key) => (obj && obj[key] !== \"undefined\" ? obj[key] : undefined),\n    nestedObj,\n  );\n};\n\n/* Get only the specified properties of an object */\nexport function filterObj<T extends AnyObject, K extends keyof T>(\n  obj: T,\n  keysToInclude: K[] = [],\n  keysToExclude: readonly (keyof T)[] = [],\n): Omit<T, (typeof keysToInclude)[number]> {\n  if (!keysToInclude.length && !keysToExclude.length) {\n    // console.warn(\"filterObj: returning empty object\");\n    return {} as T;\n  }\n  const keys = Object.keys(obj) as Array<keyof typeof obj>;\n  if (keys.length) {\n    const res: Partial<T> = {};\n    keys.forEach((k) => {\n      if (\n        (keysToInclude.length && keysToInclude.includes(k as any)) ||\n        (keysToExclude.length && !keysToExclude.includes(k))\n      ) {\n        res[k] = obj[k];\n      }\n    });\n    return res as T;\n  }\n\n  return obj;\n}\n\nexport function ifEmpty<V, R>(v: V, replaceValue: R): R | V {\n  return isEmpty(v) ? replaceValue : v;\n}\n\nexport function nFormatter(num: number, digits: number): string {\n  if (Math.abs(num) < 1) return num.toExponential(digits);\n  const lookup = [\n    { value: 1, symbol: \"\" },\n    { value: 1e3, symbol: \"k\" },\n    { value: 1e6, symbol: \"M\" },\n    { value: 1e9, symbol: \"G\" },\n    { value: 1e12, symbol: \"T\" },\n    { value: 1e15, symbol: \"P\" },\n    { value: 1e18, symbol: \"E\" },\n  ];\n  const rx = /\\.0+$|(\\.[0-9]*[1-9])0+$/;\n  const item = lookup\n    .slice()\n    .reverse()\n    .find(function (item) {\n      return Math.abs(num) >= item.value;\n    });\n  return item ?\n      (num / item.value).toFixed(digits).replace(rx, \"$1\") + item.symbol\n    : \"0\";\n}\n\ntype StrFormat = {\n  idx: number;\n  len: number;\n  val: any;\n  decimalPlaces: number;\n  type: \"c\" | \"n\" | \"s\";\n  // characted number symbol\n};\nexport function getStringFormat(s: string | undefined): StrFormat[] {\n  if (s && typeof s === \"string\") {\n    const res: StrFormat[] = [];\n    let curF: StrFormat | undefined;\n    s.split(\"\").map((c, idx) => {\n      const mt =\n        c.match(/[A-Za-z]/) ? \"c\"\n        : c.match(/[0-9.]/) ? \"n\"\n        : \"s\";\n      const _cF: StrFormat = {\n        idx,\n        len: 1,\n        val: c,\n        type: mt,\n        decimalPlaces: 0,\n      };\n      if (curF) {\n        const differentFormat = curF.type !== mt;\n        const numberWithEnoughDots =\n          !differentFormat && c === \".\" && (curF.val as string).includes(\".\");\n        if (differentFormat || numberWithEnoughDots) {\n          res.push(curF);\n          if (numberWithEnoughDots) {\n            _cF.type === \"s\";\n            res.push(_cF);\n            curF = undefined;\n          } else {\n            curF = _cF;\n          }\n        } else {\n          curF.len = idx - curF.idx + 1;\n          curF.val += c;\n        }\n      } else {\n        curF = _cF;\n      }\n\n      if (curF && idx === s.length - 1) {\n        curF.len = idx - curF.idx + 1;\n        res.push(curF);\n      }\n    });\n\n    return res.map((r) => {\n      if (r.type === \"n\" && r.val.includes(\".\")) {\n        r.decimalPlaces = r.len - 1 - r.val.indexOf(\".\");\n      }\n      return r;\n    });\n  }\n  return [];\n}\n\nexport function quickClone<T>(obj: T): T {\n  if (\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    window !== undefined &&\n    \"structuredClone\" in window &&\n    typeof window.structuredClone === \"function\"\n  ) {\n    // @ts-ignore\n    return window.structuredClone(obj);\n  }\n  if (isObject(obj)) {\n    const result = {} as T;\n    getKeys(obj).map((k) => {\n      //@ts-ignore\n      result[k] = quickClone(obj[k]);\n    });\n    return result;\n  } else if (Array.isArray(obj)) {\n    return obj.slice(0).map((v) => quickClone(v)) as T;\n  }\n  return obj;\n}\n\nconst comparableTypes = [\"number\", \"string\", \"boolean\"] as const;\nexport const areEqual = <T extends AnyObject>(\n  obj1: T,\n  obj2: T,\n  keys?: (keyof T)[],\n): boolean => {\n  if (typeof obj1 !== typeof obj2) return false;\n  if ([obj1, obj2].some((v) => comparableTypes.includes(typeof v as any))) {\n    return obj1 === obj2;\n  }\n  return !(keys ?? getKeys({ ...obj1, ...obj2 })).some(\n    (k) =>\n      typeof obj1[k] !== typeof obj2[k] ||\n      JSON.stringify(obj1[k]) !== JSON.stringify(obj2[k]),\n  );\n};\n\nexport const playwrightTestLogs: InitOptions[\"onDebug\"] = (ev) => {\n  //@ts-ignore\n  window.prostgles_logs ??= [];\n  //@ts-ignore\n  window.prostgles_logs.push({ ...ev, ts: new Date() });\n  const trackedTableNames: string[] = [];\n  if (ev.type === \"table\" && trackedTableNames.includes(ev.tableName)) {\n    // if(ev.command === \"unsubscribe\") debugger;\n    console.log(Date.now(), \"DBS client\", ev);\n  } else if (\n    ev.type === \"onReady\" ||\n    ev.type === \"onReady.call\" ||\n    ev.type === \"onReady.notMounted\"\n  ) {\n    console.log(Date.now(), \"DBS client\", ev);\n  }\n};\n\nexport const tout = (timeout: number) => {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(true);\n    }, timeout);\n  });\n};\n\nexport const scrollIntoViewIfNeeded = (\n  element: HTMLElement,\n  options?: ScrollIntoViewOptions,\n) => {\n  // @ts-ignore\n  if (element.scrollIntoViewIfNeeded) {\n    // @ts-ignore\n    element.scrollIntoViewIfNeeded(options);\n  } else {\n    // same as scrollIntoViewIfNeeded\n    element.scrollIntoView(options ?? { block: \"nearest\" });\n  }\n};\n"
  },
  {
    "path": "client/static/util.css",
    "content": ":root {\n  --gray-50: #f9fafb;\n  --gray-100: #f4f5f7;\n  --gray-200: #e5e7eb;\n  --gray-300: #d2d6dc;\n  --gray-400: #9fa6b2;\n  --gray-500: #6b7280;\n  --gray-600: #4b5563;\n  --gray-700: #40454e;\n  --gray-800: #36393e;\n  --gray-900: #161e2e;\n\n  --red-50: #fdf2f2;\n  --red-100: #fde8e8;\n  --red-200: #fbd5d5;\n  --red-300: #f8b4b4;\n  --red-400: #f98080;\n  --red-500: #f05252;\n  --red-600: #e02424;\n  --red-700: #c81e1e;\n  --red-800: #9b1c1c;\n  --red-900: #771d1d;\n  --red-1000: #360202;\n\n  --yellow-50: #fdfdea;\n  --yellow-100: #fdf6b2;\n  --yellow-200: #fce96a;\n  --yellow-300: #faca15;\n  --yellow-400: #e3a008;\n  --yellow-500: #c27803;\n  --yellow-600: #9f580a;\n  --yellow-700: #8e4b10;\n  --yellow-800: #723b13;\n  --yellow-900: #633112;\n\n  --green-50: #f3faf7;\n  --green-100: #def7ec;\n  --green-200: #bcf0da;\n  --green-300: #84e1bc;\n  --green-400: #31c48d;\n  --green-500: #0e9f6e;\n  --green-600: #057a55;\n  --green-700: #046c4e;\n  --green-800: #03543f;\n  --green-900: #014737;\n\n  --teal-400: #16bdca;\n\n  --blue-50: #ebf5ff;\n  --blue-100: #e1effe;\n  --blue-200: #c3ddfd;\n  --blue-300: #a4cafe;\n  --blue-400: #76a9fa;\n  --blue-500: #3f83f8;\n  --blue-600: #1c64f2;\n  --blue-700: #1a56db;\n  --blue-800: #1e429f;\n  --blue-900: #233876;\n\n  --indigo-50: #f0f5ff;\n  --indigo-100: #e5edff;\n  --indigo-200: #cddbfe;\n  --indigo-300: #b4c6fc;\n  --indigo-400: #8da2fb;\n  --indigo-500: #6875f5;\n  --indigo-600: #5850ec;\n  --indigo-700: #5145cd;\n  --indigo-800: #42389d;\n  --indigo-900: #362f78;\n\n  --active: #00a1ff;\n  --action: var(--blue-500);\n  --active-hover: #51bfff;\n\n  --danger: var(--red-700);\n\n  --focus-color: rgb(0, 183, 255);\n\n  --li-hover-bg: #e5e7eb69;\n  --li-focus-bg: #e5e7eb;\n\n  --color-uuid: rgb(87, 8, 63);\n  --color-number: rgb(0, 110, 0);\n  --color-text: #810909;\n  --color-boolean: #0000ff;\n  --color-date: rgb(25 91 239);\n  --color-geo: #0063bd;\n  --color-json: #0451a5;\n  --color-ticks-0: black;\n  --color-ticks-1: rgb(87, 87, 87);\n  --color-timechart-bg: white;\n\n  --text-warning: var(--yellow-700);\n  --text-danger: var(--red-700);\n  --text-default: var(--gray-600);\n  --text-action: var(--active);\n\n  --text-green: #057a55;\n\n  --b-warning: var(--yellow-400);\n  --b-danger: var(--red-700);\n  --b-default: var(--gray-200);\n  --b-action: var(--active);\n\n  --bg-warning: var(--yellow-100);\n  --bg-danger: var(--red-100);\n  --bg-default: var(--gray-100);\n  --bg-action: #00a2ff52;\n  --bg-popup: white;\n  --bg-popup-content: white;\n\n  --shadow0: rgb(32 33 36 / 28%);\n  --shadow1: rgba(0, 0, 0, 0.1);\n  --shadow2: rgba(0, 0, 0, 0.06);\n\n  --text-0: var(--gray-900);\n  --text-0p5: var(--gray-800);\n  --text-0p75: var(--gray-700);\n  --text-1: var(--gray-600);\n  --text-1p5: var(--gray-500);\n  --text-2: var(--gray-400);\n  --text-3: var(--gray-300);\n\n  --bg-color-0: white;\n  --bg-color-1: var(--gray-50);\n  --bg-color-2: var(--gray-100);\n  --bg-color-3: var(--gray-200);\n  --bg-color-4: var(--gray-300);\n\n  --b-color: var(--gray-300);\n  --b-color-0: var(--gray-200);\n  --b-color-1: var(--gray-100);\n  --b-color-2: var(--gray-50);\n\n  --bg-li-selected: var(--blue-100);\n  --formfield-bg-color: var(--bg-color-0);\n\n  --green: #009264;\n  --faded-green: #00c46e33;\n\n  --blue: var(--blue-600);\n  --faded-blue: #00b2ff33;\n\n  --red: #dc0000;\n  --faded-red: var(--red-100);\n\n  --yellow: #723b13;\n  --faded-yellow: #fff2a8;\n\n  --gray: var(--gray-600);\n  --faded-gray: var(--gray-100);\n}\n\n:root.dark-theme {\n  --li-hover-bg: #55555582;\n  --li-focus-bg: #5b5a5a;\n  --color-uuid: rgb(216, 99, 181);\n  --color-number: rgb(52, 192, 52);\n  --color-text: #fb9b9b;\n  --color-boolean: #a5a5ff;\n  --color-date: rgb(112 156 250);\n  --color-geo: #1d8ffa;\n  --color-json: #368ce8;\n  --color-ticks-0: rgb(187, 187, 187);\n  --color-ticks-1: white;\n  --color-timechart-bg: rgb(39, 39, 39);\n\n  --text-warning: #bd9500;\n  --text-danger: #eb5d5d;\n  --text-default: #c9c9c9;\n  --text-action: #00a2ff;\n  --text-green: #057a55;\n\n  --b-warning: var(--yellow-300);\n  --b-danger: var(--red-500);\n  /* var(--gray-500); */\n  --b-default: #4d4d4d;\n  --b-action: #00a2ffb0;\n\n  --bg-warning: #ae651238;\n  --bg-danger: var(--red-600);\n  --bg-default: var(--gray-600);\n  --bg-action: #0071b3;\n  --bg-popup: #2d2d2d;\n  /* --bg-popup-content: #272727;  #3b3b3b; */\n  --bg-popup-content: #2d2d2d;\n\n  /* \n  --shadow0: rgba(77, 77, 78, 0.733);\n  --shadow1: rgba(206, 200, 200, 0.658);\n  --shadow2: rgba(248, 248, 248, 0.822); */\n\n  --text-0: var(--gray-100);\n  --text-0p5: var(--gray-300);\n  --text-0p75: var(--gray-400);\n  --text-1: #b4b4b4;\n  --text-1p5: var(--gray-500);\n  --text-2: var(--gray-400);\n  --text-3: var(--gray-700);\n\n  --bg-color-0: #2c2c2c; /* rgb(69, 69, 69); */\n  --bg-color-1: #212121; /* rgb(55, 55, 55); */\n  --bg-color-2: rgb(36, 36, 36);\n  --bg-color-3: #1a1919;\n  --bg-color-4: black;\n  --b-color: var(--gray-600);\n  --b-color-0: var(--gray-600);\n  --b-color-1: var(--gray-800);\n  --b-color-2: var(--gray-900);\n\n  --bg-li-selected: #2465d463;\n  --formfield-bg-color: var(--bg-color-2);\n\n  --green: #08c489;\n  --faded-green: #00b96821;\n\n  --blue: #31c1ff;\n  --faded-blue: #00b2ff33;\n\n  --red: #e35d5d;\n  --faded-red: #fd343429;\n\n  --yellow: #dabb8c;\n  --faded-yellow: #e4ce0538;\n\n  --gray: #bfbfbf;\n  --faded-gray: #ffffff17;\n}\n\n.formfield-bg-color {\n  background-color: var(--formfield-bg-color) !important;\n}\n\n.text-0 {\n  color: var(--text-0);\n}\n.text-0p5 {\n  color: var(--text-0p5);\n}\n.text-0p75 {\n  color: var(--text-0p75);\n}\n.text-1 {\n  color: var(--text-1);\n}\n.text-1p5 {\n  color: var(--text-1p5);\n}\n.text-2 {\n  color: var(--text-2);\n}\n.text-3 {\n  color: var(--text-3);\n}\n\n.text-warning {\n  color: var(--text-warning);\n}\n.text-danger {\n  color: var(--text-danger);\n}\n.text-default {\n  color: var(--text-default);\n}\n.text-action {\n  color: var(--text-action);\n}\n.text-green {\n  color: var(--text-green);\n}\n\n.b-warning {\n  border-color: var(--b-warning);\n}\n.b-danger {\n  border-color: var(--b-danger);\n}\n.b-default {\n  border-color: var(--b-default);\n}\n.b-action {\n  border-color: var(--b-action);\n}\n\n.b-color {\n  border-color: var(--b-color);\n}\n.b-color-0 {\n  border-color: var(--b-color-0);\n}\n.b-color-1 {\n  border-color: var(--b-color-1);\n}\n.b-color-2 {\n  border-color: var(--b-color-2);\n}\n\n.bg-warning {\n  background-color: var(--bg-warning);\n}\n.bg-danger {\n  background-color: var(--bg-danger);\n}\n.bg-default {\n  background-color: var(--bg-default);\n}\n.bg-action {\n  background-color: var(--bg-action);\n}\n.bg-popup {\n  background-color: var(--bg-popup);\n}\n.bg-popup-content {\n  background-color: var(--bg-popup-content);\n}\n\n.shadow-sm {\n  box-shadow: 0 1px 2px 0 var(--shadow2);\n}\n\n.shadow {\n  box-shadow:\n    0 1px 3px 0 var(--shadow1),\n    0 1px 2px 0 var(--shadow2);\n}\n\n.drop-shadow {\n  filter: drop-shadow(0 1px 2px var(--shadow1))\n    drop-shadow(0 1px 3px var(--shadow2));\n}\n\n.shadow-l {\n  box-shadow: 0 1px 6px 0 var(--shadow0);\n}\n.shadow-xl {\n  box-shadow:\n    0 20px 25px -5px var(--shadow1),\n    0 10px 10px -5px var(--shadow2);\n}\n\n.gap-0 {\n  gap: unset;\n}\n.gap-p25 {\n  gap: 0.25em;\n}\n.gap-p5 {\n  gap: 0.5em;\n}\n.gap-p75 {\n  gap: 0.75em;\n}\n.gap-1 {\n  gap: 1em;\n}\n.gap-1p5 {\n  gap: 1.5em;\n}\n.gap-2 {\n  gap: 2em;\n}\n\n.py-p75 {\n  padding-top: 0.75rem;\n  padding-bottom: 0.75rem;\n}\n\n.leading-5 {\n  line-height: 1.25rem;\n}\n\n.py-p25 {\n  padding-top: 0.25rem;\n  padding-bottom: 0.25rem;\n}\n\n.py-p5 {\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n}\n.py-p75 {\n  padding-top: 0.75rem;\n  padding-bottom: 0.75rem;\n}\n.py-1 {\n  padding-top: 1rem;\n  padding-bottom: 1rem;\n}\n.py-2 {\n  padding-top: 2rem;\n  padding-bottom: 2rem;\n}\n.py-3 {\n  padding-top: 3rem;\n  padding-bottom: 3rem;\n}\n\n.px-p25 {\n  padding-left: 0.25rem;\n  padding-right: 0.25rem;\n}\n.px-p5 {\n  padding-left: 0.5rem;\n  padding-right: 0.5rem;\n}\n.px-p75 {\n  padding-left: 0.75rem;\n  padding-right: 0.75rem;\n}\n.px-1 {\n  padding-left: 1rem;\n  padding-right: 1rem;\n}\n.px-1p5 {\n  padding-left: 1.5rem;\n  padding-right: 1.5rem;\n}\n.px-2 {\n  padding-left: 2rem;\n  padding-right: 2rem;\n}\n\n.p-0 {\n  padding: 0;\n}\n.p-1 {\n  padding: 1em;\n}\n.p-2 {\n  padding: 2em;\n}\n.p-3 {\n  padding: 3em;\n}\n.p-4 {\n  padding: 4em;\n}\n\n.pb-0 {\n  padding-bottom: 0 !important;\n}\n.pb-p25 {\n  padding-bottom: 0.25em;\n}\n.pb-p5 {\n  padding-bottom: 0.5em;\n}\n.pb-p75 {\n  padding-bottom: 0.75em;\n}\n.pb-1 {\n  padding-bottom: 1em;\n}\n.pb-2 {\n  padding-bottom: 2em;\n}\n.pb-3 {\n  padding-bottom: 3em;\n}\n.pb-4 {\n  padding-bottom: 4em;\n}\n\n.p-p25 {\n  padding: 0.25em;\n}\n.p-p5 {\n  padding: 0.5em;\n}\n.p-p75 {\n  padding: 0.75em;\n}\n.pr-2 {\n  padding-right: 2em;\n}\n\n.m-auto {\n  margin: auto;\n}\n.m-0 {\n  margin: 0;\n}\n.m-1 {\n  margin: 1em;\n}\n.m-2 {\n  margin: 2em;\n}\n.m-3 {\n  margin: 3em;\n}\n.m-4 {\n  margin: 4em;\n}\n\n.my-auto {\n  margin-top: auto;\n  margin-bottom: auto;\n}\n.my-p25 {\n  margin-top: 0.25em;\n  margin-bottom: 0.25em;\n}\n.my-p5 {\n  margin-top: 0.5em;\n  margin-bottom: 0.5em;\n}\n.my-1 {\n  margin-top: 1em;\n  margin-bottom: 1em;\n}\n.my-2 {\n  margin-top: 2em;\n  margin-bottom: 2em;\n}\n\n.mx-auto {\n  margin-left: auto;\n  margin-right: auto;\n}\n.mx-0 {\n  margin-left: 0;\n  margin-right: 0;\n}\n.mx-p25 {\n  margin-left: 0.25em;\n  margin-right: 0.25em;\n}\n.mx-p5 {\n  margin-left: 0.5em;\n  margin-right: 0.5em;\n}\n.mx-1 {\n  margin-left: 1em;\n  margin-right: 1em;\n}\n.mx-2 {\n  margin-left: 2em;\n  margin-right: 2em;\n}\n\n.px-p25 {\n  padding-left: 0.25em;\n  padding-right: 0.25em;\n}\n.px-p5 {\n  padding-left: 0.5em;\n  padding-right: 0.5em;\n}\n.px-1 {\n  padding-left: 1em;\n  padding-right: 1em;\n}\n.px-2 {\n  padding-left: 2em;\n  padding-right: 2em;\n}\n\n.py-p25 {\n  padding-top: 0.25em;\n  padding-bottom: 0.25em;\n}\n.py-p5 {\n  padding-top: 0.5em;\n  padding-bottom: 0.5em;\n}\n.py-1 {\n  padding-top: 1em;\n  padding-bottom: 1em;\n}\n.py-2 {\n  padding-top: 2em;\n  padding-bottom: 2em;\n}\n\n.m-p25 {\n  margin: 0.25em;\n}\n.m-p5 {\n  margin: 0.5em;\n}\n.m-p75 {\n  margin: 0.75em;\n}\n\n.mt-auto {\n  margin-top: auto;\n}\n\n.mt-0 {\n  margin-top: 0;\n}\n\n.mt-p25 {\n  margin-top: 0.25em;\n}\n\n.mt-p5 {\n  margin-top: 0.5em;\n}\n\n.mt-1 {\n  margin-top: 1em;\n}\n.mt-2 {\n  margin-top: 2em;\n}\n.mt-3 {\n  margin-top: 3em;\n}\n.mt-4 {\n  margin-top: 4em;\n}\n\n.ml-auto {\n  margin-left: auto;\n}\n.ml-0 {\n  margin-left: 0;\n}\n\n.ml-1 {\n  margin-left: 1em;\n}\n.ml-1p5 {\n  margin-left: 1.5em;\n}\n.ml-2 {\n  margin-left: 2em;\n}\n.ml-3 {\n  margin-left: 3em;\n}\n.ml-4 {\n  margin-left: 4em;\n}\n\n.ml-p25 {\n  margin-left: 0.25em;\n}\n.ml-p5 {\n  margin-left: 0.5em;\n}\n\n.mr-p25 {\n  margin-right: 0.25em;\n}\n.mr-p5 {\n  margin-right: 0.5em;\n}\n.mr-1 {\n  margin-right: 1em;\n}\n.mr-2 {\n  margin-right: 2em;\n}\n.mr-3 {\n  margin-right: 3em;\n}\n.mr-auto {\n  margin-right: auto;\n}\n\n.mb-p25 {\n  margin-bottom: 0.25em;\n}\n.mb-p5 {\n  margin-bottom: 0.5em;\n}\n.mb-0 {\n  margin-bottom: 0;\n}\n.mb-1 {\n  margin-bottom: 1em;\n}\n.mb-2 {\n  margin-bottom: 2em;\n}\n.mb-3 {\n  margin-bottom: 3em;\n}\n.mb-auto {\n  margin-bottom: auto;\n}\n\n.flex-row {\n  display: flex;\n  flex-direction: row;\n}\n\n.flex-row-reverse {\n  display: flex;\n  flex-direction: row-reverse;\n}\n\n.flex-col,\n.flex-column {\n  display: flex;\n  flex-direction: column;\n}\n\n.flex-row-wrap {\n  display: flex;\n  flex-flow: row wrap;\n}\n.flex-col-wrap {\n  display: flex;\n  flex-flow: column wrap;\n}\n\n.f-0 {\n  flex: none;\n}\n.fs-1 {\n  flex-shrink: 1;\n}\n\n.f-1 {\n  flex: 1;\n  min-width: 0;\n}\n\n.fs-1 {\n  flex-shrink: 1;\n  min-width: 0;\n}\n\n.jc-center {\n  justify-content: center;\n}\n\n.ai-base {\n  align-items: baseline;\n}\n.ai-center {\n  align-items: center;\n}\n.ai-start {\n  align-items: start;\n}\n.ai-end {\n  align-items: end;\n}\n.ai-flex-start {\n  align-items: flex-start;\n}\n.ai-flex-end {\n  align-items: flex-end;\n}\n\n.as-center {\n  align-self: center;\n}\n.as-start {\n  align-self: start;\n}\n.as-end {\n  align-self: end;\n}\n.as-flex-start {\n  align-self: flex-start;\n}\n.as-flex-end {\n  align-self: flex-end;\n}\n\n.round {\n  border-radius: 1000%;\n}\n\n.rounded-md {\n  border-radius: 0.375rem;\n  border-top-left-radius: 0.375rem;\n  border-top-right-radius: 0.375rem;\n  border-bottom-right-radius: 0.375rem;\n  border-bottom-left-radius: 0.375rem;\n}\n\n:root {\n  --rounded: 0.5em;\n}\n\n.rounded {\n  border-radius: var(--rounded);\n}\n\n.rounded-r {\n  border-top-right-radius: var(--rounded);\n  border-bottom-right-radius: var(--rounded);\n}\n.rounded-l {\n  border-top-left-radius: var(--rounded);\n  border-bottom-left-radius: var(--rounded);\n}\n.rounded-t {\n  border-top-left-radius: var(--rounded);\n  border-top-right-radius: var(--rounded);\n}\n.rounded-b {\n  border-bottom-left-radius: var(--rounded);\n  border-bottom-right-radius: var(--rounded);\n}\n\n.bg-white {\n  background-color: white;\n}\n\n.br {\n  border-right-width: 1px;\n  border-right-style: solid;\n}\n.bb {\n  border-bottom-width: 1px;\n  border-bottom-style: solid;\n}\n.bb-2 {\n  border-bottom-width: 2px;\n  border-bottom-style: solid;\n}\n.bb-3 {\n  border-bottom-width: 3px;\n  border-bottom-style: solid;\n}\n.bb-4 {\n  border-bottom-width: 4px;\n  border-bottom-style: solid;\n}\n.bl {\n  border-left-width: 1px;\n  border-left-style: solid;\n}\n.bt {\n  border-top-width: 1px;\n  border-top-style: solid;\n}\n\n.divider-h {\n  background-color: var(--gray-300);\n  width: 100%;\n  height: 1px;\n  margin-top: 2em;\n  margin-bottom: 2em;\n}\n\n.noselect {\n  -webkit-touch-callout: none; /* iOS Safari */\n  -webkit-user-select: none; /* Safari */\n  -khtml-user-select: none; /* Konqueror HTML */\n  -moz-user-select: none; /* Old versions of Firefox */\n  -ms-user-select: none; /* Internet Explorer/Edge */\n  user-select: none; /* Non-prefixed version, currently\n                                  supported by Chrome, Edge, Opera and Firefox */\n}\n\ntextarea {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  background-color: var(--bg-color-0);\n  border-color: var(--b-default);\n  border-width: 1px;\n  border-radius: 0.375rem;\n  padding: 0.5rem 0.75rem;\n  font-size: 1rem;\n  line-height: 1.5;\n}\n\ntextarea:focus {\n  outline: none;\n}\n.not-allowed {\n  cursor: not-allowed;\n}\n.pointer {\n  cursor: pointer;\n}\n.cursor-default {\n  cursor: default;\n}\n.rounded-full {\n  border-radius: 2000%;\n}\n\n.w-full {\n  width: 100%;\n}\n.min-w-full {\n  min-width: 100%;\n}\n.h-full {\n  height: 100%;\n}\n.w-fit {\n  width: fit-content;\n}\n.h-fit {\n  height: fit-content;\n}\n\n.fit {\n  width: fit-content;\n  height: fit-content;\n}\n\n.max-w-fit {\n  max-width: fit-content;\n}\n.max-h-fit {\n  max-height: fit-content;\n}\n.max-h-500 {\n  max-height: 500px;\n}\n.max-s-fit {\n  max-height: fit-content;\n  max-width: fit-content;\n}\n.max-w-full {\n  max-width: 100%;\n}\n.max-h-full {\n  max-height: 100%;\n}\n.max-s-full {\n  max-height: 100%;\n  max-width: 100%;\n}\n\n.max-w {\n  max-width: 900px;\n}\n.w-2 {\n  width: 2em;\n}\n.h-2 {\n  height: 2em;\n}\n.w-1p25 {\n  width: 1.25em;\n}\n.h-1p25 {\n  height: 1.25em;\n}\n.h-p125 {\n  height: 0.125em;\n}\n.flex {\n  display: flex;\n}\n.absolute {\n  position: absolute;\n}\n.relative {\n  position: relative;\n}\n.fixed {\n  position: fixed;\n}\n.bg-transparent {\n  background-color: transparent;\n}\n.bg-inherit {\n  background-color: inherit;\n}\n\n.inset-0,\n.inset-y-0 {\n  top: 0;\n  bottom: 0;\n}\n.inset-0 {\n  right: 0;\n  left: 0;\n}\n.inset-x-0 {\n  right: 0;\n  left: 0;\n}\n\n.top-0 {\n  top: 0;\n}\n\n.right-0 {\n  right: 0;\n}\n\n.bottom-0 {\n  bottom: 0;\n}\n\n.left-0 {\n  left: 0;\n}\n\n.text-white {\n  color: white;\n}\n.border-2 {\n  border-width: 2px;\n  border-style: solid;\n}\n\n.b-unset {\n  border: unset;\n}\n\n.b-0 {\n  border-width: 0;\n}\n.b-2 {\n  border-width: 2px;\n  border-style: solid;\n}\n\n.b-white {\n  border-color: white;\n}\n.b-black {\n  border-color: black;\n}\n\n.b-action,\n.b-active {\n  border-color: var(--blue-500);\n}\n\n.bg-black {\n  background-color: black;\n}\n\n.bg-terminal {\n  background-color: var(--gray-900);\n}\n\n.border-indigo-600 {\n  border-color: #5850ec;\n}\n\n.ta-start {\n  text-align: start;\n}\n.ta-end {\n  text-align: end;\n}\n\n.ta-left,\n.text-left {\n  text-align: left;\n}\n\n.ta-center,\n.text-center {\n  text-align: center;\n}\n\n.ta-right,\n.text-right {\n  text-align: right;\n}\n\n.tracking-wider {\n  letter-spacing: 0.05em;\n}\n\n.b-y {\n  border-top-width: 1px;\n  border-bottom-width: 1px;\n}\n\n.b,\n.b-1 {\n  border-style: solid;\n  border-width: 1px;\n}\n.b-2 {\n  border-style: solid;\n  border-width: 2px;\n}\n\n.rounded-md {\n  border-radius: 0.375rem;\n}\n\n.transparent {\n  opacity: 0;\n}\n\n.hidden {\n  display: none;\n}\n\n.text-black {\n  color: black;\n}\n\n.text-xs {\n  font-size: 0.75rem;\n}\n\n.text-sm {\n  font-size: 0.875rem;\n}\n\n.text-lg {\n  font-size: 1.125rem;\n}\n\n.text-xl {\n  font-size: 1.25rem;\n}\n\n.font-normal {\n  font-weight: 400;\n}\n\n.font-medium {\n  font-weight: 500;\n}\n\n.font-semibold {\n  font-weight: 600;\n}\n\n.font-bold {\n  font-weight: 700;\n}\n\n.font-extrabold {\n  font-weight: 800;\n}\n\n.overflow-auto {\n  overflow: auto;\n}\n\n.not-allowed {\n  cursor: not-allowed;\n}\n\n.jc-between {\n  justify-content: space-between;\n}\n.jc-around {\n  justify-content: space-around;\n}\n.jc-center {\n  justify-content: center;\n}\n.jc-fstart {\n  justify-content: flex-start;\n}\n.jc-start {\n  justify-content: start;\n}\n.ji-start {\n  justify-items: start;\n}\n.jc-fend {\n  justify-content: flex-end;\n}\n.jc-end {\n  justify-content: end;\n}\n.ji-end {\n  justify-items: end;\n}\n\n.font-12 {\n  font-size: 12px;\n}\n.font-14 {\n  font-size: 14px;\n}\n.font-16 {\n  font-size: 16px;\n}\n.font-18 {\n  font-size: 18px;\n}\n.font-20 {\n  font-size: 20px;\n}\n.font-22 {\n  font-size: 22px;\n}\n.font-24 {\n  font-size: 24px;\n}\n.font-26 {\n  font-size: 26px;\n}\n.font-28 {\n  font-size: 28px;\n}\n.font-30 {\n  font-size: 30px;\n}\n\n.no-decor {\n  text-decoration: none;\n}\n\n.o-auto {\n  overflow: auto;\n}\n\n.o-auto::-webkit-scrollbar {\n  opacity: 0.5;\n}\n.o-auto:hover ::-webkit-scrollbar {\n  opacity: 0.5;\n}\n\n.o-unset {\n  overflow: unset;\n}\n\n.ox-auto {\n  overflow-x: auto;\n}\n.oy-auto {\n  overflow-y: auto;\n}\n.o-hidden {\n  overflow: hidden;\n}\n.o-visible {\n  overflow: visible;\n}\n.ox-hidden {\n  overflow-x: hidden;\n}\n.oy-hidden {\n  overflow-y: hidden;\n}\n\n.to-ellipsis {\n  text-overflow: ellipsis;\n}\n\n.min-s-0 {\n  min-height: 0;\n  min-width: 0;\n}\n\n.min-h-0 {\n  min-height: 0;\n}\n.min-w-0 {\n  min-width: 0;\n}\n.min-w-300 {\n  min-width: 300px;\n}\n.min-w-0 {\n  min-width: 0;\n}\n.min-h-fit {\n  min-height: fit-content;\n}\n.min-w-fit {\n  min-width: fit-content;\n}\n\n.max-h-100v {\n  max-height: 100vh;\n}\n.max-w-100v {\n  max-width: 100vw;\n}\n\n.min-w-600 {\n  min-width: min(600px, 100vw);\n}\n\n.max-w-700 {\n  max-width: 700px;\n}\n.max-w-800 {\n  max-width: 800px;\n}\n.max-w-1200 {\n  max-width: 1200px;\n}\n\n.bold {\n  font-weight: bold;\n}\n\n.ws-break {\n  white-space: break-spaces;\n}\n.ws-pre {\n  white-space: pre;\n}\n.ws-pre-line {\n  white-space: pre-line;\n}\n.ws-pre-wrap {\n  white-space: pre-wrap;\n}\n.ws-nowrap {\n  white-space: nowrap;\n}\n.ws-wrap {\n  white-space: normal;\n}\n\n.no-pointer-events {\n  touch-action: none;\n  pointer-events: none;\n}\n\n.no-interaction {\n  opacity: 0.5;\n  touch-action: none;\n  pointer-events: none;\n\n  -webkit-touch-callout: none; /* iOS Safari */\n  -webkit-user-select: none; /* Safari */\n  -khtml-user-select: none; /* Konqueror HTML */\n  -moz-user-select: none; /* Old versions of Firefox */\n  -ms-user-select: none; /* Internet Explorer/Edge */\n  user-select: none; /* Non-prefixed version, currently\n                                  supported by Chrome, Edge, Opera and Firefox */\n}\n.no-interaction.no-fade {\n  opacity: 1;\n}\n\n.no-scroll-bar {\n  -ms-overflow-style: none; /* Internet Explorer 10+ */\n  scrollbar-width: none; /* Firefox */\n}\n.no-scroll-bar::-webkit-scrollbar {\n  display: none; /* Safari and Chrome */\n}\n\ninput.input {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n  border: none;\n  padding: 0.5rem 0.75rem;\n  font-size: 1rem;\n  font-size: 0.875rem;\n  line-height: 1.5;\n  width: 100%;\n  flex: 1 1;\n  box-sizing: border-box;\n  min-width: 150px;\n}\n\nselect {\n  display: block;\n  background: #fff;\n  padding: 0.5rem 0.75rem;\n  border: none;\n  margin: 0;\n  outline: 0;\n  flex: 1 1;\n  width: 100%;\n  cursor: pointer;\n  border: 1px solid #d2d6dc;\n  appearance: none;\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  background: transparent;\n  background-image: url(\"data:image/svg+xml;utf8,<svg fill='black' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>\");\n  background-repeat: no-repeat;\n  background-position-x: 100%;\n  background-position-y: 5px;\n}\n\nselect:hover {\n  border-color: #888;\n}\n\n.select-button:focus,\nselect:focus {\n  border: 1px solid var(--focus-color);\n  box-shadow: 0 0 0 1px var(--focus-color);\n  outline: none;\n}\n\n.form-field .select-button:focus {\n  border-color: transparent !important;\n}\n\ninput[type=\"checkbox\"]:focus-visible {\n  border: 1px solid var(--focus-color);\n  outline: none;\n  box-shadow: 0 0 0 3px rgb(0 183 255 / 15%);\n}\n\n/* Set options to normal weight */\nselect option {\n  font-weight: normal;\n}\n\n/* Support for rtl text, explicit support for Arabic and Hebrew */\n*[dir=\"rtl\"] select-,\n:root:lang(ar) select,\n:root:lang(iw) select {\n  background-position:\n    left 0.7em top 50%,\n    0 0;\n  padding: 0.6em 0.8em 0.5em 1.4em;\n}\n\n/* Disabled styles */\nselect:disabled,\nselect[aria-disabled=\"true\"] {\n  color: graytext;\n  background-image: url(\"data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22graytext%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E\"),\n    linear-gradient(to bottom, #ffffff 0%, #e5e5e5 100%);\n}\n\nselect:disabled:hover,\nselect[aria-disabled=\"true\"] {\n  border-color: #aaa;\n}\n\n.button-css,\nbutton {\n  --text-opacity: 1;\n  --border-opacity: 1;\n  --bg-opacity: 1;\n  text-decoration: none;\n  -webkit-font-smoothing: antialiased;\n  box-sizing: border-box;\n  border: 0 solid #d2d6dc;\n  font-family: inherit;\n\n  text-transform: none;\n  background-image: none;\n  cursor: pointer;\n  /* background-color: rgba(88,80,236,var(--bg-opacity)); */\n  border-color: transparent;\n  border-width: 1px;\n  font-weight: 500;\n  font-size: 0.875rem;\n  line-height: 1.25rem;\n  padding-top: 0.5rem;\n  padding-bottom: 0.5rem;\n  padding-left: 1rem;\n  padding-right: 1rem;\n  height: fit-content;\n  user-select: none;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.button-css:not([class*=\"rounded\"]):not([class*=\"round\"]),\nbutton:not([class*=\"rounded\"]):not([class*=\"round\"]) {\n  border-radius: 0.375rem;\n}\n\n.button.secondary,\nbutton.secondary,\nbutton.toggle {\n  background-color: white;\n  background-image: none;\n  border-color: var(--gray-300);\n  color: var(--gray-700);\n}\n.button.secondary:hover,\nbutton.secondary:hover,\nbutton.toggle:hover {\n  color: var(--gray-400);\n}\n\n.button.danger,\nbutton.danger {\n  background-color: white;\n  background-image: none;\n  border-color: var(--red-300);\n  color: var(--red-700);\n}\n.button.success,\nbutton.success {\n  background-color: white;\n  background-image: none;\n  border-color: var(--green-300);\n  color: var(--green-700);\n}\n\n/* .button.primary,\nbutton.primary {\n  border-color: var(--gray-300);\n  color: var(--gray-700);\n\n} */\n\nbutton.toggle.toggled,\n.button.toggle.toggled {\n  color: var(--teal-400);\n  border-color: var(--teal-400);\n}\n\n.active-border,\n.active-border-hover:hover {\n  border-color: #a4e9fe;\n  box-shadow: 0 0 0 3px rgb(180 235 252 / 45%);\n}\n.active-shadow,\n.active-shadow-hover:hover {\n  box-shadow: 0 0 0 3px rgb(0 183 255 / 15%);\n}\n\n.focusable-inset:focus {\n  border-width: 1px;\n  border-style: solid;\n  border: 1px solid var(--focus-color) !important;\n  outline: none;\n  box-shadow: inset 0 0 0 3px rgb(0 183 255 / 15%);\n}\n\nbutton,\nno-outline {\n  outline: none;\n}\n\n.focusable:focus,\n/* \n  :has not working in firefox\n  https://bugzilla.mozilla.org/show_bug.cgi?id=418039#c62\n  \n  to enable for firefox: .focusable:focus-within,\n*/\n.focusable:focus-within:has(:focus-visible), \n.focusable:focus-visible,\ninput.input:focus,\n.button:not(.toggle, .select-button, .btn-color-danger):focus,\nbutton:not(.toggle, .select-button, .btn-color-danger):focus,\n.input-focus:focus,\na.btn:focus,\n.focus-border:focus-within {\n  -webkit-appearance: none !important;\n  outline: 2px solid var(--focus-color) !important;\n  box-shadow: 0 0 0 2px var(--focus-color) !important;\n  border-color: transparent !important;\n  z-index: 1;\n}\n\ninput::placeholder {\n  text-align: left;\n}\n\n.button.btn-color-danger:focus,\nbutton.btn-color-danger:focus {\n  border: 1px solid var(--danger) !important;\n  box-shadow: 0 0 0 1px var(--danger) !important;\n  outline: none;\n}\n\nbutton.toggle:focus {\n  outline: 0;\n}\n\n.text-hover:hover {\n  color: rgb(0, 141, 197) !important;\n}\n\n.card {\n  padding: 2em;\n  border: 1px solid var(--gray-300);\n  box-shadow:\n    0 1px 3px 0 rgba(0, 0, 0, 0.1),\n    0 1px 2px 0 rgba(0, 0, 0, 0.06);\n  border-radius: 0.5em;\n}\n\n.dark-theme .card {\n  /* Border used to make select options more visible when rendered on a popup (See access rules table options) */\n  border: 1px solid var(--b-color);\n  /* box-shadow: 0 1px 3px 0 rgb(221 221 221 / 57%), 0 1px 2px 0 rgba(0, 0, 0, .06) */\n  box-shadow:\n    inset 0 0 0.5px 1px hsla(0, 0%, 100%, 0.075),\n    0 0 0 1px hsla(0, 0%, 0%, 0.05),\n    0 0.3px 0.4px hsla(0, 0%, 0%, 0.02),\n    0 0.9px 1.5px hsla(0, 0%, 0%, 0.045),\n    0 3.5px 6px hsla(0, 0%, 0%, 0.09);\n}\n\n@media only screen and (max-width: 600px) {\n  .card {\n    padding: 1em;\n  }\n}\n\n.text-ellipsis {\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: block;\n}\n.text-ellipsis-left {\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: block;\n  direction: rtl;\n  text-align: left;\n}\n\n.fs-0 {\n  flex-shrink: none;\n}\n\n.fs-1 {\n  flex-shrink: 1;\n}\n\n.italic {\n  font-style: italic;\n}\n\n.f-p5 {\n  flex: 0.5;\n}\n\n::-webkit-scrollbar {\n  position: absolute;\n  background: transparent;\n  width: 12px;\n  height: 12px;\n}\n\n::-webkit-scrollbar {\n  opacity: 1;\n}\n\n::-webkit-scrollbar-track {\n  background-color: transparent;\n}\n\n::-webkit-scrollbar-thumb {\n  transition: 1s all;\n  background-color: transparent;\n}\n\n::-webkit-scrollbar-thumb {\n  background-color: rgba(0, 0, 0, 0.2);\n}\n\n.c--fit > * {\n  width: fit-content;\n  height: fit-content;\n}\n\n.pb-1 {\n  padding-bottom: 1em;\n}\n\n.pt-p5 {\n  padding-top: 0.5em;\n}\n.pt-p25 {\n  padding-top: 0.25em;\n}\n.pt-2 {\n  padding-top: 2em;\n}\n.pt-1 {\n  padding-top: 1em;\n}\n.pt-0 {\n  padding-top: 0;\n}\n\n.pl-3 {\n  padding-left: 3em;\n}\n.pl-2 {\n  padding-left: 2em;\n}\n.pl-1 {\n  padding-left: 1em;\n}\n.pl-p5 {\n  padding-left: 0.5em;\n}\n.pl-p25 {\n  padding-left: 0.5em;\n}\n.pr-1 {\n  padding-right: 1em;\n}\n.pr-p5 {\n  padding-right: 0.5em;\n}\n.pr-p25 {\n  padding-right: 0.25em;\n}\n\n.color-action {\n  color: var(--blue-500);\n}\n.dark-theme .color-action {\n  color: var(--blue-400);\n}\n.border-action {\n  border-color: var(--blue-500);\n}\n\n.absolute-centered {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%, -50%);\n}\n\nli.no-decor,\nul.left-nav-list {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n}\n\nnav.left-nav-list a {\n  display: flex;\n  flex-direction: row;\n  padding: 0.25em 0.5em;\n  align-items: center;\n  text-decoration: none;\n  color: var(--gray-600);\n}\n\nnav.left-nav-list a:hover {\n  background: aliceblue;\n}\nnav.left-nav-list a.active {\n  background: rgb(194, 226, 253);\n}\n\n.active-drop-target {\n  background: var(--blue-200) !important;\n}\n\n.pe-none {\n  pointer-events: none;\n}\n\n.bg-color-0 {\n  background-color: var(--bg-color-0);\n}\n.bg-color-1 {\n  background-color: var(--bg-color-1);\n}\n.bg-color-2 {\n  background-color: var(--bg-color-2);\n}\n.bg-color-3 {\n  background-color: var(--bg-color-3);\n}\n.bg-color-4 {\n  background-color: var(--bg-color-4);\n}\n\n.bg-li.selected {\n  background: var(--bg-li-selected);\n}\n\n.w-800 {\n  width: 800px;\n}\n.w-700 {\n  width: 700px;\n}\n.w-600 {\n  width: 600px;\n}\n.w-500 {\n  width: 500px;\n}\n.w-400 {\n  width: 400px;\n}\n\n.dark-theme a[target=\"_blank\"]:link {\n  color: lightskyblue;\n}\n.dark-theme a[target=\"_blank\"]:visited {\n  color: #cdaaed;\n}\n.dark-theme a[target=\"_blank\"]:hover {\n  color: #ffffff;\n}\n.dark-theme a[target=\"_blank\"]:active {\n  color: #ff4040;\n  text-decoration: none;\n  font-weight: normal;\n}\n\n@media all and (max-width: 1024px) {\n  /*styles for narrow desktop browsers and iPad landscape */\n}\n\n@media all and (max-width: 700px) {\n  .w-800 {\n    width: 700px;\n  }\n}\n@media all and (max-width: 600px) {\n  .w-800 {\n    width: 600px;\n  }\n  .w-700 {\n    width: 600px;\n  }\n}\n@media all and (max-width: 500px) {\n  .w-800 {\n    width: 500px;\n  }\n  .w-700 {\n    width: 500px;\n  }\n  .w-600 {\n    width: 500px;\n  }\n}\n@media all and (max-width: 400px) {\n  .w-800 {\n    width: 400px;\n  }\n  .w-700 {\n    width: 400px;\n  }\n  .w-600 {\n    width: 400px;\n  }\n  .w-500 {\n    width: 400px;\n  }\n}\n\n@media all and (max-width: 320px) {\n  /*styles for iPhone/Android portrait*/\n  .w-800 {\n    width: 320px;\n  }\n  .w-700 {\n    width: 320px;\n  }\n  .w-600 {\n    width: 320px;\n  }\n  .w-500 {\n    width: 320px;\n  }\n  .w-400 {\n    width: 320px;\n  }\n}\n\n.text-active {\n  color: var(--active);\n}\n.b-active {\n  border-color: var(--active);\n}\n.bg-active {\n  background-color: var(--active);\n}\n\n.text-active-hover:hover {\n  color: var(--blue-600);\n}\n.b-active.hover:hover {\n  border-color: var(--active-hover);\n}\n.bg-active.hover:hover {\n  background-color: var(--active-hover);\n}\n\n.show-on-parent-hover:not(.hover) {\n  opacity: 0;\n}\n\n.show-on-hover {\n  opacity: 0;\n}\n.show-on-hover:hover {\n  opacity: 1;\n}\n\n.show-on-parent-hover:focus-within {\n  opacity: 1 !important;\n}\n\n/* .parent-hover:hover .show-on-parent-hover, */\n*:hover > .show-on-parent-hover,\n*:focus > .show-on-parent-hover,\n*:focus-within > .show-on-parent-hover {\n  opacity: 1;\n}\n\n.display-on-trigger-hover {\n  width: 0;\n  margin-right: -10px;\n  transition: width 0.35s;\n  overflow: hidden;\n}\n.trigger-hover:hover .display-on-trigger-hover,\n.trigger-hover:focus .display-on-trigger-hover {\n  width: 250px;\n}\n\n.show-on-trigger-hover {\n  opacity: 0;\n}\n.trigger-hover:hover .show-on-trigger-hover,\n.trigger-hover:focus .show-on-trigger-hover,\n.trigger-hover:focus-within .show-on-trigger-hover,\n.trigger-hover-force .show-on-trigger-hover {\n  opacity: 1 !important;\n}\n\n/* Disable on touch-only because there's no hover */\n@media (pointer: coarse) {\n  .show-on-hover,\n  .show-on-parent-hover,\n  .show-on-trigger-hover {\n    opacity: 1 !important;\n  }\n}\n\n.shrink-label {\n  max-width: fit-content;\n  max-height: fit-content;\n  white-space: nowrap;\n  flex: 1;\n  min-width: 0;\n  text-overflow: ellipsis;\n  overflow: hidden;\n}\n\n.disabled:not(.no-fade):not(.hidden) {\n  /* pointer-events: none; \n  touch-action: none;  */\n  opacity: 0.5;\n}\n\n.disabled,\n.disabled * {\n  cursor: not-allowed !important;\n}\n\n/** Merging these rules will not work */\ninput[type=\"file\"]::file-selector-button {\n  background: transparent;\n  border: unset;\n  cursor: pointer;\n  font-weight: bold;\n\n  /* Vertical centering */\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  height: 100%;\n}\ninput[type=\"file\"]::-webkit-file-upload-button {\n  background: transparent;\n  border: unset;\n  cursor: pointer;\n  font-weight: bold;\n\n  /* Vertical centering */\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n"
  },
  {
    "path": "client/tsconfig.eslint.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}"
  },
  {
    "path": "client/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"typeRoots\": [\"node_modules/@types\", \"./@types\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\", \"es2017\", \"es2019\", \"es2022\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true, \n    \"jsx\": \"react\",\n    \"strict\": true,\n    \"baseUrl\": \"./\",\n    \"noUncheckedIndexedAccess\": true,\n    \"noImplicitAny\": false,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"ignoreDeprecations\": \"5.0\",\n    \"strictNullChecks\": true,\n    \"checkJs\": true,\n    \"paths\": {\n      \"@common/*\": [\"../common/*\"],\n      \"@docs/*\": [\"../docs/*\"],\n      \"@components/*\": [\"src/components/*\"],\n      \"@pages/*\": [\"src/pages/*\"],\n    }\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\n    \"node_modules\",\n    \"public\",\n    \"typings/browser\",\n    \"typings/browser.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "client/tsconfig_lib.json",
    "content": "{\n  \"compilerOptions\": {\n    \"typeRoots\": [\"node_modules/@types\", \"./@types\"],\n    \"types\": [\"dom-mediacapture-record\", \"resize-observer-browser\"],\n    \"target\": \"es6\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react\",\n    \"strict\": false,\n    \"baseUrl\": \"./\",\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true\n  },\n  \"assets\": [\"**/*.css\"],\n  \"include\": [\"src\"],\n  \"exclude\": [\"public\", \"typings/browser\", \"typings/browser.d.ts\"]\n}\n"
  },
  {
    "path": "client/tsconfig_rollup.json",
    "content": "{\n  \"compilerOptions\": {\n    \"typeRoots\": [\"node_modules/@types\", \"./@types\"],\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": false,\n    \"jsx\": \"react\",\n    \"strict\": false,\n    \"baseUrl\": \"./\",\n    \"declaration\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\n    \"node_modules\",\n    \"public\",\n    \"typings/browser\",\n    \"typings/browser.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "client/tslint.json",
    "content": "{\n  \"linterOptions\": {\n    \"exclude\": [\"*.json\", \"**/*.json\"]\n  }\n}\n"
  },
  {
    "path": "common/DBGeneratedSchema.d.ts",
    "content": "/* Schema definition generated by prostgles-server */\n\nexport type DBGeneratedSchema = {\n  access_control: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      created?: null | string;\n      database_id: number;\n      dbPermissions: \n       |  {  type: \"Run SQL\";  allowSQL?: boolean; }\n       |  {  type: \"All views/tables\";  allowAllTables: (\"select\" | \"insert\" | \"update\" | \"delete\")[]; }\n       |  {  type: \"Custom\";  customTables: (  {  tableName: string;  select?: | boolean |  {  fields: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>;  forcedFilterDetailed?: any;  subscribe?: {  throttle?: number; };  filterFields?: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>;  orderByFields?: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>; };  update?: | boolean |  {  fields: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>;  forcedFilterDetailed?: any;  checkFilterDetailed?: any;  filterFields?: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>;  orderByFields?: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>;  forcedDataDetail?: any[];  dynamicFields?: (  {  filterDetailed: any;  fields: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>; } )[]; };  insert?: | boolean |  {  fields: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>;  forcedDataDetail?: any[];  checkFilterDetailed?: any; };  delete?: | boolean |  {  filterFields: | string[] | \"*\" | \"\" |  Record<string, 1 | true> |  Record<string, 0 | false>;  forcedFilterDetailed?: any; };  sync?: {  id_fields: string[];  synced_field: string;  throttle?: number;  allow_delete?: boolean; }; } )[]; }\n      dbsPermissions?: null | {    createWorkspaces?: boolean;   viewPublishedWorkspaces?: {  workspaceIds: string[]; };  };\n      id?: number;\n      llm_daily_limit?: number;\n      name?: null | string;\n    };\n  };\n  access_control_allowed_llm: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      access_control_id: number;\n      llm_credential_id: number;\n      llm_prompt_id: number;\n    };\n  };\n  access_control_connections: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      access_control_id: number;\n      connection_id: string;\n    };\n  };\n  access_control_methods: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      access_control_id: number;\n      published_method_id: number;\n    };\n  };\n  access_control_user_types: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      access_control_id: number;\n      user_type: string;\n    };\n  };\n  alert_viewed_by: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      alert_id?: null | string;\n      id?: string;\n      user_id?: null | string;\n      viewed?: null | string;\n    };\n  };\n  alerts: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      connection_id?: null | string;\n      created?: null | string;\n      data?: null | any;\n      database_config_id?: null | number;\n      id?: string;\n      message?: null | string;\n      section?: null | \"access_control\" | \"backups\" | \"table_config\" | \"details\" | \"status\" | \"methods\" | \"file_storage\" | \"API\"\n      severity: \"info\" | \"warning\" | \"error\"\n      title?: null | string;\n    };\n  };\n  backups: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      connection_details?: string;\n      connection_id?: null | string;\n      content_type?: string;\n      created?: string;\n      credential_id?: null | number;\n      dbSizeInBytes: string;\n      destination: \"Local\" | \"Cloud\" | \"None (temp stream)\"\n      details?: null | any;\n      dump_command: string;\n      dump_logs?: null | string;\n      id?: string;\n      initiator?: null | string;\n      last_updated?: string;\n      local_filepath?: null | string;\n      name?: null | string;\n      options: \n       |  {  command: \"pg_dumpall\";  clean: boolean;  dataOnly?: boolean;  globalsOnly?: boolean;  rolesOnly?: boolean;  schemaOnly?: boolean;  ifExists?: boolean;  encoding?: string;  keepLogs?: boolean; }\n       |  {  command: \"pg_dump\";  format: \"p\" | \"t\" | \"c\";  dataOnly?: boolean;  clean?: boolean;  create?: boolean;  encoding?: string;  numberOfJobs?: number;  noOwner?: boolean;  compressionLevel?: number;  ifExists?: boolean;  keepLogs?: boolean;  excludeSchema?: string;  schemaOnly?: boolean; }\n      restore_command?: null | string;\n      restore_end?: null | string;\n      restore_logs?: null | string;\n      restore_options?: {    command: \"pg_restore\" | \"psql\";   format: \"p\" | \"t\" | \"c\";   clean: boolean;   excludeSchema?: string;   newDbName?: string;   create?: boolean;   dataOnly?: boolean;   noOwner?: boolean;   numberOfJobs?: number;   ifExists?: boolean;   keepLogs?: boolean;  };\n      restore_start?: null | string;\n      restore_status?: \n       | null\n       |  {  ok: string; }\n       |  {  err: string; }\n       |  {  loading: {  loaded: number;  total: number; }; }\n      sizeInBytes?: null | string;\n      status: \n       |  {  ok: string; }\n       |  {  err: string; }\n       |  {  loading?: {  loaded: number;  total?: number; }; }\n      uploaded?: null | string;\n    };\n  };\n  connections: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      config?: null | {    enabled: boolean;   path: string;  };\n      created?: null | string;\n      db_conn?: null | string;\n      db_connection_timeout?: null | number;\n      db_host?: string;\n      db_name: string;\n      db_pass?: null | string;\n      db_port?: number;\n      db_schema_filter?: \n       | null\n       |  Record<string, 1>\n       |  Record<string, 0>\n      db_ssl?: \"disable\" | \"allow\" | \"prefer\" | \"require\" | \"verify-ca\" | \"verify-full\"\n      db_user?: string;\n      db_watch_shema?: null | boolean;\n      disable_realtime?: null | boolean;\n      display_options?: null | {    prettyTableAndColumnNames: boolean;  };\n      id?: string;\n      info?: null | {    canCreateDb?: boolean;  };\n      is_state_db?: null | boolean;\n      last_updated?: string;\n      name: string;\n      on_mount_ts?: null | string;\n      on_mount_ts_disabled?: null | boolean;\n      prgl_params?: null | any;\n      prgl_url?: null | string;\n      ssl_certificate?: null | string;\n      ssl_client_certificate?: null | string;\n      ssl_client_certificate_key?: null | string;\n      ssl_reject_unauthorized?: null | boolean;\n      table_options?: null | Partial<Record<string,  {  icon?: string;  label?: string;  rowIconColumn?: string;  columns?: Partial<Record<string,  {  icon?: string; }>>;  card?: {  headerColumn?: string; }; }>>\n      type: \"Standard\" | \"Connection URI\" | \"Prostgles\"\n      url_path?: null | string;\n      user_id?: null | string;\n    };\n  };\n  credential_types: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      description?: null | string;\n      id: \"AWS\" | \"Cloudflare\"\n    };\n  };\n  credentials: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      bucket?: null | string;\n      endpoint_url?: string;\n      id?: number;\n      key_id: string;\n      key_secret: string;\n      name?: null | string;\n      region?: null | string;\n      type: \"AWS\" | \"Cloudflare\"\n      user_id?: null | string;\n    };\n  };\n  database_config_logs: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      id?: number;\n      on_mount_logs?: null | string;\n      on_run_logs?: null | string;\n      table_config_logs?: null | string;\n    };\n  };\n  database_configs: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      backups_config?: null | {    enabled?: boolean;   cloudConfig: null |  {  credential_id?: null | number; };   frequency: \"daily\" | \"monthly\" | \"weekly\" | \"hourly\";   hour?: number;   dayOfWeek?: number;   dayOfMonth?: number;   keepLast?: number;   err?: null | string;   dump_options: |  {  command: \"pg_dumpall\";  clean: boolean;  dataOnly?: boolean;  globalsOnly?: boolean;  rolesOnly?: boolean;  schemaOnly?: boolean;  ifExists?: boolean;  encoding?: string;  keepLogs?: boolean; }\n |  {  command: \"pg_dump\";  format: \"p\" | \"t\" | \"c\";  dataOnly?: boolean;  clean?: boolean;  create?: boolean;  encoding?: string;  numberOfJobs?: number;  noOwner?: boolean;  compressionLevel?: number;  ifExists?: boolean;  keepLogs?: boolean;  excludeSchema?: string;  schemaOnly?: boolean; };  };\n      db_host: string;\n      db_name: string;\n      db_port: number;\n      file_table_config?: null | {    fileTable?: string;   storageType: |  {  type: \"local\"; }\n |  {  type: \"S3\";  credential_id: number; };   referencedTables?: any;   delayedDelete?: {  deleteAfterNDays: number;  checkIntervalHours?: number; };  };\n      id?: number;\n      rest_api_enabled?: null | boolean;\n      sync_users?: null | boolean;\n      table_config?: null | Record<string, \n |  {  isLookupTable: {  values: Record<string, string>; }; }\n |  {  columns: Record<string,  | string |  {  hint?: string;  nullable?: boolean;  isText?: boolean;  trimmed?: boolean;  defaultValue?: any; } |  {  jsonbSchema: |  {  type: \"string\" | \"number\" | \"boolean\" | \"Date\" | \"time\" | \"timestamp\" | \"string[]\" | \"number[]\" | \"boolean[]\" | \"Date[]\" | \"time[]\" | \"timestamp[]\";  optional?: boolean;  description?: string; } |  {  type: \"Lookup\" | \"Lookup[]\";  optional?: boolean;  description?: string; } |  {  type: \"object\";  optional?: boolean;  description?: string; }; }>; }>\n      table_config_ts?: null | string;\n      table_config_ts_disabled?: null | boolean;\n      table_schema_positions?: null | Partial<Record<string,  {  x: number;  y: number; }>>\n      table_schema_transform?: null | {    translate: {  x: number;  y: number; };   scale: number;  };\n    };\n  };\n  global_settings: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      allowed_ips?: string[];\n      allowed_ips_enabled?: boolean;\n      allowed_origin?: null | string;\n      auth_created_user_type?: null | \"admin\" | \"public\" | \"default\"\n      auth_providers?: null | {    website_url: string;   email?: |  {  signupType: \"withMagicLink\";  enabled?: boolean;  smtp: |  {  type: \"smtp\";  host: string;  port: number;  secure?: boolean;  rejectUnauthorized?: boolean;  user: string;  pass: string; } |  {  type: \"aws-ses\";  region: string;  accessKeyId: string;  secretAccessKey: string;  sendingRate?: number; };  emailTemplate: {  from: string;  subject: string;  body: string; };  emailConfirmationEnabled?: boolean; }\n |  {  signupType: \"withPassword\";  enabled?: boolean;  minPasswordLength?: number;  smtp: |  {  type: \"smtp\";  host: string;  port: number;  secure?: boolean;  rejectUnauthorized?: boolean;  user: string;  pass: string; } |  {  type: \"aws-ses\";  region: string;  accessKeyId: string;  secretAccessKey: string;  sendingRate?: number; };  emailTemplate: {  from: string;  subject: string;  body: string; };  emailConfirmationEnabled?: boolean; };   google?: {  enabled?: boolean;  clientID: string;  clientSecret: string;  authOpts?: {  scope: (\"profile\" | \"email\" | \"calendar\" | \"calendar.readonly\" | \"calendar.events\" | \"calendar.events.readonly\")[]; }; };   github?: {  enabled?: boolean;  clientID: string;  clientSecret: string;  authOpts?: {  scope: (\"read:user\" | \"user:email\")[]; }; };   microsoft?: {  enabled?: boolean;  clientID: string;  clientSecret: string;  authOpts?: {  prompt: \"login\" | \"none\" | \"consent\" | \"select_account\" | \"create\";  scope: (\"openid\" | \"profile\" | \"email\" | \"offline_access\" | \"User.Read\" | \"User.ReadBasic.All\" | \"User.Read.All\")[]; }; };   facebook?: {  enabled?: boolean;  clientID: string;  clientSecret: string;  authOpts?: {  scope: (\"email\" | \"public_profile\" | \"user_birthday\" | \"user_friends\" | \"user_gender\" | \"user_hometown\")[]; }; };   customOAuth?: {  enabled?: boolean;  clientID: string;  clientSecret: string;  displayName: string;  displayIconPath?: string;  authorizationURL: string;  tokenURL: string;  authOpts?: {  scope: string[]; }; };  };\n      enable_logs?: boolean;\n      id?: number;\n      login_rate_limit?: {    maxAttemptsPerHour: number;   groupBy: \"x-real-ip\" | \"remote_ip\" | \"ip\";  };\n      login_rate_limit_enabled?: boolean;\n      magic_link_validity_days?: number;\n      mcp_servers_disabled?: boolean;\n      pass_process_env_vars_to_server_side_functions?: boolean;\n      prostgles_registration?: null | {    enabled: boolean;   email: string;   token: string;  };\n      session_max_age_days?: number;\n      tableConfig?: null | any;\n      trust_proxy?: boolean;\n      updated_at?: string;\n      updated_by?: \"user\" | \"app\"\n    };\n  };\n  links: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      closed?: null | boolean;\n      created?: null | string;\n      deleted?: null | boolean;\n      disabled?: null | boolean;\n      id?: string;\n      last_updated: string;\n      options: \n       |  {  type: \"table\";  colorArr?: number[];  tablePath: (  {  table: string;  on: (  Record<string, any> )[]; } )[]; }\n       |  {  type: \"map\";  dataSource?: |  {  type: \"sql\";  sql: string;  withStatement: string; } |  {  type: \"table\";  tableName: string;  joinPath?: (  {  table: string;  on: (  Record<string, any> )[]; } )[]; } |  {  type: \"local-table\";  localTableName: string;  smartGroupFilter?: |  {  $and: any[]; } |  {  $or: any[]; }; } |  {  type: \"osm\";  osmLayerQuery: string; };  title?: string;  mapIcons?: |  {  type: \"fixed\";  display?: \"icon\" | \"icon+circle\";  iconPath: string; } |  {  type: \"conditional\";  display?: \"icon\" | \"icon+circle\";  columnName: string;  conditions: (  {  value: any;  iconPath: string; } )[]; };  mapColorMode?: |  {  type: \"fixed\";  colorArr: number[]; } |  {  type: \"scale\";  columnName: string;  min: number;  max: number;  minColorArr: number[];  maxColorArr: number[]; } |  {  type: \"conditional\";  columnName: string;  conditions: (  {  value: any;  colorArr: number[]; } )[]; };  mapShowText?: {  columnName: string; };  columns: (  {  name: string;  colorArr: number[]; } )[]; }\n       |  {  type: \"timechart\";  dataSource?: |  {  type: \"sql\";  sql: string;  withStatement: string; } |  {  type: \"table\";  tableName: string;  joinPath?: (  {  table: string;  on: (  Record<string, any> )[]; } )[]; } |  {  type: \"local-table\";  localTableName: string;  smartGroupFilter?: |  {  $and: any[]; } |  {  $or: any[]; }; };  title?: string;  groupByColumn?: string;  groupByColumnColors?: (  {  value: any;  color: string; } )[];  otherColumns?: (  {  name: string;  label?: string;  udt_name: string;  is_pkey?: boolean; } )[];  columns: (  {  name: string;  colorArr: number[];  statType?: {  funcName: \"$min\" | \"$max\" | \"$countAll\" | \"$avg\" | \"$sum\" | \"$count\";  numericColumn: string; }; } )[]; }\n       |  {  type: \"barchart\";  dataSource?: |  {  type: \"sql\";  sql: string;  withStatement: string; } |  {  type: \"table\";  tableName: string;  joinPath?: (  {  table: string;  on: (  Record<string, any> )[]; } )[]; } |  {  type: \"local-table\";  localTableName: string;  smartGroupFilter?: |  {  $and: any[]; } |  {  $or: any[]; }; };  title?: string;  statType?: {  funcName: \"$min\" | \"$max\" | \"$count\" | \"$countAll\" | \"$avg\" | \"$sum\";  numericColumn: string; };  columns: (  {  name: string;  colorArr: number[]; } )[]; }\n      user_id: string;\n      w1_id: string;\n      w2_id: string;\n      workspace_id?: null | string;\n    };\n  };\n  llm_chats: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      connection_id?: null | string;\n      created?: null | string;\n      currently_typed_message?: null | string;\n      db_data_permissions?: \n       | null\n       |  {  Mode: \"None\"; }\n       |  {  Mode: \"Run readonly SQL\";  query_timeout?: number;  auto_approve?: boolean; }\n       |  {  Mode: \"Run commited SQL\";  query_timeout?: number;  auto_approve?: boolean; }\n       |  {  Mode: \"Custom\";  auto_approve?: boolean;  tables: (  {  tableName: string;  select?: boolean;  update?: boolean;  insert?: boolean;  delete?: boolean; } )[]; }\n      db_schema_permissions?: \n       | null\n       |  {  type: \"None\"; }\n       |  {  type: \"Full\"; }\n       |  {  type: \"Custom\";  tables: string[]; }\n      disabled_message?: null | string;\n      disabled_until?: null | string;\n      extra_body?: null | {    temperature?: number;   frequency_penalty?: number;   max_completion_tokens?: number;   max_tokens?: number;   presence_penalty?: number;   response_format?: \"json\" | \"text\" | \"srt\" | \"verbose_json\" | \"vtt\";   think?: boolean;   reasoning?: |  {  effort: \"high\" | \"medium\" | \"low\"; }\n |  {  max_tokens?: number; };   stream?: boolean;  };\n      extra_headers?: null | Record<string, string>\n      id?: number;\n      llm_prompt_id?: null | number;\n      max_total_cost_usd?: string;\n      maximum_consecutive_tool_fails?: number;\n      model?: null | number;\n      name?: string;\n      parent_chat_id?: null | number;\n      status?: \n       | null\n       |  {  state: \"stopped\"; }\n       |  {  state: \"loading\";  since: string; }\n      user_id: string;\n    };\n  };\n  llm_chats_allowed_functions: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      auto_approve?: null | boolean;\n      chat_id: number;\n      connection_id: string;\n      server_function_id: number;\n    };\n  };\n  llm_chats_allowed_mcp_tools: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      auto_approve?: null | boolean;\n      chat_id: number;\n      server_config_id?: null | number;\n      server_name: string;\n      tool_id: number;\n    };\n  };\n  llm_credentials: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      api_key?: string;\n      created?: null | string;\n      extra_body?: null | {    temperature?: number;   frequency_penalty?: number;   max_completion_tokens?: number;   max_tokens?: number;   presence_penalty?: number;   response_format?: \"json\" | \"text\" | \"srt\" | \"verbose_json\" | \"vtt\";   think?: boolean;   reasoning?: |  {  effort: \"high\" | \"medium\" | \"low\"; }\n |  {  max_tokens?: number; };   stream?: boolean;  };\n      extra_headers?: null | Record<string, string>\n      id?: number;\n      is_default?: boolean;\n      name?: null | string;\n      provider_id: string;\n      user_id: string;\n    };\n  };\n  llm_messages: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      chat_id: number;\n      cost?: string;\n      created?: null | string;\n      id?: string;\n      llm_model_id?: null | number;\n      message:  ( \n |  {  type: \"text\";  text: string;  reasoning?: string; }\n |  {  type: \"image\" | \"audio\" | \"video\" | \"application\" | \"text\";  source: {  type: \"base64\";  media_type: string;  data: string; }; }\n |  {  type: \"tool_result\";  tool_use_id: string;  tool_name: string;  content: | string |  (  |  {  type: \"text\";  text: string; } |  {  type: \"image\" | \"audio\";  mimeType: string;  data: string; } |  {  type: \"resource\";  resource: {  uri: string;  mimeType?: string;  text?: string;  blob?: string; }; } |  {  type: \"resource_link\";  uri: string;  name: string;  mimeType?: string;  description?: string; } )[];  is_error?: boolean; }\n |  {  type: \"tool_use\";  id: string;  name: string;  input: any; } )[]\n      meta?: null | any;\n      user_id?: null | string;\n    };\n  };\n  llm_models: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      architecture?: null | {    modality?: string;   input_modalities: string[];   output_modalities: string[];   tokenizer: string;   instruct_type: | null\n | string;  };\n      chat_suitability_rank?: null | string;\n      context_length?: number;\n      extra_body?: null | {    temperature?: number;   frequency_penalty?: number;   max_completion_tokens?: number;   max_tokens?: number;   presence_penalty?: number;   response_format?: \"json\" | \"text\" | \"srt\" | \"verbose_json\" | \"vtt\";   think?: boolean;   reasoning?: |  {  effort: \"high\" | \"medium\" | \"low\"; }\n |  {  max_tokens?: number; };   stream?: boolean;  };\n      extra_headers?: null | Record<string, string>\n      id?: number;\n      mcp_tool_support?: null | boolean;\n      model_created?: null | string;\n      name: string;\n      pricing_info?: null | {    input: number;   output: number;   cachedInput?: number;   cachedOutput?: number;   threshold?: {  tokenLimit: number;  input: number;  output: number; };  };\n      provider_id: string;\n      supported_parameters?: null |  ( string )[]\n    };\n  };\n  llm_prompts: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      created?: null | string;\n      description?: null | string;\n      id?: number;\n      name?: string;\n      options?: null | {    prompt_type?: \"dashboards\" | \"tasks\" | \"agent_workflow\";  };\n      prompt?: string;\n      user_id?: null | string;\n    };\n  };\n  llm_providers: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      api_docs_url?: null | string;\n      api_pricing_url?: null | string;\n      api_url: string;\n      extra_body?: null | {    temperature?: number;   frequency_penalty?: number;   max_completion_tokens?: number;   max_tokens?: number;   presence_penalty?: number;   response_format?: \"json\" | \"text\" | \"srt\" | \"verbose_json\" | \"vtt\";   think?: boolean;   reasoning?: |  {  effort: \"high\" | \"medium\" | \"low\"; }\n |  {  max_tokens?: number; };   stream?: boolean;  };\n      extra_headers?: null | Record<string, string>\n      id: string;\n      logo_url?: null | string;\n    };\n  };\n  login_attempts: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      auth_provider?: null | string;\n      auth_type: \"session-id\" | \"registration\" | \"email-confirmation\" | \"magic-link-registration\" | \"magic-link\" | \"otp-code\" | \"login\" | \"oauth\"\n      created?: null | string;\n      failed?: null | boolean;\n      id?: string;\n      info?: null | string;\n      ip_address: string;\n      ip_address_remote: string;\n      magic_link_id?: null | string;\n      sid?: null | string;\n      type?: \"web\" | \"api_token\" | \"mobile\"\n      user_agent: string;\n      username?: null | string;\n      x_real_ip: string;\n    };\n  };\n  logs: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      command?: null | string;\n      connection_id?: null | string;\n      created?: null | string;\n      data?: null | any;\n      duration?: null | string;\n      error?: null | any;\n      has_error?: null | boolean;\n      id?: string;\n      sid?: null | string;\n      socket_id?: null | string;\n      table_name?: null | string;\n      tx_info?: null | any;\n      type?: null | string;\n    };\n  };\n  magic_links: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      expires: string;\n      id?: string;\n      magic_link?: null | string;\n      magic_link_used?: null | string;\n      session_expires?: string;\n      user_id: string;\n    };\n  };\n  mcp_server_configs: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      config:  Record<string, any>\n      created?: null | string;\n      id?: number;\n      last_updated?: null | string;\n      server_name: string;\n    };\n  };\n  mcp_server_logs: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      error?: null | string;\n      id?: number;\n      install_error?: null | string;\n      install_log?: null | string;\n      last_updated?: null | string;\n      log?: string;\n      server_name: string;\n    };\n  };\n  mcp_server_tool_calls: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      called?: null | string;\n      chat_id?: null | number;\n      duration: { years?: number; months?: number; days?: number; hours?: number; minutes?: number; seconds?: number; milliseconds?: number; };\n      error?: null | any;\n      id?: number;\n      input?: null | any;\n      mcp_server_config_id?: null | number;\n      mcp_server_name?: null | string;\n      mcp_tool_name: string;\n      output?: null | any;\n      user_id?: null | string;\n    };\n  };\n  mcp_server_tools: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      annotations?: null | {    title?: string;   readOnlyHint?: boolean;   openWorldHint?: boolean;   idempotentHint?: boolean;   destructiveHint?: boolean;  };\n      autoApprove?: null | boolean;\n      description: string;\n      id?: number;\n      inputSchema?: null | any;\n      name: string;\n      server_name: string;\n    };\n  };\n  mcp_servers: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      args?: null | string[];\n      command: \"npx\" | \"npm\" | \"uvx\" | \"uv\" | \"docker\" | \"prostgles-local\"\n      config_schema?: null | Record<string, \n |  {  type: \"env\";  renderWithComponent?: string;  title?: string;  optional?: boolean;  description?: string; }\n |  {  type: \"arg\";  renderWithComponent?: string;  title?: string;  optional?: boolean;  description?: string;  index?: number; }>\n      created?: null | string;\n      cwd?: null | string;\n      enabled?: boolean;\n      env?: null | Record<string, string>\n      env_from_main_process?: null | string[];\n      icon_path?: null | string;\n      info?: null | string;\n      installed?: null | string;\n      name: string;\n      source?: \n       | null\n       |  {  type: \"github\";  name: string;  repoUrl: string;  installationCommands?: (  {  command: string;  args?: string[]; } )[]; }\n       |  {  type: \"code\";  packageJson: string;  tsconfigJson: string;  files: Record<string, string>; }\n      stderr?: null | string;\n    };\n  };\n  published_methods: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      arguments?:  ( \n |  {  name: string;  type: \"any\" | \"string\" | \"number\" | \"boolean\" | \"Date\" | \"time\" | \"timestamp\" | \"string[]\" | \"number[]\" | \"boolean[]\" | \"Date[]\" | \"time[]\" | \"timestamp[]\";  defaultValue?: string;  optional?: boolean;  allowedValues?: string[]; }\n |  {  name: string;  type: \"Lookup\" | \"Lookup[]\";  defaultValue?: any;  optional?: boolean;  lookup: { \"table\": string; \"column\": string; }; }\n |  {  name: string;  type: \"JsonbSchema\";  defaultValue?: any;  optional?: boolean;  schema: |  {  type: \"boolean\" | \"number\" | \"integer\" | \"string\" | \"Date\" | \"time\" | \"timestamp\" | \"any\" | \"boolean[]\" | \"number[]\" | \"integer[]\" | \"string[]\" | \"Date[]\" | \"time[]\" | \"timestamp[]\" | \"any[]\";  optional?: boolean;  nullable?: boolean;  description?: string;  title?: string;  defaultValue?: any; } |  {  type: \"object\" | \"object[]\";  optional?: boolean;  nullable?: boolean;  description?: string;  title?: string;  defaultValue?: any;  properties: Record<string,  {  type: \"boolean\" | \"number\" | \"integer\" | \"string\" | \"Date\" | \"time\" | \"timestamp\" | \"any\" | \"boolean[]\" | \"number[]\" | \"integer[]\" | \"string[]\" | \"Date[]\" | \"time[]\" | \"timestamp[]\" | \"any[]\";  optional?: boolean;  nullable?: boolean;  description?: string;  title?: string;  defaultValue?: any; }>; }; } )[]\n      connection_id?: null | string;\n      description?: string;\n      id?: number;\n      name?: string;\n      outputTable?: null | string;\n      package?: null | any;\n      run?: string;\n      tsconfig?: null | any;\n    };\n  };\n  schema_version: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      id: string;\n      table_config: any;\n    };\n  };\n  services: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      build_hash?: null | string;\n      configs?: null | Record<string,  {  label: string;  description: string;  defaultOption: string;  options: Record<string,  {  label?: string;  env: Record<string, string>; }>; }>\n      created?: null | string;\n      default_port: number;\n      description?: null | string;\n      icon: string;\n      label: string;\n      logs?: null | string;\n      name: string;\n      selected_config_options?: null | Record<string, string>\n      status: \"stopped\" | \"starting\" | \"running\" | \"error\" | \"building\" | \"building-done\" | \"build-error\"\n    };\n  };\n  session_types: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      id: \"web\" | \"api_token\" | \"mobile\"\n    };\n  };\n  sessions: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      active?: null | boolean;\n      created?: null | string;\n      expires: string;\n      id: string;\n      id_num?: number;\n      ip_address: string;\n      is_connected?: null | boolean;\n      is_mobile?: null | boolean;\n      last_used?: null | string;\n      name?: null | string;\n      project_id?: null | string;\n      socket_id?: null | string;\n      type: string;\n      user_agent?: null | string;\n      user_id: string;\n      user_type: string;\n    };\n  };\n  stats: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      application_name?: null | string;\n      backend_start?: null | string;\n      backend_type?: null | string;\n      backend_xid?: null | string;\n      backend_xmin?: null | string;\n      blocked_by?: null | number[];\n      blocked_by_num?: number;\n      client_addr?: null | string;\n      client_hostname?: null | string;\n      client_port?: null | number;\n      cmd?: null | string;\n      cpu?: null | string;\n      database_id: number;\n      datid?: null | number;\n      datname?: null | string;\n      id_query_hash?: null | string;\n      mem?: null | string;\n      memPretty?: null | string;\n      mhz?: null | string;\n      pid: number;\n      query?: null | string;\n      query_start?: null | string;\n      sampled_at?: string;\n      state?: null | string;\n      state_change?: null | string;\n      usename?: null | string;\n      usesysid?: null | number;\n      wait_event?: null | string;\n      wait_event_type?: null | string;\n      xact_start?: null | string;\n    };\n  };\n  user_statuses: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      id: \"active\" | \"disabled\" | \"public\"\n    };\n  };\n  user_types: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      description?: null | string;\n      id: \"admin\" | \"public\" | \"default\"\n    };\n  };\n  users: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      \"2fa\"?: null | {    secret: string;   recoveryCode: string;   enabled: boolean;  };\n      auth_provider?: null | string;\n      auth_provider_profile?: null | any;\n      auth_provider_user_id?: null | string;\n      created?: null | string;\n      email?: null | string;\n      has_2fa_enabled?: null | boolean;\n      id?: string;\n      last_updated?: null | string;\n      name?: null | string;\n      options?: null | {    showStateDB?: boolean;   hideNonSSLWarning?: boolean;   viewedSQLTips?: boolean;   viewedAccessInfo?: boolean;   theme?: \"dark\" | \"light\" | \"from-system\";   speech_mode?: \"off\" | \"stt-local\" | \"stt-web\" | \"audio\";   speech_send_mode?: \"manual\" | \"auto\";  };\n      password: string;\n      passwordless_admin?: null | boolean;\n      registration?: \n       | null\n       |  {  type: \"password-w-email-confirmation\";  email_confirmation: |  {  status: \"confirmed\";  date: string; } |  {  status: \"pending\";  confirmation_code: string;  date: string; }; }\n       |  {  type: \"magic-link\";  otp_code: string;  date: string;  used_on?: string; }\n       |  {  type: \"OAuth\";  provider: \"google\" | \"facebook\" | \"github\" | \"microsoft\" | \"customOAuth\";  user_id: string;  profile: any; }\n      status?: \"active\" | \"disabled\" | \"public\"\n      type?: \"admin\" | \"public\" | \"default\"\n      username: string;\n    };\n  };\n  windows: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      closed?: null | boolean;\n      columns?: null | any;\n      created?: string;\n      deleted?: null | boolean;\n      filter?: any;\n      fullscreen?: null | boolean;\n      having?: any;\n      id?: string;\n      last_updated: string;\n      limit?: null | number;\n      method_name?: null | string;\n      minimised?: null | boolean;\n      name?: null | string;\n      nested_tables?: null | any;\n      options?: any;\n      parent_window_id?: null | string;\n      selected_sql?: string;\n      show_menu?: null | boolean;\n      sort?: null | any;\n      sql?: string;\n      sql_options?: {    executeOptions?: \"full\" | \"block\" | \"smallest-block\";   errorMessageDisplay?: \"tooltip\" | \"bottom\" | \"both\";   tabSize?: number;   lineNumbers?: \"on\" | \"off\";   renderMode?: \"table\" | \"csv\" | \"JSON\";   minimap?: {  enabled: boolean; };   acceptSuggestionOnEnter?: \"on\" | \"smart\" | \"off\";   expandSuggestionDocs?: boolean;   maxCharsPerCell?: number;   theme?: \"vs\" | \"vs-dark\" | \"hc-black\" | \"hc-light\";   showRunningQueryStats?: boolean;  };\n      table_name?: null | string;\n      table_oid?: null | number;\n      title?: null | string;\n      type?: null | \"map\" | \"sql\" | \"table\" | \"timechart\" | \"card\" | \"method\" | \"barchart\"\n      user_id: string;\n      workspace_id?: null | string;\n    };\n  };\n  workspace_layout_modes: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      description?: null | string;\n      en?: null | string;\n      id: \"fixed\" | \"editable\"\n    };\n  };\n  workspaces: {\n    is_view: false;\n    select: true;\n    insert: true;\n    update: true;\n    delete: true;\n    columns: {\n      active_row?: null | any;\n      connection_id: string;\n      created?: null | string;\n      deleted?: boolean;\n      icon?: null | string;\n      id?: string;\n      last_updated: string;\n      last_used?: string;\n      layout?: null | any;\n      layout_mode?: null | \"fixed\" | \"editable\"\n      name?: string;\n      options?: {    hideCounts?: boolean;   tableListEndInfo?: \"none\" | \"count\" | \"size\";   tableListSortBy?: \"name\" | \"extraInfo\";   showAllMyQueries?: boolean;   defaultLayoutType?: \"row\" | \"tab\" | \"col\";   pinnedMenu?: boolean;   pinnedMenuWidth?: number;  };\n      parent_workspace_id?: null | string;\n      published?: boolean;\n      source?: null | {    tool_use_id: string;  };\n      url_path?: null | string;\n      user_id: string;\n    };\n  };\n  \n}\n\n"
  },
  {
    "path": "common/DashboardTypes.d.ts",
    "content": "/**\n * IMPORTANT: all table names in this file MUST be after quote_ident() has been applied.\n * For example, MY_Table will appear as '\"MY_Table\"' in any of the table name related properties below.\n */\nexport type LayoutItem = {\n    /**\n     * UUID of the window\n     */\n    id: string;\n    title?: string;\n    type: \"item\";\n    /**\n     * Table name after quote_ident() has been applied.\n     * This means that any table names with uppercase letters or special characters will be quoted.\n     */\n    tableName: string | null;\n    viewType: \"table\" | \"map\" | \"timechart\" | \"sql\" | \"barchart\";\n    /**\n     * Flex size of the item\n     */\n    size: number;\n    isRoot?: boolean;\n};\nexport type LayoutGroup = {\n    id: string;\n    size: number;\n    isRoot?: boolean;\n} & ({\n    /**\n     * Flex direction of the group\n     */\n    type: \"row\" | \"col\";\n    items: LayoutConfig[];\n} | {\n    /**\n     * Will display windows as tabs\n     */\n    type: \"tab\";\n    items: LayoutItem[];\n    /**\n     * UUID of the currently shown window\n     */\n    activeTabKey: string | undefined;\n});\nexport type LayoutConfig = LayoutItem | LayoutGroup;\n/**\n * This will render a time chart for each row in the table.\n * Useful for showing and comparing time series data for multiple entities\n */\ntype LinkedDataChart = {\n    chart: {\n        type: \"time\";\n        yAxis: {\n            colName: string;\n            funcName: \"$avg\" | \"$sum\" | \"$min\" | \"$max\" | \"$count\";\n            isCountAll: boolean;\n        };\n        dateCol: string;\n    };\n};\n/**\n * This will render nested rows for each row in the table.\n */\ntype LinkedDataTable = {\n    limit: number;\n    columns: Omit<TableColumn, \"nested\">[];\n};\n/**\n * Join to linked table.\n */\ntype TableJoin = {\n    /**\n     * Join columns.\n     * property = root table (or previous table) column name\n     * value = linked table column name\n     * @example\n     * path: {\n     *   on: [{ user_id: \"id\" }]\n     *   table: \"users\"\n     * }\n     */\n    on: Record<string, string>[];\n    /**\n     * Linked table name\n     */\n    table: string;\n};\n/**\n * Show linked data from other tables that are linked to this column through foreign keys\n */\ntype LinkedData = {\n    joinType: \"left\" | \"inner\";\n    /**\n     * Join to linked table.\n     * Last table in the path is the target table that columns will refer to.\n     */\n    path: TableJoin[];\n} & (LinkedDataChart | LinkedDataTable);\ntype Comparator = \"$eq\" | \"$ne\" | \"$lt\" | \"$lte\" | \"$gt\" | \"$gte\";\ntype BasicFilter = {\n    /**\n     * Column name\n     */\n    fieldName: string;\n} & ({\n    type: \"$in\";\n    value: (string | null)[];\n} | {\n    /** Not in */\n    type: \"$nin\";\n    value: (string | null)[];\n} | {\n    type: Comparator;\n    value: string;\n});\ntype ComplexColumnFilterFunction = {\n    /**\n     * Age to current day\n     * Implemented as pg_catalog.age(column)\n     */\n    $age: [\n        /**\n         * Column name with a timestamp / date value\n         */\n        string\n    ];\n} | {\n    /**\n     * Age to current timestamp\n     * Implemented as pg_catalog.age(now(), column)\n     */\n    $ageNow: [\n        /**\n         * Column name with a timestamp / date value\n         */\n        string\n    ];\n};\ntype ComplexColumnFilter = {\n    $filter: [ComplexColumnFilterFunction, Comparator, string | null];\n};\ntype ColumnFilter = BasicFilter | ComplexColumnFilter;\n/**\n * Filter that matches rows based on existence of related rows in another table\n */\ntype JoinedFilter = {\n    $existsJoined: {\n        path: TableJoin[];\n        /**\n         * Filter that will be applied to the joined table (last table in the path)\n         */\n        filter: ColumnFilter;\n    };\n};\ntype FilterItem = ColumnFilter | JoinedFilter;\nexport type Filter = FilterItem | {\n    $and: FilterItem[];\n} | {\n    $or: FilterItem[];\n};\ntype Filtering = {\n    filter?: FilterItem[];\n    /** Defaults to AND */\n    filterOperand?: \"AND\" | \"OR\";\n    /**\n     * Predefined quick filters that the user can toggle on/off\n     * These are shown in the filter bar under \"Quick Filters\"\n     * MUST ENSURE FILTER VALUES FOR FOREIGN KEYS EXIST IN THE DATABASE\n     */\n    quickFilterGroups?: {\n        [groupName: string]: {\n            toggledFilterName?: string;\n            filters: {\n                [filterName: string]: Filter;\n            };\n        };\n    };\n};\ntype TableColumn = {\n    /**\n     * Column name as it appears in the database.\n     * For nested columns this can be anything. Use the table name or a more descriptive name.\n     */\n    name: string;\n    /**\n     * Show linked data from other tables that are linked to this column through foreign keys.\n     * If defined then \"name\" from above should be used as a label for the nested data.\n     */\n    nested?: LinkedData;\n    /**\n     * Column width in pixels\n     */\n    width: number;\n    /**\n     * Render column value in a chip\n     * Cannot be used with nested\n     */\n    styling?: {\n        type: \"conditional\";\n        conditions: {\n            chipColor: \"red\" | \"pink\" | \"purple\" | \"blue\" | \"indigo\" | \"green\" | \"yellow\" | \"gray\";\n            operator: \"=\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\";\n            value: string;\n        }[];\n    };\n    /**\n     * If set, column value will rendered in a specific way\n     */\n    format?: {\n        /**\n         * Column value will be rendered as a link with specific behaviour\n         */\n        type: \"URL\" | \"Email\" | \"Tel\";\n    } | {\n        /**\n         * Render column value as a scannable QR code image\n         */\n        type: \"QR Code\";\n    } | {\n        /** Display large numbers with metric prefixes (e.g. 1.2K) */\n        type: \"Metric Prefix\";\n    } | {\n        /**\n         * Render column value with a currency symbol\n         */\n        type: \"Currency\";\n        params: {\n            mode: \"Fixed\";\n            /** @example \"USD\" */\n            currencyCode: string;\n            metricPrefix?: boolean;\n        } | {\n            mode: \"From column\";\n            /** Column which contains the currency code  */\n            currencyCodeField: string;\n            metricPrefix?: boolean;\n        };\n    } | {\n        /** Display the timestamp value as an age. Short variant (default) shows top two biggest units */\n        type: \"Age\";\n        params?: {\n            variant: \"short\" | \"full\";\n        };\n    } | {\n        /** Text content as sanitised html */\n        type: \"HTML\";\n    } | {\n        /** Displays the media from URL. Accepted formats: image, audio or video. Media/Mime type will be used from headers */\n        type: \"Media\";\n    };\n};\n/**\n * Represents a rendered cell in a card layout\n */\ntype CardLayoutRowColumnValue = {\n    type: \"node\";\n    columnName: string;\n    /**\n     * React.CSSProperties;\n     */\n    style?: Record<string, string | number>;\n    /**\n     * If true, label will be hidden and only value will be shown\n     */\n    hideLabel?: boolean;\n};\n/**\n * Renders a div element with specified style and contents.\n * Used to arrange children in flex row/column/row-wrapped layouts for efficient content density.\n */\nexport type CardLayout = {\n    type?: \"container\";\n    /**\n     * React.CSSProperties;\n     */\n    style?: Record<string, string | number>;\n    children: (CardLayout | CardLayoutRowColumnValue)[];\n};\nexport type TableWindowInsertModel = Filtering & {\n    id: string;\n    type: \"table\";\n    /**\n     * Optional title that will be shown in the window header (Defaults to table_name).\n     * Supports template variable ${rowCount} which will be replaced with the actual number of rows in the table.\n     */\n    title?: string;\n    table_name: string;\n    columns?: TableColumn[];\n    /**\n     * Sort order when of type 'table'\n     */\n    sort?: null | {\n        /**\n         * Column name\n         */\n        key: string;\n        asc: boolean;\n        nulls: \"first\" | \"last\";\n    }[];\n    /**\n     * Layout used when the table is switched to the card list view mode, where each row is shown as a card.\n     */\n    cardLayout: CardLayout;\n};\ntype LayerDataSource = (Filtering & {\n    type: \"local-table\";\n    table_name: string;\n    /**\n     * Join to linked table (table_name is root table).\n     * The charted columns must be from the end table while the filters are from the root table (table_name).\n     */\n    joinPath?: TableJoin[];\n}) | {\n    type: \"sql\";\n    sql: string;\n};\n/**\n * Shows GEOGRAPHY/GEOMETRY data on a map\n */\nexport type MapWindowInsertModel = {\n    id: string;\n    type: \"map\";\n    title?: string;\n    layers: (LayerDataSource & {\n        title?: string;\n        /**\n         * Column name with GEOGRAPHY/GEOMETRY data\n         */\n        geoColumn: string;\n    })[];\n};\n/**\n * Allows user to write and excute custom SQL queries with results displayed in a table\n */\nexport type SqlWindowInsertModel = {\n    id: string;\n    name: string;\n    type: \"sql\";\n    sql: string;\n};\n/**\n * Shows a time chart\n */\nexport type TimechartWindowInsertModel = {\n    id: string;\n    type: \"timechart\";\n    title?: string;\n    layers: (LayerDataSource & {\n        title?: string;\n        dateColumn: string;\n        groupByColumn?: string;\n        yAxis: \"count(*)\" | {\n            aggregation: \"sum\" | \"avg\" | \"min\" | \"max\" | \"count\";\n            column: string;\n        };\n    })[];\n};\nexport type BarchartWindowInsertModel = ((Filtering & {\n    table_name: string;\n}) | {\n    sql: string;\n}) & {\n    id: string;\n    type: \"barchart\";\n    title?: string;\n    xAxis: {\n        column: string;\n        aggregation: \"sum\" | \"avg\" | \"min\" | \"max\" | \"count\" | \"count(*)\";\n        /**\n         * Join to linked table (table_name is root table).\n         * The xAxis.column must be from the end table while the filters are from the root table (table_name).\n         */\n        joinPath?: TableJoin[];\n    };\n    yAxisColumn: string;\n};\nexport type WindowInsertModel = MapWindowInsertModel | SqlWindowInsertModel | TableWindowInsertModel | TimechartWindowInsertModel | BarchartWindowInsertModel;\nexport type WorkspaceInsertModel = {\n    name: string;\n    /**\n     * MDI camel case icon name for the workspace that will be shown near the workspace name.\n     * example: \"AccountCancel\", \"BriefcaseOutline\", \"CalendarQuestion\"\n     * Should ideally be specified when an existing icon gives a good visual description of the workspace.\n     */\n    icon?: string;\n    layout: LayoutGroup;\n    windows: WindowInsertModel[];\n};\nexport {};\n//# sourceMappingURL=DashboardTypes.d.ts.map"
  },
  {
    "path": "common/DashboardTypes.js",
    "content": "/**\n * IMPORTANT: all table names in this file MUST be after quote_ident() has been applied.\n * For example, MY_Table will appear as '\"MY_Table\"' in any of the table name related properties below.\n */\nexport {};\n"
  },
  {
    "path": "common/DashboardTypes.ts",
    "content": "/**\n * IMPORTANT: all table names in this file MUST be after quote_ident() has been applied.\n * For example, MY_Table will appear as '\"MY_Table\"' in any of the table name related properties below.\n */\n\nexport type LayoutItem = {\n  /**\n   * UUID of the window\n   */\n  id: string;\n  title?: string;\n  type: \"item\";\n  /**\n   * Table name after quote_ident() has been applied.\n   * This means that any table names with uppercase letters or special characters will be quoted.\n   */\n  tableName: string | null;\n  viewType: \"table\" | \"map\" | \"timechart\" | \"sql\" | \"barchart\";\n  /**\n   * Flex size of the item\n   */\n  size: number;\n  isRoot?: boolean;\n};\nexport type LayoutGroup = {\n  id: string;\n  size: number;\n  isRoot?: boolean;\n} & (\n  | {\n      /**\n       * Flex direction of the group\n       */\n      type: \"row\" | \"col\";\n      items: LayoutConfig[];\n    }\n  | {\n      /**\n       * Will display windows as tabs\n       */\n      type: \"tab\";\n      items: LayoutItem[];\n      /**\n       * UUID of the currently shown window\n       */\n      activeTabKey: string | undefined;\n    }\n);\n\nexport type LayoutConfig = LayoutItem | LayoutGroup;\n\n/**\n * This will render a time chart for each row in the table.\n * Useful for showing and comparing time series data for multiple entities\n */\ntype LinkedDataChart = {\n  chart: {\n    type: \"time\";\n    yAxis: {\n      colName: string;\n      funcName: \"$avg\" | \"$sum\" | \"$min\" | \"$max\" | \"$count\";\n      isCountAll: boolean;\n    };\n    dateCol: string;\n  };\n};\n\n/**\n * This will render nested rows for each row in the table.\n */\ntype LinkedDataTable = {\n  limit: number;\n  columns: Omit<TableColumn, \"nested\">[];\n};\n\n/**\n * Join to linked table.\n */\ntype TableJoin = {\n  /**\n   * Join columns.\n   * property = root table (or previous table) column name\n   * value = linked table column name\n   * @example\n   * path: {\n   *   on: [{ user_id: \"id\" }]\n   *   table: \"users\"\n   * }\n   */\n  on: Record<string, string>[];\n  /**\n   * Linked table name\n   */\n  table: string;\n};\n\n/**\n * Show linked data from other tables that are linked to this column through foreign keys\n */\ntype LinkedData = {\n  joinType: \"left\" | \"inner\";\n  /**\n   * Join to linked table.\n   * Last table in the path is the target table that columns will refer to.\n   */\n  path: TableJoin[];\n} & (LinkedDataChart | LinkedDataTable);\n\ntype Comparator = \"$eq\" | \"$ne\" | \"$lt\" | \"$lte\" | \"$gt\" | \"$gte\";\n\ntype BasicFilter = {\n  /**\n   * Column name\n   */\n  fieldName: string;\n} & (\n  | {\n      type: \"$in\";\n      value: (string | null)[];\n    }\n  | {\n      /** Not in */\n      type: \"$nin\";\n      value: (string | null)[];\n    }\n  | {\n      type: Comparator;\n      value: string;\n    }\n);\n\ntype ComplexColumnFilterFunction =\n  | {\n      /**\n       * Age to current day\n       * Implemented as pg_catalog.age(column)\n       */\n      $age: [\n        /**\n         * Column name with a timestamp / date value\n         */\n        string,\n      ];\n    }\n  | {\n      /**\n       * Age to current timestamp\n       * Implemented as pg_catalog.age(now(), column)\n       */\n      $ageNow: [\n        /**\n         * Column name with a timestamp / date value\n         */\n        string,\n      ];\n    };\ntype ComplexColumnFilter = {\n  $filter: [ComplexColumnFilterFunction, Comparator, string | null];\n};\n\ntype ColumnFilter = BasicFilter | ComplexColumnFilter;\n\n/**\n * Filter that matches rows based on existence of related rows in another table\n */\ntype JoinedFilter = {\n  $existsJoined: {\n    path: TableJoin[];\n    /**\n     * Filter that will be applied to the joined table (last table in the path)\n     */\n    filter: ColumnFilter;\n  };\n};\n\ntype FilterItem = ColumnFilter | JoinedFilter;\n\nexport type Filter =\n  | FilterItem\n  | {\n      $and: FilterItem[];\n    }\n  | {\n      $or: FilterItem[];\n    };\n\ntype Filtering = {\n  filter?: FilterItem[];\n  /** Defaults to AND */\n  filterOperand?: \"AND\" | \"OR\";\n\n  /**\n   * Predefined quick filters that the user can toggle on/off\n   * These are shown in the filter bar under \"Quick Filters\"\n   * MUST ENSURE FILTER VALUES FOR FOREIGN KEYS EXIST IN THE DATABASE\n   */\n  quickFilterGroups?: {\n    [groupName: string]: {\n      toggledFilterName?: string;\n      filters: {\n        [filterName: string]: Filter;\n      };\n    };\n  };\n};\n\ntype TableColumn = {\n  /**\n   * Column name as it appears in the database.\n   * For nested columns this can be anything. Use the table name or a more descriptive name.\n   */\n  name: string;\n\n  /**\n   * Show linked data from other tables that are linked to this column through foreign keys.\n   * If defined then \"name\" from above should be used as a label for the nested data.\n   */\n  nested?: LinkedData;\n\n  /**\n   * Column width in pixels\n   */\n  width: number;\n\n  /**\n   * Render column value in a chip\n   * Cannot be used with nested\n   */\n  styling?: {\n    type: \"conditional\";\n    conditions: {\n      chipColor:\n        | \"red\"\n        | \"pink\"\n        | \"purple\"\n        | \"blue\"\n        | \"indigo\"\n        | \"green\"\n        | \"yellow\"\n        | \"gray\";\n      operator: \"=\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\";\n      value: string;\n    }[];\n  };\n\n  /**\n   * If set, column value will rendered in a specific way\n   */\n  format?:\n    | {\n        /**\n         * Column value will be rendered as a link with specific behaviour\n         */\n        type: \"URL\" | \"Email\" | \"Tel\";\n      }\n    | {\n        /**\n         * Render column value as a scannable QR code image\n         */\n        type: \"QR Code\";\n      }\n    | {\n        /** Display large numbers with metric prefixes (e.g. 1.2K) */\n        type: \"Metric Prefix\";\n      }\n    | {\n        /**\n         * Render column value with a currency symbol\n         */\n        type: \"Currency\";\n        params:\n          | {\n              mode: \"Fixed\";\n              /** @example \"USD\" */\n              currencyCode: string;\n              metricPrefix?: boolean;\n            }\n          | {\n              mode: \"From column\";\n              /** Column which contains the currency code  */\n              currencyCodeField: string;\n              metricPrefix?: boolean;\n            };\n      }\n    | {\n        /** Display the timestamp value as an age. Short variant (default) shows top two biggest units */\n        type: \"Age\";\n        params?: {\n          variant: \"short\" | \"full\";\n        };\n      }\n    | {\n        /** Text content as sanitised html */\n        type: \"HTML\";\n      }\n    | {\n        /** Displays the media from URL. Accepted formats: image, audio or video. Media/Mime type will be used from headers */\n        type: \"Media\";\n      };\n};\n\n/**\n * Represents a rendered cell in a card layout\n */\ntype CardLayoutRowColumnValue = {\n  type: \"node\";\n  columnName: string;\n  /**\n   * React.CSSProperties;\n   */\n  style?: Record<string, string | number>;\n  /**\n   * If true, label will be hidden and only value will be shown\n   */\n  hideLabel?: boolean;\n};\n\n/**\n * Renders a div element with specified style and contents.\n * Used to arrange children in flex row/column/row-wrapped layouts for efficient content density.\n */\nexport type CardLayout = {\n  type?: \"container\";\n  /**\n   * React.CSSProperties;\n   */\n  style?: Record<string, string | number>;\n  children: (CardLayout | CardLayoutRowColumnValue)[];\n};\n\nexport type TableWindowInsertModel = Filtering & {\n  id: string;\n  type: \"table\";\n  /**\n   * Optional title that will be shown in the window header (Defaults to table_name).\n   * Supports template variable ${rowCount} which will be replaced with the actual number of rows in the table.\n   */\n  title?: string;\n  table_name: string;\n  columns?: TableColumn[];\n\n  /**\n   * Sort order when of type 'table'\n   */\n  sort?:\n    | null\n    | {\n        /**\n         * Column name\n         */\n        key: string;\n        asc: boolean;\n        nulls: \"first\" | \"last\";\n      }[];\n\n  /**\n   * Layout used when the table is switched to the card list view mode, where each row is shown as a card.\n   */\n  cardLayout: CardLayout;\n};\n\ntype LayerDataSource =\n  | (Filtering & {\n      type: \"local-table\";\n      table_name: string;\n      /**\n       * Join to linked table (table_name is root table).\n       * The charted columns must be from the end table while the filters are from the root table (table_name).\n       */\n      joinPath?: TableJoin[];\n    })\n  | {\n      type: \"sql\";\n      sql: string;\n    };\n\n/**\n * Shows GEOGRAPHY/GEOMETRY data on a map\n */\nexport type MapWindowInsertModel = {\n  id: string;\n  type: \"map\";\n  title?: string;\n  layers: (LayerDataSource & {\n    title?: string;\n    /**\n     * Column name with GEOGRAPHY/GEOMETRY data\n     */\n    geoColumn: string;\n  })[];\n};\n\n/**\n * Allows user to write and excute custom SQL queries with results displayed in a table\n */\nexport type SqlWindowInsertModel = {\n  id: string;\n  name: string;\n  type: \"sql\";\n  sql: string;\n};\n\n/**\n * Shows a time chart\n */\nexport type TimechartWindowInsertModel = {\n  id: string;\n  type: \"timechart\";\n  title?: string;\n  layers: (LayerDataSource & {\n    title?: string;\n    dateColumn: string;\n    groupByColumn?: string;\n    yAxis:\n      | \"count(*)\"\n      | {\n          aggregation: \"sum\" | \"avg\" | \"min\" | \"max\" | \"count\";\n          column: string;\n        };\n  })[];\n};\nexport type BarchartWindowInsertModel = (\n  | (Filtering & {\n      table_name: string;\n    })\n  | {\n      sql: string;\n    }\n) & {\n  id: string;\n  type: \"barchart\";\n  title?: string;\n  xAxis: {\n    column: string;\n    aggregation: \"sum\" | \"avg\" | \"min\" | \"max\" | \"count\" | \"count(*)\";\n    /**\n     * Join to linked table (table_name is root table).\n     * The xAxis.column must be from the end table while the filters are from the root table (table_name).\n     */\n    joinPath?: TableJoin[];\n  };\n  yAxisColumn: string;\n};\n\nexport type WindowInsertModel =\n  | MapWindowInsertModel\n  | SqlWindowInsertModel\n  | TableWindowInsertModel\n  | TimechartWindowInsertModel\n  | BarchartWindowInsertModel;\n\nexport type WorkspaceInsertModel = {\n  name: string;\n  /**\n   * MDI camel case icon name for the workspace that will be shown near the workspace name.\n   * example: \"AccountCancel\", \"BriefcaseOutline\", \"CalendarQuestion\"\n   * Should ideally be specified when an existing icon gives a good visual description of the workspace.\n   */\n  icon?: string;\n  layout: LayoutGroup;\n  windows: WindowInsertModel[];\n};\n"
  },
  {
    "path": "common/OAuthUtils.d.ts",
    "content": "export declare const OAuthProviderOptions: {\n    google: {\n        scopes: {\n            key: string;\n            subLabel: string;\n        }[];\n    };\n    facebook: {\n        scopes: {\n            key: string;\n            subLabel: string;\n        }[];\n    };\n    github: {\n        scopes: {\n            key: string;\n            subLabel: string;\n        }[];\n    };\n    microsoft: {\n        scopes: {\n            key: string;\n            subLabel: string;\n        }[];\n        /**\n         * Indicates the type of user interaction that is required.\n         * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/request/AuthorizationUrlRequest.ts\n         */\n        prompts: {\n            key: string;\n            subLabel: string;\n        }[];\n    };\n    customOAuth: {};\n};\nexport declare const EMAIL_CONFIRMED_SEARCH_PARAM: \"email-confirmed\";\nexport declare const PASSWORDLESS_ADMIN_USERNAME = \"passwordless_admin\";\ntype TemplateData = Record<string, {\n    required?: boolean;\n    value: string;\n}>;\nexport declare const getEmailFromTemplate: <EmailTemplate extends {\n    body: string;\n    from: string;\n    subject: string;\n}>(template: EmailTemplate, subjectData: TemplateData, bodyData: TemplateData) => EmailTemplate;\nexport declare const MOCK_SMTP_HOST = \"prostgles-test-mock\";\nexport declare const DEFAULT_EMAIL_VERIFICATION_TEMPLATE: {\n    readonly from: \"noreply@abc.com\";\n    readonly subject: \"Please verify your email address\";\n    readonly body: string;\n};\nexport declare const DEFAULT_MAGIC_LINK_TEMPLATE: {\n    readonly from: \"noreply@abc.com\";\n    readonly subject: \"Login to your account\";\n    readonly body: string;\n};\nexport declare const getMagicLinkEmailFromTemplate: ({ url, template, code, }: {\n    url: string;\n    code: string;\n    template: {\n        from: string;\n        subject: string;\n        body: string;\n    };\n}) => {\n    from: string;\n    subject: string;\n    body: string;\n};\nexport declare const getVerificationEmailFromTemplate: ({ url, code, template, }: {\n    code: string;\n    url: string;\n    template: {\n        from: string;\n        subject: string;\n        body: string;\n    };\n}) => {\n    from: string;\n    subject: string;\n    body: string;\n};\nexport declare const ELECTRON_USER_AGENT = \"electron\";\nexport declare const DOCKER_USER_AGENT = \"docker-mcp\";\nexport {};\n//# sourceMappingURL=OAuthUtils.d.ts.map"
  },
  {
    "path": "common/OAuthUtils.js",
    "content": "import { fixIndent } from \"./utils\";\nexport const OAuthProviderOptions = {\n    google: {\n        scopes: [\n            {\n                key: \"profile\",\n                subLabel: \"View your basic profile info\",\n            },\n            {\n                key: \"email\",\n                subLabel: \"View your email address\",\n            },\n            {\n                key: \"calendar\",\n                subLabel: \"View and manage your calendars\",\n            },\n            {\n                key: \"calendar.readonly\",\n                subLabel: \"View your calendars\",\n            },\n            {\n                key: \"calendar.events\",\n                subLabel: \"View and manage calendar events\",\n            },\n            {\n                key: \"calendar.events.readonly\",\n                subLabel: \"View calendar events\",\n            },\n        ],\n    },\n    facebook: {\n        scopes: [\n            {\n                key: \"email\",\n                subLabel: \"Access user's primary email address\",\n            },\n            {\n                key: \"public_profile\",\n                subLabel: \"Basic public profile information\",\n            },\n            {\n                key: \"user_birthday\",\n                subLabel: \"Access user's birthday\",\n            },\n            {\n                key: \"user_friends\",\n                subLabel: \"Access user's friend list\",\n            },\n            {\n                key: \"user_gender\",\n                subLabel: \"Access user's gender\",\n            },\n            {\n                key: \"user_hometown\",\n                subLabel: \"Access user's hometown\",\n            },\n        ],\n    },\n    github: {\n        scopes: [\n            {\n                key: \"read:user\",\n                subLabel: \"Read all user profile data\",\n            },\n            {\n                key: \"user:email\",\n                subLabel: \"Access user email addresses (read-only)\",\n            },\n        ],\n    },\n    microsoft: {\n        scopes: [\n            {\n                key: \"openid\",\n                subLabel: \"Sign you in using your OpenID Connect profile\",\n            },\n            {\n                key: \"profile\",\n                subLabel: \"View your basic profile info\",\n            },\n            {\n                key: \"email\",\n                subLabel: \"View your email address\",\n            },\n            {\n                key: \"offline_access\",\n                subLabel: \"Maintain access to data you have given it access to\",\n            },\n            {\n                key: \"User.Read\",\n                subLabel: \"Sign in and read user profile\",\n            },\n            {\n                key: \"User.ReadBasic.All\",\n                subLabel: \"Read all users' basic profiles\",\n            },\n            {\n                key: \"User.Read.All\",\n                subLabel: \"Read all users' full profiles\",\n            },\n        ],\n        /**\n         * Indicates the type of user interaction that is required.\n         * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/request/AuthorizationUrlRequest.ts\n         */\n        prompts: [\n            {\n                key: \"login\",\n                subLabel: \"will force the user to enter their credentials on that request, negating single-sign on\",\n            },\n            {\n                key: \"none\",\n                subLabel: \" will ensure that the user isn't presented with any interactive prompt. if request can't be completed via single-sign on, the endpoint will return an interaction_required error\",\n            },\n            {\n                key: \"consent\",\n                subLabel: \"will the trigger the OAuth consent dialog after the user signs in, asking the user to grant permissions to the app\",\n            },\n            {\n                key: \"select_account\",\n                subLabel: \"will interrupt single sign-on providing account selection experience listing all the accounts in session or any remembered accounts or an option to choose to use a different account\",\n            },\n            {\n                key: \"create\",\n                subLabel: \"will direct the user to the account creation experience instead of the log in experience\",\n            },\n        ],\n    },\n    customOAuth: {},\n};\nexport const EMAIL_CONFIRMED_SEARCH_PARAM = \"email-confirmed\";\nexport const PASSWORDLESS_ADMIN_USERNAME = \"passwordless_admin\";\nconst getTemplatedText = (templatedText, data, propertyName) => {\n    var _a;\n    const extraPlaceholders = (_a = templatedText\n        .match(/{{\\w+}}/g)) === null || _a === void 0 ? void 0 : _a.filter((placeHolder) => {\n        const key = placeHolder.slice(2, -2);\n        return !Object.keys(data).includes(key);\n    });\n    if (extraPlaceholders === null || extraPlaceholders === void 0 ? void 0 : extraPlaceholders.length)\n        throw `Extra placeholders: ${extraPlaceholders.join(\", \")} in ${propertyName}`;\n    return Object.entries(data).reduce((acc, [key, { value, required }]) => {\n        const placeholder = \"{{\" + key + \"}}\";\n        if (required && !value)\n            throw `Missing required value for key: ${key}`;\n        if (required && !acc.includes(placeholder)) {\n            throw `Missing placeholder: ${placeholder} from ${propertyName}`;\n        }\n        return acc.replaceAll(placeholder, value);\n    }, templatedText);\n};\nexport const getEmailFromTemplate = (template, subjectData, bodyData) => {\n    const keyValues = Object.entries(bodyData);\n    if (!keyValues.length)\n        throw \"Empty bodyData provided\";\n    if (!template.body)\n        throw \"Email template: No body provided\";\n    if (!template.from)\n        throw \"Email template: No from provided\";\n    if (!template.subject)\n        throw \"Email template: No subject provided\";\n    return Object.assign(Object.assign({}, template), { body: getTemplatedText(template.body, bodyData, \"body\"), subject: getTemplatedText(template.subject, subjectData, \"subject\") });\n};\nexport const MOCK_SMTP_HOST = \"prostgles-test-mock\";\nexport const DEFAULT_EMAIL_VERIFICATION_TEMPLATE = {\n    from: `noreply@abc.com`,\n    subject: \"Please verify your email address\",\n    body: fixIndent(`\n    Hello,\n    <br/><br/>\n    Somebody just used this email address to sign up.\n    <br/><br/>\n    If this was you, verify your email by clicking on the link below:\n    <br/><br/>\n    <a href=\"{{url}}\">{{url}}</a>.\n    <br/><br/>\n    Alternatively, you can fill in the code below on the login page:\n    <br/><br/>\n    <strong>{{code}}</strong>\n    <br/><br/>\n\n    If this was not you, any other accounts you may own, and your internet properties are not at risk.\n`),\n};\nexport const DEFAULT_MAGIC_LINK_TEMPLATE = {\n    from: `noreply@abc.com`,\n    subject: \"Login to your account\",\n    body: fixIndent(`\n    Hey,\n    <br/><br/>\n    Login by clicking <a href=\"{{url}}\">here</a>. Or by entering the code below on the login page:\n    <br/><br/>\n    {{code}}\n    <br/><br/>\n    If you didn't request this email there's nothing to worry about - you can safely ignore it.`),\n};\nexport const getMagicLinkEmailFromTemplate = ({ url, template, code, }) => {\n    return getEmailFromTemplate(template, {}, {\n        code: { required: true, value: code },\n        url: { required: true, value: url },\n    });\n};\nexport const getVerificationEmailFromTemplate = ({ url, code, template, }) => {\n    return getEmailFromTemplate(template, {\n        code: { required: false, value: code },\n    }, {\n        code: { required: true, value: code },\n        url: { required: true, value: url },\n    });\n};\ntry {\n    getMagicLinkEmailFromTemplate({\n        url: \"a\",\n        code: \"a\",\n        template: DEFAULT_MAGIC_LINK_TEMPLATE,\n    });\n    getVerificationEmailFromTemplate({\n        template: DEFAULT_EMAIL_VERIFICATION_TEMPLATE,\n        code: \"a\",\n        url: \"a\",\n    });\n}\ncatch (e) {\n    // console.trace(e);\n    throw e;\n}\nexport const ELECTRON_USER_AGENT = \"electron\";\nexport const DOCKER_USER_AGENT = \"docker-mcp\";\n"
  },
  {
    "path": "common/OAuthUtils.ts",
    "content": "import { fixIndent } from \"./utils\";\n\nexport const OAuthProviderOptions = {\n  google: {\n    scopes: [\n      {\n        key: \"profile\",\n        subLabel: \"View your basic profile info\",\n      },\n      {\n        key: \"email\",\n        subLabel: \"View your email address\",\n      },\n      {\n        key: \"calendar\",\n        subLabel: \"View and manage your calendars\",\n      },\n      {\n        key: \"calendar.readonly\",\n        subLabel: \"View your calendars\",\n      },\n      {\n        key: \"calendar.events\",\n        subLabel: \"View and manage calendar events\",\n      },\n      {\n        key: \"calendar.events.readonly\",\n        subLabel: \"View calendar events\",\n      },\n    ],\n  },\n  facebook: {\n    scopes: [\n      {\n        key: \"email\",\n        subLabel: \"Access user's primary email address\",\n      },\n      {\n        key: \"public_profile\",\n        subLabel: \"Basic public profile information\",\n      },\n      {\n        key: \"user_birthday\",\n        subLabel: \"Access user's birthday\",\n      },\n      {\n        key: \"user_friends\",\n        subLabel: \"Access user's friend list\",\n      },\n      {\n        key: \"user_gender\",\n        subLabel: \"Access user's gender\",\n      },\n      {\n        key: \"user_hometown\",\n        subLabel: \"Access user's hometown\",\n      },\n    ],\n  },\n  github: {\n    scopes: [\n      {\n        key: \"read:user\",\n        subLabel: \"Read all user profile data\",\n      },\n      {\n        key: \"user:email\",\n        subLabel: \"Access user email addresses (read-only)\",\n      },\n    ],\n  },\n  microsoft: {\n    scopes: [\n      {\n        key: \"openid\",\n        subLabel: \"Sign you in using your OpenID Connect profile\",\n      },\n      {\n        key: \"profile\",\n        subLabel: \"View your basic profile info\",\n      },\n      {\n        key: \"email\",\n        subLabel: \"View your email address\",\n      },\n      {\n        key: \"offline_access\",\n        subLabel: \"Maintain access to data you have given it access to\",\n      },\n      {\n        key: \"User.Read\",\n        subLabel: \"Sign in and read user profile\",\n      },\n      {\n        key: \"User.ReadBasic.All\",\n        subLabel: \"Read all users' basic profiles\",\n      },\n      {\n        key: \"User.Read.All\",\n        subLabel: \"Read all users' full profiles\",\n      },\n    ],\n    /**\n     * Indicates the type of user interaction that is required.\n     * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/request/AuthorizationUrlRequest.ts\n     */\n    prompts: [\n      {\n        key: \"login\",\n        subLabel:\n          \"will force the user to enter their credentials on that request, negating single-sign on\",\n      },\n      {\n        key: \"none\",\n        subLabel:\n          \" will ensure that the user isn't presented with any interactive prompt. if request can't be completed via single-sign on, the endpoint will return an interaction_required error\",\n      },\n      {\n        key: \"consent\",\n        subLabel:\n          \"will the trigger the OAuth consent dialog after the user signs in, asking the user to grant permissions to the app\",\n      },\n      {\n        key: \"select_account\",\n        subLabel:\n          \"will interrupt single sign-on providing account selection experience listing all the accounts in session or any remembered accounts or an option to choose to use a different account\",\n      },\n      {\n        key: \"create\",\n        subLabel:\n          \"will direct the user to the account creation experience instead of the log in experience\",\n      },\n    ],\n  },\n  customOAuth: {},\n};\n\nexport const EMAIL_CONFIRMED_SEARCH_PARAM = \"email-confirmed\" as const;\n\nexport const PASSWORDLESS_ADMIN_USERNAME = \"passwordless_admin\";\n\ntype TemplateData = Record<\n  string,\n  {\n    required?: boolean;\n    value: string;\n  }\n>;\n\nconst getTemplatedText = (\n  templatedText: string,\n  data: TemplateData,\n  propertyName: string,\n) => {\n  const extraPlaceholders = templatedText\n    .match(/{{\\w+}}/g)\n    ?.filter((placeHolder) => {\n      const key = placeHolder.slice(2, -2);\n      return !Object.keys(data).includes(key);\n    });\n  if (extraPlaceholders?.length)\n    throw `Extra placeholders: ${extraPlaceholders.join(\", \")} in ${propertyName}`;\n  return Object.entries(data).reduce((acc, [key, { value, required }]) => {\n    const placeholder = \"{{\" + key + \"}}\";\n    if (required && !value) throw `Missing required value for key: ${key}`;\n    if (required && !acc.includes(placeholder)) {\n      throw `Missing placeholder: ${placeholder} from ${propertyName}`;\n    }\n    return acc.replaceAll(placeholder, value);\n  }, templatedText);\n};\n\nexport const getEmailFromTemplate = <\n  EmailTemplate extends { body: string; from: string; subject: string },\n>(\n  template: EmailTemplate,\n  subjectData: TemplateData,\n  bodyData: TemplateData,\n): EmailTemplate => {\n  const keyValues = Object.entries(bodyData);\n  if (!keyValues.length) throw \"Empty bodyData provided\";\n  if (!template.body) throw \"Email template: No body provided\";\n  if (!template.from) throw \"Email template: No from provided\";\n  if (!template.subject) throw \"Email template: No subject provided\";\n\n  return {\n    ...template,\n    body: getTemplatedText(template.body, bodyData, \"body\"),\n    subject: getTemplatedText(template.subject, subjectData, \"subject\"),\n  };\n};\n\nexport const MOCK_SMTP_HOST = \"prostgles-test-mock\";\n\nexport const DEFAULT_EMAIL_VERIFICATION_TEMPLATE = {\n  from: `noreply@abc.com`,\n  subject: \"Please verify your email address\",\n  body: fixIndent(`\n    Hello,\n    <br/><br/>\n    Somebody just used this email address to sign up.\n    <br/><br/>\n    If this was you, verify your email by clicking on the link below:\n    <br/><br/>\n    <a href=\"{{url}}\">{{url}}</a>.\n    <br/><br/>\n    Alternatively, you can fill in the code below on the login page:\n    <br/><br/>\n    <strong>{{code}}</strong>\n    <br/><br/>\n\n    If this was not you, any other accounts you may own, and your internet properties are not at risk.\n`),\n} as const;\n\nexport const DEFAULT_MAGIC_LINK_TEMPLATE = {\n  from: `noreply@abc.com`,\n  subject: \"Login to your account\",\n  body: fixIndent(`\n    Hey,\n    <br/><br/>\n    Login by clicking <a href=\"{{url}}\">here</a>. Or by entering the code below on the login page:\n    <br/><br/>\n    {{code}}\n    <br/><br/>\n    If you didn't request this email there's nothing to worry about - you can safely ignore it.`),\n} as const;\n\nexport const getMagicLinkEmailFromTemplate = ({\n  url,\n  template,\n  code,\n}: {\n  url: string;\n  code: string;\n  template: { from: string; subject: string; body: string };\n}) => {\n  return getEmailFromTemplate(\n    template,\n    {},\n    {\n      code: { required: true, value: code },\n      url: { required: true, value: url },\n    },\n  );\n};\n\nexport const getVerificationEmailFromTemplate = ({\n  url,\n  code,\n  template,\n}: {\n  code: string;\n  url: string;\n  template: { from: string; subject: string; body: string };\n}) => {\n  return getEmailFromTemplate(\n    template,\n    {\n      code: { required: false, value: code },\n    },\n    {\n      code: { required: true, value: code },\n      url: { required: true, value: url },\n    },\n  );\n};\ntry {\n  getMagicLinkEmailFromTemplate({\n    url: \"a\",\n    code: \"a\",\n    template: DEFAULT_MAGIC_LINK_TEMPLATE,\n  });\n  getVerificationEmailFromTemplate({\n    template: DEFAULT_EMAIL_VERIFICATION_TEMPLATE,\n    code: \"a\",\n    url: \"a\",\n  });\n} catch (e) {\n  // console.trace(e);\n  throw e;\n}\n\nexport const ELECTRON_USER_AGENT = \"electron\";\nexport const DOCKER_USER_AGENT = \"docker-mcp\";\n"
  },
  {
    "path": "common/dashboardTypesContent.d.ts",
    "content": "/**\n * Generated file. Do not edit.\n * https://github.com/electron-userland/electron-builder/issues/5064\n */\nexport declare const dashboardTypesContent = \"/**\\n * IMPORTANT: all table names in this file MUST be after quote_ident() has been applied.\\n * For example, MY_Table will appear as '\\\"MY_Table\\\"' in any of the table name related properties below.\\n */\\n\\nexport type LayoutItem = {\\n  /**\\n   * UUID of the window\\n   */\\n  id: string;\\n  title?: string;\\n  type: \\\"item\\\";\\n  /**\\n   * Table name after quote_ident() has been applied.\\n   * This means that any table names with uppercase letters or special characters will be quoted.\\n   */\\n  tableName: string | null;\\n  viewType: \\\"table\\\" | \\\"map\\\" | \\\"timechart\\\" | \\\"sql\\\" | \\\"barchart\\\";\\n  /**\\n   * Flex size of the item\\n   */\\n  size: number;\\n  isRoot?: boolean;\\n};\\nexport type LayoutGroup = {\\n  id: string;\\n  size: number;\\n  isRoot?: boolean;\\n} & (\\n  | {\\n      /**\\n       * Flex direction of the group\\n       */\\n      type: \\\"row\\\" | \\\"col\\\";\\n      items: LayoutConfig[];\\n    }\\n  | {\\n      /**\\n       * Will display windows as tabs\\n       */\\n      type: \\\"tab\\\";\\n      items: LayoutItem[];\\n      /**\\n       * UUID of the currently shown window\\n       */\\n      activeTabKey: string | undefined;\\n    }\\n);\\n\\nexport type LayoutConfig = LayoutItem | LayoutGroup;\\n\\n/**\\n * This will render a time chart for each row in the table.\\n * Useful for showing and comparing time series data for multiple entities\\n */\\ntype LinkedDataChart = {\\n  chart: {\\n    type: \\\"time\\\";\\n    yAxis: {\\n      colName: string;\\n      funcName: \\\"$avg\\\" | \\\"$sum\\\" | \\\"$min\\\" | \\\"$max\\\" | \\\"$count\\\";\\n      isCountAll: boolean;\\n    };\\n    dateCol: string;\\n  };\\n};\\n\\n/**\\n * This will render nested rows for each row in the table.\\n */\\ntype LinkedDataTable = {\\n  limit: number;\\n  columns: Omit<TableColumn, \\\"nested\\\">[];\\n};\\n\\n/**\\n * Join to linked table.\\n */\\ntype TableJoin = {\\n  /**\\n   * Join columns.\\n   * property = root table (or previous table) column name\\n   * value = linked table column name\\n   * @example\\n   * path: {\\n   *   on: [{ user_id: \\\"id\\\" }]\\n   *   table: \\\"users\\\"\\n   * }\\n   */\\n  on: Record<string, string>[];\\n  /**\\n   * Linked table name\\n   */\\n  table: string;\\n};\\n\\n/**\\n * Show linked data from other tables that are linked to this column through foreign keys\\n */\\ntype LinkedData = {\\n  joinType: \\\"left\\\" | \\\"inner\\\";\\n  /**\\n   * Join to linked table.\\n   * Last table in the path is the target table that columns will refer to.\\n   */\\n  path: TableJoin[];\\n} & (LinkedDataChart | LinkedDataTable);\\n\\ntype Comparator = \\\"$eq\\\" | \\\"$ne\\\" | \\\"$lt\\\" | \\\"$lte\\\" | \\\"$gt\\\" | \\\"$gte\\\";\\n\\ntype BasicFilter = {\\n  /**\\n   * Column name\\n   */\\n  fieldName: string;\\n} & (\\n  | {\\n      type: \\\"$in\\\";\\n      value: (string | null)[];\\n    }\\n  | {\\n      /** Not in */\\n      type: \\\"$nin\\\";\\n      value: (string | null)[];\\n    }\\n  | {\\n      type: Comparator;\\n      value: string;\\n    }\\n);\\n\\ntype ComplexColumnFilterFunction =\\n  | {\\n      /**\\n       * Age to current day\\n       * Implemented as pg_catalog.age(column)\\n       */\\n      $age: [\\n        /**\\n         * Column name with a timestamp / date value\\n         */\\n        string,\\n      ];\\n    }\\n  | {\\n      /**\\n       * Age to current timestamp\\n       * Implemented as pg_catalog.age(now(), column)\\n       */\\n      $ageNow: [\\n        /**\\n         * Column name with a timestamp / date value\\n         */\\n        string,\\n      ];\\n    };\\ntype ComplexColumnFilter = {\\n  $filter: [ComplexColumnFilterFunction, Comparator, string | null];\\n};\\n\\ntype ColumnFilter = BasicFilter | ComplexColumnFilter;\\n\\n/**\\n * Filter that matches rows based on existence of related rows in another table\\n */\\ntype JoinedFilter = {\\n  $existsJoined: {\\n    path: TableJoin[];\\n    /**\\n     * Filter that will be applied to the joined table (last table in the path)\\n     */\\n    filter: ColumnFilter;\\n  };\\n};\\n\\ntype FilterItem = ColumnFilter | JoinedFilter;\\n\\nexport type Filter =\\n  | FilterItem\\n  | {\\n      $and: FilterItem[];\\n    }\\n  | {\\n      $or: FilterItem[];\\n    };\\n\\ntype Filtering = {\\n  filter?: FilterItem[];\\n  /** Defaults to AND */\\n  filterOperand?: \\\"AND\\\" | \\\"OR\\\";\\n\\n  /**\\n   * Predefined quick filters that the user can toggle on/off\\n   * These are shown in the filter bar under \\\"Quick Filters\\\"\\n   * MUST ENSURE FILTER VALUES FOR FOREIGN KEYS EXIST IN THE DATABASE\\n   */\\n  quickFilterGroups?: {\\n    [groupName: string]: {\\n      toggledFilterName?: string;\\n      filters: {\\n        [filterName: string]: Filter;\\n      };\\n    };\\n  };\\n};\\n\\ntype TableColumn = {\\n  /**\\n   * Column name as it appears in the database.\\n   * For nested columns this can be anything. Use the table name or a more descriptive name.\\n   */\\n  name: string;\\n\\n  /**\\n   * Show linked data from other tables that are linked to this column through foreign keys.\\n   * If defined then \\\"name\\\" from above should be used as a label for the nested data.\\n   */\\n  nested?: LinkedData;\\n\\n  /**\\n   * Column width in pixels\\n   */\\n  width: number;\\n\\n  /**\\n   * Render column value in a chip\\n   * Cannot be used with nested\\n   */\\n  styling?: {\\n    type: \\\"conditional\\\";\\n    conditions: {\\n      chipColor:\\n        | \\\"red\\\"\\n        | \\\"pink\\\"\\n        | \\\"purple\\\"\\n        | \\\"blue\\\"\\n        | \\\"indigo\\\"\\n        | \\\"green\\\"\\n        | \\\"yellow\\\"\\n        | \\\"gray\\\";\\n      operator: \\\"=\\\" | \\\"!=\\\" | \\\">\\\" | \\\"<\\\" | \\\">=\\\" | \\\"<=\\\";\\n      value: string;\\n    }[];\\n  };\\n\\n  /**\\n   * If set, column value will rendered in a specific way\\n   */\\n  format?:\\n    | {\\n        /**\\n         * Column value will be rendered as a link with specific behaviour\\n         */\\n        type: \\\"URL\\\" | \\\"Email\\\" | \\\"Tel\\\";\\n      }\\n    | {\\n        /**\\n         * Render column value as a scannable QR code image\\n         */\\n        type: \\\"QR Code\\\";\\n      }\\n    | {\\n        /** Display large numbers with metric prefixes (e.g. 1.2K) */\\n        type: \\\"Metric Prefix\\\";\\n      }\\n    | {\\n        /**\\n         * Render column value with a currency symbol\\n         */\\n        type: \\\"Currency\\\";\\n        params:\\n          | {\\n              mode: \\\"Fixed\\\";\\n              /** @example \\\"USD\\\" */\\n              currencyCode: string;\\n              metricPrefix?: boolean;\\n            }\\n          | {\\n              mode: \\\"From column\\\";\\n              /** Column which contains the currency code  */\\n              currencyCodeField: string;\\n              metricPrefix?: boolean;\\n            };\\n      }\\n    | {\\n        /** Display the timestamp value as an age. Short variant (default) shows top two biggest units */\\n        type: \\\"Age\\\";\\n        params?: {\\n          variant: \\\"short\\\" | \\\"full\\\";\\n        };\\n      }\\n    | {\\n        /** Text content as sanitised html */\\n        type: \\\"HTML\\\";\\n      }\\n    | {\\n        /** Displays the media from URL. Accepted formats: image, audio or video. Media/Mime type will be used from headers */\\n        type: \\\"Media\\\";\\n      };\\n};\\n\\n/**\\n * Represents a rendered cell in a card layout\\n */\\ntype CardLayoutRowColumnValue = {\\n  type: \\\"node\\\";\\n  columnName: string;\\n  /**\\n   * React.CSSProperties;\\n   */\\n  style?: Record<string, string | number>;\\n  /**\\n   * If true, label will be hidden and only value will be shown\\n   */\\n  hideLabel?: boolean;\\n};\\n\\n/**\\n * Renders a div element with specified style and contents.\\n * Used to arrange children in flex row/column/row-wrapped layouts for efficient content density.\\n */\\nexport type CardLayout = {\\n  type?: \\\"container\\\";\\n  /**\\n   * React.CSSProperties;\\n   */\\n  style?: Record<string, string | number>;\\n  children: (CardLayout | CardLayoutRowColumnValue)[];\\n};\\n\\nexport type TableWindowInsertModel = Filtering & {\\n  id: string;\\n  type: \\\"table\\\";\\n  /**\\n   * Optional title that will be shown in the window header (Defaults to table_name).\\n   * Supports template variable ${rowCount} which will be replaced with the actual number of rows in the table.\\n   */\\n  title?: string;\\n  table_name: string;\\n  columns?: TableColumn[];\\n\\n  /**\\n   * Sort order when of type 'table'\\n   */\\n  sort?:\\n    | null\\n    | {\\n        /**\\n         * Column name\\n         */\\n        key: string;\\n        asc: boolean;\\n        nulls: \\\"first\\\" | \\\"last\\\";\\n      }[];\\n\\n  /**\\n   * Layout used when the table is switched to the card list view mode, where each row is shown as a card.\\n   */\\n  cardLayout: CardLayout;\\n};\\n\\ntype LayerDataSource =\\n  | (Filtering & {\\n      type: \\\"local-table\\\";\\n      table_name: string;\\n      /**\\n       * Join to linked table (table_name is root table).\\n       * The charted columns must be from the end table while the filters are from the root table (table_name).\\n       */\\n      joinPath?: TableJoin[];\\n    })\\n  | {\\n      type: \\\"sql\\\";\\n      sql: string;\\n    };\\n\\n/**\\n * Shows GEOGRAPHY/GEOMETRY data on a map\\n */\\nexport type MapWindowInsertModel = {\\n  id: string;\\n  type: \\\"map\\\";\\n  title?: string;\\n  layers: (LayerDataSource & {\\n    title?: string;\\n    /**\\n     * Column name with GEOGRAPHY/GEOMETRY data\\n     */\\n    geoColumn: string;\\n  })[];\\n};\\n\\n/**\\n * Allows user to write and excute custom SQL queries with results displayed in a table\\n */\\nexport type SqlWindowInsertModel = {\\n  id: string;\\n  name: string;\\n  type: \\\"sql\\\";\\n  sql: string;\\n};\\n\\n/**\\n * Shows a time chart\\n */\\nexport type TimechartWindowInsertModel = {\\n  id: string;\\n  type: \\\"timechart\\\";\\n  title?: string;\\n  layers: (LayerDataSource & {\\n    title?: string;\\n    dateColumn: string;\\n    groupByColumn?: string;\\n    yAxis:\\n      | \\\"count(*)\\\"\\n      | {\\n          aggregation: \\\"sum\\\" | \\\"avg\\\" | \\\"min\\\" | \\\"max\\\" | \\\"count\\\";\\n          column: string;\\n        };\\n  })[];\\n};\\nexport type BarchartWindowInsertModel = (\\n  | (Filtering & {\\n      table_name: string;\\n    })\\n  | {\\n      sql: string;\\n    }\\n) & {\\n  id: string;\\n  type: \\\"barchart\\\";\\n  title?: string;\\n  xAxis: {\\n    column: string;\\n    aggregation: \\\"sum\\\" | \\\"avg\\\" | \\\"min\\\" | \\\"max\\\" | \\\"count\\\" | \\\"count(*)\\\";\\n    /**\\n     * Join to linked table (table_name is root table).\\n     * The xAxis.column must be from the end table while the filters are from the root table (table_name).\\n     */\\n    joinPath?: TableJoin[];\\n  };\\n  yAxisColumn: string;\\n};\\n\\nexport type WindowInsertModel =\\n  | MapWindowInsertModel\\n  | SqlWindowInsertModel\\n  | TableWindowInsertModel\\n  | TimechartWindowInsertModel\\n  | BarchartWindowInsertModel;\\n\\nexport type WorkspaceInsertModel = {\\n  name: string;\\n  /**\\n   * MDI camel case icon name for the workspace that will be shown near the workspace name.\\n   * example: \\\"AccountCancel\\\", \\\"BriefcaseOutline\\\", \\\"CalendarQuestion\\\"\\n   * Should ideally be specified when an existing icon gives a good visual description of the workspace.\\n   */\\n  icon?: string;\\n  layout: LayoutGroup;\\n  windows: WindowInsertModel[];\\n};\\n\\n\";\n//# sourceMappingURL=dashboardTypesContent.d.ts.map"
  },
  {
    "path": "common/dashboardTypesContent.js",
    "content": "/**\n * Generated file. Do not edit.\n * https://github.com/electron-userland/electron-builder/issues/5064\n */\nexport const dashboardTypesContent = `/**\n * IMPORTANT: all table names in this file MUST be after quote_ident() has been applied.\n * For example, MY_Table will appear as '\"MY_Table\"' in any of the table name related properties below.\n */\n\nexport type LayoutItem = {\n  /**\n   * UUID of the window\n   */\n  id: string;\n  title?: string;\n  type: \"item\";\n  /**\n   * Table name after quote_ident() has been applied.\n   * This means that any table names with uppercase letters or special characters will be quoted.\n   */\n  tableName: string | null;\n  viewType: \"table\" | \"map\" | \"timechart\" | \"sql\" | \"barchart\";\n  /**\n   * Flex size of the item\n   */\n  size: number;\n  isRoot?: boolean;\n};\nexport type LayoutGroup = {\n  id: string;\n  size: number;\n  isRoot?: boolean;\n} & (\n  | {\n      /**\n       * Flex direction of the group\n       */\n      type: \"row\" | \"col\";\n      items: LayoutConfig[];\n    }\n  | {\n      /**\n       * Will display windows as tabs\n       */\n      type: \"tab\";\n      items: LayoutItem[];\n      /**\n       * UUID of the currently shown window\n       */\n      activeTabKey: string | undefined;\n    }\n);\n\nexport type LayoutConfig = LayoutItem | LayoutGroup;\n\n/**\n * This will render a time chart for each row in the table.\n * Useful for showing and comparing time series data for multiple entities\n */\ntype LinkedDataChart = {\n  chart: {\n    type: \"time\";\n    yAxis: {\n      colName: string;\n      funcName: \"$avg\" | \"$sum\" | \"$min\" | \"$max\" | \"$count\";\n      isCountAll: boolean;\n    };\n    dateCol: string;\n  };\n};\n\n/**\n * This will render nested rows for each row in the table.\n */\ntype LinkedDataTable = {\n  limit: number;\n  columns: Omit<TableColumn, \"nested\">[];\n};\n\n/**\n * Join to linked table.\n */\ntype TableJoin = {\n  /**\n   * Join columns.\n   * property = root table (or previous table) column name\n   * value = linked table column name\n   * @example\n   * path: {\n   *   on: [{ user_id: \"id\" }]\n   *   table: \"users\"\n   * }\n   */\n  on: Record<string, string>[];\n  /**\n   * Linked table name\n   */\n  table: string;\n};\n\n/**\n * Show linked data from other tables that are linked to this column through foreign keys\n */\ntype LinkedData = {\n  joinType: \"left\" | \"inner\";\n  /**\n   * Join to linked table.\n   * Last table in the path is the target table that columns will refer to.\n   */\n  path: TableJoin[];\n} & (LinkedDataChart | LinkedDataTable);\n\ntype Comparator = \"$eq\" | \"$ne\" | \"$lt\" | \"$lte\" | \"$gt\" | \"$gte\";\n\ntype BasicFilter = {\n  /**\n   * Column name\n   */\n  fieldName: string;\n} & (\n  | {\n      type: \"$in\";\n      value: (string | null)[];\n    }\n  | {\n      /** Not in */\n      type: \"$nin\";\n      value: (string | null)[];\n    }\n  | {\n      type: Comparator;\n      value: string;\n    }\n);\n\ntype ComplexColumnFilterFunction =\n  | {\n      /**\n       * Age to current day\n       * Implemented as pg_catalog.age(column)\n       */\n      $age: [\n        /**\n         * Column name with a timestamp / date value\n         */\n        string,\n      ];\n    }\n  | {\n      /**\n       * Age to current timestamp\n       * Implemented as pg_catalog.age(now(), column)\n       */\n      $ageNow: [\n        /**\n         * Column name with a timestamp / date value\n         */\n        string,\n      ];\n    };\ntype ComplexColumnFilter = {\n  $filter: [ComplexColumnFilterFunction, Comparator, string | null];\n};\n\ntype ColumnFilter = BasicFilter | ComplexColumnFilter;\n\n/**\n * Filter that matches rows based on existence of related rows in another table\n */\ntype JoinedFilter = {\n  $existsJoined: {\n    path: TableJoin[];\n    /**\n     * Filter that will be applied to the joined table (last table in the path)\n     */\n    filter: ColumnFilter;\n  };\n};\n\ntype FilterItem = ColumnFilter | JoinedFilter;\n\nexport type Filter =\n  | FilterItem\n  | {\n      $and: FilterItem[];\n    }\n  | {\n      $or: FilterItem[];\n    };\n\ntype Filtering = {\n  filter?: FilterItem[];\n  /** Defaults to AND */\n  filterOperand?: \"AND\" | \"OR\";\n\n  /**\n   * Predefined quick filters that the user can toggle on/off\n   * These are shown in the filter bar under \"Quick Filters\"\n   * MUST ENSURE FILTER VALUES FOR FOREIGN KEYS EXIST IN THE DATABASE\n   */\n  quickFilterGroups?: {\n    [groupName: string]: {\n      toggledFilterName?: string;\n      filters: {\n        [filterName: string]: Filter;\n      };\n    };\n  };\n};\n\ntype TableColumn = {\n  /**\n   * Column name as it appears in the database.\n   * For nested columns this can be anything. Use the table name or a more descriptive name.\n   */\n  name: string;\n\n  /**\n   * Show linked data from other tables that are linked to this column through foreign keys.\n   * If defined then \"name\" from above should be used as a label for the nested data.\n   */\n  nested?: LinkedData;\n\n  /**\n   * Column width in pixels\n   */\n  width: number;\n\n  /**\n   * Render column value in a chip\n   * Cannot be used with nested\n   */\n  styling?: {\n    type: \"conditional\";\n    conditions: {\n      chipColor:\n        | \"red\"\n        | \"pink\"\n        | \"purple\"\n        | \"blue\"\n        | \"indigo\"\n        | \"green\"\n        | \"yellow\"\n        | \"gray\";\n      operator: \"=\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\";\n      value: string;\n    }[];\n  };\n\n  /**\n   * If set, column value will rendered in a specific way\n   */\n  format?:\n    | {\n        /**\n         * Column value will be rendered as a link with specific behaviour\n         */\n        type: \"URL\" | \"Email\" | \"Tel\";\n      }\n    | {\n        /**\n         * Render column value as a scannable QR code image\n         */\n        type: \"QR Code\";\n      }\n    | {\n        /** Display large numbers with metric prefixes (e.g. 1.2K) */\n        type: \"Metric Prefix\";\n      }\n    | {\n        /**\n         * Render column value with a currency symbol\n         */\n        type: \"Currency\";\n        params:\n          | {\n              mode: \"Fixed\";\n              /** @example \"USD\" */\n              currencyCode: string;\n              metricPrefix?: boolean;\n            }\n          | {\n              mode: \"From column\";\n              /** Column which contains the currency code  */\n              currencyCodeField: string;\n              metricPrefix?: boolean;\n            };\n      }\n    | {\n        /** Display the timestamp value as an age. Short variant (default) shows top two biggest units */\n        type: \"Age\";\n        params?: {\n          variant: \"short\" | \"full\";\n        };\n      }\n    | {\n        /** Text content as sanitised html */\n        type: \"HTML\";\n      }\n    | {\n        /** Displays the media from URL. Accepted formats: image, audio or video. Media/Mime type will be used from headers */\n        type: \"Media\";\n      };\n};\n\n/**\n * Represents a rendered cell in a card layout\n */\ntype CardLayoutRowColumnValue = {\n  type: \"node\";\n  columnName: string;\n  /**\n   * React.CSSProperties;\n   */\n  style?: Record<string, string | number>;\n  /**\n   * If true, label will be hidden and only value will be shown\n   */\n  hideLabel?: boolean;\n};\n\n/**\n * Renders a div element with specified style and contents.\n * Used to arrange children in flex row/column/row-wrapped layouts for efficient content density.\n */\nexport type CardLayout = {\n  type?: \"container\";\n  /**\n   * React.CSSProperties;\n   */\n  style?: Record<string, string | number>;\n  children: (CardLayout | CardLayoutRowColumnValue)[];\n};\n\nexport type TableWindowInsertModel = Filtering & {\n  id: string;\n  type: \"table\";\n  /**\n   * Optional title that will be shown in the window header (Defaults to table_name).\n   * Supports template variable \\${rowCount} which will be replaced with the actual number of rows in the table.\n   */\n  title?: string;\n  table_name: string;\n  columns?: TableColumn[];\n\n  /**\n   * Sort order when of type 'table'\n   */\n  sort?:\n    | null\n    | {\n        /**\n         * Column name\n         */\n        key: string;\n        asc: boolean;\n        nulls: \"first\" | \"last\";\n      }[];\n\n  /**\n   * Layout used when the table is switched to the card list view mode, where each row is shown as a card.\n   */\n  cardLayout: CardLayout;\n};\n\ntype LayerDataSource =\n  | (Filtering & {\n      type: \"local-table\";\n      table_name: string;\n      /**\n       * Join to linked table (table_name is root table).\n       * The charted columns must be from the end table while the filters are from the root table (table_name).\n       */\n      joinPath?: TableJoin[];\n    })\n  | {\n      type: \"sql\";\n      sql: string;\n    };\n\n/**\n * Shows GEOGRAPHY/GEOMETRY data on a map\n */\nexport type MapWindowInsertModel = {\n  id: string;\n  type: \"map\";\n  title?: string;\n  layers: (LayerDataSource & {\n    title?: string;\n    /**\n     * Column name with GEOGRAPHY/GEOMETRY data\n     */\n    geoColumn: string;\n  })[];\n};\n\n/**\n * Allows user to write and excute custom SQL queries with results displayed in a table\n */\nexport type SqlWindowInsertModel = {\n  id: string;\n  name: string;\n  type: \"sql\";\n  sql: string;\n};\n\n/**\n * Shows a time chart\n */\nexport type TimechartWindowInsertModel = {\n  id: string;\n  type: \"timechart\";\n  title?: string;\n  layers: (LayerDataSource & {\n    title?: string;\n    dateColumn: string;\n    groupByColumn?: string;\n    yAxis:\n      | \"count(*)\"\n      | {\n          aggregation: \"sum\" | \"avg\" | \"min\" | \"max\" | \"count\";\n          column: string;\n        };\n  })[];\n};\nexport type BarchartWindowInsertModel = (\n  | (Filtering & {\n      table_name: string;\n    })\n  | {\n      sql: string;\n    }\n) & {\n  id: string;\n  type: \"barchart\";\n  title?: string;\n  xAxis: {\n    column: string;\n    aggregation: \"sum\" | \"avg\" | \"min\" | \"max\" | \"count\" | \"count(*)\";\n    /**\n     * Join to linked table (table_name is root table).\n     * The xAxis.column must be from the end table while the filters are from the root table (table_name).\n     */\n    joinPath?: TableJoin[];\n  };\n  yAxisColumn: string;\n};\n\nexport type WindowInsertModel =\n  | MapWindowInsertModel\n  | SqlWindowInsertModel\n  | TableWindowInsertModel\n  | TimechartWindowInsertModel\n  | BarchartWindowInsertModel;\n\nexport type WorkspaceInsertModel = {\n  name: string;\n  /**\n   * MDI camel case icon name for the workspace that will be shown near the workspace name.\n   * example: \"AccountCancel\", \"BriefcaseOutline\", \"CalendarQuestion\"\n   * Should ideally be specified when an existing icon gives a good visual description of the workspace.\n   */\n  icon?: string;\n  layout: LayoutGroup;\n  windows: WindowInsertModel[];\n};\n\n`;\n"
  },
  {
    "path": "common/dashboardTypesContent.ts",
    "content": "/**\n * Generated file. Do not edit.\n * https://github.com/electron-userland/electron-builder/issues/5064\n */\nexport const dashboardTypesContent = `/**\n * IMPORTANT: all table names in this file MUST be after quote_ident() has been applied.\n * For example, MY_Table will appear as '\"MY_Table\"' in any of the table name related properties below.\n */\n\nexport type LayoutItem = {\n  /**\n   * UUID of the window\n   */\n  id: string;\n  title?: string;\n  type: \"item\";\n  /**\n   * Table name after quote_ident() has been applied.\n   * This means that any table names with uppercase letters or special characters will be quoted.\n   */\n  tableName: string | null;\n  viewType: \"table\" | \"map\" | \"timechart\" | \"sql\" | \"barchart\";\n  /**\n   * Flex size of the item\n   */\n  size: number;\n  isRoot?: boolean;\n};\nexport type LayoutGroup = {\n  id: string;\n  size: number;\n  isRoot?: boolean;\n} & (\n  | {\n      /**\n       * Flex direction of the group\n       */\n      type: \"row\" | \"col\";\n      items: LayoutConfig[];\n    }\n  | {\n      /**\n       * Will display windows as tabs\n       */\n      type: \"tab\";\n      items: LayoutItem[];\n      /**\n       * UUID of the currently shown window\n       */\n      activeTabKey: string | undefined;\n    }\n);\n\nexport type LayoutConfig = LayoutItem | LayoutGroup;\n\n/**\n * This will render a time chart for each row in the table.\n * Useful for showing and comparing time series data for multiple entities\n */\ntype LinkedDataChart = {\n  chart: {\n    type: \"time\";\n    yAxis: {\n      colName: string;\n      funcName: \"$avg\" | \"$sum\" | \"$min\" | \"$max\" | \"$count\";\n      isCountAll: boolean;\n    };\n    dateCol: string;\n  };\n};\n\n/**\n * This will render nested rows for each row in the table.\n */\ntype LinkedDataTable = {\n  limit: number;\n  columns: Omit<TableColumn, \"nested\">[];\n};\n\n/**\n * Join to linked table.\n */\ntype TableJoin = {\n  /**\n   * Join columns.\n   * property = root table (or previous table) column name\n   * value = linked table column name\n   * @example\n   * path: {\n   *   on: [{ user_id: \"id\" }]\n   *   table: \"users\"\n   * }\n   */\n  on: Record<string, string>[];\n  /**\n   * Linked table name\n   */\n  table: string;\n};\n\n/**\n * Show linked data from other tables that are linked to this column through foreign keys\n */\ntype LinkedData = {\n  joinType: \"left\" | \"inner\";\n  /**\n   * Join to linked table.\n   * Last table in the path is the target table that columns will refer to.\n   */\n  path: TableJoin[];\n} & (LinkedDataChart | LinkedDataTable);\n\ntype Comparator = \"$eq\" | \"$ne\" | \"$lt\" | \"$lte\" | \"$gt\" | \"$gte\";\n\ntype BasicFilter = {\n  /**\n   * Column name\n   */\n  fieldName: string;\n} & (\n  | {\n      type: \"$in\";\n      value: (string | null)[];\n    }\n  | {\n      /** Not in */\n      type: \"$nin\";\n      value: (string | null)[];\n    }\n  | {\n      type: Comparator;\n      value: string;\n    }\n);\n\ntype ComplexColumnFilterFunction =\n  | {\n      /**\n       * Age to current day\n       * Implemented as pg_catalog.age(column)\n       */\n      $age: [\n        /**\n         * Column name with a timestamp / date value\n         */\n        string,\n      ];\n    }\n  | {\n      /**\n       * Age to current timestamp\n       * Implemented as pg_catalog.age(now(), column)\n       */\n      $ageNow: [\n        /**\n         * Column name with a timestamp / date value\n         */\n        string,\n      ];\n    };\ntype ComplexColumnFilter = {\n  $filter: [ComplexColumnFilterFunction, Comparator, string | null];\n};\n\ntype ColumnFilter = BasicFilter | ComplexColumnFilter;\n\n/**\n * Filter that matches rows based on existence of related rows in another table\n */\ntype JoinedFilter = {\n  $existsJoined: {\n    path: TableJoin[];\n    /**\n     * Filter that will be applied to the joined table (last table in the path)\n     */\n    filter: ColumnFilter;\n  };\n};\n\ntype FilterItem = ColumnFilter | JoinedFilter;\n\nexport type Filter =\n  | FilterItem\n  | {\n      $and: FilterItem[];\n    }\n  | {\n      $or: FilterItem[];\n    };\n\ntype Filtering = {\n  filter?: FilterItem[];\n  /** Defaults to AND */\n  filterOperand?: \"AND\" | \"OR\";\n\n  /**\n   * Predefined quick filters that the user can toggle on/off\n   * These are shown in the filter bar under \"Quick Filters\"\n   * MUST ENSURE FILTER VALUES FOR FOREIGN KEYS EXIST IN THE DATABASE\n   */\n  quickFilterGroups?: {\n    [groupName: string]: {\n      toggledFilterName?: string;\n      filters: {\n        [filterName: string]: Filter;\n      };\n    };\n  };\n};\n\ntype TableColumn = {\n  /**\n   * Column name as it appears in the database.\n   * For nested columns this can be anything. Use the table name or a more descriptive name.\n   */\n  name: string;\n\n  /**\n   * Show linked data from other tables that are linked to this column through foreign keys.\n   * If defined then \"name\" from above should be used as a label for the nested data.\n   */\n  nested?: LinkedData;\n\n  /**\n   * Column width in pixels\n   */\n  width: number;\n\n  /**\n   * Render column value in a chip\n   * Cannot be used with nested\n   */\n  styling?: {\n    type: \"conditional\";\n    conditions: {\n      chipColor:\n        | \"red\"\n        | \"pink\"\n        | \"purple\"\n        | \"blue\"\n        | \"indigo\"\n        | \"green\"\n        | \"yellow\"\n        | \"gray\";\n      operator: \"=\" | \"!=\" | \">\" | \"<\" | \">=\" | \"<=\";\n      value: string;\n    }[];\n  };\n\n  /**\n   * If set, column value will rendered in a specific way\n   */\n  format?:\n    | {\n        /**\n         * Column value will be rendered as a link with specific behaviour\n         */\n        type: \"URL\" | \"Email\" | \"Tel\";\n      }\n    | {\n        /**\n         * Render column value as a scannable QR code image\n         */\n        type: \"QR Code\";\n      }\n    | {\n        /** Display large numbers with metric prefixes (e.g. 1.2K) */\n        type: \"Metric Prefix\";\n      }\n    | {\n        /**\n         * Render column value with a currency symbol\n         */\n        type: \"Currency\";\n        params:\n          | {\n              mode: \"Fixed\";\n              /** @example \"USD\" */\n              currencyCode: string;\n              metricPrefix?: boolean;\n            }\n          | {\n              mode: \"From column\";\n              /** Column which contains the currency code  */\n              currencyCodeField: string;\n              metricPrefix?: boolean;\n            };\n      }\n    | {\n        /** Display the timestamp value as an age. Short variant (default) shows top two biggest units */\n        type: \"Age\";\n        params?: {\n          variant: \"short\" | \"full\";\n        };\n      }\n    | {\n        /** Text content as sanitised html */\n        type: \"HTML\";\n      }\n    | {\n        /** Displays the media from URL. Accepted formats: image, audio or video. Media/Mime type will be used from headers */\n        type: \"Media\";\n      };\n};\n\n/**\n * Represents a rendered cell in a card layout\n */\ntype CardLayoutRowColumnValue = {\n  type: \"node\";\n  columnName: string;\n  /**\n   * React.CSSProperties;\n   */\n  style?: Record<string, string | number>;\n  /**\n   * If true, label will be hidden and only value will be shown\n   */\n  hideLabel?: boolean;\n};\n\n/**\n * Renders a div element with specified style and contents.\n * Used to arrange children in flex row/column/row-wrapped layouts for efficient content density.\n */\nexport type CardLayout = {\n  type?: \"container\";\n  /**\n   * React.CSSProperties;\n   */\n  style?: Record<string, string | number>;\n  children: (CardLayout | CardLayoutRowColumnValue)[];\n};\n\nexport type TableWindowInsertModel = Filtering & {\n  id: string;\n  type: \"table\";\n  /**\n   * Optional title that will be shown in the window header (Defaults to table_name).\n   * Supports template variable \\${rowCount} which will be replaced with the actual number of rows in the table.\n   */\n  title?: string;\n  table_name: string;\n  columns?: TableColumn[];\n\n  /**\n   * Sort order when of type 'table'\n   */\n  sort?:\n    | null\n    | {\n        /**\n         * Column name\n         */\n        key: string;\n        asc: boolean;\n        nulls: \"first\" | \"last\";\n      }[];\n\n  /**\n   * Layout used when the table is switched to the card list view mode, where each row is shown as a card.\n   */\n  cardLayout: CardLayout;\n};\n\ntype LayerDataSource =\n  | (Filtering & {\n      type: \"local-table\";\n      table_name: string;\n      /**\n       * Join to linked table (table_name is root table).\n       * The charted columns must be from the end table while the filters are from the root table (table_name).\n       */\n      joinPath?: TableJoin[];\n    })\n  | {\n      type: \"sql\";\n      sql: string;\n    };\n\n/**\n * Shows GEOGRAPHY/GEOMETRY data on a map\n */\nexport type MapWindowInsertModel = {\n  id: string;\n  type: \"map\";\n  title?: string;\n  layers: (LayerDataSource & {\n    title?: string;\n    /**\n     * Column name with GEOGRAPHY/GEOMETRY data\n     */\n    geoColumn: string;\n  })[];\n};\n\n/**\n * Allows user to write and excute custom SQL queries with results displayed in a table\n */\nexport type SqlWindowInsertModel = {\n  id: string;\n  name: string;\n  type: \"sql\";\n  sql: string;\n};\n\n/**\n * Shows a time chart\n */\nexport type TimechartWindowInsertModel = {\n  id: string;\n  type: \"timechart\";\n  title?: string;\n  layers: (LayerDataSource & {\n    title?: string;\n    dateColumn: string;\n    groupByColumn?: string;\n    yAxis:\n      | \"count(*)\"\n      | {\n          aggregation: \"sum\" | \"avg\" | \"min\" | \"max\" | \"count\";\n          column: string;\n        };\n  })[];\n};\nexport type BarchartWindowInsertModel = (\n  | (Filtering & {\n      table_name: string;\n    })\n  | {\n      sql: string;\n    }\n) & {\n  id: string;\n  type: \"barchart\";\n  title?: string;\n  xAxis: {\n    column: string;\n    aggregation: \"sum\" | \"avg\" | \"min\" | \"max\" | \"count\" | \"count(*)\";\n    /**\n     * Join to linked table (table_name is root table).\n     * The xAxis.column must be from the end table while the filters are from the root table (table_name).\n     */\n    joinPath?: TableJoin[];\n  };\n  yAxisColumn: string;\n};\n\nexport type WindowInsertModel =\n  | MapWindowInsertModel\n  | SqlWindowInsertModel\n  | TableWindowInsertModel\n  | TimechartWindowInsertModel\n  | BarchartWindowInsertModel;\n\nexport type WorkspaceInsertModel = {\n  name: string;\n  /**\n   * MDI camel case icon name for the workspace that will be shown near the workspace name.\n   * example: \"AccountCancel\", \"BriefcaseOutline\", \"CalendarQuestion\"\n   * Should ideally be specified when an existing icon gives a good visual description of the workspace.\n   */\n  icon?: string;\n  layout: LayoutGroup;\n  windows: WindowInsertModel[];\n};\n\n`;"
  },
  {
    "path": "common/electronInitTypes.d.ts",
    "content": "export type ProstglesInitState<T extends Record<string, unknown> = Record<string, unknown>> = {\n    error?: undefined;\n    state: \"loading\";\n} | ({\n    error?: undefined;\n    state: \"ok\";\n} & T) | {\n    error: Error | string | number | bigint | Record<string, any>;\n    state: \"error\";\n    errorType: \"init\" | \"connection\";\n};\nexport type ProstglesState<T extends Record<string, unknown> = Record<string, unknown>> = {\n    isElectron: boolean;\n    xRealIpSpoofable?: boolean;\n    electronCredsProvided?: boolean;\n    electronCreds?: {\n        db_conn: string;\n        db_host: string;\n        db_port: number;\n        db_user: string;\n        db_name: string;\n        db_pass: string;\n        db_ssl: string;\n    };\n    initState: ProstglesInitState<T>;\n};\ntype OS = \"Windows\" | \"Linux\" | \"Mac\" | \"\";\nexport declare const programList: readonly [\"psql\", \"pg_dump\", \"pg_restore\", \"docker\"];\nexport type InstalledPrograms = {\n    os: OS;\n    filePath: string;\n} & Record<(typeof programList)[number], string | undefined>;\nexport declare const DEFAULT_ELECTRON_CONNECTION: {\n    readonly type: \"Standard\";\n    readonly db_host: \"localhost\";\n    readonly db_port: 5432;\n    readonly db_user: \"prostgles_desktop\";\n    readonly db_name: \"prostgles_desktop_db\";\n};\nexport {};\n//# sourceMappingURL=electronInitTypes.d.ts.map"
  },
  {
    "path": "common/electronInitTypes.js",
    "content": "export const programList = [\n    /** Used for dump/restore */\n    \"psql\",\n    \"pg_dump\",\n    \"pg_restore\",\n    /** Used for docker-mcp */\n    \"docker\",\n];\nexport const DEFAULT_ELECTRON_CONNECTION = {\n    type: \"Standard\",\n    db_host: \"localhost\",\n    db_port: 5432,\n    db_user: \"prostgles_desktop\",\n    db_name: \"prostgles_desktop_db\",\n};\n"
  },
  {
    "path": "common/electronInitTypes.ts",
    "content": "export type ProstglesInitState<\n  T extends Record<string, unknown> = Record<string, unknown>,\n> =\n  | {\n      error?: undefined;\n      state: \"loading\";\n    }\n  | ({\n      error?: undefined;\n      state: \"ok\";\n    } & T)\n  | {\n      error: Error | string | number | bigint | Record<string, any>;\n      state: \"error\";\n      errorType: \"init\" | \"connection\";\n    };\n\nexport type ProstglesState<\n  T extends Record<string, unknown> = Record<string, unknown>,\n> = {\n  isElectron: boolean;\n  xRealIpSpoofable?: boolean;\n  electronCredsProvided?: boolean;\n  electronCreds?: {\n    db_conn: string;\n    db_host: string;\n    db_port: number;\n    db_user: string;\n    db_name: string;\n    db_pass: string;\n    db_ssl: string;\n  };\n  initState: ProstglesInitState<T>;\n};\n\ntype OS = \"Windows\" | \"Linux\" | \"Mac\" | \"\";\nexport const programList = [\n  /** Used for dump/restore */\n  \"psql\",\n  \"pg_dump\",\n  \"pg_restore\",\n  /** Used for docker-mcp */\n  \"docker\",\n] as const;\nexport type InstalledPrograms = {\n  os: OS;\n  filePath: string;\n} & Record<(typeof programList)[number], string | undefined>;\n\nexport const DEFAULT_ELECTRON_CONNECTION = {\n  type: \"Standard\",\n  db_host: \"localhost\",\n  db_port: 5432,\n  db_user: \"prostgles_desktop\",\n  db_name: \"prostgles_desktop_db\",\n} as const;\n"
  },
  {
    "path": "common/filterUtils.d.ts",
    "content": "import { ContextDataObject, ContextValue } from \"./publishUtils\";\ntype AnyObject = Record<string, any>;\nexport declare const isDefined: <T>(v: T | undefined | void) => v is T;\nexport declare const CORE_FILTER_TYPES: readonly [{\n    readonly key: \"=\";\n    readonly label: \"=\";\n}, {\n    readonly key: \"$eq\";\n    readonly label: \"=\";\n}, {\n    readonly key: \"<>\";\n    readonly label: \"!=\";\n}, {\n    readonly key: \"$ne\";\n    readonly label: \"!=\";\n}, {\n    readonly key: \"$in\";\n    readonly label: \"IN\";\n}, {\n    readonly key: \"$nin\";\n    readonly label: \"NOT IN\";\n}, {\n    readonly key: \"not null\";\n    readonly label: \"IS NOT NULL\";\n}, {\n    readonly key: \"null\";\n    readonly label: \"IS NULL\";\n}, {\n    readonly key: \"$term_highlight\";\n    readonly label: \"CONTAINS\";\n}];\nexport declare const FTS_FILTER_TYPES: readonly [{\n    readonly key: \"@@.to_tsquery\";\n    readonly label: \"Search\";\n    readonly subLabel: \"(to_tsquery) normalizes each token into a lexeme using the specified or default configuration, and discards any tokens that are stop words according to the configuration\";\n}, {\n    readonly key: \"@@.plainto_tsquery\";\n    readonly label: \"Plain search\";\n    readonly subLabel: \"(plainto_tsquery) The text is parsed and normalized much as for to_tsvector, then the & (AND) tsquery operator is inserted between surviving words\";\n}, {\n    readonly key: \"@@.phraseto_tsquery\";\n    readonly label: \"Phrase search\";\n    readonly subLabel: \"(phraseto_tsquery) phraseto_tsquery behaves much like plainto_tsquery, except that it inserts the <-> (FOLLOWED BY) operator between surviving words instead of the & (AND) operator. Also, stop words are not simply discarded, but are accounted for by inserting <N> operators rather than <-> operators. This function is useful when searching for exact lexeme sequences, since the FOLLOWED BY operators check lexeme order not just the presence of all the lexemes\";\n}, {\n    readonly key: \"@@.websearch_to_tsquery\";\n    readonly label: \"Web search\";\n    readonly subLabel: \"(websearch_to_tsquery) Unlike plainto_tsquery and phraseto_tsquery, it also recognizes certain operators. Moreover, this function will never raise syntax errors, which makes it possible to use raw user-supplied input for search. The following syntax is supported\";\n}];\nexport declare const TEXT_FILTER_TYPES: readonly [{\n    readonly key: \"$ilike\";\n    readonly label: \"ILIKE\";\n    readonly subLabel: string;\n}, {\n    readonly key: \"$like\";\n    readonly label: \"LIKE\";\n    readonly subLabel: string;\n}, {\n    readonly key: \"$nilike\";\n    readonly label: \"NOT ILIKE\";\n}, {\n    readonly key: \"$nlike\";\n    readonly label: \"NOT LIKE\";\n}];\nexport declare const NUMERIC_FILTER_TYPES: readonly [{\n    readonly key: \"$between\";\n    readonly label: \"Between\";\n}, {\n    readonly key: \">\";\n    readonly label: \">\";\n}, {\n    readonly key: \">=\";\n    readonly label: \">=\";\n}, {\n    readonly key: \"<\";\n    readonly label: \"<\";\n}, {\n    readonly key: \"<=\";\n    readonly label: \"<=\";\n}, {\n    readonly key: \"$gt\";\n    readonly label: \">\";\n}, {\n    readonly key: \"$gte\";\n    readonly label: \">=\";\n}, {\n    readonly key: \"$lt\";\n    readonly label: \"<\";\n}, {\n    readonly key: \"$lte\";\n    readonly label: \"<=\";\n}];\nexport declare const DATE_FILTER_TYPES: readonly [{\n    readonly key: \"$age\";\n    readonly label: \"Age at start of day\";\n}, {\n    readonly key: \"$ageNow\";\n    readonly label: \"Age\";\n}, {\n    readonly key: \"$duration\";\n    readonly label: \"Duration\";\n}];\nexport declare const GEO_FILTER_TYPES: readonly [{\n    readonly key: \"$ST_DWithin\";\n    readonly label: \"Within\";\n}];\nexport type FilterType = (typeof CORE_FILTER_TYPES)[number][\"key\"] | (typeof FTS_FILTER_TYPES)[number][\"key\"] | (typeof TEXT_FILTER_TYPES)[number][\"key\"] | (typeof NUMERIC_FILTER_TYPES)[number][\"key\"] | (typeof DATE_FILTER_TYPES)[number][\"key\"] | (typeof GEO_FILTER_TYPES)[number][\"key\"];\nexport type BaseFilter = {\n    minimised?: boolean;\n    disabled?: boolean;\n};\nexport declare const JOINED_FILTER_TYPES: readonly [\"$existsJoined\", \"$notExistsJoined\"];\ntype ComplexFilterDetailed = {\n    type: \"controlled\";\n    funcName: string | undefined;\n    argsLeftToRight: boolean;\n    comparator: string;\n    otherField?: string | null;\n} | {\n    type: \"$filter\";\n    leftExpression: Record<string, any[]>;\n};\nexport type DetailedFilterBase = BaseFilter & {\n    fieldName: string;\n    type?: FilterType;\n    value?: any;\n    contextValue?: ContextValue;\n    ftsFilterOptions?: {\n        lang: string;\n    };\n    complexFilter?: ComplexFilterDetailed;\n};\ntype JoinPath = {\n    table: string;\n    on?: Record<string, string>[] | undefined;\n};\nexport type DetailedJoinedFilter = BaseFilter & {\n    type: (typeof JOINED_FILTER_TYPES)[number];\n    path: (string | JoinPath)[];\n    filter: DetailedFilterBase;\n};\nexport type DetailedFilter = DetailedFilterBase | DetailedJoinedFilter;\nexport type DetailedGroupFilter = {\n    $and: DetailedFilter[];\n} | {\n    $or: DetailedFilter[];\n};\nexport declare const isJoinedFilter: (f: DetailedFilter) => f is DetailedJoinedFilter;\nexport declare const isDetailedFilter: (f: DetailedFilter) => f is DetailedFilterBase;\ntype InfoType = \"pg\";\nexport declare const getFinalFilterInfo: (fullFilter?: GroupedDetailedFilter | DetailedFilter, context?: ContextDataObject, depth?: number, opts?: {\n    for: InfoType;\n}) => string;\nexport declare const parseContextVal: (f: DetailedFilterBase, context: ContextDataObject | undefined, { forInfoOnly }?: GetFinalFilterOpts) => any;\ntype GetFinalFilterOpts = {\n    forInfoOnly?: boolean | InfoType;\n    columns?: string[];\n};\nexport declare const getFinalFilter: (detailedFilter: DetailedFilter, context?: ContextDataObject, opts?: GetFinalFilterOpts) => Record<string, any> | undefined;\nexport declare const simplifyFilter: (f: AnyObject | undefined) => AnyObject | undefined;\nexport declare const getSmartGroupFilter: (detailedFilter?: DetailedFilter[], extraFilters?: {\n    detailed?: DetailedFilter[];\n    filters?: AnyObject[];\n}, operand?: \"and\" | \"or\") => AnyObject;\nexport declare const getTableFilterFromDetailedGroupFilter: (detailedGroupFilter: DetailedGroupFilter) => AnyObject;\nexport type GroupedDetailedFilter = {\n    $and: (DetailedFilter | GroupedDetailedFilter)[];\n} | {\n    $or: (DetailedFilter | GroupedDetailedFilter)[];\n};\nexport {};\n//# sourceMappingURL=filterUtils.d.ts.map"
  },
  {
    "path": "common/filterUtils.js",
    "content": "export const isDefined = (v) => v !== undefined && v !== null;\nexport const CORE_FILTER_TYPES = [\n    { key: \"=\", label: \"=\" },\n    { key: \"$eq\", label: \"=\" },\n    { key: \"<>\", label: \"!=\" },\n    { key: \"$ne\", label: \"!=\" },\n    { key: \"$in\", label: \"IN\" },\n    { key: \"$nin\", label: \"NOT IN\" },\n    { key: \"not null\", label: \"IS NOT NULL\" },\n    { key: \"null\", label: \"IS NULL\" },\n    { key: \"$term_highlight\", label: \"CONTAINS\" },\n];\nexport const FTS_FILTER_TYPES = [\n    {\n        key: \"@@.to_tsquery\",\n        label: \"Search\",\n        subLabel: \"(to_tsquery) normalizes each token into a lexeme using the specified or default configuration, and discards any tokens that are stop words according to the configuration\",\n    },\n    {\n        key: \"@@.plainto_tsquery\",\n        label: \"Plain search\",\n        subLabel: \"(plainto_tsquery) The text is parsed and normalized much as for to_tsvector, then the & (AND) tsquery operator is inserted between surviving words\",\n    },\n    {\n        key: \"@@.phraseto_tsquery\",\n        label: \"Phrase search\",\n        subLabel: \"(phraseto_tsquery) phraseto_tsquery behaves much like plainto_tsquery, except that it inserts the <-> (FOLLOWED BY) operator between surviving words instead of the & (AND) operator. Also, stop words are not simply discarded, but are accounted for by inserting <N> operators rather than <-> operators. This function is useful when searching for exact lexeme sequences, since the FOLLOWED BY operators check lexeme order not just the presence of all the lexemes\",\n    },\n    {\n        key: \"@@.websearch_to_tsquery\",\n        label: \"Web search\",\n        subLabel: \"(websearch_to_tsquery) Unlike plainto_tsquery and phraseto_tsquery, it also recognizes certain operators. Moreover, this function will never raise syntax errors, which makes it possible to use raw user-supplied input for search. The following syntax is supported\",\n    },\n];\nconst likeInfo = \"Operators: '%' - match any sequence of characters; '_' - match any single character \";\nexport const TEXT_FILTER_TYPES = [\n    {\n        key: \"$ilike\",\n        label: \"ILIKE\",\n        subLabel: \"Case-insensitive text search. \" + likeInfo,\n    },\n    {\n        key: \"$like\",\n        label: \"LIKE\",\n        subLabel: \"Case-sensitive text search. \" + likeInfo,\n    },\n    { key: \"$nilike\", label: \"NOT ILIKE\" },\n    { key: \"$nlike\", label: \"NOT LIKE\" },\n    // { key: \"$term_highlightNOT\", label: \"DOES NOT CONTAIN\"},\n];\nexport const NUMERIC_FILTER_TYPES = [\n    { key: \"$between\", label: \"Between\" },\n    { key: \">\", label: \">\" },\n    { key: \">=\", label: \">=\" },\n    { key: \"<\", label: \"<\" },\n    { key: \"<=\", label: \"<=\" },\n    { key: \"$gt\", label: \">\" },\n    { key: \"$gte\", label: \">=\" },\n    { key: \"$lt\", label: \"<\" },\n    { key: \"$lte\", label: \"<=\" },\n];\nexport const DATE_FILTER_TYPES = [\n    { key: \"$age\", label: \"Age at start of day\" },\n    { key: \"$ageNow\", label: \"Age\" },\n    { key: \"$duration\", label: \"Duration\" },\n];\nexport const GEO_FILTER_TYPES = [\n    { key: \"$ST_DWithin\", label: \"Within\" },\n];\nexport const JOINED_FILTER_TYPES = [\n    \"$existsJoined\",\n    \"$notExistsJoined\",\n];\nexport const isJoinedFilter = (f) => Boolean(f.type && JOINED_FILTER_TYPES.includes(f.type));\nexport const isDetailedFilter = (f) => !isJoinedFilter(f.type);\nexport const getFinalFilterInfo = (fullFilter, context, depth = 0, opts) => {\n    var _a;\n    const forPg = (opts === null || opts === void 0 ? void 0 : opts.for) === \"pg\";\n    const filterToString = (filter) => {\n        var _a, _b, _c, _d;\n        if (!Object.keys(filter).length) {\n            return undefined;\n        }\n        if (filter.type === \"$ST_DWithin\") {\n            const v = filter.value;\n            if (forPg) {\n                return `ST_DWithin(${filter.fieldName}, 'SRID=4326;POINT(${v.lng} ${v.lat})', ${v.distance})`;\n            }\n            return `${(v.distance / 1000).toFixed(3)}Km of ${(_a = v === null || v === void 0 ? void 0 : v.name) !== null && _a !== void 0 ? _a : [v.lat, v.lng].join(\", \")}`;\n        }\n        if (filter.type === \"$existsJoined\" || filter.type === \"$notExistsJoined\") {\n            const path = filter.path\n                .map((p) => (typeof p === \"string\" ? p : p.table))\n                .join(\" -> \");\n            return `${filter.type === \"$existsJoined\" ? \"Exists\" : \"Does not exist\"} in ${path} where ${filterToString(filter.filter)}`;\n        }\n        const f = getFinalFilter(filter, context, {\n            forInfoOnly: (_b = opts === null || opts === void 0 ? void 0 : opts.for) !== null && _b !== void 0 ? _b : true,\n        });\n        if (!f)\n            return undefined;\n        const fieldNameAndOperator = Object.keys(f)[0];\n        if (!fieldNameAndOperator)\n            return undefined;\n        if (fieldNameAndOperator === \"$term_highlight\") {\n            const [fields, value, args] = f[fieldNameAndOperator];\n            const { matchCase } = args;\n            if (forPg) {\n                return `(${fields.map((f) => `${f} ${matchCase ? \"LIKE\" : \"ILIKE\"} '%${value}%'`).join(\" OR \")})`;\n            }\n            return `${fields} contain ${matchCase ? \"(case sensitive)\" : \"\"} ${value}`;\n        }\n        const [fieldName, operatorRaw = \"=\"] = fieldNameAndOperator.split(\".$\");\n        const operator = ((_c = CORE_FILTER_TYPES.find(({ key }) => key === `$${operatorRaw}`)) === null || _c === void 0 ? void 0 : _c.label) ||\n            operatorRaw;\n        const value = f[fieldNameAndOperator];\n        if (\"fieldName\" in filter && ((_d = filter.contextValue) === null || _d === void 0 ? void 0 : _d.objectName) === \"user\") {\n            return `${fieldName}::TEXT ${operator} ${value}`;\n        }\n        const valueStr = [\"number\", \"boolean\"].includes(typeof value) ? value\n            : value === null ? `null`\n                : value === undefined ? ``\n                    : `'${JSON.stringify(value).slice(1, -1)}'`;\n        return `${fieldName} ${operator} ${valueStr}`;\n    };\n    let result = \"\";\n    if (fullFilter) {\n        const isAnd = \"$and\" in fullFilter;\n        if (isAnd || \"$or\" in fullFilter) {\n            // @ts-ignore\n            const finalFilters = fullFilter[isAnd ? \"$and\" : \"$or\"]\n                .map((f) => getFinalFilterInfo(f, context, depth + 1, opts))\n                .filter(isDefined)\n                .filter((v) => v.trim().length);\n            const finalFilterStr = finalFilters.join(isAnd ? \" AND \" : \" OR \");\n            return finalFilters.length > 1 && depth > 1 ?\n                `( ${finalFilterStr} )`\n                : finalFilterStr;\n        }\n        return (_a = filterToString(fullFilter)) !== null && _a !== void 0 ? _a : \"\";\n    }\n    return result;\n};\nexport const parseContextVal = (f, context, { forInfoOnly } = {}) => {\n    var _a;\n    if (f.contextValue) {\n        if (forInfoOnly) {\n            const objPath = `${f.contextValue.objectName}.${f.contextValue.objectPropertyName}`;\n            if (forInfoOnly === \"pg\") {\n                if (f.contextValue.objectName === \"user\") {\n                    return `prostgles.user('${f.contextValue.objectPropertyName}')`;\n                }\n                return `current_setting('${objPath}')`;\n            }\n            return `{{${objPath}}}`;\n        }\n        if (context) {\n            //@ts-ignore\n            return (_a = context[f.contextValue.objectName]) === null || _a === void 0 ? void 0 : _a[f.contextValue.objectPropertyName];\n        }\n        return undefined;\n    }\n    return Object.assign({}, f).value;\n};\nexport const getFinalFilter = (detailedFilter, context, opts) => {\n    const { forInfoOnly = false } = opts !== null && opts !== void 0 ? opts : {};\n    const checkFieldname = (f, columns) => {\n        if ((columns === null || columns === void 0 ? void 0 : columns.length) && !columns.includes(f)) {\n            throw new Error(`${f} is not a valid field name. \\nExpecting one of: ${columns.join(\", \")}`);\n        }\n        return f;\n    };\n    if ((\"fieldName\" in detailedFilter && detailedFilter.disabled) ||\n        (isJoinedFilter(detailedFilter) && detailedFilter.filter.disabled))\n        return undefined;\n    const getFilter = (f, columns) => {\n        var _a, _b, _c;\n        const val = parseContextVal(f, context, opts);\n        const fieldName = checkFieldname(f.fieldName, columns);\n        if (f.contextValue && !context && !forInfoOnly) {\n            return {};\n        }\n        if (FTS_FILTER_TYPES.some((fts) => fts.key === f.type) &&\n            \"fieldName\" in f) {\n            const fieldName = checkFieldname(f.fieldName, opts === null || opts === void 0 ? void 0 : opts.columns);\n            const { ftsFilterOptions } = f;\n            return {\n                [`${fieldName}.${f.type}`]: [\n                    ...(ftsFilterOptions ? [ftsFilterOptions.lang] : []),\n                    parseContextVal(f, context, opts),\n                ],\n            };\n        }\n        else if (f.type === \"$term_highlight\") {\n            const fieldName = f.fieldName ? checkFieldname(f.fieldName, opts === null || opts === void 0 ? void 0 : opts.columns) : \"*\";\n            return {\n                $term_highlight: [\n                    [fieldName],\n                    parseContextVal(f, context, opts),\n                    { matchCase: false, edgeTruncate: 30, returnType: \"boolean\" },\n                ],\n            };\n        }\n        else if (f.type == \"$ST_DWithin\") {\n            return {\n                $filter: [{ $ST_DWithin: [fieldName, Object.assign({}, val)] }],\n            };\n        }\n        else if (f.complexFilter ||\n            ((_a = f.type) === null || _a === void 0 ? void 0 : _a.startsWith(\"$age\")) ||\n            f.type === \"$duration\") {\n            const isAgeOrDuration = ((_b = f.type) === null || _b === void 0 ? void 0 : _b.startsWith(\"$age\")) || f.type === \"$duration\";\n            if (isAgeOrDuration) {\n                if (f.complexFilter && f.complexFilter.type !== \"controlled\") {\n                    throw new Error(\"Only controlled complex filters are allowed for age and duration filters\");\n                }\n                const { comparator, argsLeftToRight = true, otherField, } = (_c = f.complexFilter) !== null && _c !== void 0 ? _c : {};\n                const filterArgs = f.type === \"$age\" ?\n                    [fieldName]\n                    : [fieldName, otherField].filter(isDefined);\n                if (!argsLeftToRight)\n                    filterArgs.reverse();\n                return {\n                    $filter: [\n                        { [f.type === \"$ageNow\" ? \"$ageNow\" : \"$age\"]: filterArgs },\n                        comparator,\n                        val,\n                    ],\n                };\n            }\n            else if (f.complexFilter) {\n                if (f.complexFilter.type !== \"$filter\") {\n                    throw new Error(\"Unexpected complex filter\");\n                }\n                return {\n                    $filter: [f.complexFilter.leftExpression, f.type, val],\n                };\n            }\n        }\n        if (f.type === \"not null\") {\n            return {\n                [fieldName + \".<>\"]: null,\n            };\n        }\n        if (f.type === \"null\") {\n            return {\n                [fieldName]: null,\n            };\n        }\n        return {\n            [[fieldName, f.type === \"=\" ? null : f.type].filter((v) => v).join(\".\")]: val,\n        };\n    };\n    if (isJoinedFilter(detailedFilter)) {\n        return {\n            [detailedFilter.type]: {\n                path: detailedFilter.path,\n                filter: getFilter(detailedFilter.filter),\n            },\n        };\n    }\n    return getFilter(detailedFilter, opts === null || opts === void 0 ? void 0 : opts.columns);\n};\nexport const simplifyFilter = (f) => {\n    var _a, _b, _c, _d;\n    let result = f;\n    if (result) {\n        while ((result &&\n            \"$and\" in result &&\n            Array.isArray(result.$and) &&\n            result.$and.length < 2) ||\n            (result &&\n                \"$or\" in result &&\n                Array.isArray(result.$or) &&\n                result.$or.length < 2)) {\n            if (result.$and)\n                result = (_b = (_a = result.$and) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : {};\n            if (result === null || result === void 0 ? void 0 : result.$or)\n                result = (_d = (_c = result.$or) === null || _c === void 0 ? void 0 : _c[0]) !== null && _d !== void 0 ? _d : {};\n        }\n        result !== null && result !== void 0 ? result : (result = {});\n    }\n    return result;\n};\nexport const getSmartGroupFilter = (detailedFilter = [], extraFilters, operand) => {\n    var _a, _b;\n    const filterItems = detailedFilter\n        .concat((_a = extraFilters === null || extraFilters === void 0 ? void 0 : extraFilters.detailed) !== null && _a !== void 0 ? _a : [])\n        .map((f) => getFinalFilter(f))\n        .concat((_b = extraFilters === null || extraFilters === void 0 ? void 0 : extraFilters.filters) !== null && _b !== void 0 ? _b : []);\n    const result = simplifyFilter({\n        [`$${operand || \"and\"}`]: filterItems.filter(isDefined),\n    });\n    return result !== null && result !== void 0 ? result : {};\n};\nexport const getTableFilterFromDetailedGroupFilter = (detailedGroupFilter) => {\n    const [operand, filterItems] = \"$and\" in detailedGroupFilter ?\n        [\"and\", detailedGroupFilter.$and]\n        : [\"or\", detailedGroupFilter.$or];\n    return getSmartGroupFilter(filterItems, undefined, operand);\n};\n"
  },
  {
    "path": "common/filterUtils.ts",
    "content": "import { ContextDataObject, ContextValue } from \"./publishUtils\";\n\ntype AnyObject = Record<string, any>;\n\nexport const isDefined = <T>(v: T | undefined | void): v is T =>\n  v !== undefined && v !== null;\n\nexport const CORE_FILTER_TYPES = [\n  { key: \"=\", label: \"=\" },\n  { key: \"$eq\", label: \"=\" },\n  { key: \"<>\", label: \"!=\" },\n  { key: \"$ne\", label: \"!=\" },\n  { key: \"$in\", label: \"IN\" },\n  { key: \"$nin\", label: \"NOT IN\" },\n  { key: \"not null\", label: \"IS NOT NULL\" },\n  { key: \"null\", label: \"IS NULL\" },\n  { key: \"$term_highlight\", label: \"CONTAINS\" },\n] as const;\n\nexport const FTS_FILTER_TYPES = [\n  {\n    key: \"@@.to_tsquery\",\n    label: \"Search\",\n    subLabel:\n      \"(to_tsquery) normalizes each token into a lexeme using the specified or default configuration, and discards any tokens that are stop words according to the configuration\",\n  },\n  {\n    key: \"@@.plainto_tsquery\",\n    label: \"Plain search\",\n    subLabel:\n      \"(plainto_tsquery) The text is parsed and normalized much as for to_tsvector, then the & (AND) tsquery operator is inserted between surviving words\",\n  },\n  {\n    key: \"@@.phraseto_tsquery\",\n    label: \"Phrase search\",\n    subLabel:\n      \"(phraseto_tsquery) phraseto_tsquery behaves much like plainto_tsquery, except that it inserts the <-> (FOLLOWED BY) operator between surviving words instead of the & (AND) operator. Also, stop words are not simply discarded, but are accounted for by inserting <N> operators rather than <-> operators. This function is useful when searching for exact lexeme sequences, since the FOLLOWED BY operators check lexeme order not just the presence of all the lexemes\",\n  },\n  {\n    key: \"@@.websearch_to_tsquery\",\n    label: \"Web search\",\n    subLabel:\n      \"(websearch_to_tsquery) Unlike plainto_tsquery and phraseto_tsquery, it also recognizes certain operators. Moreover, this function will never raise syntax errors, which makes it possible to use raw user-supplied input for search. The following syntax is supported\",\n  },\n] as const;\nconst likeInfo =\n  \"Operators: '%' - match any sequence of characters; '_' - match any single character \" as const;\nexport const TEXT_FILTER_TYPES = [\n  {\n    key: \"$ilike\",\n    label: \"ILIKE\",\n    subLabel: \"Case-insensitive text search. \" + likeInfo,\n  },\n  {\n    key: \"$like\",\n    label: \"LIKE\",\n    subLabel: \"Case-sensitive text search. \" + likeInfo,\n  },\n  { key: \"$nilike\", label: \"NOT ILIKE\" },\n  { key: \"$nlike\", label: \"NOT LIKE\" },\n  // { key: \"$term_highlightNOT\", label: \"DOES NOT CONTAIN\"},\n] as const;\n\nexport const NUMERIC_FILTER_TYPES = [\n  { key: \"$between\", label: \"Between\" },\n  { key: \">\", label: \">\" },\n  { key: \">=\", label: \">=\" },\n  { key: \"<\", label: \"<\" },\n  { key: \"<=\", label: \"<=\" },\n  { key: \"$gt\", label: \">\" },\n  { key: \"$gte\", label: \">=\" },\n  { key: \"$lt\", label: \"<\" },\n  { key: \"$lte\", label: \"<=\" },\n] as const;\n\nexport const DATE_FILTER_TYPES = [\n  { key: \"$age\", label: \"Age at start of day\" },\n  { key: \"$ageNow\", label: \"Age\" },\n  { key: \"$duration\", label: \"Duration\" },\n] as const;\n\nexport const GEO_FILTER_TYPES = [\n  { key: \"$ST_DWithin\", label: \"Within\" },\n] as const;\n\nexport type FilterType =\n  | (typeof CORE_FILTER_TYPES)[number][\"key\"]\n  | (typeof FTS_FILTER_TYPES)[number][\"key\"]\n  | (typeof TEXT_FILTER_TYPES)[number][\"key\"]\n  | (typeof NUMERIC_FILTER_TYPES)[number][\"key\"]\n  | (typeof DATE_FILTER_TYPES)[number][\"key\"]\n  | (typeof GEO_FILTER_TYPES)[number][\"key\"];\n\nexport type BaseFilter = {\n  minimised?: boolean;\n  disabled?: boolean;\n};\nexport const JOINED_FILTER_TYPES = [\n  \"$existsJoined\",\n  \"$notExistsJoined\",\n] as const;\ntype ComplexFilterDetailed =\n  | {\n      type: \"controlled\";\n      funcName: string | undefined;\n      argsLeftToRight: boolean;\n      comparator: string;\n      otherField?: string | null;\n    }\n  | {\n      type: \"$filter\";\n      leftExpression: Record<string, any[]>;\n    };\nexport type DetailedFilterBase = BaseFilter & {\n  fieldName: string;\n  type?: FilterType;\n  value?: any;\n  contextValue?: ContextValue;\n  ftsFilterOptions?: {\n    lang: string;\n  };\n  complexFilter?: ComplexFilterDetailed;\n};\ntype JoinPath = {\n  table: string;\n  on?: Record<string, string>[] | undefined;\n};\nexport type DetailedJoinedFilter = BaseFilter & {\n  type: (typeof JOINED_FILTER_TYPES)[number];\n  path: (string | JoinPath)[];\n  filter: DetailedFilterBase;\n};\nexport type DetailedFilter = DetailedFilterBase | DetailedJoinedFilter;\nexport type DetailedGroupFilter =\n  | { $and: DetailedFilter[] }\n  | { $or: DetailedFilter[] };\n\nexport const isJoinedFilter = (f: DetailedFilter): f is DetailedJoinedFilter =>\n  Boolean(f.type && JOINED_FILTER_TYPES.includes(f.type as any));\nexport const isDetailedFilter = (f: DetailedFilter): f is DetailedFilterBase =>\n  !isJoinedFilter(f.type as any);\n\ntype InfoType = \"pg\";\nexport const getFinalFilterInfo = (\n  fullFilter?: GroupedDetailedFilter | DetailedFilter,\n  context?: ContextDataObject,\n  depth = 0,\n  opts?: { for: InfoType },\n): string => {\n  const forPg = opts?.for === \"pg\";\n  const filterToString = (filter: DetailedFilter): string | undefined => {\n    if (!Object.keys(filter).length) {\n      return undefined;\n    }\n    if (filter.type === \"$ST_DWithin\") {\n      const v = filter.value;\n      if (forPg) {\n        return `ST_DWithin(${filter.fieldName}, 'SRID=4326;POINT(${v.lng} ${v.lat})', ${v.distance})`;\n      }\n      return `${(v.distance / 1000).toFixed(3)}Km of ${v?.name ?? [v.lat, v.lng].join(\", \")}`;\n    }\n\n    if (filter.type === \"$existsJoined\" || filter.type === \"$notExistsJoined\") {\n      const path = filter.path\n        .map((p) => (typeof p === \"string\" ? p : p.table))\n        .join(\" -> \");\n      return `${filter.type === \"$existsJoined\" ? \"Exists\" : \"Does not exist\"} in ${path} where ${filterToString(filter.filter)}`;\n    }\n\n    const f = getFinalFilter(filter, context, {\n      forInfoOnly: opts?.for ?? true,\n    });\n    if (!f) return undefined;\n\n    const fieldNameAndOperator = Object.keys(f)[0];\n    if (!fieldNameAndOperator) return undefined;\n    if (fieldNameAndOperator === \"$term_highlight\") {\n      const [fields, value, args] = f[fieldNameAndOperator];\n      const { matchCase } = args;\n      if (forPg) {\n        return `(${fields.map((f: string) => `${f} ${matchCase ? \"LIKE\" : \"ILIKE\"} '%${value}%'`).join(\" OR \")})`;\n      }\n      return `${fields} contain ${matchCase ? \"(case sensitive)\" : \"\"} ${value}`;\n    }\n    const [fieldName, operatorRaw = \"=\"] = fieldNameAndOperator.split(\".$\");\n    const operator =\n      CORE_FILTER_TYPES.find(({ key }) => key === `$${operatorRaw}`)?.label ||\n      operatorRaw;\n    const value = f[fieldNameAndOperator];\n    if (\"fieldName\" in filter && filter.contextValue?.objectName === \"user\") {\n      return `${fieldName}::TEXT ${operator} ${value}`;\n    }\n    const valueStr =\n      [\"number\", \"boolean\"].includes(typeof value) ? value\n      : value === null ? `null`\n      : value === undefined ? ``\n      : `'${JSON.stringify(value).slice(1, -1)}'`;\n    return `${fieldName} ${operator} ${valueStr}`;\n  };\n\n  let result = \"\";\n  if (fullFilter) {\n    const isAnd = \"$and\" in fullFilter;\n    if (isAnd || \"$or\" in fullFilter) {\n      // @ts-ignore\n      const finalFilters = fullFilter[isAnd ? \"$and\" : \"$or\"]\n        .map((f: any) => getFinalFilterInfo(f, context, depth + 1, opts))\n        .filter(isDefined)\n        .filter((v: string) => v.trim().length);\n      const finalFilterStr = finalFilters.join(isAnd ? \" AND \" : \" OR \");\n      return finalFilters.length > 1 && depth > 1 ?\n          `( ${finalFilterStr} )`\n        : finalFilterStr;\n    }\n\n    return filterToString(fullFilter) ?? \"\";\n  }\n\n  return result;\n};\n\nexport const parseContextVal = (\n  f: DetailedFilterBase,\n  context: ContextDataObject | undefined,\n  { forInfoOnly }: GetFinalFilterOpts = {},\n): any => {\n  if (f.contextValue) {\n    if (forInfoOnly) {\n      const objPath = `${f.contextValue.objectName}.${f.contextValue.objectPropertyName}`;\n      if (forInfoOnly === \"pg\") {\n        if (f.contextValue.objectName === \"user\") {\n          return `prostgles.user('${f.contextValue.objectPropertyName}')`;\n        }\n        return `current_setting('${objPath}')`;\n      }\n      return `{{${objPath}}}`;\n    }\n    if (context) {\n      //@ts-ignore\n      return context[f.contextValue.objectName]?.[\n        f.contextValue.objectPropertyName\n      ];\n    }\n\n    return undefined;\n  }\n\n  return { ...f }.value;\n};\n\ntype GetFinalFilterOpts = {\n  forInfoOnly?: boolean | InfoType;\n  columns?: string[];\n};\nexport const getFinalFilter = (\n  detailedFilter: DetailedFilter,\n  context?: ContextDataObject,\n  opts?: GetFinalFilterOpts,\n) => {\n  const { forInfoOnly = false } = opts ?? {};\n\n  const checkFieldname = (f: string, columns?: string[]) => {\n    if (columns?.length && !columns.includes(f)) {\n      throw new Error(\n        `${f} is not a valid field name. \\nExpecting one of: ${columns.join(\", \")}`,\n      );\n    }\n\n    return f;\n  };\n\n  if (\n    (\"fieldName\" in detailedFilter && detailedFilter.disabled) ||\n    (isJoinedFilter(detailedFilter) && detailedFilter.filter.disabled)\n  )\n    return undefined;\n\n  const getFilter = (\n    f: DetailedFilterBase,\n    columns?: string[],\n  ): Record<string, any> => {\n    const val = parseContextVal(f, context, opts);\n    const fieldName = checkFieldname(f.fieldName, columns);\n\n    if (f.contextValue && !context && !forInfoOnly) {\n      return {};\n    }\n\n    if (\n      FTS_FILTER_TYPES.some((fts) => fts.key === f.type) &&\n      \"fieldName\" in f\n    ) {\n      const fieldName = checkFieldname(f.fieldName, opts?.columns);\n      const { ftsFilterOptions } = f;\n      return {\n        [`${fieldName}.${f.type}`]: [\n          ...(ftsFilterOptions ? [ftsFilterOptions.lang] : []),\n          parseContextVal(f, context, opts),\n        ],\n      };\n    } else if (f.type === \"$term_highlight\") {\n      const fieldName =\n        f.fieldName ? checkFieldname(f.fieldName, opts?.columns) : \"*\";\n      return {\n        $term_highlight: [\n          [fieldName],\n          parseContextVal(f, context, opts),\n          { matchCase: false, edgeTruncate: 30, returnType: \"boolean\" },\n        ],\n      };\n    } else if (f.type == \"$ST_DWithin\") {\n      return {\n        $filter: [{ $ST_DWithin: [fieldName, { ...val }] }],\n      };\n    } else if (\n      f.complexFilter ||\n      f.type?.startsWith(\"$age\") ||\n      f.type === \"$duration\"\n    ) {\n      const isAgeOrDuration =\n        f.type?.startsWith(\"$age\") || f.type === \"$duration\";\n      if (isAgeOrDuration) {\n        if (f.complexFilter && f.complexFilter.type !== \"controlled\") {\n          throw new Error(\n            \"Only controlled complex filters are allowed for age and duration filters\",\n          );\n        }\n        const {\n          comparator,\n          argsLeftToRight = true,\n          otherField,\n        } = f.complexFilter ?? {};\n\n        const filterArgs =\n          f.type === \"$age\" ?\n            [fieldName]\n          : [fieldName, otherField].filter(isDefined);\n\n        if (!argsLeftToRight) filterArgs.reverse();\n        return {\n          $filter: [\n            { [f.type === \"$ageNow\" ? \"$ageNow\" : \"$age\"]: filterArgs },\n            comparator,\n            val,\n          ],\n        };\n      } else if (f.complexFilter) {\n        if (f.complexFilter.type !== \"$filter\") {\n          throw new Error(\"Unexpected complex filter\");\n        }\n\n        return {\n          $filter: [f.complexFilter.leftExpression, f.type, val],\n        };\n      }\n    }\n    if (f.type === \"not null\") {\n      return {\n        [fieldName + \".<>\"]: null,\n      };\n    }\n    if (f.type === \"null\") {\n      return {\n        [fieldName]: null,\n      };\n    }\n    return {\n      [[fieldName, f.type === \"=\" ? null : f.type].filter((v) => v).join(\".\")]:\n        val,\n    };\n  };\n\n  if (isJoinedFilter(detailedFilter)) {\n    return {\n      [detailedFilter.type]: {\n        path: detailedFilter.path,\n        filter: getFilter(detailedFilter.filter),\n      },\n    };\n  }\n\n  return getFilter(detailedFilter, opts?.columns);\n};\n\nexport const simplifyFilter = (f: AnyObject | undefined) => {\n  let result = f;\n  if (result) {\n    while (\n      (result &&\n        \"$and\" in result &&\n        Array.isArray(result.$and) &&\n        result.$and.length < 2) ||\n      (result &&\n        \"$or\" in result &&\n        Array.isArray(result.$or) &&\n        result.$or.length < 2)\n    ) {\n      if (result.$and) result = result.$and?.[0] ?? {};\n      if (result?.$or) result = result.$or?.[0] ?? {};\n    }\n    result ??= {};\n  }\n\n  return result;\n};\n\nexport const getSmartGroupFilter = (\n  detailedFilter: DetailedFilter[] = [],\n  extraFilters?: { detailed?: DetailedFilter[]; filters?: AnyObject[] },\n  operand?: \"and\" | \"or\",\n): AnyObject => {\n  const filterItems = detailedFilter\n    .concat(extraFilters?.detailed ?? [])\n    .map((f) => getFinalFilter(f))\n    .concat(extraFilters?.filters ?? []);\n\n  const result = simplifyFilter({\n    [`$${operand || \"and\"}`]: filterItems.filter(isDefined),\n  });\n\n  return result ?? {};\n};\n\nexport const getTableFilterFromDetailedGroupFilter = (\n  detailedGroupFilter: DetailedGroupFilter,\n): AnyObject => {\n  const [operand, filterItems] =\n    \"$and\" in detailedGroupFilter ?\n      [\"and\" as const, detailedGroupFilter.$and]\n    : [\"or\" as const, detailedGroupFilter.$or];\n  return getSmartGroupFilter(filterItems, undefined, operand);\n};\n\nexport type GroupedDetailedFilter =\n  | { $and: (DetailedFilter | GroupedDetailedFilter)[] }\n  | { $or: (DetailedFilter | GroupedDetailedFilter)[] };\n"
  },
  {
    "path": "common/llmUtils.d.ts",
    "content": "import { DBSSchema } from \"./publishUtils\";\nexport type LLMMessage = DBSSchema[\"llm_messages\"];\nexport declare const getLLMMessageText: ({ message, }: Pick<LLMMessage, \"message\">) => string;\nexport declare const getLLMMessageToolUse: ({ message, }: Pick<LLMMessage, \"message\">) => {\n    type: \"tool_use\";\n    id: string;\n    name: string;\n    input: any;\n}[];\nexport declare const getLLMMessageToolUseResult: ({ message, }: Pick<LLMMessage, \"message\">) => {\n    type: \"tool_result\";\n    tool_use_id: string;\n    tool_name: string;\n    content: string | ({\n        type: \"text\";\n        text: string;\n    } | {\n        type: \"image\" | \"audio\";\n        mimeType: string;\n        data: string;\n    } | {\n        type: \"resource\";\n        resource: {\n            uri: string;\n            mimeType?: string;\n            text?: string;\n            blob?: string;\n        };\n    } | {\n        type: \"resource_link\";\n        uri: string;\n        name: string;\n        mimeType?: string;\n        description?: string;\n    })[];\n    is_error?: boolean;\n}[];\ntype FilterMatch<T, U> = T extends U ? T : never;\ntype FilterUnMatch<T, U> = T extends U ? never : T;\nexport declare const filterArr: <T, U extends Partial<T>>(arr: T[] | readonly T[], pattern: U) => FilterMatch<T, U>[];\nexport declare const findArr: <T, U extends Partial<T>>(arr: T[] | readonly T[], pattern: U) => FilterMatch<T, U> | undefined;\nexport declare const filterArrInverse: <T, U extends Partial<T>>(arr: T[], pattern: U) => FilterUnMatch<T, U>[];\nexport declare const LLM_PROMPT_VARIABLES: {\n    readonly PROSTGLES_SOFTWARE_NAME: \"${prostglesSoftwareName}\";\n    readonly SCHEMA: \"${schema}\";\n    readonly DASHBOARD_TYPES: \"${dashboardTypes}\";\n    readonly TODAY: \"${today}\";\n};\nexport declare const wrapCode: (language: \"sql\" | \"typescript\", code: string) => string;\nexport declare const reachedMaximumNumberOfConsecutiveToolRequests: (messages: Pick<DBSSchema[\"llm_messages\"], \"message\">[], limit: number, onlyFailed?: boolean) => boolean;\nexport declare const isAssistantMessageRequestingToolUse: (message: Pick<DBSSchema[\"llm_messages\"], \"message\"> | undefined) => message is DBSSchema[\"llm_messages\"];\nexport {};\n//# sourceMappingURL=llmUtils.d.ts.map"
  },
  {
    "path": "common/llmUtils.js",
    "content": "export const getLLMMessageText = ({ message, }) => {\n    var _a, _b;\n    if (typeof message === \"string\")\n        return message;\n    const textMessages = filterArr(message, { type: \"text\" });\n    const text = textMessages.map((m) => m.text).join(\"\\n\");\n    const toolsUsed = getLLMMessageToolUse({ message }).map((m) => m.name);\n    const toolsResponse = getLLMMessageToolUseResult({ message })[0];\n    const toolResponseText = typeof (toolsResponse === null || toolsResponse === void 0 ? void 0 : toolsResponse.content) === \"string\" ?\n        toolsResponse.content\n        : (_b = filterArr((_a = toolsResponse === null || toolsResponse === void 0 ? void 0 : toolsResponse.content) !== null && _a !== void 0 ? _a : [], { type: \"text\" })[0]) === null || _b === void 0 ? void 0 : _b.text;\n    return [\n        toolsUsed.length ? `**Tools used: ${toolsUsed.join(\", \")}**` : null,\n        toolsResponse ?\n            (toolsResponse.is_error ? `**Tool use error**` : (`**Tool ${toolsUsed.join(\", \")} response**`)) + `\\n\\n${toolResponseText}`\n            : null,\n        text,\n    ]\n        .filter((v) => v)\n        .join(\"\\n\");\n};\nexport const getLLMMessageToolUse = ({ message, }) => {\n    if (typeof message === \"string\")\n        return [];\n    return filterArr(message, { type: \"tool_use\" });\n};\nexport const getLLMMessageToolUseResult = ({ message, }) => {\n    if (typeof message === \"string\")\n        return [];\n    return filterArr(message, { type: \"tool_result\" });\n};\nexport const filterArr = (arr, pattern) => {\n    const patternEntries = Object.entries(pattern);\n    return arr.filter((item) => {\n        return patternEntries.every(([key, value]) => item[key] === value);\n    });\n};\nexport const findArr = (arr, pattern) => {\n    const patternEntries = Object.entries(pattern);\n    return arr.find((item) => {\n        return patternEntries.every(([key, value]) => item[key] === value);\n    });\n};\nexport const filterArrInverse = (arr, pattern) => {\n    const patternEntries = Object.entries(pattern);\n    return arr.filter((item) => {\n        return patternEntries.every(([key, value]) => item[key] !== value);\n    });\n};\nexport const LLM_PROMPT_VARIABLES = {\n    PROSTGLES_SOFTWARE_NAME: \"${prostglesSoftwareName}\",\n    SCHEMA: \"${schema}\",\n    DASHBOARD_TYPES: \"${dashboardTypes}\",\n    TODAY: \"${today}\",\n};\nexport const wrapCode = (language, code) => {\n    return \"```\" + language + \"\\n\" + code + \"\\n```\";\n};\nexport const reachedMaximumNumberOfConsecutiveToolRequests = (messages, limit, onlyFailed = false) => {\n    const reversedMessages = messages.slice().reverse();\n    let count = 0;\n    for (let i = 0; i < reversedMessages.length; i = i + 2) {\n        const message = reversedMessages[i];\n        const nextMessage = reversedMessages[i + 1];\n        if (!message || !nextMessage) {\n            break; // No more pairs to check\n        }\n        const isToolUseResult = message.message.some((m) => m.type === \"tool_result\" && (!onlyFailed || m.is_error));\n        const isToolUseRequest = isAssistantMessageRequestingToolUse(nextMessage);\n        if (!isToolUseResult || !isToolUseRequest) {\n            break;\n        }\n        count++;\n    }\n    if (count >= limit)\n        return true;\n    return false;\n};\nexport const isAssistantMessageRequestingToolUse = (message) => {\n    return Boolean(message && getLLMMessageToolUse(message).length);\n};\n"
  },
  {
    "path": "common/llmUtils.ts",
    "content": "import { DBSSchema } from \"./publishUtils\";\n\nexport type LLMMessage = DBSSchema[\"llm_messages\"];\n\nexport const getLLMMessageText = ({\n  message,\n}: Pick<LLMMessage, \"message\">): string => {\n  if (typeof message === \"string\") return message;\n  const textMessages = filterArr(message, { type: \"text\" } as const);\n  const text = textMessages.map((m) => m.text).join(\"\\n\");\n\n  const toolsUsed = getLLMMessageToolUse({ message }).map((m) => m.name);\n  const toolsResponse = getLLMMessageToolUseResult({ message })[0];\n  const toolResponseText =\n    typeof toolsResponse?.content === \"string\" ?\n      toolsResponse.content\n    : filterArr(toolsResponse?.content ?? [], { type: \"text\" } as const)[0]\n        ?.text;\n  return [\n    toolsUsed.length ? `**Tools used: ${toolsUsed.join(\", \")}**` : null,\n    toolsResponse ?\n      (toolsResponse.is_error ? `**Tool use error**` : (\n        `**Tool ${toolsUsed.join(\", \")} response**`\n      )) + `\\n\\n${toolResponseText}`\n    : null,\n    text,\n  ]\n    .filter((v) => v)\n    .join(\"\\n\");\n};\n\nexport const getLLMMessageToolUse = ({\n  message,\n}: Pick<LLMMessage, \"message\">) => {\n  if (typeof message === \"string\") return [];\n  return filterArr(message, { type: \"tool_use\" } as const);\n};\n\nexport const getLLMMessageToolUseResult = ({\n  message,\n}: Pick<LLMMessage, \"message\">) => {\n  if (typeof message === \"string\") return [];\n  return filterArr(message, { type: \"tool_result\" } as const);\n};\n\ntype FilterMatch<T, U> = T extends U ? T : never;\ntype FilterUnMatch<T, U> = T extends U ? never : T;\n\nexport const filterArr = <T, U extends Partial<T>>(\n  arr: T[] | readonly T[],\n  pattern: U,\n): FilterMatch<T, U>[] => {\n  const patternEntries = Object.entries(pattern);\n  return arr.filter((item) => {\n    return patternEntries.every(\n      ([key, value]) => item[key as keyof T] === value,\n    );\n  }) as FilterMatch<T, U>[];\n};\n\nexport const findArr = <T, U extends Partial<T>>(\n  arr: T[] | readonly T[],\n  pattern: U,\n): FilterMatch<T, U> | undefined => {\n  const patternEntries = Object.entries(pattern);\n  return arr.find((item) => {\n    return patternEntries.every(\n      ([key, value]) => item[key as keyof T] === value,\n    );\n  }) as FilterMatch<T, U> | undefined;\n};\n\nexport const filterArrInverse = <T, U extends Partial<T>>(\n  arr: T[],\n  pattern: U,\n): FilterUnMatch<T, U>[] => {\n  const patternEntries = Object.entries(pattern);\n  return arr.filter((item) => {\n    return patternEntries.every(\n      ([key, value]) => item[key as keyof T] !== value,\n    );\n  }) as FilterUnMatch<T, U>[];\n};\n\nexport const LLM_PROMPT_VARIABLES = {\n  PROSTGLES_SOFTWARE_NAME: \"${prostglesSoftwareName}\",\n  SCHEMA: \"${schema}\",\n  DASHBOARD_TYPES: \"${dashboardTypes}\",\n  TODAY: \"${today}\",\n} as const;\n\nexport const wrapCode = (language: \"sql\" | \"typescript\", code: string) => {\n  return \"```\" + language + \"\\n\" + code + \"\\n```\";\n};\n\nexport const reachedMaximumNumberOfConsecutiveToolRequests = (\n  messages: Pick<DBSSchema[\"llm_messages\"], \"message\">[],\n  limit: number,\n  onlyFailed = false,\n): boolean => {\n  const reversedMessages = messages.slice().reverse();\n  let count = 0;\n  for (let i = 0; i < reversedMessages.length; i = i + 2) {\n    const message = reversedMessages[i];\n    const nextMessage = reversedMessages[i + 1];\n    if (!message || !nextMessage) {\n      break; // No more pairs to check\n    }\n    const isToolUseResult = message.message.some(\n      (m) => m.type === \"tool_result\" && (!onlyFailed || m.is_error),\n    );\n    const isToolUseRequest = isAssistantMessageRequestingToolUse(nextMessage);\n    if (!isToolUseResult || !isToolUseRequest) {\n      break;\n    }\n\n    count++;\n  }\n  if (count >= limit) return true;\n\n  return false;\n};\n\nexport const isAssistantMessageRequestingToolUse = (\n  message: Pick<DBSSchema[\"llm_messages\"], \"message\"> | undefined,\n): message is DBSSchema[\"llm_messages\"] => {\n  return Boolean(message && getLLMMessageToolUse(message).length);\n};\n"
  },
  {
    "path": "common/mcp.d.ts",
    "content": "import type { DBSSchemaForInsert } from \"./publishUtils\";\nexport type MCPServerInfo = Omit<DBSSchemaForInsert[\"mcp_servers\"], \"id\" | \"cwd\" | \"enabled\" | \"name\"> & {\n    mcp_server_tools?: Omit<DBSSchemaForInsert[\"mcp_server_tools\"], \"id\" | \"server_name\">[];\n};\nexport type McpToolCallResponse = {\n    _meta?: Record<string, any>;\n    content: Array<{\n        type: \"text\";\n        text: string;\n    } | {\n        type: \"image\" | \"audio\";\n        data: string;\n        mimeType: string;\n    } | {\n        type: \"resource\";\n        resource: {\n            uri: string;\n            mimeType?: string;\n            text?: string;\n            blob?: string;\n        };\n    } | {\n        type: \"resource_link\";\n        uri: string;\n        name: string;\n        mimeType?: string;\n        description?: string;\n    }>;\n    isError?: boolean;\n};\nexport declare const DEFAULT_MCP_SERVER_NAMES: readonly [\"filesystem\", \"fetch\", \"git\", \"github\", \"google-maps\", \"memory\", \"playwright\", \"websearch\", \"docker-sandbox\", \"slack\"];\n//# sourceMappingURL=mcp.d.ts.map"
  },
  {
    "path": "common/mcp.js",
    "content": "export const DEFAULT_MCP_SERVER_NAMES = [\n    \"filesystem\",\n    \"fetch\",\n    \"git\",\n    \"github\",\n    \"google-maps\",\n    \"memory\",\n    \"playwright\",\n    \"websearch\",\n    \"docker-sandbox\",\n    \"slack\",\n];\n"
  },
  {
    "path": "common/mcp.ts",
    "content": "import type { DBSSchemaForInsert } from \"./publishUtils\";\n\nexport type MCPServerInfo = Omit<\n  DBSSchemaForInsert[\"mcp_servers\"],\n  \"id\" | \"cwd\" | \"enabled\" | \"name\"\n> & {\n  mcp_server_tools?: Omit<\n    DBSSchemaForInsert[\"mcp_server_tools\"],\n    \"id\" | \"server_name\"\n  >[];\n};\n\nexport type McpToolCallResponse = {\n  _meta?: Record<string, any>;\n  content: Array<\n    | {\n        type: \"text\";\n        text: string;\n      }\n    | {\n        type: \"image\" | \"audio\";\n        data: string;\n        mimeType: string;\n      }\n    | {\n        type: \"resource\";\n        resource: {\n          uri: string;\n          mimeType?: string;\n          text?: string;\n          blob?: string;\n        };\n      }\n    | {\n        type: \"resource_link\";\n        uri: string;\n        name: string;\n        mimeType?: string;\n        description?: string;\n      }\n  >;\n  isError?: boolean;\n};\n\nexport const DEFAULT_MCP_SERVER_NAMES = [\n  \"filesystem\",\n  \"fetch\",\n  \"git\",\n  \"github\",\n  \"google-maps\",\n  \"memory\",\n  \"playwright\",\n  \"websearch\",\n  \"docker-sandbox\",\n  \"slack\",\n] as const;\n"
  },
  {
    "path": "common/prostglesMcp.d.ts",
    "content": "export declare const PROSTGLES_MCP_SERVERS_AND_TOOLS: {\n    readonly \"prostgles-db-methods\": {\n        readonly [x: string]: \"\";\n    };\n    readonly \"prostgles-db\": {\n        readonly execute_sql_with_rollback: {\n            readonly description: \"Executes a SQL query on the connected database in readonly mode (no data can be changed, the transaction is rolled back at the end).\";\n            readonly schema: {\n                readonly type: {\n                    readonly sql: {\n                        readonly type: \"string\";\n                        readonly description: \"SQL query to execute\";\n                    };\n                    readonly query_timeout: {\n                        readonly type: \"number\";\n                        readonly optional: true;\n                        readonly description: \"Maximum time in milliseconds the query will be allowed to run. Defaults to 30000.\";\n                    };\n                    readonly query_params: {\n                        readonly optional: true;\n                        readonly description: \"Query parameters to use in the SQL query. Must satisfy the query schema.\";\n                        readonly type: \"any\";\n                    };\n                };\n            };\n        };\n        readonly execute_sql_with_commit: {\n            readonly description: \"Executes a SQL query on the connected database in commit mode (data can be changed, the transaction commited at the end).\";\n            readonly schema: {\n                readonly type: {\n                    readonly sql: {\n                        readonly type: \"string\";\n                        readonly description: \"SQL query to execute\";\n                    };\n                    readonly query_timeout: {\n                        readonly type: \"number\";\n                        readonly optional: true;\n                        readonly description: \"Maximum time in milliseconds the query will be allowed to run. Defaults to 30000.\";\n                    };\n                    readonly query_params: {\n                        readonly optional: true;\n                        readonly description: \"Query parameters to use in the SQL query. Must satisfy the query schema.\";\n                        readonly type: \"any\";\n                    };\n                };\n            };\n        };\n        readonly select: {\n            readonly description: \"Selects rows from a table.\";\n            readonly schema: {\n                readonly type: {\n                    readonly limit: \"integer\";\n                    readonly filter: {\n                        readonly record: {\n                            readonly values: \"any\";\n                        };\n                        readonly description: \"Row filter. Must satisfy the table schema. Example filters: { id: 1 } or { name: 'John' }\";\n                    };\n                    readonly tableName: {\n                        readonly type: \"string\";\n                        readonly description: \"Table to select from\";\n                    };\n                };\n            };\n        };\n        readonly insert: {\n            readonly description: \"Inserts rows into a table.\";\n            readonly schema: {\n                readonly type: {\n                    readonly tableName: {\n                        readonly type: \"string\";\n                        readonly description: \"Table to insert into\";\n                    };\n                    readonly data: {\n                        readonly description: \"Data to insert into the table. Must satisfy the table schema.\";\n                        readonly arrayOf: \"any\";\n                    };\n                };\n            };\n        };\n        readonly update: {\n            readonly description: \"Updates rows in a table.\";\n            readonly schema: {\n                readonly type: {\n                    readonly data: {\n                        readonly description: \"Data to insert into the table. Must satisfy the table schema.\";\n                        readonly record: {\n                            readonly values: \"any\";\n                        };\n                    };\n                    readonly filter: {\n                        readonly record: {\n                            readonly values: \"any\";\n                        };\n                        readonly description: \"Row filter. Must satisfy the table schema. Example filters: { id: 1 } or { name: 'John' }\";\n                    };\n                    readonly tableName: {\n                        readonly type: \"string\";\n                        readonly description: \"Table to insert into\";\n                    };\n                };\n            };\n        };\n        readonly delete: {\n            readonly description: \"Deletes rows from a table.\";\n            readonly schema: {\n                readonly type: {\n                    readonly filter: {\n                        readonly record: {\n                            readonly values: \"any\";\n                        };\n                        readonly description: \"Row filter. Must satisfy the table schema. Example filters: { id: 1 } or { name: 'John' }\";\n                    };\n                    readonly tableName: {\n                        readonly type: \"string\";\n                        readonly description: \"Table to delete from\";\n                    };\n                };\n            };\n        };\n    };\n    readonly \"prostgles-ui\": {\n        readonly suggest_agent_workflow: {\n            readonly schema: {\n                readonly type: {\n                    readonly allowed_mcp_tool_names: {\n                        readonly description: \"List of MCP tools that can be used to complete the task\";\n                        readonly arrayOf: \"string\";\n                    };\n                    readonly database_access: {\n                        readonly description: \"If access to the database is needed, an access type can be specified. Use the most restrictive access type that is needed to complete the task. If new tables are needed, use the 'execute_sql_commit' access type.\";\n                        readonly oneOfType: readonly [{\n                            readonly Mode: {\n                                readonly enum: readonly [\"None\"];\n                            };\n                        }, {\n                            readonly Mode: {\n                                readonly enum: readonly [\"execute_sql_rollback\"];\n                            };\n                        }, {\n                            readonly Mode: {\n                                readonly enum: readonly [\"execute_sql_commit\"];\n                            };\n                        }, {\n                            readonly Mode: {\n                                readonly enum: readonly [\"Custom\"];\n                            };\n                            readonly tables: {\n                                readonly arrayOfType: {\n                                    readonly tableName: \"string\";\n                                    readonly select: \"boolean\";\n                                    readonly insert: \"boolean\";\n                                    readonly update: \"boolean\";\n                                    readonly delete: \"boolean\";\n                                };\n                            };\n                        }];\n                    };\n                    readonly agent_definitions: {\n                        readonly description: string;\n                        readonly record: {\n                            readonly values: {\n                                readonly type: {\n                                    readonly prompt: \"string\";\n                                    readonly inputJSONSchema: \"any\";\n                                    readonly outputJSONSchema: \"any\";\n                                    readonly maxCostUSD: {\n                                        readonly type: \"number\";\n                                        readonly optional: true;\n                                    };\n                                    readonly maxIterations: {\n                                        readonly type: \"number\";\n                                        readonly optional: true;\n                                    };\n                                    readonly allowedToolNames: \"string[]\";\n                                    readonly allowDatabaseAccess: {\n                                        readonly type: \"boolean\";\n                                        readonly optional: true;\n                                    };\n                                };\n                            };\n                        };\n                    };\n                    readonly workflow_function_definition: {\n                        readonly description: string;\n                        readonly type: \"string\";\n                    };\n                };\n            };\n        };\n        readonly suggest_tools_and_prompt: {\n            readonly schema: {\n                readonly type: {\n                    readonly suggested_mcp_tool_names: {\n                        readonly description: \"List of MCP tools that can be used to complete the task\";\n                        readonly arrayOf: \"string\";\n                    };\n                    readonly suggested_database_tool_names: {\n                        readonly description: \"List of database tools that can be used to complete the task\";\n                        readonly arrayOf: \"string\";\n                        readonly optional: true;\n                    };\n                    readonly suggested_prompt: {\n                        readonly description: \"System prompt that will be used in the LLM chat in conjunction with the selected tools to complete the task. Expand on the task description and include any relevant details and edge cases.\";\n                        readonly type: \"string\";\n                    };\n                    readonly suggested_database_access: {\n                        readonly description: \"If access to the database is needed, an access type can be specified. Use the most restrictive access type that is needed to complete the task. If new tables are needed, use the 'execute_sql_commit' access type.\";\n                        readonly oneOfType: readonly [{\n                            readonly Mode: {\n                                readonly enum: readonly [\"None\"];\n                            };\n                        }, {\n                            readonly Mode: {\n                                readonly enum: readonly [\"execute_sql_rollback\"];\n                            };\n                        }, {\n                            readonly Mode: {\n                                readonly enum: readonly [\"execute_sql_commit\"];\n                            };\n                        }, {\n                            readonly Mode: {\n                                readonly enum: readonly [\"Custom\"];\n                            };\n                            readonly tables: {\n                                readonly arrayOfType: {\n                                    readonly tableName: \"string\";\n                                    readonly select: \"boolean\";\n                                    readonly insert: \"boolean\";\n                                    readonly update: \"boolean\";\n                                    readonly delete: \"boolean\";\n                                };\n                            };\n                        }];\n                    };\n                };\n            };\n        };\n        readonly suggest_dashboards: {\n            readonly schema: {\n                readonly type: {\n                    readonly prostglesWorkspaces: {\n                        readonly description: \"Workspace to create. Must satisfy the typescript WorkspaceInsertModel type\";\n                        readonly arrayOf: \"any\";\n                    };\n                };\n            };\n        };\n    };\n    readonly \"docker-sandbox\": {\n        readonly create_container: {\n            readonly description: \"Creates a docker container. Useful for doing bulk data insert/analysis/processing/ETL.\";\n            readonly schema: {\n                readonly type: {\n                    readonly files: {\n                        readonly description: \"Files to copy into the container. Must include a Dockerfile. Example { \\\"index.ts\\\": \\\"import type { JSONB } from \\\"prostgles-types\\\";\\\" }\";\n                        readonly record: {\n                            readonly partial: true;\n                            readonly values: {\n                                readonly type: \"string\";\n                                readonly description: \"File content. E.g.: 'import type { JSONB } from \\\"prostgles-types\\\";' \";\n                            };\n                        };\n                    };\n                    readonly timeout: {\n                        readonly type: \"number\";\n                        readonly optional: true;\n                        readonly description: \"Maximum time in milliseconds the container will be allowed to run. Defaults to 30000. \";\n                    };\n                    readonly networkMode: {\n                        readonly enum: readonly [\"none\", \"bridge\", \"host\"];\n                        readonly description: \"Network mode for the container. Defaults to 'none'\";\n                        readonly optional: true;\n                    };\n                    readonly environment: {\n                        readonly description: \"Environment variables to set in the container\";\n                        readonly record: {\n                            readonly values: \"string\";\n                            readonly partial: true;\n                        };\n                        readonly optional: true;\n                    };\n                    readonly memory: {\n                        readonly type: \"string\";\n                        readonly description: \"Memory limit (e.g., '512m', '1g'). Defaults to 512m\";\n                        readonly optional: true;\n                    };\n                    readonly cpus: {\n                        readonly type: \"string\";\n                        readonly description: \"CPU limit (e.g., '0.5', '1'). Defaults to 1\";\n                        readonly optional: true;\n                    };\n                };\n            };\n            readonly outputSchema: {\n                readonly type: {\n                    readonly state: {\n                        readonly enum: readonly [\"finished\", \"error\", \"build-error\", \"timed-out\", \"aborted\"];\n                    };\n                    readonly name: \"string\";\n                    readonly command: \"string\";\n                    readonly log: {\n                        readonly arrayOfType: {\n                            readonly type: {\n                                readonly enum: readonly [\"stdout\", \"stderr\", \"error\"];\n                            };\n                            readonly text: \"string\";\n                        };\n                    };\n                    readonly exitCode: \"number\";\n                    readonly runDuration: \"number\";\n                    readonly buildDuration: \"number\";\n                };\n            };\n        };\n    };\n    readonly websearch: {\n        readonly websearch: {\n            readonly description: \"Perform a web search and return results\";\n            readonly schema: {\n                readonly type: {\n                    readonly q: {\n                        readonly type: \"string\";\n                        readonly description: \"The search query. This string is passed to external search services. Supports service-specific syntax (e.g., \\\"site:github.com SearXNG\\\" for Google)\";\n                    };\n                    readonly categories: {\n                        readonly type: \"string\";\n                        readonly optional: true;\n                        readonly description: \" Comma-separated list of active search categories. Categories to search in (e.g., 'general,images,videos')\";\n                    };\n                    readonly engines: {\n                        readonly type: \"string\";\n                        readonly optional: true;\n                        readonly description: \"Comma-separated list of active search engines (e.g., 'google,bing,duckduckgo')\";\n                    };\n                    readonly language: {\n                        readonly type: \"string\";\n                        readonly optional: true;\n                        readonly description: \"Language code for the search results (e.g., 'en' for English, 'fr' for French)\";\n                    };\n                    readonly pageno: {\n                        readonly type: \"integer\";\n                        readonly optional: true;\n                        readonly description: \"Search result page number. Defaults to 1.\";\n                    };\n                    readonly time_range: {\n                        readonly enum: readonly [\"day\", \"month\", \"year\"];\n                        readonly optional: true;\n                        readonly description: \"Time range filter for results ('day' = past day, 'month' = past month, 'year' = past year). Only supported by engines that implement time range filtering\";\n                    };\n                };\n            };\n            readonly outputSchema: {\n                readonly arrayOfType: {\n                    readonly title: \"string\";\n                    readonly content: \"string\";\n                    readonly url: \"string\";\n                    readonly score: \"number\";\n                    readonly category: \"string\";\n                    readonly engine: \"string\";\n                    readonly img_src: \"string\";\n                    readonly thumbnail: \"string\";\n                };\n            };\n        };\n        readonly get_snapshot: {\n            readonly description: \"Get a snapshot of a web page\";\n            readonly schema: {\n                readonly type: {\n                    readonly url: {\n                        readonly type: \"string\";\n                        readonly description: \"URL of the web page to snapshot\";\n                    };\n                };\n            };\n            readonly outputSchema: {\n                readonly type: {\n                    readonly content: \"string\";\n                };\n            };\n        };\n    };\n};\ntype ProstglesMcpTools = typeof PROSTGLES_MCP_SERVERS_AND_TOOLS;\nexport type ProstglesMcpTool = {\n    [K in keyof ProstglesMcpTools]: {\n        type: K;\n        tool_name: keyof ProstglesMcpTools[K];\n    };\n}[keyof ProstglesMcpTools];\ndeclare const MCP_TOOL_NAME_SEPARATOR = \"--\";\nexport declare const getMCPFullToolName: <Name extends string, ServerName extends string>(server_name: ServerName, name: Name) => `${ServerName}${typeof MCP_TOOL_NAME_SEPARATOR}${Name}`;\nexport declare const getProstglesMCPFullToolName: <ServerName extends keyof ProstglesMcpTools, Name extends keyof ProstglesMcpTools[ServerName] & string>(server_name: ServerName, name: Name) => `${ServerName}--${Name}`;\nexport declare const getMCPToolNameParts: (fullName: string) => {\n    serverName: string;\n    toolName: string;\n} | undefined;\nexport {};\n//# sourceMappingURL=prostglesMcp.d.ts.map"
  },
  {
    "path": "common/prostglesMcp.js",
    "content": "import { fixIndent } from \"./utils\";\nconst runSQLSchema = {\n    type: {\n        sql: {\n            type: \"string\",\n            description: \"SQL query to execute\",\n        },\n        query_timeout: {\n            type: \"number\",\n            optional: true,\n            description: \"Maximum time in milliseconds the query will be allowed to run. Defaults to 30000.\",\n        },\n        query_params: {\n            optional: true,\n            description: \"Query parameters to use in the SQL query. Must satisfy the query schema.\",\n            type: \"any\",\n        },\n    },\n};\nconst filesSchema = {\n    description: 'Files to copy into the container. Must include a Dockerfile. Example { \"index.ts\": \"import type { JSONB } from \"prostgles-types\";\" }',\n    record: {\n        partial: true,\n        values: {\n            type: \"string\",\n            description: \"File content. E.g.: 'import type { JSONB } from \\\"prostgles-types\\\";' \",\n        },\n    },\n};\nconst filterSchema = {\n    filter: {\n        record: { values: \"any\" },\n        description: \"Row filter. Must satisfy the table schema. Example filters: { id: 1 } or { name: 'John' }\",\n    },\n};\nexport const PROSTGLES_MCP_SERVERS_AND_TOOLS = {\n    \"prostgles-db-methods\": { [\"\"]: \"\" },\n    \"prostgles-db\": {\n        execute_sql_with_rollback: {\n            description: \"Executes a SQL query on the connected database in readonly mode (no data can be changed, the transaction is rolled back at the end).\",\n            schema: runSQLSchema,\n        },\n        execute_sql_with_commit: {\n            description: \"Executes a SQL query on the connected database in commit mode (data can be changed, the transaction commited at the end).\",\n            schema: runSQLSchema,\n        },\n        select: {\n            description: \"Selects rows from a table.\",\n            schema: {\n                type: Object.assign(Object.assign({ tableName: {\n                        type: \"string\",\n                        description: \"Table to select from\",\n                    } }, filterSchema), { limit: \"integer\" }),\n            },\n        },\n        insert: {\n            description: \"Inserts rows into a table.\",\n            schema: {\n                type: {\n                    tableName: {\n                        type: \"string\",\n                        description: \"Table to insert into\",\n                    },\n                    data: {\n                        description: \"Data to insert into the table. Must satisfy the table schema.\",\n                        arrayOf: \"any\",\n                    },\n                },\n            },\n        },\n        update: {\n            description: \"Updates rows in a table.\",\n            schema: {\n                type: Object.assign(Object.assign({ tableName: {\n                        type: \"string\",\n                        description: \"Table to insert into\",\n                    } }, filterSchema), { data: {\n                        description: \"Data to insert into the table. Must satisfy the table schema.\",\n                        record: {\n                            values: \"any\",\n                        },\n                    } }),\n            },\n        },\n        delete: {\n            description: \"Deletes rows from a table.\",\n            schema: {\n                type: Object.assign({ tableName: {\n                        type: \"string\",\n                        description: \"Table to delete from\",\n                    } }, filterSchema),\n            },\n        },\n    },\n    \"prostgles-ui\": {\n        suggest_agent_workflow: {\n            schema: {\n                type: {\n                    allowed_mcp_tool_names: {\n                        description: \"List of MCP tools that can be used to complete the task\",\n                        arrayOf: \"string\",\n                    },\n                    database_access: {\n                        description: \"If access to the database is needed, an access type can be specified. Use the most restrictive access type that is needed to complete the task. If new tables are needed, use the 'execute_sql_commit' access type.\",\n                        oneOfType: [\n                            { Mode: { enum: [\"None\"] } },\n                            { Mode: { enum: [\"execute_sql_rollback\"] } },\n                            { Mode: { enum: [\"execute_sql_commit\"] } },\n                            {\n                                Mode: { enum: [\"Custom\"] },\n                                tables: {\n                                    arrayOfType: {\n                                        tableName: \"string\",\n                                        select: \"boolean\",\n                                        insert: \"boolean\",\n                                        update: \"boolean\",\n                                        delete: \"boolean\",\n                                    },\n                                },\n                            },\n                        ],\n                    },\n                    agent_definitions: {\n                        description: fixIndent(`\n              The agent definitions are used to invoke an LLM chat with the specified inputs and constraints to return the output schema. \n              The agent can only use from the suggested tools to complete the task.\n              The workflow_function_definition can invoke these agents as needed.\n            `),\n                        record: {\n                            values: {\n                                type: {\n                                    prompt: \"string\",\n                                    inputJSONSchema: \"any\",\n                                    outputJSONSchema: \"any\",\n                                    maxCostUSD: { type: \"number\", optional: true },\n                                    maxIterations: { type: \"number\", optional: true },\n                                    allowedToolNames: \"string[]\",\n                                    allowDatabaseAccess: { type: \"boolean\", optional: true },\n                                },\n                            },\n                        },\n                    },\n                    workflow_function_definition: {\n                        description: fixIndent(` \n                  The workflow function must satisfy the following definition: \n                   \n                  type WorkflowFunction = ({\n                    runTableAction?: (tableName: string, action: \"select\" | \"update\" | \"insert\" | \"delete\") => Record<string, any>[];\n                    runSQL?: (sql: string) => Record<string, any>[];\n                    agents: { [AgentName: keyof typeof agentDefinitions]: (input: (typeof agentDefinitions)[AgentName][\"inputSchema]) => Promise<(typeof agentDefinitions)[AgentName][\"outputSchema]>>;\n                  }) => Promise<void>;\n                   \n                  /*\n                    Example workflow_function_definition:\n                    \n                    const workflow_function = async ({ runSQL, agents }) => {\n                      \n                      const rows = runSQL(\"SELECT * FROM my_table\");\n                      \n                      for(const row of rows) {\n                        const rowEnhanced = await agents.rowEnhancer({ row });\n                        await runSQL(\\`\n                          UPDATE my_table SET enhanced_data = \\${rowEnhanced.enhanced_data} WHERE id = \\${row.id}\n                        \\`, { row, rowEnhanced });\n                      }\n                    };\n\n                  */\n                `),\n                        type: \"string\",\n                    },\n                },\n            },\n        },\n        suggest_tools_and_prompt: {\n            schema: {\n                type: {\n                    suggested_mcp_tool_names: {\n                        description: \"List of MCP tools that can be used to complete the task\",\n                        arrayOf: \"string\",\n                    },\n                    suggested_database_tool_names: {\n                        description: \"List of database tools that can be used to complete the task\",\n                        arrayOf: \"string\",\n                        optional: true,\n                    },\n                    suggested_prompt: {\n                        description: \"System prompt that will be used in the LLM chat in conjunction with the selected tools to complete the task. Expand on the task description and include any relevant details and edge cases.\",\n                        type: \"string\",\n                    },\n                    suggested_database_access: {\n                        description: \"If access to the database is needed, an access type can be specified. Use the most restrictive access type that is needed to complete the task. If new tables are needed, use the 'execute_sql_commit' access type.\",\n                        oneOfType: [\n                            { Mode: { enum: [\"None\"] } },\n                            { Mode: { enum: [\"execute_sql_rollback\"] } },\n                            { Mode: { enum: [\"execute_sql_commit\"] } },\n                            {\n                                Mode: { enum: [\"Custom\"] },\n                                tables: {\n                                    arrayOfType: {\n                                        tableName: \"string\",\n                                        select: \"boolean\",\n                                        insert: \"boolean\",\n                                        update: \"boolean\",\n                                        delete: \"boolean\",\n                                    },\n                                },\n                            },\n                        ],\n                    },\n                },\n            },\n        },\n        suggest_dashboards: {\n            schema: {\n                type: {\n                    prostglesWorkspaces: {\n                        description: \"Workspace to create. Must satisfy the typescript WorkspaceInsertModel type\",\n                        arrayOf: \"any\",\n                    },\n                },\n            },\n        },\n    },\n    \"docker-sandbox\": {\n        create_container: {\n            description: \"Creates a docker container. Useful for doing bulk data insert/analysis/processing/ETL.\",\n            schema: {\n                type: {\n                    files: filesSchema,\n                    timeout: {\n                        type: \"number\",\n                        optional: true,\n                        description: \"Maximum time in milliseconds the container will be allowed to run. Defaults to 30000. \",\n                        // default: 30000,\n                    },\n                    networkMode: {\n                        enum: [\"none\", \"bridge\", \"host\"],\n                        description: \"Network mode for the container. Defaults to 'none'\",\n                        // default: \"none\",\n                        optional: true,\n                    },\n                    environment: {\n                        description: \"Environment variables to set in the container\",\n                        record: { values: \"string\", partial: true },\n                        optional: true,\n                    },\n                    memory: {\n                        type: \"string\",\n                        description: \"Memory limit (e.g., '512m', '1g'). Defaults to 512m\",\n                        optional: true,\n                        // default: \"512m\",\n                    },\n                    cpus: {\n                        type: \"string\",\n                        description: \"CPU limit (e.g., '0.5', '1'). Defaults to 1\",\n                        optional: true,\n                        // default: \"1\",\n                    },\n                },\n            },\n            outputSchema: {\n                type: {\n                    state: {\n                        enum: [\"finished\", \"error\", \"build-error\", \"timed-out\", \"aborted\"],\n                    },\n                    name: \"string\",\n                    command: \"string\",\n                    log: {\n                        arrayOfType: {\n                            type: { enum: [\"stdout\", \"stderr\", \"error\"] },\n                            text: \"string\",\n                        },\n                    },\n                    exitCode: \"number\",\n                    runDuration: \"number\",\n                    buildDuration: \"number\",\n                },\n            },\n        },\n    },\n    websearch: {\n        websearch: {\n            description: \"Perform a web search and return results\",\n            schema: {\n                type: {\n                    q: {\n                        type: \"string\",\n                        description: 'The search query. This string is passed to external search services. Supports service-specific syntax (e.g., \"site:github.com SearXNG\" for Google)',\n                    },\n                    categories: {\n                        type: \"string\",\n                        optional: true,\n                        description: \" Comma-separated list of active search categories. Categories to search in (e.g., 'general,images,videos')\",\n                    },\n                    engines: {\n                        type: \"string\",\n                        optional: true,\n                        description: \"Comma-separated list of active search engines (e.g., 'google,bing,duckduckgo')\",\n                    },\n                    language: {\n                        type: \"string\",\n                        optional: true,\n                        description: \"Language code for the search results (e.g., 'en' for English, 'fr' for French)\",\n                    },\n                    pageno: {\n                        type: \"integer\",\n                        optional: true,\n                        description: \"Search result page number. Defaults to 1.\",\n                    },\n                    time_range: {\n                        enum: [\"day\", \"month\", \"year\"],\n                        optional: true,\n                        description: \"Time range filter for results ('day' = past day, 'month' = past month, 'year' = past year). Only supported by engines that implement time range filtering\",\n                    },\n                },\n            },\n            outputSchema: {\n                arrayOfType: {\n                    title: \"string\",\n                    content: \"string\",\n                    url: \"string\",\n                    score: \"number\",\n                    category: \"string\",\n                    engine: \"string\",\n                    img_src: \"string\",\n                    thumbnail: \"string\",\n                },\n            },\n        },\n        get_snapshot: {\n            description: \"Get a snapshot of a web page\",\n            schema: {\n                type: {\n                    url: {\n                        type: \"string\",\n                        description: \"URL of the web page to snapshot\",\n                    },\n                },\n            },\n            outputSchema: {\n                type: {\n                    content: \"string\",\n                },\n            },\n        },\n    },\n};\nconst MCP_TOOL_NAME_SEPARATOR = \"--\";\nexport const getMCPFullToolName = (server_name, name) => {\n    return `${server_name}${MCP_TOOL_NAME_SEPARATOR}${name}`;\n};\nexport const getProstglesMCPFullToolName = (server_name, name) => getMCPFullToolName(server_name, name);\nexport const getMCPToolNameParts = (fullName) => {\n    const [serverName, toolName] = fullName.split(MCP_TOOL_NAME_SEPARATOR);\n    if (serverName && toolName) {\n        return { serverName, toolName };\n    }\n};\n"
  },
  {
    "path": "common/prostglesMcp.ts",
    "content": "import { fixIndent } from \"./utils\";\n\nconst runSQLSchema = {\n  type: {\n    sql: {\n      type: \"string\",\n      description: \"SQL query to execute\",\n    },\n    query_timeout: {\n      type: \"number\",\n      optional: true,\n      description:\n        \"Maximum time in milliseconds the query will be allowed to run. Defaults to 30000.\",\n    },\n    query_params: {\n      optional: true,\n      description:\n        \"Query parameters to use in the SQL query. Must satisfy the query schema.\",\n      type: \"any\",\n    },\n  },\n} as const;\n\nconst filesSchema = {\n  description:\n    'Files to copy into the container. Must include a Dockerfile. Example { \"index.ts\": \"import type { JSONB } from \"prostgles-types\";\" }',\n  record: {\n    partial: true,\n    values: {\n      type: \"string\",\n      description:\n        \"File content. E.g.: 'import type { JSONB } from \\\"prostgles-types\\\";' \",\n    },\n  },\n} as const;\n\nconst filterSchema = {\n  filter: {\n    record: { values: \"any\" },\n    description:\n      \"Row filter. Must satisfy the table schema. Example filters: { id: 1 } or { name: 'John' }\",\n  },\n} as const;\n\nexport const PROSTGLES_MCP_SERVERS_AND_TOOLS = {\n  \"prostgles-db-methods\": { [\"\" as string]: \"\" },\n  \"prostgles-db\": {\n    execute_sql_with_rollback: {\n      description:\n        \"Executes a SQL query on the connected database in readonly mode (no data can be changed, the transaction is rolled back at the end).\",\n      schema: runSQLSchema,\n    },\n    execute_sql_with_commit: {\n      description:\n        \"Executes a SQL query on the connected database in commit mode (data can be changed, the transaction commited at the end).\",\n      schema: runSQLSchema,\n    },\n    select: {\n      description: \"Selects rows from a table.\",\n      schema: {\n        type: {\n          tableName: {\n            type: \"string\",\n            description: \"Table to select from\",\n          },\n          ...filterSchema,\n          limit: \"integer\",\n        },\n      },\n    },\n    insert: {\n      description: \"Inserts rows into a table.\",\n      schema: {\n        type: {\n          tableName: {\n            type: \"string\",\n            description: \"Table to insert into\",\n          },\n          data: {\n            description:\n              \"Data to insert into the table. Must satisfy the table schema.\",\n            arrayOf: \"any\",\n          },\n        },\n      },\n    },\n    update: {\n      description: \"Updates rows in a table.\",\n      schema: {\n        type: {\n          tableName: {\n            type: \"string\",\n            description: \"Table to insert into\",\n          },\n          ...filterSchema,\n          data: {\n            description:\n              \"Data to insert into the table. Must satisfy the table schema.\",\n            record: {\n              values: \"any\",\n            },\n          },\n        },\n      },\n    },\n    delete: {\n      description: \"Deletes rows from a table.\",\n      schema: {\n        type: {\n          tableName: {\n            type: \"string\",\n            description: \"Table to delete from\",\n          },\n          ...filterSchema,\n        },\n      },\n    },\n  },\n  \"prostgles-ui\": {\n    suggest_agent_workflow: {\n      schema: {\n        type: {\n          allowed_mcp_tool_names: {\n            description:\n              \"List of MCP tools that can be used to complete the task\",\n            arrayOf: \"string\",\n          },\n          database_access: {\n            description:\n              \"If access to the database is needed, an access type can be specified. Use the most restrictive access type that is needed to complete the task. If new tables are needed, use the 'execute_sql_commit' access type.\",\n            oneOfType: [\n              { Mode: { enum: [\"None\"] } },\n              { Mode: { enum: [\"execute_sql_rollback\"] } },\n              { Mode: { enum: [\"execute_sql_commit\"] } },\n              {\n                Mode: { enum: [\"Custom\"] },\n                tables: {\n                  arrayOfType: {\n                    tableName: \"string\",\n                    select: \"boolean\",\n                    insert: \"boolean\",\n                    update: \"boolean\",\n                    delete: \"boolean\",\n                  },\n                },\n              },\n            ],\n          },\n          agent_definitions: {\n            description: fixIndent(`\n              The agent definitions are used to invoke an LLM chat with the specified inputs and constraints to return the output schema. \n              The agent can only use from the suggested tools to complete the task.\n              The workflow_function_definition can invoke these agents as needed.\n            `),\n            record: {\n              values: {\n                type: {\n                  prompt: \"string\",\n                  inputJSONSchema: \"any\",\n                  outputJSONSchema: \"any\",\n                  maxCostUSD: { type: \"number\", optional: true },\n                  maxIterations: { type: \"number\", optional: true },\n                  allowedToolNames: \"string[]\",\n                  allowDatabaseAccess: { type: \"boolean\", optional: true },\n                },\n              },\n            },\n          },\n          workflow_function_definition: {\n            description: fixIndent(` \n                  The workflow function must satisfy the following definition: \n                   \n                  type WorkflowFunction = ({\n                    runTableAction?: (tableName: string, action: \"select\" | \"update\" | \"insert\" | \"delete\") => Record<string, any>[];\n                    runSQL?: (sql: string) => Record<string, any>[];\n                    agents: { [AgentName: keyof typeof agentDefinitions]: (input: (typeof agentDefinitions)[AgentName][\"inputSchema]) => Promise<(typeof agentDefinitions)[AgentName][\"outputSchema]>>;\n                  }) => Promise<void>;\n                   \n                  /*\n                    Example workflow_function_definition:\n                    \n                    const workflow_function = async ({ runSQL, agents }) => {\n                      \n                      const rows = runSQL(\"SELECT * FROM my_table\");\n                      \n                      for(const row of rows) {\n                        const rowEnhanced = await agents.rowEnhancer({ row });\n                        await runSQL(\\`\n                          UPDATE my_table SET enhanced_data = \\${rowEnhanced.enhanced_data} WHERE id = \\${row.id}\n                        \\`, { row, rowEnhanced });\n                      }\n                    };\n\n                  */\n                `),\n            type: \"string\",\n          },\n        },\n      },\n    },\n    suggest_tools_and_prompt: {\n      schema: {\n        type: {\n          suggested_mcp_tool_names: {\n            description:\n              \"List of MCP tools that can be used to complete the task\",\n            arrayOf: \"string\",\n          },\n          suggested_database_tool_names: {\n            description:\n              \"List of database tools that can be used to complete the task\",\n            arrayOf: \"string\",\n            optional: true,\n          },\n          suggested_prompt: {\n            description:\n              \"System prompt that will be used in the LLM chat in conjunction with the selected tools to complete the task. Expand on the task description and include any relevant details and edge cases.\",\n            type: \"string\",\n          },\n          suggested_database_access: {\n            description:\n              \"If access to the database is needed, an access type can be specified. Use the most restrictive access type that is needed to complete the task. If new tables are needed, use the 'execute_sql_commit' access type.\",\n            oneOfType: [\n              { Mode: { enum: [\"None\"] } },\n              { Mode: { enum: [\"execute_sql_rollback\"] } },\n              { Mode: { enum: [\"execute_sql_commit\"] } },\n              {\n                Mode: { enum: [\"Custom\"] },\n                tables: {\n                  arrayOfType: {\n                    tableName: \"string\",\n                    select: \"boolean\",\n                    insert: \"boolean\",\n                    update: \"boolean\",\n                    delete: \"boolean\",\n                  },\n                },\n              },\n            ],\n          },\n        },\n      },\n    },\n    suggest_dashboards: {\n      schema: {\n        type: {\n          prostglesWorkspaces: {\n            description:\n              \"Workspace to create. Must satisfy the typescript WorkspaceInsertModel type\",\n            arrayOf: \"any\",\n          },\n        },\n      },\n    },\n  },\n  \"docker-sandbox\": {\n    create_container: {\n      description:\n        \"Creates a docker container. Useful for doing bulk data insert/analysis/processing/ETL.\",\n      schema: {\n        type: {\n          files: filesSchema,\n          timeout: {\n            type: \"number\",\n            optional: true,\n            description:\n              \"Maximum time in milliseconds the container will be allowed to run. Defaults to 30000. \",\n            // default: 30000,\n          },\n          networkMode: {\n            enum: [\"none\", \"bridge\", \"host\"],\n            description: \"Network mode for the container. Defaults to 'none'\",\n            // default: \"none\",\n            optional: true,\n          },\n          environment: {\n            description: \"Environment variables to set in the container\",\n            record: { values: \"string\", partial: true },\n            optional: true,\n          },\n          memory: {\n            type: \"string\",\n            description: \"Memory limit (e.g., '512m', '1g'). Defaults to 512m\",\n            optional: true,\n            // default: \"512m\",\n          },\n          cpus: {\n            type: \"string\",\n            description: \"CPU limit (e.g., '0.5', '1'). Defaults to 1\",\n            optional: true,\n            // default: \"1\",\n          },\n        },\n      },\n      outputSchema: {\n        type: {\n          state: {\n            enum: [\"finished\", \"error\", \"build-error\", \"timed-out\", \"aborted\"],\n          },\n          name: \"string\",\n          command: \"string\",\n          log: {\n            arrayOfType: {\n              type: { enum: [\"stdout\", \"stderr\", \"error\"] },\n              text: \"string\",\n            },\n          },\n          exitCode: \"number\",\n          runDuration: \"number\",\n          buildDuration: \"number\",\n        },\n      },\n    },\n  },\n  websearch: {\n    websearch: {\n      description: \"Perform a web search and return results\",\n      schema: {\n        type: {\n          q: {\n            type: \"string\",\n            description:\n              'The search query. This string is passed to external search services. Supports service-specific syntax (e.g., \"site:github.com SearXNG\" for Google)',\n          },\n          categories: {\n            type: \"string\",\n            optional: true,\n            description:\n              \" Comma-separated list of active search categories. Categories to search in (e.g., 'general,images,videos')\",\n          },\n          engines: {\n            type: \"string\",\n            optional: true,\n            description:\n              \"Comma-separated list of active search engines (e.g., 'google,bing,duckduckgo')\",\n          },\n          language: {\n            type: \"string\",\n            optional: true,\n            description:\n              \"Language code for the search results (e.g., 'en' for English, 'fr' for French)\",\n          },\n          pageno: {\n            type: \"integer\",\n            optional: true,\n            description: \"Search result page number. Defaults to 1.\",\n          },\n          time_range: {\n            enum: [\"day\", \"month\", \"year\"],\n            optional: true,\n            description:\n              \"Time range filter for results ('day' = past day, 'month' = past month, 'year' = past year). Only supported by engines that implement time range filtering\",\n          },\n        },\n      },\n      outputSchema: {\n        arrayOfType: {\n          title: \"string\",\n          content: \"string\",\n          url: \"string\",\n          score: \"number\",\n          category: \"string\",\n          engine: \"string\",\n          img_src: \"string\",\n          thumbnail: \"string\",\n        },\n      },\n    },\n    get_snapshot: {\n      description: \"Get a snapshot of a web page\",\n      schema: {\n        type: {\n          url: {\n            type: \"string\",\n            description: \"URL of the web page to snapshot\",\n          },\n        },\n      },\n      outputSchema: {\n        type: {\n          content: \"string\",\n        },\n      },\n    },\n  },\n} as const;\n\ntype ProstglesMcpTools = typeof PROSTGLES_MCP_SERVERS_AND_TOOLS;\nexport type ProstglesMcpTool = {\n  [K in keyof ProstglesMcpTools]: {\n    type: K;\n    tool_name: keyof ProstglesMcpTools[K];\n  };\n}[keyof ProstglesMcpTools];\n\nconst MCP_TOOL_NAME_SEPARATOR = \"--\";\nexport const getMCPFullToolName = <\n  Name extends string,\n  ServerName extends string,\n>(\n  server_name: ServerName,\n  name: Name,\n): `${ServerName}${typeof MCP_TOOL_NAME_SEPARATOR}${Name}` => {\n  return `${server_name}${MCP_TOOL_NAME_SEPARATOR}${name}` as const;\n};\n\nexport const getProstglesMCPFullToolName = <\n  ServerName extends keyof ProstglesMcpTools,\n  Name extends keyof ProstglesMcpTools[ServerName] & string,\n>(\n  server_name: ServerName,\n  name: Name,\n) => getMCPFullToolName(server_name, name);\n\nexport const getMCPToolNameParts = (fullName: string) => {\n  const [serverName, toolName] = fullName.split(MCP_TOOL_NAME_SEPARATOR);\n  if (serverName && toolName) {\n    return { serverName, toolName };\n  }\n};\n\nexport type AllowedChatTool = {\n  server_name: string;\n  name: string;\n  tool_name: string;\n  description: string;\n  input_schema: any;\n  auto_approve: boolean;\n} & (\n  | {\n      type: \"mcp\";\n      tool_id: number;\n    }\n  | {\n      type: \"prostgles-db-methods\";\n      server_function_id: number;\n    }\n  | Exclude<ProstglesMcpTool, { type: \"prostgles-db-methods\" }>\n);\n"
  },
  {
    "path": "common/psql_queries.json",
    "content": "[\n  {\n    \"cmd\": \"\\\\d\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list tables, views, and sequences\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','v','m','S','f','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list tables, views, and sequences\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','t','v','m','S','s','f','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list tables, views, and sequences\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as \\\"Persistence\\\",\\n  am.amname as \\\"Access method\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \\\"Size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','t','v','m','S','s','f','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\d\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"describe table, view, sequence, or index\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','v','m','S','f','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"describe table, view, sequence, or index\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','t','v','m','S','s','f','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"describe table, view, sequence, or index\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as \\\"Persistence\\\",\\n  am.amname as \\\"Access method\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \\\"Size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','t','v','m','S','s','f','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\da\",\n    \"opts\": \"[S]\",\n    \"desc\": \"list aggregates\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  p.proname AS \\\"Name\\\",\\n  pg_catalog.format_type(p.prorettype, NULL) AS \\\"Result data type\\\",\\n  CASE WHEN p.pronargs = 0\\n    THEN CAST('*' AS pg_catalog.text)\\n    ELSE pg_catalog.pg_get_function_arguments(p.oid)\\n  END AS \\\"Argument data types\\\",\\n  pg_catalog.obj_description(p.oid, 'pg_proc') as \\\"Description\\\"\\nFROM pg_catalog.pg_proc p\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\\nWHERE p.prokind = 'a'\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_function_is_visible(p.oid)\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\daS\",\n    \"opts\": \"[S]\",\n    \"desc\": \"list aggregates\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  p.proname AS \\\"Name\\\",\\n  pg_catalog.format_type(p.prorettype, NULL) AS \\\"Result data type\\\",\\n  CASE WHEN p.pronargs = 0\\n    THEN CAST('*' AS pg_catalog.text)\\n    ELSE pg_catalog.pg_get_function_arguments(p.oid)\\n  END AS \\\"Argument data types\\\",\\n  pg_catalog.obj_description(p.oid, 'pg_proc') as \\\"Description\\\"\\nFROM pg_catalog.pg_proc p\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\\nWHERE p.prokind = 'a'\\n  AND pg_catalog.pg_function_is_visible(p.oid)\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dA\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list access methods\",\n    \"query\": \"\\nSELECT amname AS \\\"Name\\\",\\n  CASE amtype WHEN 'i' THEN 'Index' WHEN 't' THEN 'Table' END AS \\\"Type\\\"\\nFROM pg_catalog.pg_am\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dA+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list access methods\",\n    \"query\": \"\\nSELECT amname AS \\\"Name\\\",\\n  CASE amtype WHEN 'i' THEN 'Index' WHEN 't' THEN 'Table' END AS \\\"Type\\\",\\n  amhandler AS \\\"Handler\\\",\\n  pg_catalog.obj_description(oid, 'pg_am') AS \\\"Description\\\"\\nFROM pg_catalog.pg_am\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dAc\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list operator classes\",\n    \"query\": \"\\nSELECT\\n  am.amname AS \\\"AM\\\",\\n  pg_catalog.format_type(c.opcintype, NULL) AS \\\"Input type\\\",\\n  CASE\\n    WHEN c.opckeytype <> 0 AND c.opckeytype <> c.opcintype\\n    THEN pg_catalog.format_type(c.opckeytype, NULL)\\n    ELSE NULL\\n  END AS \\\"Storage type\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opclass_is_visible(c.oid)\\n    THEN pg_catalog.format('%I', c.opcname)\\n    ELSE pg_catalog.format('%I.%I', n.nspname, c.opcname)\\n  END AS \\\"Operator class\\\",\\n  (CASE WHEN c.opcdefault\\n    THEN 'yes'\\n    ELSE 'no'\\n  END) AS \\\"Default?\\\"\\nFROM pg_catalog.pg_opclass c\\n  LEFT JOIN pg_catalog.pg_am am on am.oid = c.opcmethod\\n  LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.opcnamespace\\n  LEFT JOIN pg_catalog.pg_type t ON t.oid = c.opcintype\\n  LEFT JOIN pg_catalog.pg_namespace tn ON tn.oid = t.typnamespace\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dAc+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list operator classes\",\n    \"query\": \"\\nSELECT\\n  am.amname AS \\\"AM\\\",\\n  pg_catalog.format_type(c.opcintype, NULL) AS \\\"Input type\\\",\\n  CASE\\n    WHEN c.opckeytype <> 0 AND c.opckeytype <> c.opcintype\\n    THEN pg_catalog.format_type(c.opckeytype, NULL)\\n    ELSE NULL\\n  END AS \\\"Storage type\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opclass_is_visible(c.oid)\\n    THEN pg_catalog.format('%I', c.opcname)\\n    ELSE pg_catalog.format('%I.%I', n.nspname, c.opcname)\\n  END AS \\\"Operator class\\\",\\n  (CASE WHEN c.opcdefault\\n    THEN 'yes'\\n    ELSE 'no'\\n  END) AS \\\"Default?\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opfamily_is_visible(of.oid)\\n    THEN pg_catalog.format('%I', of.opfname)\\n    ELSE pg_catalog.format('%I.%I', ofn.nspname, of.opfname)\\n  END AS \\\"Operator family\\\",\\n pg_catalog.pg_get_userbyid(c.opcowner) AS \\\"Owner\\\"\\n\\nFROM pg_catalog.pg_opclass c\\n  LEFT JOIN pg_catalog.pg_am am on am.oid = c.opcmethod\\n  LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.opcnamespace\\n  LEFT JOIN pg_catalog.pg_type t ON t.oid = c.opcintype\\n  LEFT JOIN pg_catalog.pg_namespace tn ON tn.oid = t.typnamespace\\n  LEFT JOIN pg_catalog.pg_opfamily of ON of.oid = c.opcfamily\\n  LEFT JOIN pg_catalog.pg_namespace ofn ON ofn.oid = of.opfnamespace\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dAf\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list operator families\",\n    \"query\": \"\\nSELECT\\n  am.amname AS \\\"AM\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opfamily_is_visible(f.oid)\\n    THEN pg_catalog.format('%I', f.opfname)\\n    ELSE pg_catalog.format('%I.%I', n.nspname, f.opfname)\\n  END AS \\\"Operator family\\\",\\n  (SELECT\\n     pg_catalog.string_agg(pg_catalog.format_type(oc.opcintype, NULL), ', ')\\n   FROM pg_catalog.pg_opclass oc\\n   WHERE oc.opcfamily = f.oid) \\\"Applicable types\\\"\\nFROM pg_catalog.pg_opfamily f\\n  LEFT JOIN pg_catalog.pg_am am on am.oid = f.opfmethod\\n  LEFT JOIN pg_catalog.pg_namespace n ON n.oid = f.opfnamespace\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dAf+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list operator families\",\n    \"query\": \"\\nSELECT\\n  am.amname AS \\\"AM\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opfamily_is_visible(f.oid)\\n    THEN pg_catalog.format('%I', f.opfname)\\n    ELSE pg_catalog.format('%I.%I', n.nspname, f.opfname)\\n  END AS \\\"Operator family\\\",\\n  (SELECT\\n     pg_catalog.string_agg(pg_catalog.format_type(oc.opcintype, NULL), ', ')\\n   FROM pg_catalog.pg_opclass oc\\n   WHERE oc.opcfamily = f.oid) \\\"Applicable types\\\",\\n  pg_catalog.pg_get_userbyid(f.opfowner) AS \\\"Owner\\\"\\n\\nFROM pg_catalog.pg_opfamily f\\n  LEFT JOIN pg_catalog.pg_am am on am.oid = f.opfmethod\\n  LEFT JOIN pg_catalog.pg_namespace n ON n.oid = f.opfnamespace\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dAo\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list operators of operator families\",\n    \"query\": \"\\nSELECT\\n  am.amname AS \\\"AM\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opfamily_is_visible(of.oid)\\n    THEN pg_catalog.format('%I', of.opfname)\\n    ELSE pg_catalog.format('%I.%I', nsf.nspname, of.opfname)\\n  END AS \\\"Operator family\\\",\\n  o.amopopr::pg_catalog.regoperator AS \\\"Operator\\\"\\n,  o.amopstrategy AS \\\"Strategy\\\",\\n  CASE o.amoppurpose\\n    WHEN 'o' THEN 'ordering'\\n    WHEN 's' THEN 'search'\\n  END AS \\\"Purpose\\\"\\nFROM pg_catalog.pg_amop o\\n  LEFT JOIN pg_catalog.pg_opfamily of ON of.oid = o.amopfamily\\n  LEFT JOIN pg_catalog.pg_am am ON am.oid = of.opfmethod AND am.oid = o.amopmethod\\n  LEFT JOIN pg_catalog.pg_namespace nsf ON of.opfnamespace = nsf.oid\\nORDER BY 1, 2,\\n  o.amoplefttype = o.amoprighttype DESC,\\n  pg_catalog.format_type(o.amoplefttype, NULL),\\n  pg_catalog.format_type(o.amoprighttype, NULL),\\n  o.amopstrategy;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dAo+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list operators of operator families\",\n    \"query\": \"\\nSELECT\\n  am.amname AS \\\"AM\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opfamily_is_visible(of.oid)\\n    THEN pg_catalog.format('%I', of.opfname)\\n    ELSE pg_catalog.format('%I.%I', nsf.nspname, of.opfname)\\n  END AS \\\"Operator family\\\",\\n  o.amopopr::pg_catalog.regoperator AS \\\"Operator\\\"\\n,  o.amopstrategy AS \\\"Strategy\\\",\\n  CASE o.amoppurpose\\n    WHEN 'o' THEN 'ordering'\\n    WHEN 's' THEN 'search'\\n  END AS \\\"Purpose\\\"\\n, ofs.opfname AS \\\"Sort opfamily\\\"\\nFROM pg_catalog.pg_amop o\\n  LEFT JOIN pg_catalog.pg_opfamily of ON of.oid = o.amopfamily\\n  LEFT JOIN pg_catalog.pg_am am ON am.oid = of.opfmethod AND am.oid = o.amopmethod\\n  LEFT JOIN pg_catalog.pg_namespace nsf ON of.opfnamespace = nsf.oid\\n  LEFT JOIN pg_catalog.pg_opfamily ofs ON ofs.oid = o.amopsortfamily\\nORDER BY 1, 2,\\n  o.amoplefttype = o.amoprighttype DESC,\\n  pg_catalog.format_type(o.amoplefttype, NULL),\\n  pg_catalog.format_type(o.amoprighttype, NULL),\\n  o.amopstrategy;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dAp\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list support functions of operator families\",\n    \"query\": \"\\nSELECT\\n  am.amname AS \\\"AM\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opfamily_is_visible(of.oid)\\n    THEN pg_catalog.format('%I', of.opfname)\\n    ELSE pg_catalog.format('%I.%I', ns.nspname, of.opfname)\\n  END AS \\\"Operator family\\\",\\n  pg_catalog.format_type(ap.amproclefttype, NULL) AS \\\"Registered left type\\\",\\n  pg_catalog.format_type(ap.amprocrighttype, NULL) AS \\\"Registered right type\\\",\\n  ap.amprocnum AS \\\"Number\\\"\\n, p.proname AS \\\"Function\\\"\\nFROM pg_catalog.pg_amproc ap\\n  LEFT JOIN pg_catalog.pg_opfamily of ON of.oid = ap.amprocfamily\\n  LEFT JOIN pg_catalog.pg_am am ON am.oid = of.opfmethod\\n  LEFT JOIN pg_catalog.pg_namespace ns ON of.opfnamespace = ns.oid\\n  LEFT JOIN pg_catalog.pg_proc p ON ap.amproc = p.oid\\nORDER BY 1, 2,\\n  ap.amproclefttype = ap.amprocrighttype DESC,\\n  3, 4, 5;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dAp+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list support functions of operator families\",\n    \"query\": \"\\nSELECT\\n  am.amname AS \\\"AM\\\",\\n  CASE\\n    WHEN pg_catalog.pg_opfamily_is_visible(of.oid)\\n    THEN pg_catalog.format('%I', of.opfname)\\n    ELSE pg_catalog.format('%I.%I', ns.nspname, of.opfname)\\n  END AS \\\"Operator family\\\",\\n  pg_catalog.format_type(ap.amproclefttype, NULL) AS \\\"Registered left type\\\",\\n  pg_catalog.format_type(ap.amprocrighttype, NULL) AS \\\"Registered right type\\\",\\n  ap.amprocnum AS \\\"Number\\\"\\n, ap.amproc::pg_catalog.regprocedure AS \\\"Function\\\"\\nFROM pg_catalog.pg_amproc ap\\n  LEFT JOIN pg_catalog.pg_opfamily of ON of.oid = ap.amprocfamily\\n  LEFT JOIN pg_catalog.pg_am am ON am.oid = of.opfmethod\\n  LEFT JOIN pg_catalog.pg_namespace ns ON of.opfnamespace = ns.oid\\n  LEFT JOIN pg_catalog.pg_proc p ON ap.amproc = p.oid\\nORDER BY 1, 2,\\n  ap.amproclefttype = ap.amprocrighttype DESC,\\n  3, 4, 5;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\db\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list tablespaces\",\n    \"query\": \"\\nSELECT spcname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(spcowner) AS \\\"Owner\\\",\\n  pg_catalog.pg_tablespace_location(oid) AS \\\"Location\\\"\\nFROM pg_catalog.pg_tablespace\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\db+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list tablespaces\",\n    \"query\": \"\\nSELECT spcname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(spcowner) AS \\\"Owner\\\",\\n  pg_catalog.pg_tablespace_location(oid) AS \\\"Location\\\",\\n  pg_catalog.array_to_string(spcacl, E'\\\\n') AS \\\"Access privileges\\\",\\n  spcoptions AS \\\"Options\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \\\"Size\\\",\\n  pg_catalog.shobj_description(oid, 'pg_tablespace') AS \\\"Description\\\"\\nFROM pg_catalog.pg_tablespace\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dc\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list conversions\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Schema\\\",\\n       c.conname AS \\\"Name\\\",\\n       pg_catalog.pg_encoding_to_char(c.conforencoding) AS \\\"Source\\\",\\n       pg_catalog.pg_encoding_to_char(c.contoencoding) AS \\\"Destination\\\",\\n       CASE WHEN c.condefault THEN 'yes'\\n       ELSE 'no' END AS \\\"Default?\\\"\\nFROM pg_catalog.pg_conversion c\\n     JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace\\nWHERE true\\n  AND n.nspname <> 'pg_catalog'\\n  AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_conversion_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dcS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list conversions\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Schema\\\",\\n       c.conname AS \\\"Name\\\",\\n       pg_catalog.pg_encoding_to_char(c.conforencoding) AS \\\"Source\\\",\\n       pg_catalog.pg_encoding_to_char(c.contoencoding) AS \\\"Destination\\\",\\n       CASE WHEN c.condefault THEN 'yes'\\n       ELSE 'no' END AS \\\"Default?\\\"\\nFROM pg_catalog.pg_conversion c\\n     JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace\\nWHERE true\\n  AND pg_catalog.pg_conversion_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dcS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list conversions\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Schema\\\",\\n       c.conname AS \\\"Name\\\",\\n       pg_catalog.pg_encoding_to_char(c.conforencoding) AS \\\"Source\\\",\\n       pg_catalog.pg_encoding_to_char(c.contoencoding) AS \\\"Destination\\\",\\n       CASE WHEN c.condefault THEN 'yes'\\n       ELSE 'no' END AS \\\"Default?\\\",\\n       d.description AS \\\"Description\\\"\\nFROM pg_catalog.pg_conversion c\\n     JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace\\nLEFT JOIN pg_catalog.pg_description d ON d.classoid = c.tableoid\\n          AND d.objoid = c.oid AND d.objsubid = 0\\nWHERE true\\n  AND pg_catalog.pg_conversion_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dC\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list casts\",\n    \"query\": \"\\nSELECT pg_catalog.format_type(castsource, NULL) AS \\\"Source type\\\",\\n       pg_catalog.format_type(casttarget, NULL) AS \\\"Target type\\\",\\n       CASE WHEN c.castmethod = 'b' THEN '(binary coercible)'\\n            WHEN c.castmethod = 'i' THEN '(with inout)'\\n            ELSE p.proname\\n       END AS \\\"Function\\\",\\n       CASE WHEN c.castcontext = 'e' THEN 'no'\\n            WHEN c.castcontext = 'a' THEN 'in assignment'\\n            ELSE 'yes'\\n       END AS \\\"Implicit?\\\"\\nFROM pg_catalog.pg_cast c LEFT JOIN pg_catalog.pg_proc p\\n     ON c.castfunc = p.oid\\n     LEFT JOIN pg_catalog.pg_type ts\\n     ON c.castsource = ts.oid\\n     LEFT JOIN pg_catalog.pg_namespace ns\\n     ON ns.oid = ts.typnamespace\\n     LEFT JOIN pg_catalog.pg_type tt\\n     ON c.casttarget = tt.oid\\n     LEFT JOIN pg_catalog.pg_namespace nt\\n     ON nt.oid = tt.typnamespace\\nWHERE ( (true  AND pg_catalog.pg_type_is_visible(ts.oid)\\n) OR (true  AND pg_catalog.pg_type_is_visible(tt.oid)\\n) )\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dC+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list casts\",\n    \"query\": \"\\nSELECT pg_catalog.format_type(castsource, NULL) AS \\\"Source type\\\",\\n       pg_catalog.format_type(casttarget, NULL) AS \\\"Target type\\\",\\n       CASE WHEN c.castmethod = 'b' THEN '(binary coercible)'\\n            WHEN c.castmethod = 'i' THEN '(with inout)'\\n            ELSE p.proname\\n       END AS \\\"Function\\\",\\n       CASE WHEN c.castcontext = 'e' THEN 'no'\\n            WHEN c.castcontext = 'a' THEN 'in assignment'\\n            ELSE 'yes'\\n       END AS \\\"Implicit?\\\",\\n       d.description AS \\\"Description\\\"\\nFROM pg_catalog.pg_cast c LEFT JOIN pg_catalog.pg_proc p\\n     ON c.castfunc = p.oid\\n     LEFT JOIN pg_catalog.pg_type ts\\n     ON c.castsource = ts.oid\\n     LEFT JOIN pg_catalog.pg_namespace ns\\n     ON ns.oid = ts.typnamespace\\n     LEFT JOIN pg_catalog.pg_type tt\\n     ON c.casttarget = tt.oid\\n     LEFT JOIN pg_catalog.pg_namespace nt\\n     ON nt.oid = tt.typnamespace\\n     LEFT JOIN pg_catalog.pg_description d\\n     ON d.classoid = c.tableoid AND d.objoid = c.oid AND d.objsubid = 0\\nWHERE ( (true  AND pg_catalog.pg_type_is_visible(ts.oid)\\n) OR (true  AND pg_catalog.pg_type_is_visible(tt.oid)\\n) )\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dd\",\n    \"opts\": \"[S]\",\n    \"desc\": \"show object descriptions not displayed elsewhere\",\n    \"query\": \"\\nSELECT DISTINCT tt.nspname AS \\\"Schema\\\", tt.name AS \\\"Name\\\", tt.object AS \\\"Object\\\", d.description AS \\\"Description\\\"\\nFROM (\\n  SELECT pgc.oid as oid, pgc.tableoid AS tableoid,\\n  n.nspname as nspname,\\n  CAST(pgc.conname AS pg_catalog.text) as name,  CAST('table constraint' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_constraint pgc\\n    JOIN pg_catalog.pg_class c ON c.oid = pgc.conrelid\\n    LEFT JOIN pg_catalog.pg_namespace n     ON n.oid = c.relnamespace\\nWHERE n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nUNION ALL\\n  SELECT pgc.oid as oid, pgc.tableoid AS tableoid,\\n  n.nspname as nspname,\\n  CAST(pgc.conname AS pg_catalog.text) as name,  CAST('domain constraint' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_constraint pgc\\n    JOIN pg_catalog.pg_type t ON t.oid = pgc.contypid\\n    LEFT JOIN pg_catalog.pg_namespace n     ON n.oid = t.typnamespace\\nWHERE n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_type_is_visible(t.oid)\\nUNION ALL\\n  SELECT o.oid as oid, o.tableoid as tableoid,\\n  n.nspname as nspname,\\n  CAST(o.opcname AS pg_catalog.text) as name,\\n  CAST('operator class' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_opclass o\\n    JOIN pg_catalog.pg_am am ON o.opcmethod = am.oid\\n    JOIN pg_catalog.pg_namespace n ON n.oid = o.opcnamespace\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_opclass_is_visible(o.oid)\\nUNION ALL\\n  SELECT opf.oid as oid, opf.tableoid as tableoid,\\n  n.nspname as nspname,\\n  CAST(opf.opfname AS pg_catalog.text) AS name,\\n  CAST('operator family' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_opfamily opf\\n    JOIN pg_catalog.pg_am am ON opf.opfmethod = am.oid\\n    JOIN pg_catalog.pg_namespace n ON opf.opfnamespace = n.oid\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_opfamily_is_visible(opf.oid)\\nUNION ALL\\n  SELECT r.oid as oid, r.tableoid as tableoid,\\n  n.nspname as nspname,\\n  CAST(r.rulename AS pg_catalog.text) as name,  CAST('rule' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_rewrite r\\n       JOIN pg_catalog.pg_class c ON c.oid = r.ev_class\\n       LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n  WHERE r.rulename != '_RETURN'\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nUNION ALL\\n  SELECT t.oid as oid, t.tableoid as tableoid,\\n  n.nspname as nspname,\\n  CAST(t.tgname AS pg_catalog.text) as name,  CAST('trigger' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_trigger t\\n       JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid\\n       LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\n) AS tt\\n  JOIN pg_catalog.pg_description d ON (tt.oid = d.objoid AND tt.tableoid = d.classoid AND d.objsubid = 0)\\nORDER BY 1, 2, 3;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\ddS\",\n    \"opts\": \"[S]\",\n    \"desc\": \"show object descriptions not displayed elsewhere\",\n    \"query\": \"\\nSELECT DISTINCT tt.nspname AS \\\"Schema\\\", tt.name AS \\\"Name\\\", tt.object AS \\\"Object\\\", d.description AS \\\"Description\\\"\\nFROM (\\n  SELECT pgc.oid as oid, pgc.tableoid AS tableoid,\\n  n.nspname as nspname,\\n  CAST(pgc.conname AS pg_catalog.text) as name,  CAST('table constraint' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_constraint pgc\\n    JOIN pg_catalog.pg_class c ON c.oid = pgc.conrelid\\n    LEFT JOIN pg_catalog.pg_namespace n     ON n.oid = c.relnamespace\\nWHERE pg_catalog.pg_table_is_visible(c.oid)\\nUNION ALL\\n  SELECT pgc.oid as oid, pgc.tableoid AS tableoid,\\n  n.nspname as nspname,\\n  CAST(pgc.conname AS pg_catalog.text) as name,  CAST('domain constraint' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_constraint pgc\\n    JOIN pg_catalog.pg_type t ON t.oid = pgc.contypid\\n    LEFT JOIN pg_catalog.pg_namespace n     ON n.oid = t.typnamespace\\nWHERE pg_catalog.pg_type_is_visible(t.oid)\\nUNION ALL\\n  SELECT o.oid as oid, o.tableoid as tableoid,\\n  n.nspname as nspname,\\n  CAST(o.opcname AS pg_catalog.text) as name,\\n  CAST('operator class' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_opclass o\\n    JOIN pg_catalog.pg_am am ON o.opcmethod = am.oid\\n    JOIN pg_catalog.pg_namespace n ON n.oid = o.opcnamespace\\n  AND pg_catalog.pg_opclass_is_visible(o.oid)\\nUNION ALL\\n  SELECT opf.oid as oid, opf.tableoid as tableoid,\\n  n.nspname as nspname,\\n  CAST(opf.opfname AS pg_catalog.text) AS name,\\n  CAST('operator family' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_opfamily opf\\n    JOIN pg_catalog.pg_am am ON opf.opfmethod = am.oid\\n    JOIN pg_catalog.pg_namespace n ON opf.opfnamespace = n.oid\\n  AND pg_catalog.pg_opfamily_is_visible(opf.oid)\\nUNION ALL\\n  SELECT r.oid as oid, r.tableoid as tableoid,\\n  n.nspname as nspname,\\n  CAST(r.rulename AS pg_catalog.text) as name,  CAST('rule' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_rewrite r\\n       JOIN pg_catalog.pg_class c ON c.oid = r.ev_class\\n       LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n  WHERE r.rulename != '_RETURN'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nUNION ALL\\n  SELECT t.oid as oid, t.tableoid as tableoid,\\n  n.nspname as nspname,\\n  CAST(t.tgname AS pg_catalog.text) as name,  CAST('trigger' AS pg_catalog.text) as object\\n  FROM pg_catalog.pg_trigger t\\n       JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid\\n       LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE pg_catalog.pg_table_is_visible(c.oid)\\n) AS tt\\n  JOIN pg_catalog.pg_description d ON (tt.oid = d.objoid AND tt.tableoid = d.classoid AND d.objsubid = 0)\\nORDER BY 1, 2, 3;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dD\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list domains\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n       t.typname as \\\"Name\\\",\\n       pg_catalog.format_type(t.typbasetype, t.typtypmod) as \\\"Type\\\",\\n       (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\\n        WHERE c.oid = t.typcollation AND bt.oid = t.typbasetype AND t.typcollation <> bt.typcollation) as \\\"Collation\\\",\\n       CASE WHEN t.typnotnull THEN 'not null' END as \\\"Nullable\\\",\\n       t.typdefault as \\\"Default\\\",\\n       pg_catalog.array_to_string(ARRAY(\\n         SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid\\n       ), ' ') as \\\"Check\\\"\\nFROM pg_catalog.pg_type t\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\\nWHERE t.typtype = 'd'\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_type_is_visible(t.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dDS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list domains\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n       t.typname as \\\"Name\\\",\\n       pg_catalog.format_type(t.typbasetype, t.typtypmod) as \\\"Type\\\",\\n       (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\\n        WHERE c.oid = t.typcollation AND bt.oid = t.typbasetype AND t.typcollation <> bt.typcollation) as \\\"Collation\\\",\\n       CASE WHEN t.typnotnull THEN 'not null' END as \\\"Nullable\\\",\\n       t.typdefault as \\\"Default\\\",\\n       pg_catalog.array_to_string(ARRAY(\\n         SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid\\n       ), ' ') as \\\"Check\\\"\\nFROM pg_catalog.pg_type t\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\\nWHERE t.typtype = 'd'\\n  AND pg_catalog.pg_type_is_visible(t.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dDS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list domains\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n       t.typname as \\\"Name\\\",\\n       pg_catalog.format_type(t.typbasetype, t.typtypmod) as \\\"Type\\\",\\n       (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\\n        WHERE c.oid = t.typcollation AND bt.oid = t.typbasetype AND t.typcollation <> bt.typcollation) as \\\"Collation\\\",\\n       CASE WHEN t.typnotnull THEN 'not null' END as \\\"Nullable\\\",\\n       t.typdefault as \\\"Default\\\",\\n       pg_catalog.array_to_string(ARRAY(\\n         SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid\\n       ), ' ') as \\\"Check\\\",\\n  pg_catalog.array_to_string(t.typacl, E'\\\\n') AS \\\"Access privileges\\\",\\n       d.description as \\\"Description\\\"\\nFROM pg_catalog.pg_type t\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\\n     LEFT JOIN pg_catalog.pg_description d ON d.classoid = t.tableoid AND d.objoid = t.oid AND d.objsubid = 0\\nWHERE t.typtype = 'd'\\n  AND pg_catalog.pg_type_is_visible(t.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\ddp\",\n    \"desc\": \"list default privileges\",\n    \"query\": \"\\nSELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \\\"Owner\\\",\\n  n.nspname AS \\\"Schema\\\",\\n  CASE d.defaclobjtype WHEN 'r' THEN 'table' WHEN 'S' THEN 'sequence' WHEN 'f' THEN 'function' WHEN 'T' THEN 'type' WHEN 'n' THEN 'schema' END AS \\\"Type\\\",\\n  pg_catalog.array_to_string(d.defaclacl, E'\\\\n') AS \\\"Access privileges\\\"\\nFROM pg_catalog.pg_default_acl d\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.defaclnamespace\\nORDER BY 1, 2, 3;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dE\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list foreign tables\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('f','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dES\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list foreign tables\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('s','f','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dES+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list foreign tables\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as \\\"Persistence\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \\\"Size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('s','f','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\des\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list foreign servers\",\n    \"query\": \"\\nSELECT s.srvname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(s.srvowner) AS \\\"Owner\\\",\\n  f.fdwname AS \\\"Foreign-data wrapper\\\"\\nFROM pg_catalog.pg_foreign_server s\\n     JOIN pg_catalog.pg_foreign_data_wrapper f ON f.oid=s.srvfdw\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\des+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list foreign servers\",\n    \"query\": \"\\nSELECT s.srvname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(s.srvowner) AS \\\"Owner\\\",\\n  f.fdwname AS \\\"Foreign-data wrapper\\\",\\n  pg_catalog.array_to_string(s.srvacl, E'\\\\n') AS \\\"Access privileges\\\",\\n  s.srvtype AS \\\"Type\\\",\\n  s.srvversion AS \\\"Version\\\",\\n  CASE WHEN srvoptions IS NULL THEN '' ELSE   '(' || pg_catalog.array_to_string(ARRAY(SELECT   pg_catalog.quote_ident(option_name) ||  ' ' ||   pg_catalog.quote_literal(option_value)  FROM   pg_catalog.pg_options_to_table(srvoptions)),  ', ') || ')'   END AS \\\"FDW options\\\",\\n  d.description AS \\\"Description\\\"\\nFROM pg_catalog.pg_foreign_server s\\n     JOIN pg_catalog.pg_foreign_data_wrapper f ON f.oid=s.srvfdw\\nLEFT JOIN pg_catalog.pg_description d\\n       ON d.classoid = s.tableoid AND d.objoid = s.oid AND d.objsubid = 0\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\det\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list foreign tables\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Schema\\\",\\n  c.relname AS \\\"Table\\\",\\n  s.srvname AS \\\"Server\\\"\\nFROM pg_catalog.pg_foreign_table ft\\n  INNER JOIN pg_catalog.pg_class c ON c.oid = ft.ftrelid\\n  INNER JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n  INNER JOIN pg_catalog.pg_foreign_server s ON s.oid = ft.ftserver\\nWHERE pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\det+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list foreign tables\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Schema\\\",\\n  c.relname AS \\\"Table\\\",\\n  s.srvname AS \\\"Server\\\",\\n CASE WHEN ftoptions IS NULL THEN '' ELSE   '(' || pg_catalog.array_to_string(ARRAY(SELECT   pg_catalog.quote_ident(option_name) ||  ' ' ||   pg_catalog.quote_literal(option_value)  FROM   pg_catalog.pg_options_to_table(ftoptions)),  ', ') || ')'   END AS \\\"FDW options\\\",\\n  d.description AS \\\"Description\\\"\\nFROM pg_catalog.pg_foreign_table ft\\n  INNER JOIN pg_catalog.pg_class c ON c.oid = ft.ftrelid\\n  INNER JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n  INNER JOIN pg_catalog.pg_foreign_server s ON s.oid = ft.ftserver\\n   LEFT JOIN pg_catalog.pg_description d\\n          ON d.classoid = c.tableoid AND d.objoid = c.oid AND d.objsubid = 0\\nWHERE pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\deu\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list user mappings\",\n    \"query\": \"\\nSELECT um.srvname AS \\\"Server\\\",\\n  um.usename AS \\\"User name\\\"\\nFROM pg_catalog.pg_user_mappings um\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\deu+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list user mappings\",\n    \"query\": \"\\nSELECT um.srvname AS \\\"Server\\\",\\n  um.usename AS \\\"User name\\\",\\n CASE WHEN umoptions IS NULL THEN '' ELSE   '(' || pg_catalog.array_to_string(ARRAY(SELECT   pg_catalog.quote_ident(option_name) ||  ' ' ||   pg_catalog.quote_literal(option_value)  FROM   pg_catalog.pg_options_to_table(umoptions)),  ', ') || ')'   END AS \\\"FDW options\\\"\\nFROM pg_catalog.pg_user_mappings um\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dew\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list foreign-data wrappers\",\n    \"query\": \"\\nSELECT fdw.fdwname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(fdw.fdwowner) AS \\\"Owner\\\",\\n  fdw.fdwhandler::pg_catalog.regproc AS \\\"Handler\\\",\\n  fdw.fdwvalidator::pg_catalog.regproc AS \\\"Validator\\\"\\nFROM pg_catalog.pg_foreign_data_wrapper fdw\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dew+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list foreign-data wrappers\",\n    \"query\": \"\\nSELECT fdw.fdwname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(fdw.fdwowner) AS \\\"Owner\\\",\\n  fdw.fdwhandler::pg_catalog.regproc AS \\\"Handler\\\",\\n  fdw.fdwvalidator::pg_catalog.regproc AS \\\"Validator\\\",\\n  pg_catalog.array_to_string(fdwacl, E'\\\\n') AS \\\"Access privileges\\\",\\n CASE WHEN fdwoptions IS NULL THEN '' ELSE   '(' || pg_catalog.array_to_string(ARRAY(SELECT   pg_catalog.quote_ident(option_name) ||  ' ' ||   pg_catalog.quote_literal(option_value)  FROM   pg_catalog.pg_options_to_table(fdwoptions)),  ', ') || ')'   END AS \\\"FDW options\\\",\\n  d.description AS \\\"Description\\\" \\nFROM pg_catalog.pg_foreign_data_wrapper fdw\\nLEFT JOIN pg_catalog.pg_description d\\n       ON d.classoid = fdw.tableoid AND d.objoid = fdw.oid AND d.objsubid = 0\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\df\",\n    \"opts\": \"[anptw]\",\n    \"desc\": \"list [only agg/normal/procedure/trigger/window]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  p.proname as \\\"Name\\\",\\n  pg_catalog.pg_get_function_result(p.oid) as \\\"Result data type\\\",\\n  pg_catalog.pg_get_function_arguments(p.oid) as \\\"Argument data types\\\",\\n CASE p.prokind\\n  WHEN 'a' THEN 'agg'\\n  WHEN 'w' THEN 'window'\\n  WHEN 'p' THEN 'proc'\\n  ELSE 'func'\\n END as \\\"Type\\\"\\nFROM pg_catalog.pg_proc p\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\\nWHERE pg_catalog.pg_function_is_visible(p.oid)\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dfa\",\n    \"opts\": \"[anptw]\",\n    \"desc\": \"list [only agg/normal/procedure/trigger/window]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  p.proname as \\\"Name\\\",\\n  pg_catalog.pg_get_function_result(p.oid) as \\\"Result data type\\\",\\n  pg_catalog.pg_get_function_arguments(p.oid) as \\\"Argument data types\\\",\\n CASE p.prokind\\n  WHEN 'a' THEN 'agg'\\n  WHEN 'w' THEN 'window'\\n  WHEN 'p' THEN 'proc'\\n  ELSE 'func'\\n END as \\\"Type\\\"\\nFROM pg_catalog.pg_proc p\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\\nWHERE (\\n       p.prokind = 'a'\\n      )\\n  AND pg_catalog.pg_function_is_visible(p.oid)\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dfan\",\n    \"opts\": \"[anptw]\",\n    \"desc\": \"list [only agg/normal/procedure/trigger/window]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  p.proname as \\\"Name\\\",\\n  pg_catalog.pg_get_function_result(p.oid) as \\\"Result data type\\\",\\n  pg_catalog.pg_get_function_arguments(p.oid) as \\\"Argument data types\\\",\\n CASE p.prokind\\n  WHEN 'a' THEN 'agg'\\n  WHEN 'w' THEN 'window'\\n  WHEN 'p' THEN 'proc'\\n  ELSE 'func'\\n END as \\\"Type\\\"\\nFROM pg_catalog.pg_proc p\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\\nWHERE p.prokind <> 'p'\\n      AND p.prorettype <> 'pg_catalog.trigger'::pg_catalog.regtype\\n      AND p.prokind <> 'w'\\n  AND pg_catalog.pg_function_is_visible(p.oid)\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dfanp\",\n    \"opts\": \"[anptw]\",\n    \"desc\": \"list [only agg/normal/procedure/trigger/window]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  p.proname as \\\"Name\\\",\\n  pg_catalog.pg_get_function_result(p.oid) as \\\"Result data type\\\",\\n  pg_catalog.pg_get_function_arguments(p.oid) as \\\"Argument data types\\\",\\n CASE p.prokind\\n  WHEN 'a' THEN 'agg'\\n  WHEN 'w' THEN 'window'\\n  WHEN 'p' THEN 'proc'\\n  ELSE 'func'\\n END as \\\"Type\\\"\\nFROM pg_catalog.pg_proc p\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\\nWHERE p.prorettype <> 'pg_catalog.trigger'::pg_catalog.regtype\\n      AND p.prokind <> 'w'\\n  AND pg_catalog.pg_function_is_visible(p.oid)\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dfanpt\",\n    \"opts\": \"[anptw]\",\n    \"desc\": \"list [only agg/normal/procedure/trigger/window]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  p.proname as \\\"Name\\\",\\n  pg_catalog.pg_get_function_result(p.oid) as \\\"Result data type\\\",\\n  pg_catalog.pg_get_function_arguments(p.oid) as \\\"Argument data types\\\",\\n CASE p.prokind\\n  WHEN 'a' THEN 'agg'\\n  WHEN 'w' THEN 'window'\\n  WHEN 'p' THEN 'proc'\\n  ELSE 'func'\\n END as \\\"Type\\\"\\nFROM pg_catalog.pg_proc p\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\\nWHERE p.prokind <> 'w'\\n  AND pg_catalog.pg_function_is_visible(p.oid)\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dfanptw\",\n    \"opts\": \"[anptw]\",\n    \"desc\": \"list [only agg/normal/procedure/trigger/window]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  p.proname as \\\"Name\\\",\\n  pg_catalog.pg_get_function_result(p.oid) as \\\"Result data type\\\",\\n  pg_catalog.pg_get_function_arguments(p.oid) as \\\"Argument data types\\\",\\n CASE p.prokind\\n  WHEN 'a' THEN 'agg'\\n  WHEN 'w' THEN 'window'\\n  WHEN 'p' THEN 'proc'\\n  ELSE 'func'\\n END as \\\"Type\\\"\\nFROM pg_catalog.pg_proc p\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace\\nWHERE pg_catalog.pg_function_is_visible(p.oid)\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\nORDER BY 1, 2, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dF\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list text search configurations\",\n    \"query\": \"\\nSELECT\\n   n.nspname as \\\"Schema\\\",\\n   c.cfgname as \\\"Name\\\",\\n   pg_catalog.obj_description(c.oid, 'pg_ts_config') as \\\"Description\\\"\\nFROM pg_catalog.pg_ts_config c\\nLEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.cfgnamespace\\nWHERE pg_catalog.pg_ts_config_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dF+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list text search configurations\",\n    \"query\": \"\\nSELECT c.oid, c.cfgname,\\n   n.nspname,\\n   p.prsname,\\n   np.nspname as pnspname\\nFROM pg_catalog.pg_ts_config c\\n   LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.cfgnamespace,\\n pg_catalog.pg_ts_parser p\\n   LEFT JOIN pg_catalog.pg_namespace np ON np.oid = p.prsnamespace\\nWHERE  p.oid = c.cfgparser\\n  AND pg_catalog.pg_ts_config_is_visible(c.oid)\\nORDER BY 3, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dFd\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list text search dictionaries\",\n    \"query\": \"\\nSELECT\\n  n.nspname as \\\"Schema\\\",\\n  d.dictname as \\\"Name\\\",\\n  pg_catalog.obj_description(d.oid, 'pg_ts_dict') as \\\"Description\\\"\\nFROM pg_catalog.pg_ts_dict d\\nLEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.dictnamespace\\nWHERE pg_catalog.pg_ts_dict_is_visible(d.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dFd+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list text search dictionaries\",\n    \"query\": \"\\nSELECT\\n  n.nspname as \\\"Schema\\\",\\n  d.dictname as \\\"Name\\\",\\n  ( SELECT COALESCE(nt.nspname, '(null)')::pg_catalog.text || '.' || t.tmplname FROM\\n    pg_catalog.pg_ts_template t\\n    LEFT JOIN pg_catalog.pg_namespace nt ON nt.oid = t.tmplnamespace\\n    WHERE d.dicttemplate = t.oid ) AS  \\\"Template\\\",\\n  d.dictinitoption as \\\"Init options\\\",\\n  pg_catalog.obj_description(d.oid, 'pg_ts_dict') as \\\"Description\\\"\\nFROM pg_catalog.pg_ts_dict d\\nLEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.dictnamespace\\nWHERE pg_catalog.pg_ts_dict_is_visible(d.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dFp\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list text search parsers\",\n    \"query\": \"\\nSELECT\\n  n.nspname as \\\"Schema\\\",\\n  p.prsname as \\\"Name\\\",\\n  pg_catalog.obj_description(p.oid, 'pg_ts_parser') as \\\"Description\\\"\\nFROM pg_catalog.pg_ts_parser p\\nLEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.prsnamespace\\nWHERE pg_catalog.pg_ts_parser_is_visible(p.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dFp+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list text search parsers\",\n    \"query\": \"\\nSELECT p.oid,\\n  n.nspname,\\n  p.prsname\\nFROM pg_catalog.pg_ts_parser p\\nLEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.prsnamespace\\nWHERE pg_catalog.pg_ts_parser_is_visible(p.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dFt\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list text search templates\",\n    \"query\": \"\\nSELECT\\n  n.nspname AS \\\"Schema\\\",\\n  t.tmplname AS \\\"Name\\\",\\n  pg_catalog.obj_description(t.oid, 'pg_ts_template') AS \\\"Description\\\"\\nFROM pg_catalog.pg_ts_template t\\nLEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.tmplnamespace\\nWHERE pg_catalog.pg_ts_template_is_visible(t.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dFt+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list text search templates\",\n    \"query\": \"\\nSELECT\\n  n.nspname AS \\\"Schema\\\",\\n  t.tmplname AS \\\"Name\\\",\\n  t.tmplinit::pg_catalog.regproc AS \\\"Init\\\",\\n  t.tmpllexize::pg_catalog.regproc AS \\\"Lexize\\\",\\n  pg_catalog.obj_description(t.oid, 'pg_ts_template') AS \\\"Description\\\"\\nFROM pg_catalog.pg_ts_template t\\nLEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.tmplnamespace\\nWHERE pg_catalog.pg_ts_template_is_visible(t.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dg\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list roles\",\n    \"query\": \"\\nSELECT r.rolname, r.rolsuper, r.rolinherit,\\n  r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\\n  r.rolconnlimit, r.rolvaliduntil,\\n  ARRAY(SELECT b.rolname\\n        FROM pg_catalog.pg_auth_members m\\n        JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)\\n        WHERE m.member = r.oid) as memberof\\n, r.rolreplication\\n, r.rolbypassrls\\nFROM pg_catalog.pg_roles r\\nWHERE r.rolname !~ '^pg_'\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dgS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list roles\",\n    \"query\": \"\\nSELECT r.rolname, r.rolsuper, r.rolinherit,\\n  r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\\n  r.rolconnlimit, r.rolvaliduntil,\\n  ARRAY(SELECT b.rolname\\n        FROM pg_catalog.pg_auth_members m\\n        JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)\\n        WHERE m.member = r.oid) as memberof\\n, r.rolreplication\\n, r.rolbypassrls\\nFROM pg_catalog.pg_roles r\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dgS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list roles\",\n    \"query\": \"\\nSELECT r.rolname, r.rolsuper, r.rolinherit,\\n  r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\\n  r.rolconnlimit, r.rolvaliduntil,\\n  ARRAY(SELECT b.rolname\\n        FROM pg_catalog.pg_auth_members m\\n        JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)\\n        WHERE m.member = r.oid) as memberof\\n, pg_catalog.shobj_description(r.oid, 'pg_authid') AS description\\n, r.rolreplication\\n, r.rolbypassrls\\nFROM pg_catalog.pg_roles r\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\di\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list indexes\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  c2.relname as \\\"Table\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\n     LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\\n     LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\\nWHERE c.relkind IN ('i','I','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\diS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list indexes\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  c2.relname as \\\"Table\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\n     LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\\n     LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\\nWHERE c.relkind IN ('i','I','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\diS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list indexes\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  c2.relname as \\\"Table\\\",\\n  CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as \\\"Persistence\\\",\\n  am.amname as \\\"Access method\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \\\"Size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\n     LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\\n     LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\\nWHERE c.relkind IN ('i','I','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dl\",\n    \"desc\": \"list large objects, same as lo_list\",\n    \"query\": \"\\nSELECT oid as \\\"ID\\\",\\n  pg_catalog.pg_get_userbyid(lomowner) as \\\"Owner\\\",\\n  pg_catalog.obj_description(oid, 'pg_largeobject') as \\\"Description\\\"\\n  FROM pg_catalog.pg_largeobject_metadata   ORDER BY oid\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dL\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list procedural languages\",\n    \"query\": \"\\nSELECT l.lanname AS \\\"Name\\\",\\n       pg_catalog.pg_get_userbyid(l.lanowner) as \\\"Owner\\\",\\n       l.lanpltrusted AS \\\"Trusted\\\",\\n       d.description AS \\\"Description\\\"\\nFROM pg_catalog.pg_language l\\nLEFT JOIN pg_catalog.pg_description d\\n  ON d.classoid = l.tableoid AND d.objoid = l.oid\\n  AND d.objsubid = 0\\nWHERE l.lanplcallfoid != 0\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dLS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list procedural languages\",\n    \"query\": \"\\nSELECT l.lanname AS \\\"Name\\\",\\n       pg_catalog.pg_get_userbyid(l.lanowner) as \\\"Owner\\\",\\n       l.lanpltrusted AS \\\"Trusted\\\",\\n       d.description AS \\\"Description\\\"\\nFROM pg_catalog.pg_language l\\nLEFT JOIN pg_catalog.pg_description d\\n  ON d.classoid = l.tableoid AND d.objoid = l.oid\\n  AND d.objsubid = 0\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dLS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list procedural languages\",\n    \"query\": \"\\nSELECT l.lanname AS \\\"Name\\\",\\n       pg_catalog.pg_get_userbyid(l.lanowner) as \\\"Owner\\\",\\n       l.lanpltrusted AS \\\"Trusted\\\",\\n       NOT l.lanispl AS \\\"Internal language\\\",\\n       l.lanplcallfoid::pg_catalog.regprocedure AS \\\"Call handler\\\",\\n       l.lanvalidator::pg_catalog.regprocedure AS \\\"Validator\\\",\\n       l.laninline::pg_catalog.regprocedure AS \\\"Inline handler\\\",\\n       pg_catalog.array_to_string(l.lanacl, E'\\\\n') AS \\\"Access privileges\\\",\\n       d.description AS \\\"Description\\\"\\nFROM pg_catalog.pg_language l\\nLEFT JOIN pg_catalog.pg_description d\\n  ON d.classoid = l.tableoid AND d.objoid = l.oid\\n  AND d.objsubid = 0\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dm\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list materialized views\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('m','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dmS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list materialized views\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('m','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dmS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list materialized views\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as \\\"Persistence\\\",\\n  am.amname as \\\"Access method\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \\\"Size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('m','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dn\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list schemas\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(n.nspowner) AS \\\"Owner\\\"\\nFROM pg_catalog.pg_namespace n\\nWHERE n.nspname !~ '^pg_' AND n.nspname <> 'information_schema'\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dnS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list schemas\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(n.nspowner) AS \\\"Owner\\\"\\nFROM pg_catalog.pg_namespace n\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dnS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list schemas\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(n.nspowner) AS \\\"Owner\\\",\\n  pg_catalog.array_to_string(n.nspacl, E'\\\\n') AS \\\"Access privileges\\\",\\n  pg_catalog.obj_description(n.oid, 'pg_namespace') AS \\\"Description\\\"\\nFROM pg_catalog.pg_namespace n\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\do\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list operators\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  o.oprname AS \\\"Name\\\",\\n  CASE WHEN o.oprkind='l' THEN NULL ELSE pg_catalog.format_type(o.oprleft, NULL) END AS \\\"Left arg type\\\",\\n  CASE WHEN o.oprkind='r' THEN NULL ELSE pg_catalog.format_type(o.oprright, NULL) END AS \\\"Right arg type\\\",\\n  pg_catalog.format_type(o.oprresult, NULL) AS \\\"Result type\\\",\\n  coalesce(pg_catalog.obj_description(o.oid, 'pg_operator'),\\n           pg_catalog.obj_description(o.oprcode, 'pg_proc')) AS \\\"Description\\\"\\nFROM pg_catalog.pg_operator o\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace\\nWHERE n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_operator_is_visible(o.oid)\\nORDER BY 1, 2, 3, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\doS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list operators\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  o.oprname AS \\\"Name\\\",\\n  CASE WHEN o.oprkind='l' THEN NULL ELSE pg_catalog.format_type(o.oprleft, NULL) END AS \\\"Left arg type\\\",\\n  CASE WHEN o.oprkind='r' THEN NULL ELSE pg_catalog.format_type(o.oprright, NULL) END AS \\\"Right arg type\\\",\\n  pg_catalog.format_type(o.oprresult, NULL) AS \\\"Result type\\\",\\n  coalesce(pg_catalog.obj_description(o.oid, 'pg_operator'),\\n           pg_catalog.obj_description(o.oprcode, 'pg_proc')) AS \\\"Description\\\"\\nFROM pg_catalog.pg_operator o\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace\\nWHERE pg_catalog.pg_operator_is_visible(o.oid)\\nORDER BY 1, 2, 3, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\doS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list operators\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  o.oprname AS \\\"Name\\\",\\n  CASE WHEN o.oprkind='l' THEN NULL ELSE pg_catalog.format_type(o.oprleft, NULL) END AS \\\"Left arg type\\\",\\n  CASE WHEN o.oprkind='r' THEN NULL ELSE pg_catalog.format_type(o.oprright, NULL) END AS \\\"Right arg type\\\",\\n  pg_catalog.format_type(o.oprresult, NULL) AS \\\"Result type\\\",\\n  o.oprcode AS \\\"Function\\\",\\n  coalesce(pg_catalog.obj_description(o.oid, 'pg_operator'),\\n           pg_catalog.obj_description(o.oprcode, 'pg_proc')) AS \\\"Description\\\"\\nFROM pg_catalog.pg_operator o\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = o.oprnamespace\\nWHERE pg_catalog.pg_operator_is_visible(o.oid)\\nORDER BY 1, 2, 3, 4;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dO\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list collations\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Schema\\\",\\n       c.collname AS \\\"Name\\\",\\n       c.collcollate AS \\\"Collate\\\",\\n       c.collctype AS \\\"Ctype\\\",\\n       CASE c.collprovider WHEN 'd' THEN 'default' WHEN 'c' THEN 'libc' WHEN 'i' THEN 'icu' END AS \\\"Provider\\\",\\n       CASE WHEN c.collisdeterministic THEN 'yes' ELSE 'no' END AS \\\"Deterministic?\\\"\\nFROM pg_catalog.pg_collation c, pg_catalog.pg_namespace n\\nWHERE n.oid = c.collnamespace\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n      AND c.collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding()))\\n  AND pg_catalog.pg_collation_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dOS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list collations\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Schema\\\",\\n       c.collname AS \\\"Name\\\",\\n       c.collcollate AS \\\"Collate\\\",\\n       c.collctype AS \\\"Ctype\\\",\\n       CASE c.collprovider WHEN 'd' THEN 'default' WHEN 'c' THEN 'libc' WHEN 'i' THEN 'icu' END AS \\\"Provider\\\",\\n       CASE WHEN c.collisdeterministic THEN 'yes' ELSE 'no' END AS \\\"Deterministic?\\\"\\nFROM pg_catalog.pg_collation c, pg_catalog.pg_namespace n\\nWHERE n.oid = c.collnamespace\\n      AND c.collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding()))\\n  AND pg_catalog.pg_collation_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dOS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list collations\",\n    \"query\": \"\\nSELECT n.nspname AS \\\"Schema\\\",\\n       c.collname AS \\\"Name\\\",\\n       c.collcollate AS \\\"Collate\\\",\\n       c.collctype AS \\\"Ctype\\\",\\n       CASE c.collprovider WHEN 'd' THEN 'default' WHEN 'c' THEN 'libc' WHEN 'i' THEN 'icu' END AS \\\"Provider\\\",\\n       CASE WHEN c.collisdeterministic THEN 'yes' ELSE 'no' END AS \\\"Deterministic?\\\",\\n       pg_catalog.obj_description(c.oid, 'pg_collation') AS \\\"Description\\\"\\nFROM pg_catalog.pg_collation c, pg_catalog.pg_namespace n\\nWHERE n.oid = c.collnamespace\\n      AND c.collencoding IN (-1, pg_catalog.pg_char_to_encoding(pg_catalog.getdatabaseencoding()))\\n  AND pg_catalog.pg_collation_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dp\",\n    \"desc\": \"list table, view, and sequence access privileges\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'S' THEN 'sequence' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' END as \\\"Type\\\",\\n  pg_catalog.array_to_string(c.relacl, E'\\\\n') AS \\\"Access privileges\\\",\\n  pg_catalog.array_to_string(ARRAY(\\n    SELECT attname || E':\\\\n  ' || pg_catalog.array_to_string(attacl, E'\\\\n  ')\\n    FROM pg_catalog.pg_attribute a\\n    WHERE attrelid = c.oid AND NOT attisdropped AND attacl IS NOT NULL\\n  ), E'\\\\n') AS \\\"Column privileges\\\",\\n  pg_catalog.array_to_string(ARRAY(\\n    SELECT polname\\n    || CASE WHEN NOT polpermissive THEN\\n       E' (RESTRICTIVE)'\\n       ELSE '' END\\n    || CASE WHEN polcmd != '*' THEN\\n           E' (' || polcmd || E'):'\\n       ELSE E':'\\n       END\\n    || CASE WHEN polqual IS NOT NULL THEN\\n           E'\\\\n  (u): ' || pg_catalog.pg_get_expr(polqual, polrelid)\\n       ELSE E''\\n       END\\n    || CASE WHEN polwithcheck IS NOT NULL THEN\\n           E'\\\\n  (c): ' || pg_catalog.pg_get_expr(polwithcheck, polrelid)\\n       ELSE E''\\n       END    || CASE WHEN polroles <> '{0}' THEN\\n           E'\\\\n  to: ' || pg_catalog.array_to_string(\\n               ARRAY(\\n                   SELECT rolname\\n                   FROM pg_catalog.pg_roles\\n                   WHERE oid = ANY (polroles)\\n                   ORDER BY 1\\n               ), E', ')\\n       ELSE E''\\n       END\\n    FROM pg_catalog.pg_policy pol\\n    WHERE polrelid = c.oid), E'\\\\n')\\n    AS \\\"Policies\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('r','v','m','S','f','p')\\n  AND n.nspname !~ '^pg_' AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dP\",\n    \"opts\": \"[itn+]\",\n    \"desc\": \"list [only index/table] partitioned relations [n=nested]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relkind WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n c2.oid::pg_catalog.regclass as \\\"Table\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\\n     LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\\nWHERE c.relkind IN ('p','I','')\\n AND NOT c.relispartition\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY \\\"Schema\\\", \\\"Type\\\" DESC, \\\"Name\\\";\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dPi\",\n    \"opts\": \"[itn+]\",\n    \"desc\": \"list [only index/table] partitioned relations [n=nested]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n c2.oid::pg_catalog.regclass as \\\"Table\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\\n     LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\\nWHERE c.relkind IN ('I','')\\n AND NOT c.relispartition\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY \\\"Schema\\\", \\\"Name\\\";\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dPit\",\n    \"opts\": \"[itn+]\",\n    \"desc\": \"list [only index/table] partitioned relations [n=nested]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relkind WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n c2.oid::pg_catalog.regclass as \\\"Table\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\\n     LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\\nWHERE c.relkind IN ('p','I','')\\n AND NOT c.relispartition\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY \\\"Schema\\\", \\\"Type\\\" DESC, \\\"Name\\\";\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dPitn\",\n    \"opts\": \"[itn+]\",\n    \"desc\": \"list [only index/table] partitioned relations [n=nested]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relkind WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  inh.inhparent::pg_catalog.regclass as \\\"Parent name\\\",\\n c2.oid::pg_catalog.regclass as \\\"Table\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\\n     LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\\n     LEFT JOIN pg_catalog.pg_inherits inh ON c.oid = inh.inhrelid\\nWHERE c.relkind IN ('p','I','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY \\\"Schema\\\", \\\"Type\\\" DESC, \\\"Parent name\\\" NULLS FIRST, \\\"Name\\\";\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dPitn+\",\n    \"opts\": \"[itn+]\",\n    \"desc\": \"list [only index/table] partitioned relations [n=nested]\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relkind WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  inh.inhparent::pg_catalog.regclass as \\\"Parent name\\\",\\n c2.oid::pg_catalog.regclass as \\\"Table\\\",\\n  s.dps as \\\"Leaf partition size\\\",\\n  s.tps as \\\"Total size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid\\n     LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid\\n     LEFT JOIN pg_catalog.pg_inherits inh ON c.oid = inh.inhrelid,\\n     LATERAL (SELECT pg_catalog.pg_size_pretty(sum(\\n                 CASE WHEN ppt.isleaf AND ppt.level = 1\\n                      THEN pg_catalog.pg_table_size(ppt.relid) ELSE 0 END)) AS dps,\\n                     pg_catalog.pg_size_pretty(sum(pg_catalog.pg_table_size(ppt.relid))) AS tps\\n              FROM pg_catalog.pg_partition_tree(c.oid) ppt) s\\nWHERE c.relkind IN ('p','I','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY \\\"Schema\\\", \\\"Type\\\" DESC, \\\"Parent name\\\" NULLS FIRST, \\\"Name\\\";\\n\"\n  },\n  {\n    \"cmd\": \"\\\\drds\",\n    \"desc\": \"list per-database role settings\",\n    \"query\": \"\\nSELECT rolname AS \\\"Role\\\", datname AS \\\"Database\\\",\\npg_catalog.array_to_string(setconfig, E'\\\\n') AS \\\"Settings\\\"\\nFROM pg_catalog.pg_db_role_setting s\\nLEFT JOIN pg_catalog.pg_database d ON d.oid = setdatabase\\nLEFT JOIN pg_catalog.pg_roles r ON r.oid = setrole\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dRp\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list replication publications\",\n    \"query\": \"\\nSELECT pubname AS \\\"Name\\\",\\n  pg_catalog.pg_get_userbyid(pubowner) AS \\\"Owner\\\",\\n  puballtables AS \\\"All tables\\\",\\n  pubinsert AS \\\"Inserts\\\",\\n  pubupdate AS \\\"Updates\\\",\\n  pubdelete AS \\\"Deletes\\\",\\n  pubtruncate AS \\\"Truncates\\\"\\nFROM pg_catalog.pg_publication\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dRs\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list replication subscriptions\",\n    \"query\": \"\\nSELECT subname AS \\\"Name\\\"\\n,  pg_catalog.pg_get_userbyid(subowner) AS \\\"Owner\\\"\\n,  subenabled AS \\\"Enabled\\\"\\n,  subpublications AS \\\"Publication\\\"\\nFROM pg_catalog.pg_subscription\\nWHERE subdbid = (SELECT oid\\n                 FROM pg_catalog.pg_database\\n                 WHERE datname = pg_catalog.current_database())ORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dRs+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list replication subscriptions\",\n    \"query\": \"\\nSELECT subname AS \\\"Name\\\"\\n,  pg_catalog.pg_get_userbyid(subowner) AS \\\"Owner\\\"\\n,  subenabled AS \\\"Enabled\\\"\\n,  subpublications AS \\\"Publication\\\"\\n,  subsynccommit AS \\\"Synchronous commit\\\"\\n,  subconninfo AS \\\"Conninfo\\\"\\nFROM pg_catalog.pg_subscription\\nWHERE subdbid = (SELECT oid\\n                 FROM pg_catalog.pg_database\\n                 WHERE datname = pg_catalog.current_database())ORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\ds\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list sequences\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('S','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dsS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list sequences\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('S','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dsS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list sequences\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as \\\"Persistence\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \\\"Size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('S','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dt\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list tables\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dtS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list tables\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','t','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dtS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list tables\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as \\\"Persistence\\\",\\n  am.amname as \\\"Access method\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \\\"Size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\n     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam\\nWHERE c.relkind IN ('r','p','t','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dT\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list data types\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  pg_catalog.format_type(t.oid, NULL) AS \\\"Name\\\",\\n  pg_catalog.obj_description(t.oid, 'pg_type') as \\\"Description\\\"\\nFROM pg_catalog.pg_type t\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\\nWHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))\\n  AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_type_is_visible(t.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dTS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list data types\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  pg_catalog.format_type(t.oid, NULL) AS \\\"Name\\\",\\n  pg_catalog.obj_description(t.oid, 'pg_type') as \\\"Description\\\"\\nFROM pg_catalog.pg_type t\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\\nWHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))\\n  AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)\\n  AND pg_catalog.pg_type_is_visible(t.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dTS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list data types\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  pg_catalog.format_type(t.oid, NULL) AS \\\"Name\\\",\\n  t.typname AS \\\"Internal name\\\",\\n  CASE WHEN t.typrelid != 0\\n      THEN CAST('tuple' AS pg_catalog.text)\\n    WHEN t.typlen < 0\\n      THEN CAST('var' AS pg_catalog.text)\\n    ELSE CAST(t.typlen AS pg_catalog.text)\\n  END AS \\\"Size\\\",\\n  pg_catalog.array_to_string(\\n      ARRAY(\\n          SELECT e.enumlabel\\n          FROM pg_catalog.pg_enum e\\n          WHERE e.enumtypid = t.oid\\n          ORDER BY e.enumsortorder\\n      ),\\n      E'\\\\n'\\n  ) AS \\\"Elements\\\",\\n  pg_catalog.pg_get_userbyid(t.typowner) AS \\\"Owner\\\",\\npg_catalog.array_to_string(t.typacl, E'\\\\n') AS \\\"Access privileges\\\",\\n    pg_catalog.obj_description(t.oid, 'pg_type') as \\\"Description\\\"\\nFROM pg_catalog.pg_type t\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace\\nWHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))\\n  AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)\\n  AND pg_catalog.pg_type_is_visible(t.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\du\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list roles\",\n    \"query\": \"\\nSELECT r.rolname, r.rolsuper, r.rolinherit,\\n  r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\\n  r.rolconnlimit, r.rolvaliduntil,\\n  ARRAY(SELECT b.rolname\\n        FROM pg_catalog.pg_auth_members m\\n        JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)\\n        WHERE m.member = r.oid) as memberof\\n, r.rolreplication\\n, r.rolbypassrls\\nFROM pg_catalog.pg_roles r\\nWHERE r.rolname !~ '^pg_'\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\duS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list roles\",\n    \"query\": \"\\nSELECT r.rolname, r.rolsuper, r.rolinherit,\\n  r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\\n  r.rolconnlimit, r.rolvaliduntil,\\n  ARRAY(SELECT b.rolname\\n        FROM pg_catalog.pg_auth_members m\\n        JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)\\n        WHERE m.member = r.oid) as memberof\\n, r.rolreplication\\n, r.rolbypassrls\\nFROM pg_catalog.pg_roles r\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\duS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list roles\",\n    \"query\": \"\\nSELECT r.rolname, r.rolsuper, r.rolinherit,\\n  r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\\n  r.rolconnlimit, r.rolvaliduntil,\\n  ARRAY(SELECT b.rolname\\n        FROM pg_catalog.pg_auth_members m\\n        JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)\\n        WHERE m.member = r.oid) as memberof\\n, pg_catalog.shobj_description(r.oid, 'pg_authid') AS description\\n, r.rolreplication\\n, r.rolbypassrls\\nFROM pg_catalog.pg_roles r\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dv\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list views\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('v','')\\n      AND n.nspname <> 'pg_catalog'\\n      AND n.nspname !~ '^pg_toast'\\n      AND n.nspname <> 'information_schema'\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dvS\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list views\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('v','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dvS+\",\n    \"opts\": \"[S+]\",\n    \"desc\": \"list views\",\n    \"query\": \"\\nSELECT n.nspname as \\\"Schema\\\",\\n  c.relname as \\\"Name\\\",\\n  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as \\\"Type\\\",\\n  pg_catalog.pg_get_userbyid(c.relowner) as \\\"Owner\\\",\\n  CASE c.relpersistence WHEN 'p' THEN 'permanent' WHEN 't' THEN 'temporary' WHEN 'u' THEN 'unlogged' END as \\\"Persistence\\\",\\n  pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \\\"Size\\\",\\n  pg_catalog.obj_description(c.oid, 'pg_class') as \\\"Description\\\"\\nFROM pg_catalog.pg_class c\\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\\nWHERE c.relkind IN ('v','s','')\\n  AND pg_catalog.pg_table_is_visible(c.oid)\\nORDER BY 1,2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dx\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list extensions\",\n    \"query\": \"\\nSELECT e.extname AS \\\"Name\\\", e.extversion AS \\\"Version\\\", n.nspname AS \\\"Schema\\\", c.description AS \\\"Description\\\"\\nFROM pg_catalog.pg_extension e LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace LEFT JOIN pg_catalog.pg_description c ON c.objoid = e.oid AND c.classoid = 'pg_catalog.pg_extension'::pg_catalog.regclass\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dx+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list extensions\",\n    \"query\": \"\\nSELECT e.extname, e.oid\\nFROM pg_catalog.pg_extension e\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dX\",\n    \"desc\": \"list extended statistics\",\n    \"query\": \"\\nSELECT \\nes.stxnamespace::pg_catalog.regnamespace::pg_catalog.text AS \\\"Schema\\\", \\nes.stxname AS \\\"Name\\\", \\npg_catalog.format('%s FROM %s', \\n  (SELECT pg_catalog.string_agg(pg_catalog.quote_ident(a.attname),', ') \\n   FROM pg_catalog.unnest(es.stxkeys) s(attnum) \\n   JOIN pg_catalog.pg_attribute a \\n   ON (es.stxrelid = a.attrelid \\n   AND a.attnum = s.attnum \\n   AND NOT a.attisdropped)), \\nes.stxrelid::pg_catalog.regclass) AS \\\"Definition\\\",\\nCASE WHEN 'd' = any(es.stxkind) THEN 'defined' \\nEND AS \\\"Ndistinct\\\", \\nCASE WHEN 'f' = any(es.stxkind) THEN 'defined' \\nEND AS \\\"Dependencies\\\",\\nCASE WHEN 'm' = any(es.stxkind) THEN 'defined' \\nEND AS \\\"MCV\\\"  \\nFROM pg_catalog.pg_statistic_ext es \\nWHERE pg_catalog.pg_statistics_obj_is_visible(es.oid)\\nORDER BY 1, 2;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dy\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list event triggers\",\n    \"query\": \"\\nSELECT evtname as \\\"Name\\\", evtevent as \\\"Event\\\", pg_catalog.pg_get_userbyid(e.evtowner) as \\\"Owner\\\",\\n case evtenabled when 'O' then 'enabled'  when 'R' then 'replica'  when 'A' then 'always'  when 'D' then 'disabled' end as \\\"Enabled\\\",\\n e.evtfoid::pg_catalog.regproc as \\\"Function\\\", pg_catalog.array_to_string(array(select x from pg_catalog.unnest(evttags) as t(x)), ', ') as \\\"Tags\\\"\\nFROM pg_catalog.pg_event_trigger e ORDER BY 1\\n\"\n  },\n  {\n    \"cmd\": \"\\\\dy+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list event triggers\",\n    \"query\": \"\\nSELECT evtname as \\\"Name\\\", evtevent as \\\"Event\\\", pg_catalog.pg_get_userbyid(e.evtowner) as \\\"Owner\\\",\\n case evtenabled when 'O' then 'enabled'  when 'R' then 'replica'  when 'A' then 'always'  when 'D' then 'disabled' end as \\\"Enabled\\\",\\n e.evtfoid::pg_catalog.regproc as \\\"Function\\\", pg_catalog.array_to_string(array(select x from pg_catalog.unnest(evttags) as t(x)), ', ') as \\\"Tags\\\",\\npg_catalog.obj_description(e.oid, 'pg_event_trigger') as \\\"Description\\\"\\nFROM pg_catalog.pg_event_trigger e ORDER BY 1\\n\"\n  },\n  {\n    \"cmd\": \"\\\\l\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list databases\",\n    \"query\": \"\\nSELECT d.datname as \\\"Name\\\",\\n       pg_catalog.pg_get_userbyid(d.datdba) as \\\"Owner\\\",\\n       pg_catalog.pg_encoding_to_char(d.encoding) as \\\"Encoding\\\",\\n       d.datcollate as \\\"Collate\\\",\\n       d.datctype as \\\"Ctype\\\",\\n       pg_catalog.array_to_string(d.datacl, E'\\\\n') AS \\\"Access privileges\\\"\\nFROM pg_catalog.pg_database d\\nORDER BY 1;\\n\"\n  },\n  {\n    \"cmd\": \"\\\\l+\",\n    \"opts\": \"[+]\",\n    \"desc\": \"list databases\",\n    \"query\": \"\\nSELECT d.datname as \\\"Name\\\",\\n       pg_catalog.pg_get_userbyid(d.datdba) as \\\"Owner\\\",\\n       pg_catalog.pg_encoding_to_char(d.encoding) as \\\"Encoding\\\",\\n       d.datcollate as \\\"Collate\\\",\\n       d.datctype as \\\"Ctype\\\",\\n       pg_catalog.array_to_string(d.datacl, E'\\\\n') AS \\\"Access privileges\\\",\\n       CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\\n            THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\\n            ELSE 'No Access'\\n       END as \\\"Size\\\",\\n       t.spcname as \\\"Tablespace\\\",\\n       pg_catalog.shobj_description(d.oid, 'pg_database') as \\\"Description\\\"\\nFROM pg_catalog.pg_database d\\n  JOIN pg_catalog.pg_tablespace t on d.dattablespace = t.oid\\nORDER BY 1;\\n\"\n  }\n]\n"
  },
  {
    "path": "common/publishUtils.d.ts",
    "content": "import { DBGeneratedSchema } from \"./DBGeneratedSchema\";\nimport { GroupedDetailedFilter } from \"./filterUtils\";\nexport type CustomTableRules = {\n    type: \"Custom\";\n    customTables: ({\n        tableName: string;\n    } & TableRules)[];\n};\ndeclare const OBJ_DEF_TYPES: readonly [\"boolean\", \"string\", \"number\", \"Date\", \"string[]\", \"number[]\", \"Date[]\", \"boolean[]\"];\ntype DataTypes = (typeof OBJ_DEF_TYPES)[number];\ntype ArgObjDef = {\n    type: DataTypes;\n    allowedValues?: readonly string[] | readonly number[] | readonly Date[];\n    defaultValue?: string;\n    optional?: boolean;\n    label?: string;\n    /**\n     * These can only be used on client side\n     * */\n    referencesFormatColumnContext?: {\n        columnFilter: AnyObject;\n    };\n    references?: {\n        table: string;\n        column: string;\n        /**\n         * If true then the argument will represent the entire row and\n         *  the specified column will only be used to display the chosen row\n         */\n        isFullRow?: boolean;\n        /**\n         * If true and isFullRow=true then a button will be shown\n         *  in the row edit card to display this action\n         */\n        showInRowCard?: {\n            actionLabel?: string;\n        };\n    };\n};\ntype _ObjDef = DataTypes | ArgObjDef;\ntype ObjDef = _ObjDef | {\n    oneOf: readonly _ObjDef[];\n} | {\n    arrayOf: _ObjDef;\n};\nexport type ArgDef = ArgObjDef & {\n    name: string;\n};\nexport type ParamDef = ObjDef;\nexport type UXParamDefinition = {\n    param: Record<string, ArgDef>;\n    paramOneOf?: undefined;\n} | {\n    param?: undefined;\n    paramOneOf: Record<string, ArgDef>[];\n};\nexport type MethodClientDef = {\n    name: string;\n    func: string;\n    args: ArgDef[];\n    outputTable?: string;\n};\nexport type ContextValue = {\n    objectName: string;\n    objectPropertyName: string;\n};\nexport type ForcedData = {\n    type: \"fixed\";\n    fieldName: string;\n    value: any;\n} | ({\n    type: \"context\";\n    fieldName: string;\n} & ContextValue);\nexport type SelectRule = {\n    subscribe?: {\n        throttle?: number;\n    };\n    fields: FieldFilter;\n    forcedFilterDetailed?: GroupedDetailedFilter;\n    filterFields?: FieldFilter;\n    orderByFields?: FieldFilter;\n};\nexport type UpdateRule = {\n    fields: FieldFilter;\n    forcedFilterDetailed?: GroupedDetailedFilter;\n    filterFields?: FieldFilter;\n    forcedDataDetail?: ForcedData[];\n    checkFilterDetailed?: GroupedDetailedFilter;\n    dynamicFields?: {\n        filterDetailed: GroupedDetailedFilter;\n        fields: FieldFilter;\n    }[];\n    forcedDataFrom?: \"InsertRule\";\n    checkFilterFrom?: \"InsertRule\";\n    fieldsFrom?: \"SelectRule\" | \"InsertRule\";\n    forcedFilterFrom?: \"SelectRule\" | \"DeleteRule\";\n    filterFieldsFrom?: \"SelectRule\" | \"DeleteRule\";\n};\nexport type InsertRule = {\n    fields: FieldFilter;\n    forcedDataDetail?: ForcedData[];\n    checkFilterDetailed?: GroupedDetailedFilter;\n    checkFilterFrom?: \"UpdateRule\";\n    forcedDataFrom?: \"InsertRule\";\n};\nexport type DeleteRule = {\n    filterFields: FieldFilter;\n    forcedFilterDetailed?: GroupedDetailedFilter;\n    filterFieldsFrom?: \"SelectRule\" | \"UpdateRule\";\n    forcedFilterFrom?: \"SelectRule\" | \"UpdateRule\";\n};\nexport type DBSSchema = {\n    [K in keyof DBGeneratedSchema]: Required<DBGeneratedSchema[K][\"columns\"]>;\n};\nexport type DBSSchemaForInsert = {\n    [K in keyof DBGeneratedSchema]: DBGeneratedSchema[K][\"columns\"];\n};\nexport type SyncRule = {\n    id_fields: string[];\n    synced_field: string;\n    allow_delete?: boolean;\n    batch_size?: number;\n    throttle?: number;\n};\nexport type TableRules = {\n    select?: boolean | SelectRule;\n    update?: boolean | UpdateRule;\n    insert?: boolean | InsertRule;\n    delete?: boolean | DeleteRule;\n    subscribe?: boolean | {\n        throttle?: number;\n    };\n    sync?: SyncRule;\n};\nexport type BasicTablePermissions = Partial<Record<Exclude<keyof TableRules, \"sync\">, boolean>>;\ntype AnyObject = Record<string, any>;\ntype PublishedResultUpdate = {\n    fields: FieldFilter;\n    dynamicFields?: {\n        filter: {\n            $and: AnyObject[];\n        } | {\n            $or: AnyObject[];\n        } | AnyObject;\n        fields: FieldFilter;\n    }[];\n    forcedFilter?: AnyObject;\n    forcedData?: AnyObject;\n    filterFields?: FieldFilter;\n    returningFields?: FieldFilter;\n};\ntype PublishedResult = boolean | {\n    select?: boolean | {\n        fields: FieldFilter;\n        filterFields?: FieldFilter;\n        forcedFilter?: AnyObject;\n        orderByFields?: FieldFilter;\n    };\n    update?: boolean | PublishedResultUpdate;\n    insert?: boolean | {\n        fields: FieldFilter;\n        forcedData?: AnyObject;\n    };\n    delete?: boolean | {\n        filterFields: FieldFilter;\n        forcedFilter?: AnyObject;\n    };\n    sync?: SyncRule;\n    subscribe?: boolean | {\n        throttle?: number;\n    };\n};\nexport declare function isObject<T extends Record<string, any>>(obj: any): obj is T;\nexport type FieldFilter = \"\" | \"*\" | string[] | Record<string, 1 | true> | Record<string, 0 | false>;\nexport declare const parseFieldFilter: (args: {\n    columns: string[];\n    fieldFilter: FieldFilter;\n}) => string[];\nexport declare const parseFullFilter: (filter: GroupedDetailedFilter, context: ContextDataObject | undefined, columns: string[] | undefined) => {\n    $and: AnyObject[];\n} | {\n    $or: AnyObject[];\n} | undefined;\ntype ParsedFilter = {\n    $and: AnyObject[];\n} | {\n    $or: AnyObject[];\n};\ntype ParsedRuleFilters = {\n    forcedFilter?: ParsedFilter;\n    checkFilter?: ParsedFilter;\n};\nexport declare const parseCheckForcedFilters: (rule: TableRules[keyof TableRules], context: ContextDataObject | undefined, columns: string[] | undefined) => ParsedRuleFilters | undefined;\nexport type ContextDataObject = {\n    user: DBSSchema[\"users\"];\n};\nexport declare const parseTableRules: (rules: TableRules, isView: boolean | undefined, columns: string[], context: ContextDataObject) => PublishedResult | undefined;\nexport type TableRulesErrors = Partial<Record<keyof TableRules, any>> & {\n    all?: string;\n};\nexport declare const getTableRulesErrors: (rules: TableRules, tableColumns: string[], contextData: ContextDataObject) => Promise<TableRulesErrors>;\nexport declare const validateDynamicFields: (dynamicFields: UpdateRule[\"dynamicFields\"], tableHandler: {\n    find?: any;\n    findOne?: any;\n} | undefined, context: ContextDataObject, columns: string[]) => Promise<{\n    error?: any;\n}>;\nexport declare const getCIDRRangesQuery: (arg: {\n    cidr: string;\n    returns: [\"from\", \"to\"];\n}) => string;\nexport {};\n//# sourceMappingURL=publishUtils.d.ts.map"
  },
  {
    "path": "common/publishUtils.js",
    "content": "import { getFinalFilter, isDefined, } from \"./filterUtils\";\nconst OBJ_DEF_TYPES = [\n    \"boolean\",\n    \"string\",\n    \"number\",\n    \"Date\",\n    \"string[]\",\n    \"number[]\",\n    \"Date[]\",\n    \"boolean[]\",\n];\nexport function isObject(obj) {\n    return Boolean(obj && typeof obj === \"object\" && !Array.isArray(obj));\n}\nexport const parseFieldFilter = (args) => {\n    const { columns, fieldFilter } = args;\n    if (!fieldFilter)\n        return [];\n    else if (fieldFilter === \"*\")\n        return columns.slice();\n    else if (Array.isArray(fieldFilter)) {\n        if (!fieldFilter.length)\n            return [];\n        return columns.filter((c) => fieldFilter.includes(c.toString()));\n    }\n    else if (isObject(fieldFilter) && Object.keys(fieldFilter).length) {\n        const fields = Object.keys(fieldFilter);\n        const isExcept = !Object.values(fieldFilter)[0];\n        return columns.filter((c) => isExcept ? !fields.includes(c) : fields.includes(c));\n    }\n    return [];\n};\nexport const parseFullFilter = (filter, context, columns) => {\n    const isAnd = \"$and\" in filter;\n    const filters = isAnd ? filter.$and : filter.$or;\n    const finalFilters = filters\n        .map((f) => getFinalFilter(f, context, { columns }))\n        .filter(isDefined);\n    const f = isAnd ? { $and: finalFilters } : { $or: finalFilters };\n    return f;\n};\nexport const parseCheckForcedFilters = (rule, context, columns) => {\n    let parsedRuleFilters;\n    if (isObject(rule)) {\n        if (\"forcedFilterDetailed\" in rule && rule.forcedFilterDetailed) {\n            const forcedFilter = parseFullFilter(rule.forcedFilterDetailed, context, columns);\n            if (forcedFilter) {\n                parsedRuleFilters !== null && parsedRuleFilters !== void 0 ? parsedRuleFilters : (parsedRuleFilters = {});\n                parsedRuleFilters.forcedFilter = forcedFilter;\n            }\n        }\n        if (\"checkFilterDetailed\" in rule && rule.checkFilterDetailed) {\n            const checkFilter = parseFullFilter(rule.checkFilterDetailed, context, columns);\n            if (checkFilter) {\n                parsedRuleFilters !== null && parsedRuleFilters !== void 0 ? parsedRuleFilters : (parsedRuleFilters = {});\n                parsedRuleFilters.checkFilter = checkFilter;\n            }\n        }\n    }\n    return parsedRuleFilters;\n};\nconst getValidatedFieldFilter = (value, columns, expectAtLeastOne = true) => {\n    if (value === \"*\")\n        return value;\n    const values = Object.values(value);\n    const keys = Object.keys(value);\n    if (!keys.length && expectAtLeastOne)\n        throw new Error(\"Must select at least a field\");\n    if (values.some((v) => v) && values.some((v) => !v)) {\n        throw new Error(\"Invalid field filter: must have only include or exclude. Cannot have both\");\n    }\n    if (!values.every((v) => [0, 1, true, false].includes(v))) {\n        throw new Error(\"Invalid field filter: field values can only be one of 0,1,true,false\");\n    }\n    const badCols = keys.filter((c) => !columns.includes(c));\n    if (badCols.length) {\n        throw new Error(`Invalid columns provided: ${badCols}`);\n    }\n    return value;\n};\nconst parseForcedData = (value, context, columns) => {\n    var _a;\n    /** TODO: retire forced data completely */\n    if (!((_a = value === null || value === void 0 ? void 0 : value.forcedDataDetail) === null || _a === void 0 ? void 0 : _a.length)) {\n        if (value === null || value === void 0 ? void 0 : value.checkFilterDetailed) {\n            const checkFilter = value === null || value === void 0 ? void 0 : value.checkFilterDetailed;\n            if (\"$and\" in checkFilter && checkFilter.$and.length) {\n                const forcedContextData = checkFilter.$and\n                    .map((f) => {\n                    var _a;\n                    if (f.type !== \"=\")\n                        return undefined;\n                    if (f.contextValue && ((_a = f.contextValue) === null || _a === void 0 ? void 0 : _a.objectName) === \"user\") {\n                        const userKey = f.contextValue.objectPropertyName;\n                        if (!(userKey in context.user))\n                            throw new Error(`Invalid objectPropertyName (${f.contextValue.objectPropertyName}) found in forcedData`);\n                        return [f.fieldName, context.user[userKey]];\n                    }\n                    else if (f.value !== undefined) {\n                        return [f.fieldName, f.value];\n                    }\n                    return undefined;\n                })\n                    .filter(isDefined);\n                if (!forcedContextData.length)\n                    return undefined;\n                const forcedData = Object.fromEntries(forcedContextData);\n                return {\n                    forcedData,\n                };\n            }\n        }\n        return undefined;\n    }\n    let forcedData = {};\n    value === null || value === void 0 ? void 0 : value.forcedDataDetail.forEach((v) => {\n        if (!columns.includes(v.fieldName))\n            new Error(`Invalid fieldName in forced data ${v.fieldName}`);\n        if (v.fieldName in forcedData)\n            throw new Error(`Duplicate forced data (${v.fieldName}) found in ${JSON.stringify(value)}`);\n        if (v.type === \"fixed\") {\n            forcedData[v.fieldName] = v.value;\n        }\n        else {\n            const obj = context[v.objectName];\n            if (!obj)\n                throw new Error(`Missing objectName (${v.objectName}) in forcedData`);\n            if (!(v.objectPropertyName in obj))\n                throw new Error(`Invalid/missing objectPropertyName (${v.objectPropertyName}) found in forcedData`);\n            forcedData[v.fieldName] = obj[v.objectPropertyName];\n        }\n    });\n    return { forcedData };\n};\nconst parseSelect = (rule, columns, context) => {\n    if (!rule || rule === true)\n        return rule;\n    return Object.assign(Object.assign(Object.assign({ fields: getValidatedFieldFilter(rule.fields, columns) }, parseCheckForcedFilters(rule, context, columns)), (rule.orderByFields && {\n        orderByFields: getValidatedFieldFilter(rule.orderByFields, columns, false),\n    })), (rule.filterFields && {\n        filterFields: getValidatedFieldFilter(rule.filterFields, columns, false),\n    }));\n};\nconst parseUpdate = (rule, columns, context) => {\n    var _a;\n    if (!rule || rule === true)\n        return rule;\n    return Object.assign(Object.assign(Object.assign(Object.assign({ fields: getValidatedFieldFilter(rule.fields, columns) }, parseCheckForcedFilters(rule, context, columns)), parseForcedData(rule, context, columns)), (rule.filterFields && {\n        filterFields: getValidatedFieldFilter(rule.filterFields, columns, false),\n    })), (((_a = rule.dynamicFields) === null || _a === void 0 ? void 0 : _a.length) && {\n        dynamicFields: rule.dynamicFields.map((v) => ({\n            fields: getValidatedFieldFilter(v.fields, columns),\n            filter: parseFullFilter(v.filterDetailed, context, columns),\n        })),\n    }));\n};\nconst parseInsert = (rule, columns, context) => {\n    if (!rule || rule === true)\n        return rule;\n    return Object.assign(Object.assign({ fields: getValidatedFieldFilter(rule.fields, columns) }, parseForcedData(rule, context, columns)), parseCheckForcedFilters(rule, context, columns));\n};\nconst parseDelete = (rule, columns, context) => {\n    if (!rule || rule === true)\n        return rule;\n    return Object.assign(Object.assign({}, parseCheckForcedFilters(rule, context, columns)), { filterFields: getValidatedFieldFilter(rule.filterFields, columns) });\n};\nexport const parseTableRules = (rules, isView = false, columns, context) => {\n    if ([true, \"*\"].includes(rules)) {\n        return true;\n    }\n    if (isObject(rules)) {\n        return Object.assign({ select: parseSelect(rules.select, columns, context), subscribe: isObject(rules.select) ? rules.select.subscribe : rules.subscribe }, (!isView ?\n            {\n                insert: parseInsert(rules.insert, columns, context),\n                update: parseUpdate(rules.update, columns, context),\n                delete: parseDelete(rules.delete, columns, context),\n                sync: rules.sync,\n            }\n            : {}));\n    }\n    return false;\n};\nexport const getTableRulesErrors = async (rules, tableColumns, contextData) => {\n    let result = {};\n    await Promise.all(Object.keys(rules).map(async (ruleKey) => {\n        const key = ruleKey;\n        const rule = rules[key];\n        try {\n            parseTableRules({ [key]: rule }, false, tableColumns, contextData);\n        }\n        catch (err) {\n            result[key] = err;\n        }\n    }));\n    return result;\n};\nexport const validateDynamicFields = async (dynamicFields, tableHandler, context, columns) => {\n    if (!dynamicFields || !tableHandler)\n        return {};\n    for (const [dfIndex, dfRule] of dynamicFields.entries()) {\n        const filter = await parseFullFilter(dfRule.filterDetailed, context, columns);\n        if (!filter)\n            throw new Error(\"dynamicFields.filter cannot be empty: \" + JSON.stringify(dfRule));\n        await tableHandler.find(filter, { limit: 0 });\n        /** Ensure dynamicFields filters do not overlap */\n        for (const [_dfIndex, _dfRule] of dynamicFields.entries()) {\n            if (dfIndex !== _dfIndex) {\n                const _filter = await parseFullFilter(_dfRule.filterDetailed, context, columns);\n                if (await tableHandler.findOne({ $and: [filter, _filter] }, { select: \"\" })) {\n                    const error = `dynamicFields.filter cannot overlap each other. \\n\n          Overlapping dynamicFields rules:\n              ${JSON.stringify(dfRule)} \n              AND\n              ${JSON.stringify(_dfRule)} \n          `;\n                    return { error };\n                }\n            }\n        }\n    }\n    return {};\n};\nexport const getCIDRRangesQuery = (arg) => 'select \\\n  host(${cidr}::cidr) AS \"from\",  \\\n  host(broadcast(${cidr}::cidr)) AS \"to\" ';\n"
  },
  {
    "path": "common/publishUtils.ts",
    "content": "import { DBGeneratedSchema } from \"./DBGeneratedSchema\";\nimport {\n  GroupedDetailedFilter,\n  getFinalFilter,\n  isDefined,\n  DetailedFilter,\n} from \"./filterUtils\";\n\nexport type CustomTableRules = {\n  type: \"Custom\";\n  customTables: ({\n    tableName: string;\n  } & TableRules)[];\n};\n\nconst OBJ_DEF_TYPES = [\n  \"boolean\",\n  \"string\",\n  \"number\",\n  \"Date\",\n  \"string[]\",\n  \"number[]\",\n  \"Date[]\",\n  \"boolean[]\",\n] as const;\ntype DataTypes = (typeof OBJ_DEF_TYPES)[number];\n\ntype ArgObjDef = {\n  type: DataTypes;\n  allowedValues?: readonly string[] | readonly number[] | readonly Date[];\n  defaultValue?: string;\n  optional?: boolean;\n  label?: string;\n\n  /**\n   * These can only be used on client side\n   * */\n  referencesFormatColumnContext?: {\n    columnFilter: AnyObject;\n  };\n  references?: {\n    table: string;\n    column: string;\n    /**\n     * If true then the argument will represent the entire row and\n     *  the specified column will only be used to display the chosen row\n     */\n    isFullRow?: boolean;\n\n    /**\n     * If true and isFullRow=true then a button will be shown\n     *  in the row edit card to display this action\n     */\n    showInRowCard?: {\n      actionLabel?: string;\n    };\n  };\n};\n\ntype _ObjDef = DataTypes | ArgObjDef;\n\ntype ObjDef = _ObjDef | { oneOf: readonly _ObjDef[] } | { arrayOf: _ObjDef };\n\n// type ObjDefObj =\n// | ArgObjDef\n// | { oneOf: readonly ArgObjDef[]; }\n// | { arrayOf: ArgObjDef; }\n\nexport type ArgDef = ArgObjDef & {\n  name: string;\n};\nexport type ParamDef = ObjDef;\n\nexport type UXParamDefinition =\n  | {\n      param: Record<string, ArgDef>;\n      paramOneOf?: undefined;\n    }\n  | {\n      param?: undefined;\n      paramOneOf: Record<string, ArgDef>[];\n    };\n\nexport type MethodClientDef = {\n  name: string;\n  func: string;\n  args: ArgDef[];\n  outputTable?: string;\n};\n\nexport type ContextValue = {\n  objectName: string;\n  objectPropertyName: string;\n};\n\nexport type ForcedData =\n  | {\n      type: \"fixed\";\n      fieldName: string;\n      value: any;\n    }\n  | ({\n      type: \"context\";\n      fieldName: string;\n    } & ContextValue);\n\nexport type SelectRule = {\n  subscribe?: {\n    throttle?: number;\n  };\n  fields: FieldFilter;\n  forcedFilterDetailed?: GroupedDetailedFilter;\n  filterFields?: FieldFilter;\n  orderByFields?: FieldFilter;\n};\nexport type UpdateRule = {\n  fields: FieldFilter;\n  forcedFilterDetailed?: GroupedDetailedFilter;\n  filterFields?: FieldFilter;\n  forcedDataDetail?: ForcedData[];\n  checkFilterDetailed?: GroupedDetailedFilter;\n\n  dynamicFields?: {\n    filterDetailed: GroupedDetailedFilter;\n    fields: FieldFilter;\n  }[];\n  forcedDataFrom?: \"InsertRule\";\n  checkFilterFrom?: \"InsertRule\";\n  fieldsFrom?: \"SelectRule\" | \"InsertRule\";\n  forcedFilterFrom?: \"SelectRule\" | \"DeleteRule\";\n  filterFieldsFrom?: \"SelectRule\" | \"DeleteRule\";\n};\n\nexport type InsertRule = {\n  fields: FieldFilter;\n  forcedDataDetail?: ForcedData[];\n  checkFilterDetailed?: GroupedDetailedFilter;\n  checkFilterFrom?: \"UpdateRule\";\n  forcedDataFrom?: \"InsertRule\";\n};\nexport type DeleteRule = {\n  filterFields: FieldFilter;\n  forcedFilterDetailed?: GroupedDetailedFilter;\n  filterFieldsFrom?: \"SelectRule\" | \"UpdateRule\";\n  forcedFilterFrom?: \"SelectRule\" | \"UpdateRule\";\n};\n\nexport type DBSSchema = {\n  [K in keyof DBGeneratedSchema]: Required<DBGeneratedSchema[K][\"columns\"]>;\n};\n\nexport type DBSSchemaForInsert = {\n  [K in keyof DBGeneratedSchema]: DBGeneratedSchema[K][\"columns\"];\n};\n\nexport type SyncRule = {\n  id_fields: string[];\n  synced_field: string;\n  allow_delete?: boolean;\n  batch_size?: number;\n  throttle?: number;\n};\n\nexport type TableRules = {\n  select?: boolean | SelectRule;\n  update?: boolean | UpdateRule;\n  insert?: boolean | InsertRule;\n  delete?: boolean | DeleteRule;\n  subscribe?:\n    | boolean\n    | {\n        throttle?: number;\n      };\n  sync?: SyncRule;\n};\n\nexport type BasicTablePermissions = Partial<\n  Record<Exclude<keyof TableRules, \"sync\">, boolean>\n>;\n\ntype AnyObject = Record<string, any>;\n\ntype PublishedResultUpdate = {\n  fields: FieldFilter;\n\n  dynamicFields?: {\n    filter: { $and: AnyObject[] } | { $or: AnyObject[] } | AnyObject;\n    fields: FieldFilter;\n  }[];\n\n  forcedFilter?: AnyObject;\n\n  forcedData?: AnyObject;\n\n  filterFields?: FieldFilter;\n\n  returningFields?: FieldFilter;\n};\n\ntype PublishedResult =\n  | boolean\n  | {\n      select?:\n        | boolean\n        | {\n            fields: FieldFilter;\n            filterFields?: FieldFilter;\n            forcedFilter?: AnyObject;\n            orderByFields?: FieldFilter;\n          };\n      update?: boolean | PublishedResultUpdate;\n      insert?:\n        | boolean\n        | {\n            fields: FieldFilter;\n            forcedData?: AnyObject;\n          };\n      delete?:\n        | boolean\n        | {\n            filterFields: FieldFilter;\n            forcedFilter?: AnyObject;\n          };\n      sync?: SyncRule;\n      subscribe?:\n        | boolean\n        | {\n            throttle?: number;\n          };\n    };\n\nexport function isObject<T extends Record<string, any>>(obj: any): obj is T {\n  return Boolean(obj && typeof obj === \"object\" && !Array.isArray(obj));\n}\n\nexport type FieldFilter =\n  | \"\"\n  | \"*\"\n  | string[]\n  | Record<string, 1 | true>\n  | Record<string, 0 | false>;\n\nexport const parseFieldFilter = (args: {\n  columns: string[];\n  fieldFilter: FieldFilter;\n}): string[] => {\n  const { columns, fieldFilter } = args;\n  if (!fieldFilter) return [];\n  else if (fieldFilter === \"*\") return columns.slice();\n  else if (Array.isArray(fieldFilter)) {\n    if (!fieldFilter.length) return [];\n\n    return columns.filter((c) =>\n      (fieldFilter as string[]).includes(c.toString()),\n    );\n  } else if (isObject(fieldFilter) && Object.keys(fieldFilter).length) {\n    const fields = Object.keys(fieldFilter);\n    const isExcept = !Object.values(fieldFilter)[0];\n    return columns.filter((c) =>\n      isExcept ? !fields.includes(c) : fields.includes(c),\n    );\n  }\n\n  return [];\n};\n\nexport const parseFullFilter = (\n  filter: GroupedDetailedFilter,\n  context: ContextDataObject | undefined,\n  columns: string[] | undefined,\n): { $and: AnyObject[] } | { $or: AnyObject[] } | undefined => {\n  const isAnd = \"$and\" in filter;\n  const filters = isAnd ? filter.$and : filter.$or;\n  const finalFilters = (filters as DetailedFilter[])\n    .map((f) => getFinalFilter(f, context, { columns }))\n    .filter(isDefined);\n  const f = isAnd ? { $and: finalFilters } : { $or: finalFilters };\n  return f;\n};\n\ntype ParsedFilter = { $and: AnyObject[] } | { $or: AnyObject[] };\ntype ParsedRuleFilters = {\n  forcedFilter?: ParsedFilter;\n  checkFilter?: ParsedFilter;\n};\nexport const parseCheckForcedFilters = (\n  rule: TableRules[keyof TableRules],\n  context: ContextDataObject | undefined,\n  columns: string[] | undefined,\n): ParsedRuleFilters | undefined => {\n  let parsedRuleFilters: ParsedRuleFilters | undefined;\n  if (isObject(rule)) {\n    if (\"forcedFilterDetailed\" in rule && rule.forcedFilterDetailed) {\n      const forcedFilter = parseFullFilter(\n        rule.forcedFilterDetailed,\n        context,\n        columns,\n      );\n      if (forcedFilter) {\n        parsedRuleFilters ??= {};\n        parsedRuleFilters.forcedFilter = forcedFilter;\n      }\n    }\n    if (\"checkFilterDetailed\" in rule && rule.checkFilterDetailed) {\n      const checkFilter = parseFullFilter(\n        rule.checkFilterDetailed,\n        context,\n        columns,\n      );\n      if (checkFilter) {\n        parsedRuleFilters ??= {};\n        parsedRuleFilters.checkFilter = checkFilter;\n      }\n    }\n  }\n  return parsedRuleFilters;\n};\n\nconst getValidatedFieldFilter = (\n  value: FieldFilter,\n  columns: string[],\n  expectAtLeastOne = true,\n): FieldFilter => {\n  if (value === \"*\") return value;\n  const values = Object.values(value);\n  const keys = Object.keys(value);\n  if (!keys.length && expectAtLeastOne)\n    throw new Error(\"Must select at least a field\");\n  if (values.some((v) => v) && values.some((v) => !v)) {\n    throw new Error(\n      \"Invalid field filter: must have only include or exclude. Cannot have both\",\n    );\n  }\n  if (!values.every((v) => [0, 1, true, false].includes(v))) {\n    throw new Error(\n      \"Invalid field filter: field values can only be one of 0,1,true,false\",\n    );\n  }\n  const badCols = keys.filter((c) => !columns.includes(c));\n  if (badCols.length) {\n    throw new Error(`Invalid columns provided: ${badCols}`);\n  }\n  return value;\n};\n\nexport type ContextDataObject = {\n  user: DBSSchema[\"users\"];\n};\n\nconst parseForcedData = (\n  value: Pick<UpdateRule, \"forcedDataDetail\" | \"checkFilterDetailed\">,\n  context: ContextDataObject,\n  columns: string[],\n): { forcedData: AnyObject } | undefined => {\n  /** TODO: retire forced data completely */\n  if (!value?.forcedDataDetail?.length) {\n    if (value?.checkFilterDetailed) {\n      const checkFilter = value?.checkFilterDetailed;\n      if (\"$and\" in checkFilter && checkFilter.$and.length) {\n        const forcedContextData = (checkFilter.$and as DetailedFilter[])\n          .map((f) => {\n            if (f.type !== \"=\") return undefined;\n\n            if (f.contextValue && f.contextValue?.objectName === \"user\") {\n              const userKey = f.contextValue.objectPropertyName;\n              if (!(userKey in context.user))\n                throw new Error(\n                  `Invalid objectPropertyName (${f.contextValue.objectPropertyName}) found in forcedData`,\n                );\n              return [f.fieldName, (context.user as any)[userKey]];\n            } else if (f.value !== undefined) {\n              return [f.fieldName, f.value];\n            }\n\n            return undefined;\n          })\n          .filter(isDefined);\n        if (!forcedContextData.length) return undefined;\n        const forcedData: AnyObject = Object.fromEntries(forcedContextData);\n        return {\n          forcedData,\n        };\n      }\n    }\n    return undefined;\n  }\n  let forcedData: AnyObject = {};\n  value?.forcedDataDetail.forEach((v) => {\n    if (!columns.includes(v.fieldName))\n      new Error(`Invalid fieldName in forced data ${v.fieldName}`);\n    if (v.fieldName in forcedData)\n      throw new Error(\n        `Duplicate forced data (${v.fieldName}) found in ${JSON.stringify(value)}`,\n      );\n    if (v.type === \"fixed\") {\n      forcedData[v.fieldName] = v.value;\n    } else {\n      const obj: AnyObject = context[v.objectName as keyof ContextDataObject];\n      if (!obj)\n        throw new Error(`Missing objectName (${v.objectName}) in forcedData`);\n      if (!(v.objectPropertyName in obj))\n        throw new Error(\n          `Invalid/missing objectPropertyName (${v.objectPropertyName}) found in forcedData`,\n        );\n      forcedData[v.fieldName] = obj[v.objectPropertyName];\n    }\n  });\n  return { forcedData };\n};\n\nconst parseSelect = (\n  rule: undefined | boolean | SelectRule,\n  columns: string[],\n  context: ContextDataObject | undefined,\n) => {\n  if (!rule || rule === true) return rule;\n\n  return {\n    fields: getValidatedFieldFilter(rule.fields, columns),\n    ...parseCheckForcedFilters(rule, context, columns),\n    ...(rule.orderByFields && {\n      orderByFields: getValidatedFieldFilter(\n        rule.orderByFields,\n        columns,\n        false,\n      ),\n    }),\n    ...(rule.filterFields && {\n      filterFields: getValidatedFieldFilter(rule.filterFields, columns, false),\n    }),\n  };\n};\nconst parseUpdate = (\n  rule: undefined | boolean | UpdateRule,\n  columns: string[],\n  context: ContextDataObject,\n) => {\n  if (!rule || rule === true) return rule;\n\n  return {\n    fields: getValidatedFieldFilter(rule.fields, columns),\n    ...parseCheckForcedFilters(rule, context, columns),\n    ...parseForcedData(rule, context, columns),\n    ...(rule.filterFields && {\n      filterFields: getValidatedFieldFilter(rule.filterFields, columns, false),\n    }),\n    ...(rule.dynamicFields?.length && {\n      dynamicFields: rule.dynamicFields.map((v) => ({\n        fields: getValidatedFieldFilter(v.fields, columns),\n        filter: parseFullFilter(v.filterDetailed, context, columns),\n      })),\n    }),\n  } as PublishedResultUpdate;\n};\nconst parseInsert = (\n  rule: undefined | boolean | InsertRule,\n  columns: string[],\n  context: ContextDataObject,\n) => {\n  if (!rule || rule === true) return rule;\n\n  return {\n    fields: getValidatedFieldFilter(rule.fields, columns),\n    ...parseForcedData(rule, context, columns),\n    ...parseCheckForcedFilters(rule, context, columns),\n  };\n};\nconst parseDelete = (\n  rule: undefined | boolean | DeleteRule,\n  columns: string[],\n  context: ContextDataObject,\n) => {\n  if (!rule || rule === true) return rule;\n\n  return {\n    ...parseCheckForcedFilters(rule, context, columns),\n    filterFields: getValidatedFieldFilter(rule.filterFields, columns),\n  };\n};\n\nexport const parseTableRules = (\n  rules: TableRules,\n  isView = false,\n  columns: string[],\n  context: ContextDataObject,\n): PublishedResult | undefined => {\n  if ([true, \"*\"].includes(rules as any)) {\n    return true;\n  }\n\n  if (isObject(rules)) {\n    return {\n      select: parseSelect(rules.select, columns, context),\n      subscribe:\n        isObject(rules.select) ? rules.select.subscribe : rules.subscribe,\n      ...(!isView ?\n        {\n          insert: parseInsert(rules.insert, columns, context),\n          update: parseUpdate(rules.update, columns, context),\n          delete: parseDelete(rules.delete, columns, context),\n          sync: rules.sync,\n        }\n      : {}),\n    };\n  }\n\n  return false;\n};\n\nexport type TableRulesErrors = Partial<Record<keyof TableRules, any>> & {\n  all?: string;\n};\n\nexport const getTableRulesErrors = async (\n  rules: TableRules,\n  tableColumns: string[],\n  contextData: ContextDataObject,\n): Promise<TableRulesErrors> => {\n  let result: TableRulesErrors = {};\n\n  await Promise.all(\n    Object.keys(rules).map(async (ruleKey) => {\n      const key = ruleKey as keyof TableRules;\n      const rule = rules[key];\n\n      try {\n        parseTableRules({ [key]: rule }, false, tableColumns, contextData);\n      } catch (err) {\n        result[key] = err;\n      }\n    }),\n  );\n\n  return result;\n};\n\nexport const validateDynamicFields = async (\n  dynamicFields: UpdateRule[\"dynamicFields\"],\n  tableHandler: { find?: any; findOne?: any } | undefined,\n  context: ContextDataObject,\n  columns: string[],\n): Promise<{ error?: any }> => {\n  if (!dynamicFields || !tableHandler) return {};\n\n  for (const [dfIndex, dfRule] of dynamicFields.entries()) {\n    const filter = await parseFullFilter(\n      dfRule.filterDetailed,\n      context,\n      columns,\n    );\n    if (!filter)\n      throw new Error(\n        \"dynamicFields.filter cannot be empty: \" + JSON.stringify(dfRule),\n      );\n    await tableHandler.find(filter, { limit: 0 });\n\n    /** Ensure dynamicFields filters do not overlap */\n    for (const [_dfIndex, _dfRule] of dynamicFields.entries()) {\n      if (dfIndex !== _dfIndex) {\n        const _filter = await parseFullFilter(\n          _dfRule.filterDetailed,\n          context,\n          columns,\n        );\n        if (\n          await tableHandler.findOne(\n            { $and: [filter, _filter] },\n            { select: \"\" },\n          )\n        ) {\n          const error = `dynamicFields.filter cannot overlap each other. \\n\n          Overlapping dynamicFields rules:\n              ${JSON.stringify(dfRule)} \n              AND\n              ${JSON.stringify(_dfRule)} \n          `;\n          return { error };\n        }\n      }\n    }\n  }\n\n  return {};\n};\n\nexport const getCIDRRangesQuery = (arg: {\n  cidr: string;\n  returns: [\"from\", \"to\"];\n}) =>\n  'select \\\n  host(${cidr}::cidr) AS \"from\",  \\\n  host(broadcast(${cidr}::cidr)) AS \"to\" ';\n"
  },
  {
    "path": "common/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\", \n    \"lib\": [\"ES2017\", \"es2019\", \"ES2021.String\"],\n    \"module\": \"esnext\",\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"strict\": true,\n    \"noImplicitAny\": false,\n    \"declaration\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"ignoreDeprecations\": \"5.0\",\n    \"declarationMap\": true\n  }, \n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "common/utils.d.ts",
    "content": "import { DBSSchema } from \"./publishUtils\";\nexport declare const SECOND = 1000;\nexport declare const MINUTE: number;\nexport declare const HOUR: number;\nexport declare const DAY: number;\nexport declare const MONTH: number;\nexport declare const YEAR: number;\nexport type AGE = {\n    years?: number;\n    months?: number;\n    days?: number;\n    hours?: number;\n    minutes?: number;\n    seconds?: number;\n    milliseconds?: number;\n};\nexport declare const EXCLUDE_FROM_SCHEMA_WATCH = \"prostgles internal query that should be excluded from schema watch \";\nexport declare const STATUS_MONITOR_IGNORE_QUERY = \"prostgles-status-monitor-query\";\nexport declare const getAgeFromDiff: (millisecondDiff: number) => {\n    years: number;\n    months: number;\n    days: number;\n    hours: number;\n    minutes: number;\n    seconds: number;\n    milliseconds: number;\n};\nexport declare const getAge: <ReturnALL extends boolean = false>(date1: number, date2: number, returnAll?: ReturnALL) => ReturnALL extends true ? Required<AGE> : AGE;\nexport declare const DESTINATIONS: readonly [{\n    readonly key: \"Local\";\n    readonly subLabel: \"Saved locally (server in address bar)\";\n}, {\n    readonly key: \"Cloud\";\n    readonly subLabel: \"Saved to Amazon S3\";\n}];\nexport type DumpOpts = DBSSchema[\"backups\"][\"options\"];\nexport type PGDumpParams = {\n    options: DumpOpts;\n    credentialID?: DBSSchema[\"backups\"][\"credential_id\"];\n    destination: (typeof DESTINATIONS)[number][\"key\"];\n    initiator?: string;\n    name?: string;\n};\nexport type DeepWriteable<T> = {\n    -readonly [P in keyof T]: DeepWriteable<T[P]>;\n};\ntype AnyObject = Record<string, any>;\nexport type WithUndef<T extends AnyObject | undefined> = T extends AnyObject ? {\n    [K in keyof T]: T[K] | undefined;\n} : T;\nexport type GetElementType<T extends any[] | readonly any[]> = T extends (infer U)[] ? U : never;\nexport type OmitDistributive<T, K extends keyof any> = T extends any ? Omit<T, K> : never;\nexport type PG_STAT_ACTIVITY = {\n    datid: number | null;\n    datname: string | null;\n    pid: number;\n    usesysid: number | null;\n    usename: string | null;\n    application_name: string;\n    client_addr: string | null;\n    client_hostname: string | null;\n    client_port: number | null;\n    backend_start: string;\n    xact_start: string | null;\n    query_start: string | null;\n    state_change: string | null;\n    wait_event_type: string | null;\n    wait_event: string | null;\n    state: string | null;\n    backend_xid: any | null;\n    backend_xmin: any | null;\n    query: string;\n    backend_type: string;\n    blocked_by: number[];\n    running_time: AnyObject;\n};\nexport type PG_STAT_DATABASE = {\n    datid: number;\n    datname: string;\n    numbackends: number;\n    xact_commit: number;\n    xact_rollback: number;\n    blks_read: number;\n    blks_hit: number;\n    tup_returned: number;\n    tup_fetched: number;\n    tup_inserted: number;\n    tup_updated: number;\n    tup_deleted: number;\n    conflicts: number;\n    temp_files: number;\n    temp_bytes: number;\n    deadlocks: number;\n    checksum_failures: number | null;\n    checksum_last_failure: string | null;\n    blk_read_time: number;\n    blk_write_time: number;\n    stats_reset: string;\n};\nexport type IOStats = {\n    majorNumber: number;\n    minorNumber: number;\n    deviceName: string;\n    readsCompletedSuccessfully: number;\n    readsMerged: number;\n    sectorsRead: number;\n    timeSpentReadingMs: number;\n    writesCompleted: number;\n    writesMerged: number;\n    sectorsWritten: number;\n    timeSpentWritingMs: number;\n    IOsCurrentlyInProgress: number;\n    timeSpentDoingIOms: number;\n    weightedTimeSpentDoingIOms: number;\n};\nexport type ServerStatus = {\n    clock_ticks: number;\n    total_memoryKb: number;\n    free_memoryKb: number;\n    uptimeSeconds: number;\n    cpu_model: string;\n    cpu_cores_mhz: string;\n    cpu_mhz: string;\n    disk_space: string;\n    memAvailable: number;\n    ioInfo?: IOStats[];\n};\nexport type ConnectionStatus = {\n    queries: PG_STAT_ACTIVITY[];\n    topQueries: AnyObject[];\n    blockedQueries: AnyObject[];\n    connections: PG_STAT_DATABASE[];\n    maxConnections: number;\n    noBash: boolean;\n    getPidStatsErrors: Partial<Record<string, any>>;\n    serverStatus?: ServerStatus;\n};\nexport type SampleSchema = {\n    name: string;\n    path: string;\n} & ({\n    type: \"sql\";\n    file: string;\n} | SampleSchemaDir);\nexport type SampleSchemaDir = {\n    type: \"dir\";\n    tableConfigTs: string;\n    onMountTs: string;\n    onInitSQL: string;\n    workspaceConfig: {\n        workspaces: DBSSchema[\"workspaces\"][];\n    } | undefined;\n    connection: Pick<DBSSchema[\"connections\"], \"db_schema_filter\" | \"info\" | \"table_options\"> | undefined;\n    databaseConfig: Pick<DBSSchema[\"database_configs\"], \"table_schema_positions\" | \"table_schema_transform\"> | undefined;\n};\nexport type ProcStats = {\n    pid: number;\n    cpu: number;\n    mem: number;\n    uptime: number;\n};\nexport declare function matchObj(obj1: AnyObject | undefined, obj2: AnyObject | undefined): boolean;\nexport declare function sliceText<T extends string | undefined>(_text: T, maxLen: number, ellipseText?: string, midEllipse?: boolean): T;\nexport type ColType = {\n    table_oid: number | undefined;\n    column_name: string;\n    escaped_column_name: string;\n    data_type: string;\n    udt_name: string;\n    schema: string;\n};\nexport declare const RELOAD_NOTIFICATION = \"Prostgles UI accessible at\";\nexport declare function throttle<Params extends any[]>(func: (...args: Params) => any, timeout: number): (...args: Params) => void;\nexport declare const SPOOF_TEST_VALUE = \"trustme\";\nexport declare const getEntries: <T extends AnyObject>(obj: T) => [keyof T, T[keyof T]][];\nexport declare const fromEntries: <K extends string | number | symbol, V>(entries: readonly (readonly [K, V])[]) => Record<K, V>;\nexport declare const CONNECTION_CONFIG_SECTIONS: readonly [\"access_control\", \"backups\", \"table_config\", \"details\", \"status\", \"methods\", \"file_storage\", \"API\"];\n/**\n * Ensure that multi-line strings are indented correctly\n */\nexport declare const fixIndent: (_str: string | TemplateStringsArray) => string;\nexport declare const getConnectionPaths: ({ id, url_path, }: {\n    id: string;\n    url_path: string | null;\n}) => {\n    rest: string;\n    ws: string;\n    dashboard: string;\n    config: string;\n};\nexport declare const API_ENDPOINTS: {\n    readonly REST: \"/rest-api\";\n    readonly WS_DB: \"/ws-api-db\";\n    readonly WS_DBS: \"/ws-api-dbs\";\n};\nexport declare const ROUTES: {\n    readonly MAGIC_LINK: \"/magic-link\";\n    readonly LOGIN: \"/login\";\n    readonly LOGOUT: \"/logout\";\n    readonly ACCOUNT: \"/account\";\n    readonly CONNECTIONS: \"/connections\";\n    readonly CONFIG: \"/connection-config\";\n    readonly DOCUMENTATION: \"/documentation\";\n    readonly SERVER_SETTINGS: \"/server-settings\";\n    readonly COMPONENT_LIST: \"/component-list\";\n    readonly EDIT_CONNECTION: \"/edit-connection\";\n    readonly NEW_CONNECTION: \"/new-connection\";\n    readonly USERS: \"/users\";\n    readonly BACKUPS: \"/prostgles_backups\";\n    readonly STORAGE: \"/prostgles_storage\";\n};\nexport declare const PROSTGLES_CLOUD_URL = \"https://cloud1.prostgles.com\";\nexport declare const FORKED_PROC_ENV_NAME: \"IS_FORKED_PROC\";\nexport declare function debouncePromise<Args extends any[], T>(promiseFuncDef: (...pArgs: Args) => Promise<T>): (...args: Args) => Promise<T>;\nexport declare const getCaller: () => string[];\nexport type FileTable = {\n    original_name: string;\n};\nexport declare const getProperty: <T extends object, K extends string>(obj: T, key: K | string) => K extends keyof T ? T[K] : K extends string ? T[keyof T] | undefined : undefined;\nexport {};\n//# sourceMappingURL=utils.d.ts.map"
  },
  {
    "path": "common/utils.js",
    "content": "import { isDefined } from \"./filterUtils\";\nexport const SECOND = 1000;\nexport const MINUTE = SECOND * 60;\nexport const HOUR = MINUTE * 60;\nexport const DAY = HOUR * 24;\nexport const MONTH = DAY * 30;\nexport const YEAR = DAY * 365;\nexport const EXCLUDE_FROM_SCHEMA_WATCH = \"prostgles internal query that should be excluded from schema watch \";\nexport const STATUS_MONITOR_IGNORE_QUERY = \"prostgles-status-monitor-query\";\nexport const getAgeFromDiff = (millisecondDiff) => {\n    const roundFunc = millisecondDiff > 0 ? Math.floor : Math.ceil;\n    const years = roundFunc(millisecondDiff / YEAR);\n    const months = roundFunc((millisecondDiff % YEAR) / MONTH);\n    const days = roundFunc((millisecondDiff % MONTH) / DAY);\n    const hours = roundFunc((millisecondDiff % DAY) / HOUR);\n    const minutes = roundFunc((millisecondDiff % HOUR) / MINUTE);\n    const seconds = roundFunc((millisecondDiff % MINUTE) / SECOND);\n    const milliseconds = millisecondDiff % SECOND;\n    return { years, months, days, hours, minutes, seconds, milliseconds };\n};\nexport const getAge = (date1, date2, returnAll) => {\n    const diff = +date2 - +date1;\n    const roundFunc = diff > 0 ? Math.floor : Math.ceil;\n    const years = roundFunc(diff / YEAR);\n    const months = roundFunc(diff / MONTH);\n    const days = roundFunc(diff / DAY);\n    const hours = roundFunc(diff / HOUR);\n    const minutes = roundFunc(diff / MINUTE);\n    const seconds = roundFunc(diff / SECOND);\n    if (returnAll && returnAll === true) {\n        return getAgeFromDiff(diff);\n    }\n    if (years >= 1) {\n        return { years, months };\n    }\n    else if (months >= 1) {\n        return { months, days };\n    }\n    else if (days >= 1) {\n        return { days, hours };\n    }\n    else if (hours >= 1) {\n        return { hours, minutes };\n    }\n    else {\n        return { minutes, seconds };\n    }\n};\nexport const DESTINATIONS = [\n    { key: \"Local\", subLabel: \"Saved locally (server in address bar)\" },\n    { key: \"Cloud\", subLabel: \"Saved to Amazon S3\" },\n];\nexport function matchObj(obj1, obj2) {\n    if (obj1 && obj2) {\n        return !Object.keys(obj1).some((k) => obj1[k] !== obj2[k]);\n    }\n    return false;\n}\nexport function sliceText(_text, maxLen, ellipseText = \"...\", midEllipse = false) {\n    const text = _text;\n    if (isDefined(text) && text.length > maxLen) {\n        if (!midEllipse)\n            return `${text.slice(0, maxLen)}${ellipseText}`;\n        return `${text.slice(0, maxLen / 2)}${ellipseText}${text.slice(text.length - maxLen / 2 + 3)}`;\n    }\n    return _text;\n}\nexport const RELOAD_NOTIFICATION = \"Prostgles UI accessible at\";\nexport function throttle(func, timeout) {\n    //@ts-ignore\n    let timer;\n    let lastCallArgs;\n    const throttledFunc = (...args) => {\n        if (timer !== undefined) {\n            lastCallArgs = args;\n            return;\n        }\n        else {\n            lastCallArgs = undefined;\n        }\n        //@ts-ignore\n        timer = setTimeout(() => {\n            func(...args);\n            timer = undefined;\n            if (lastCallArgs) {\n                throttledFunc(...lastCallArgs);\n            }\n        }, timeout);\n    };\n    return throttledFunc;\n}\nexport const SPOOF_TEST_VALUE = \"trustme\";\nexport const getEntries = (obj) => Object.entries(obj);\nexport const fromEntries = (entries) => {\n    return Object.fromEntries(entries);\n};\nexport const CONNECTION_CONFIG_SECTIONS = [\n    \"access_control\",\n    \"backups\",\n    \"table_config\",\n    \"details\",\n    \"status\",\n    \"methods\",\n    \"file_storage\",\n    \"API\",\n];\n/**\n * Ensure that multi-line strings are indented correctly\n */\nexport const fixIndent = (_str) => {\n    var _a;\n    const str = typeof _str === \"string\" ? _str : ((_a = _str[0]) !== null && _a !== void 0 ? _a : \"\");\n    const lines = str.split(\"\\n\");\n    if (!lines.some((l) => l.trim()))\n        return str;\n    let minIdentOffset = lines.reduce((a, line) => {\n        if (!line.trim())\n            return a;\n        const indent = line.length - line.trimStart().length;\n        return Math.min(a !== null && a !== void 0 ? a : indent, indent);\n    }, undefined);\n    minIdentOffset = Math.max(minIdentOffset !== null && minIdentOffset !== void 0 ? minIdentOffset : 0, 0);\n    return lines\n        .map((l, i) => (i === 0 ? l : l.slice(minIdentOffset)))\n        .join(\"\\n\")\n        .trim();\n};\nexport const getConnectionPaths = ({ id, url_path, }) => {\n    return {\n        rest: `${API_ENDPOINTS.REST}/${url_path || id}`,\n        ws: `${API_ENDPOINTS.WS_DB}/${url_path || id}`,\n        dashboard: `${ROUTES.CONNECTIONS}/${id}`,\n        config: `${ROUTES.CONFIG}/${id}`,\n    };\n};\nexport const API_ENDPOINTS = {\n    REST: \"/rest-api\",\n    WS_DB: \"/ws-api-db\",\n    WS_DBS: \"/ws-api-dbs\",\n};\nexport const ROUTES = {\n    MAGIC_LINK: \"/magic-link\",\n    LOGIN: \"/login\",\n    LOGOUT: \"/logout\",\n    ACCOUNT: \"/account\",\n    CONNECTIONS: \"/connections\",\n    CONFIG: \"/connection-config\",\n    DOCUMENTATION: \"/documentation\",\n    SERVER_SETTINGS: \"/server-settings\",\n    COMPONENT_LIST: \"/component-list\",\n    EDIT_CONNECTION: \"/edit-connection\",\n    NEW_CONNECTION: \"/new-connection\",\n    USERS: \"/users\",\n    BACKUPS: \"/prostgles_backups\",\n    STORAGE: \"/prostgles_storage\",\n};\nconst testForDuplicateValues = (obj, name) => {\n    if (new Set(Object.values(obj)).size !== Object.keys(obj).length) {\n        throw new Error(`${name} must not have duplicate values: ${Object.values(obj)}`);\n    }\n};\ntestForDuplicateValues(API_ENDPOINTS, \"API_ENDPOINTS\");\ntestForDuplicateValues(ROUTES, \"ROUTES\");\nexport const PROSTGLES_CLOUD_URL = \"https://cloud1.prostgles.com\";\nexport const FORKED_PROC_ENV_NAME = \"IS_FORKED_PROC\";\nexport function debouncePromise(promiseFuncDef) {\n    let currentPromise;\n    return function (...args) {\n        // If there's no active promise, create a new one\n        if (!currentPromise) {\n            currentPromise = promiseFuncDef(...args).finally(() => {\n                currentPromise = undefined;\n            });\n            return currentPromise;\n        }\n        // Otherwise, wait for the current promise to finish, then run the new one\n        return currentPromise.then(() => promiseFuncDef(...args));\n    };\n}\nexport const getCaller = () => {\n    //@ts-ignore\n    // Error.stackTraceLimit = 30;\n    var _a, _b, _c;\n    const error = new Error();\n    const stackLines = (_b = (_a = error.stack) === null || _a === void 0 ? void 0 : _a.split(\"\\n\")) !== null && _b !== void 0 ? _b : [];\n    const callerLine = (_c = stackLines[2]) !== null && _c !== void 0 ? _c : \"\";\n    return stackLines;\n};\nexport const getProperty = (obj, key) => {\n    if (!Object.keys(obj).includes(key))\n        return undefined;\n    return obj[key];\n};\n"
  },
  {
    "path": "common/utils.ts",
    "content": "import { isDefined } from \"./filterUtils\";\nimport { DBSSchema } from \"./publishUtils\";\n\nexport const SECOND = 1000;\nexport const MINUTE = SECOND * 60;\nexport const HOUR = MINUTE * 60;\nexport const DAY = HOUR * 24;\nexport const MONTH = DAY * 30;\nexport const YEAR = DAY * 365;\n\nexport type AGE = {\n  years?: number;\n  months?: number;\n  days?: number;\n  hours?: number;\n  minutes?: number;\n  seconds?: number;\n  milliseconds?: number;\n};\n\nexport const EXCLUDE_FROM_SCHEMA_WATCH =\n  \"prostgles internal query that should be excluded from schema watch \";\nexport const STATUS_MONITOR_IGNORE_QUERY = \"prostgles-status-monitor-query\";\n\nexport const getAgeFromDiff = (millisecondDiff: number) => {\n  const roundFunc = millisecondDiff > 0 ? Math.floor : Math.ceil;\n\n  const years = roundFunc(millisecondDiff / YEAR);\n  const months = roundFunc((millisecondDiff % YEAR) / MONTH);\n  const days = roundFunc((millisecondDiff % MONTH) / DAY);\n  const hours = roundFunc((millisecondDiff % DAY) / HOUR);\n  const minutes = roundFunc((millisecondDiff % HOUR) / MINUTE);\n  const seconds = roundFunc((millisecondDiff % MINUTE) / SECOND);\n  const milliseconds = millisecondDiff % SECOND;\n\n  return { years, months, days, hours, minutes, seconds, milliseconds };\n};\nexport const getAge = <ReturnALL extends boolean = false>(\n  date1: number,\n  date2: number,\n  returnAll?: ReturnALL,\n): ReturnALL extends true ? Required<AGE> : AGE => {\n  const diff = +date2 - +date1;\n  const roundFunc = diff > 0 ? Math.floor : Math.ceil;\n  const years = roundFunc(diff / YEAR);\n  const months = roundFunc(diff / MONTH);\n  const days = roundFunc(diff / DAY);\n  const hours = roundFunc(diff / HOUR);\n  const minutes = roundFunc(diff / MINUTE);\n  const seconds = roundFunc(diff / SECOND);\n\n  if (returnAll && returnAll === true) {\n    return getAgeFromDiff(diff);\n  }\n\n  if (years >= 1) {\n    return { years, months } as any;\n  } else if (months >= 1) {\n    return { months, days } as any;\n  } else if (days >= 1) {\n    return { days, hours } as any;\n  } else if (hours >= 1) {\n    return { hours, minutes } as any;\n  } else {\n    return { minutes, seconds } as any;\n  }\n};\n\nexport const DESTINATIONS = [\n  { key: \"Local\", subLabel: \"Saved locally (server in address bar)\" },\n  { key: \"Cloud\", subLabel: \"Saved to Amazon S3\" },\n] as const;\n\nexport type DumpOpts = DBSSchema[\"backups\"][\"options\"];\n\nexport type PGDumpParams = {\n  options: DumpOpts;\n  credentialID?: DBSSchema[\"backups\"][\"credential_id\"];\n  destination: (typeof DESTINATIONS)[number][\"key\"];\n  initiator?: string;\n  name?: string;\n};\n\nexport type DeepWriteable<T> = {\n  -readonly [P in keyof T]: DeepWriteable<T[P]>;\n};\ntype AnyObject = Record<string, any>;\n\nexport type WithUndef<T extends AnyObject | undefined> =\n  T extends AnyObject ?\n    {\n      [K in keyof T]: T[K] | undefined;\n    }\n  : T;\n\nexport type GetElementType<T extends any[] | readonly any[]> =\n  T extends (infer U)[] ? U : never;\n\nexport type OmitDistributive<T, K extends keyof any> =\n  T extends any ? Omit<T, K> : never;\n\nexport type PG_STAT_ACTIVITY = {\n  datid: number | null;\n  datname: string | null;\n  pid: number;\n  usesysid: number | null;\n  usename: string | null;\n  application_name: string;\n  client_addr: string | null;\n  client_hostname: string | null;\n  client_port: number | null;\n  backend_start: string;\n  xact_start: string | null;\n  query_start: string | null;\n  state_change: string | null;\n  wait_event_type: string | null;\n  wait_event: string | null;\n  state: string | null;\n  backend_xid: any | null;\n  backend_xmin: any | null;\n  query: string;\n  backend_type: string;\n  blocked_by: number[];\n  running_time: AnyObject;\n};\n\nexport type PG_STAT_DATABASE = {\n  datid: number;\n  datname: string;\n  numbackends: number;\n  xact_commit: number;\n  xact_rollback: number;\n  blks_read: number;\n  blks_hit: number;\n  tup_returned: number;\n  tup_fetched: number;\n  tup_inserted: number;\n  tup_updated: number;\n  tup_deleted: number;\n  conflicts: number;\n  temp_files: number;\n  temp_bytes: number;\n  deadlocks: number;\n  checksum_failures: number | null;\n  checksum_last_failure: string | null;\n  blk_read_time: number;\n  blk_write_time: number;\n  stats_reset: string;\n};\n\nexport type IOStats = {\n  majorNumber: number;\n  minorNumber: number;\n  deviceName: string;\n  readsCompletedSuccessfully: number;\n  readsMerged: number;\n  sectorsRead: number;\n  timeSpentReadingMs: number;\n  writesCompleted: number;\n  writesMerged: number;\n  sectorsWritten: number;\n  timeSpentWritingMs: number;\n  IOsCurrentlyInProgress: number;\n  timeSpentDoingIOms: number;\n  weightedTimeSpentDoingIOms: number;\n};\n\nexport type ServerStatus = {\n  clock_ticks: number;\n  total_memoryKb: number;\n  free_memoryKb: number;\n  uptimeSeconds: number;\n  cpu_model: string;\n  cpu_cores_mhz: string;\n  cpu_mhz: string;\n  disk_space: string;\n  memAvailable: number;\n  ioInfo?: IOStats[];\n};\n\nexport type ConnectionStatus = {\n  queries: PG_STAT_ACTIVITY[];\n  topQueries: AnyObject[];\n  blockedQueries: AnyObject[];\n  connections: PG_STAT_DATABASE[];\n  maxConnections: number;\n  noBash: boolean;\n  getPidStatsErrors: Partial<Record<string, any>>;\n  serverStatus?: ServerStatus;\n};\n\nexport type SampleSchema = {\n  name: string;\n  path: string;\n} & (\n  | {\n      type: \"sql\";\n      file: string;\n    }\n  | SampleSchemaDir\n);\nexport type SampleSchemaDir = {\n  type: \"dir\";\n  tableConfigTs: string;\n  onMountTs: string;\n  onInitSQL: string;\n  workspaceConfig: { workspaces: DBSSchema[\"workspaces\"][] } | undefined;\n  connection:\n    | Pick<\n        DBSSchema[\"connections\"],\n        \"db_schema_filter\" | \"info\" | \"table_options\"\n      >\n    | undefined;\n  databaseConfig:\n    | Pick<\n        DBSSchema[\"database_configs\"],\n        \"table_schema_positions\" | \"table_schema_transform\"\n      >\n    | undefined;\n};\n\nexport type ProcStats = {\n  pid: number;\n  cpu: number;\n  mem: number;\n  uptime: number;\n};\n\nexport function matchObj(\n  obj1: AnyObject | undefined,\n  obj2: AnyObject | undefined,\n): boolean {\n  if (obj1 && obj2) {\n    return !Object.keys(obj1).some((k) => obj1[k] !== obj2[k]);\n  }\n  return false;\n}\n\nexport function sliceText<T extends string | undefined>(\n  _text: T,\n  maxLen: number,\n  ellipseText = \"...\",\n  midEllipse = false,\n): T {\n  const text = _text as string;\n  if (isDefined(text) && text.length > maxLen) {\n    if (!midEllipse) return `${text.slice(0, maxLen)}${ellipseText}` as T;\n    return `${text.slice(0, maxLen / 2)}${ellipseText}${text.slice(text.length - maxLen / 2 + 3)}` as T;\n  }\n\n  return _text;\n}\n\nexport type ColType = {\n  table_oid: number | undefined;\n  column_name: string;\n  escaped_column_name: string;\n  data_type: string;\n  udt_name: string;\n  schema: string;\n};\nexport const RELOAD_NOTIFICATION = \"Prostgles UI accessible at\";\n\nexport function throttle<Params extends any[]>(\n  func: (...args: Params) => any,\n  timeout: number,\n): (...args: Params) => void {\n  //@ts-ignore\n  let timer: NodeJS.Timeout | undefined;\n  let lastCallArgs: Params | undefined;\n  const throttledFunc = (...args: Params) => {\n    if (timer !== undefined) {\n      lastCallArgs = args;\n      return;\n    } else {\n      lastCallArgs = undefined;\n    }\n    //@ts-ignore\n    timer = setTimeout(() => {\n      func(...args);\n      timer = undefined;\n      if (lastCallArgs) {\n        throttledFunc(...lastCallArgs);\n      }\n    }, timeout);\n  };\n  return throttledFunc;\n}\n\nexport const SPOOF_TEST_VALUE = \"trustme\";\n\nexport const getEntries = <T extends AnyObject>(obj: T) =>\n  Object.entries(obj) as [keyof T, T[keyof T]][];\n\nexport const fromEntries = <K extends string | number | symbol, V>(\n  entries: readonly (readonly [K, V])[],\n): Record<K, V> => {\n  return Object.fromEntries(entries) as Record<K, V>;\n};\nexport const CONNECTION_CONFIG_SECTIONS = [\n  \"access_control\",\n  \"backups\",\n  \"table_config\",\n  \"details\",\n  \"status\",\n  \"methods\",\n  \"file_storage\",\n  \"API\",\n] as const;\n\n/**\n * Ensure that multi-line strings are indented correctly\n */\nexport const fixIndent = (_str: string | TemplateStringsArray): string => {\n  const str = typeof _str === \"string\" ? _str : (_str[0] ?? \"\");\n  const lines = str.split(\"\\n\");\n  if (!lines.some((l) => l.trim())) return str;\n  let minIdentOffset = lines.reduce(\n    (a, line) => {\n      if (!line.trim()) return a;\n      const indent = line.length - line.trimStart().length;\n      return Math.min(a ?? indent, indent);\n    },\n    undefined as number | undefined,\n  );\n  minIdentOffset = Math.max(minIdentOffset ?? 0, 0);\n\n  return lines\n    .map((l, i) => (i === 0 ? l : l.slice(minIdentOffset)))\n    .join(\"\\n\")\n    .trim();\n};\n\nexport const getConnectionPaths = ({\n  id,\n  url_path,\n}: {\n  id: string;\n  url_path: string | null;\n}) => {\n  return {\n    rest: `${API_ENDPOINTS.REST}/${url_path || id}`,\n    ws: `${API_ENDPOINTS.WS_DB}/${url_path || id}`,\n    dashboard: `${ROUTES.CONNECTIONS}/${id}`,\n    config: `${ROUTES.CONFIG}/${id}`,\n  };\n};\n\nexport const API_ENDPOINTS = {\n  REST: \"/rest-api\",\n  WS_DB: \"/ws-api-db\",\n  WS_DBS: \"/ws-api-dbs\",\n} as const;\n\nexport const ROUTES = {\n  MAGIC_LINK: \"/magic-link\",\n  LOGIN: \"/login\",\n  LOGOUT: \"/logout\",\n  ACCOUNT: \"/account\",\n  CONNECTIONS: \"/connections\",\n  CONFIG: \"/connection-config\",\n  DOCUMENTATION: \"/documentation\",\n  SERVER_SETTINGS: \"/server-settings\",\n  COMPONENT_LIST: \"/component-list\",\n  EDIT_CONNECTION: \"/edit-connection\",\n  NEW_CONNECTION: \"/new-connection\",\n  USERS: \"/users\",\n  BACKUPS: \"/prostgles_backups\",\n  STORAGE: \"/prostgles_storage\",\n} as const;\n\nconst testForDuplicateValues = <T extends AnyObject>(obj: T, name: string) => {\n  if (new Set(Object.values(obj)).size !== Object.keys(obj).length) {\n    throw new Error(\n      `${name} must not have duplicate values: ${Object.values(obj)}`,\n    );\n  }\n};\ntestForDuplicateValues(API_ENDPOINTS, \"API_ENDPOINTS\");\ntestForDuplicateValues(ROUTES, \"ROUTES\");\n\nexport const PROSTGLES_CLOUD_URL = \"https://cloud1.prostgles.com\";\n\nexport const FORKED_PROC_ENV_NAME = \"IS_FORKED_PROC\" as const;\n\nexport function debouncePromise<Args extends any[], T>(\n  promiseFuncDef: (...pArgs: Args) => Promise<T>,\n): (...args: Args) => Promise<T> {\n  let currentPromise: Promise<any> | undefined;\n\n  return function (...args: Args): Promise<T> {\n    // If there's no active promise, create a new one\n    if (!currentPromise) {\n      currentPromise = promiseFuncDef(...args).finally(() => {\n        currentPromise = undefined;\n      });\n      return currentPromise;\n    }\n\n    // Otherwise, wait for the current promise to finish, then run the new one\n    return currentPromise.then(() => promiseFuncDef(...args));\n  };\n}\n\nexport const getCaller = () => {\n  //@ts-ignore\n  // Error.stackTraceLimit = 30;\n\n  const error = new Error();\n  const stackLines = error.stack?.split(\"\\n\") ?? [];\n  const callerLine = stackLines[2] ?? \"\";\n  return stackLines;\n};\n\n//TODO: add file table column info to prostgles-types\nexport type FileTable = {\n  original_name: string;\n};\n\nexport const getProperty = <T extends object, K extends string>(\n  obj: T,\n  key: K | string,\n): K extends keyof T ? T[K]\n: K extends string ? T[keyof T] | undefined\n: undefined => {\n  if (!Object.keys(obj).includes(key))\n    return undefined as K extends keyof T ? T[K] : undefined;\n  return obj[key as keyof T] as K extends keyof T ? T[K] : undefined;\n};\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "name: prostgles\n\n# Define reusable blocks\nx-ui-environment: &ui-environment\n  POSTGRES_HOST: db\n  POSTGRES_DB: ${POSTGRES_DB}\n  POSTGRES_USER: ${POSTGRES_USER}\n  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n  PROSTGLES_UI_HOST: \"0.0.0.0\"\n\nx-ui-base: &ui-base\n  image: prostgles/ui:v2.2.2\n  restart: unless-stopped\n  build:\n    context: .\n    dockerfile: Dockerfile\n  environment:\n    <<: *ui-environment\n  depends_on:\n    db:\n      condition: service_healthy\n  command: node /usr/src/app/server/dist/server/src/index.js\n\nservices:\n  db:\n    image: prostgles/ui-db:v2.2.2\n    container_name: prostgles-ui-db\n    restart: unless-stopped\n    build:\n      context: .\n      dockerfile: DB-Dockerfile \n    environment:\n      POSTGRES_DB: ${POSTGRES_DB}\n      POSTGRES_USER: ${POSTGRES_USER}\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n    command: >\n      postgres \n      -c shared_preload_libraries=pg_stat_statements  \n      -c max_connections=200\n    volumes:\n      - db:/var/lib/postgresql/data\n    healthcheck:\n      test:\n        [\n          \"CMD-SHELL\",\n          \"sleep 1 && pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}\",\n        ]\n      timeout: 3s\n      retries: 5\n\n  # Default UI service (default profile)\n  ui:\n    profiles: [\"default\", \"\"]\n    <<: *ui-base\n    container_name: prostgles-ui-default\n    ports:\n      - \"${PRGL_DOCKER_IP:-127.0.0.1}:${PRGL_DOCKER_PORT:-3004}:3004\"\n\n  # Debug profile - extends base with Node.js inspector\n  ui-debug:\n    profiles: [\"debug\"]\n    <<: *ui-base\n    container_name: prostgles-ui-debug\n    command: node --inspect=0.0.0.0:9229 /usr/src/app/server/dist/server/src/index.js\n    ports:\n      - \"${PRGL_DOCKER_IP:-127.0.0.1}:${PRGL_DOCKER_PORT:-3004}:3004\"\n      - \"${PRGL_DOCKER_IP:-127.0.0.1}:9229:9229\"\n\n  # Docker MCP profile - extends base with Docker capabilities\n  ui-docker-mcp:\n    profiles: [\"docker-mcp\"]\n    <<: *ui-base\n    container_name: prostgles-ui-docker-mcp\n    environment:\n      <<: *ui-environment\n      DOCKER_HOST: unix:///var/run/docker.sock\n    ports:\n      - \"${PRGL_DOCKER_IP:-127.0.0.1}:${PRGL_DOCKER_PORT:-3004}:3004\"\n      - \"${PRGL_DOCKER_IP:-127.0.0.1}:3009:3009\"\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - /usr/bin/docker:/usr/bin/docker:ro\n\nvolumes:\n  db:\n# Used for docker mcp\nnetworks:\n  default:\n    name: prostgles-bridge-net\n    driver: bridge"
  },
  {
    "path": "docs/01_Overview.md",
    "content": "<h1 id=\"overview\"> Overview </h1> \n\nProstgles UI is a user-friendly way for interacting with PostgreSQL, creating dashboards and internal tools.\n\n<img src=\"./screenshots/overview.svgif.svg\" alt=\"Prostgles UI Overview\" width=\"100%\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n## Features\n- SQL Editor with syntax highlighting and auto-completion\n- Real-time dashboards with charts\n- AI assistant with MCP support\n- User authentication (email, third-party OAuth and two-factor authentication)\n- Role-based access control\n- Database management\n- File storage and backups (locally or to AWS S3 compatible storage)\n- TypeScript API with database schema types and end to end type safety\n- LISTEN NOTIFY support\n- Mobile friendly\n\nIt comes in two versions: \n- **Prostgles UI** - a web-based application with the complete feature set accessible through any modern browser.\n- **Prostgles Desktop** - a native desktop application based on Electron available for Linux, MacOS and Windows. \nIt has a subset of the core features from Prostgles UI for data exploration and database management. \nUser Management and other multi-user focused features are not available in the desktop version.\n\n\n"
  },
  {
    "path": "docs/02_Installation.md",
    "content": "<h1 id=\"installation\"> Installation </h1> \n\nThe quickest way to start is with Docker. \nIf you don't have Docker installed, please follow the official \n<a href=\"https://docs.docker.com/engine/install/\" target=\"_blank\">Docker installation guide</a>\n\nDownload the source code:\n\n```bash\ngit clone https://github.com/prostgles/ui.git\ncd ui\n```\n\nStart the application:\n\n```docker-compose.sh\ndocker compose up -d --build\n``` \n\nOnce running, Prostgles UI will be available at [localhost:3004](http://localhost:3004)\n\n### Initial Setup & Authentication\n\nWhen first launching Prostgles UI, an admin user will be created automatically:\n- If `PRGL_USERNAME` and `PRGL_PASSWORD` environment variables are provided, an admin user is created with these credentials. \n- Otherwise, a passwordless admin user is created. \nIt gets assigned to the first client that accesses the app. \nSubsequent clients accessing the app will be rejected with an appropriate error message detailing that the passwordless admin user has already been assigned.\n\nTo setup multiple users, the passwordless admin user must be converted to a normal admin account by setting up a password.\nThis will allow accessing /users page where you can manage users.\n\nUsers login using their username and password. Two-factor authentication is provided through TOTP (Time-based One-Time Password) and can be enabled in the account section.\n\nEmail and third-party (OAuth) authentication can be configured in Server Settings section. It allows users to register and log in using their email address or third-party accounts like Google, GitHub, etc.\n\n\n"
  },
  {
    "path": "docs/03_Installation_(Desktop_Version).md",
    "content": "<h1 id=\"installation_(desktop_version)\"> Installation (Desktop Version) </h1> \n\nTo get started with Prostgles Desktop, download and install the binary file that's appropriate for your operating system (Windows, macOS, or Linux) from [our website](/download).\n\n- **Linux**: We provide **.deb**, **.rpm** or **.AppImage** files to suit your distribution,\n- **macOS**: Open the downloaded **.dmg** file, drag the Prostgles Desktop icon into your Applications folder, and launch the application.\n- **Windows** - Run the downloaded **.exe** file and follow the on-screen instructions to complete the installation.\n\nAlternatively, you can visit the [releases page](https://github.com/prostgles/ui/releases) for checksums, release notes or older versions.\n\n## Setting up\n\nWhen you open Prostgles Desktop, you see the Welcome screen while it loads.\nYou'll need to complete two initial setup steps:\n\n1. Accept the privacy policy\n2. Connect to a state database\n\n#### State database\n\nProstgles Desktop stores its state and configuration data in a PostgreSQL database.\nTo maintain a secure and responsive environment, we highly recommend [installing Postgres](https://www.postgresql.org/download/) on your local machine.\nYou will need to create a dedicated database and superuser account with a strong password for Prostgles Desktop.\n\n<img src=\"./screenshots/electron_setup.svgif.svg\" alt=\"Electron Setup\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n\n"
  },
  {
    "path": "docs/04_Navigation_bar.md",
    "content": "<h1 id=\"navigation_bar\"> Navigation bar </h1> \n\nThe top navigation bar provides quick access to all major sections of Prostgles UI. \nLocated at the top of the interface, it allows you to switch between database connections, manage users and server settings, and access your account preferences. \nThe navigation adapts to your user role, showing admin-only sections like Users and Server Settings only to authorized users.\n\n## Key sections of the app\n\n### Connections\n\nA connection represents a unique postgres database instance (unique host, port, user and database name).\nThe connection list page shows all available connections you can access based on your user permissions.\n\n### Connection dashboard\n\nClicking a connection from the connection list will take you to the dashboard page where you can explore and interact with the database.\nAvailable views and tools include SQL Editor, Table, Map, Schema Diagram, AI Assistant, and more.\n\n### Dashboard workspaces\n\nThe views you open in the dashboard are saved automatically to the current workspace.\nThis allows you to return to the same views later, even after closing the application.\nYou can create multiple workspaces to organize your views by project, team, task, or any other criteria.\n\n\n<img src=\"./screenshots/navbar.svgif.svg\" alt=\"Navigation\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n### Navbar items\n- **Connections**: Manage database connections  \n- **Users**: Manage user accounts (admin only)  \n- **Server Settings**: Configure server settings (admin only)  \n- **Account**: Manage your account  \n- **Logout**: Logout of your account  \n- **Theme Selector**: Switch between light and dark themes  \n- **Language Selector**: Change the interface language  \n- **Toggle Menu (visible on small screens)**: Toggle the mobile navigation menu on smaller screens  \n\n"
  },
  {
    "path": "docs/05_Connections.md",
    "content": "<h1 id=\"connections\"> Connections </h1> \n\nThe Connections page serves as the central hub within Prostgles UI for managing all your PostgreSQL database connections. \nFrom here, you can add and open connections, modify existing ones, and gain an immediate overview of their status and associated workspaces. \n\n<img src=\"./screenshots/connections.svg\" alt=\"Connections page screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n### Connection controls\n- <a href=\"#new_connection\">New connection</a>: Opens the form to add a new database connection.  \n- **Display options**: Customize how the list of connections is displayed (e.g., show/hide state database, show database names).  \n- <a href=\"#connection_list\">Connection list</a>: Controls to open and manage your database connections.  \n\n<h1 id=\"new_connection\"> Adding a connection </h1> \n\nUse the **New Connection** button to add a new database connection.\n\nThis will open a form where you can enter the connection details such as host, port, database name, user, and password.\n\n<img src=\"./screenshots/new_connection.svgif.svg\" alt=\"New connection form screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n### New connection form fields\n  - **PostgreSQL Installation Instructions**: Instructions for installing PostgreSQL on your system.  \n  - **Connection Name**: The name of the connection.  \n  - **Connection Type**: Allows you change the connection details format: standard or connection string.  \n  - **Connection String**: The connection string for the database.   \n  - **Database Host**: The hostname or IP address of the database server.  \n  - **Database Port**: The port number on which the database server is listening.  \n  - **Database User**: The username used to connect to the database.  \n  - **Database Password**: The password for the database user.  \n  - **Database Name**: The name of the database to connect to.  \n  - **More Options**: Additional connection options.  \n    - **Schema Filter**: Controls which schemas are visible in the dashboard (public by default).  \n    - **Connection Timeout**: The maximum time to wait for a connection to the database before timing out.  \n    - **SSL Mode**: Configure SSL settings for the connection.  \n    - **Watch Schema**: Enabled by default. Enables schema change tracking. Any changes made to the database schema are reflected in the API and UI.  \n    - **Enable Realtime**: Enabled by default. Enables realtime data change tracking for tables and views. Requires trigger permissions to the underlying tables.  \n  - **Update Connection**: Save the changes made to the connection.  \n\n<h2 id=\"connection_list\"> Connection list </h2> \n\nThe connection list displays all your database connections grouped by database host, port and user.\n\n<img src=\"./screenshots/connections.svg\" alt=\"Connections list screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n### Connection actions\n  - **Open connection**: Opens the selected connection dashboard on the default workspace.  \n  - **Add new database**: Adds a new connection to the selected server.   \n    - <a href=\"#create_new_database\">Create new database</a>: Create a new database within the server.  \n    - <a href=\"#connect_to_an_existing_database\">Connect to an existing database</a>: Selects a database from the server to connect to.   \n  - **Debug: Close All Windows**: Force-closes all windows/tabs for this connection. Use if the workspace becomes unresponsive or encounters a bug.  \n  - **Status monitor**: View real-time statistics, running queries, and system resource usage (CPU, RAM, Disk) for this connection.  \n  - **Connection configuration**: Access and modify settings for this connection, such as access control, file storage, backup/restore options, and server-side functions.  \n  - <a href=\"#edit_connection_details\">Edit connection details</a>: Modify the connection parameters (e.g., display name, database details like host and port). Also allows deleting or cloning the connection.  \n  - **Status indicator/Disconnect**: Shows the current connection status (green indicates connected). Click to disconnect from the database.  \n  - **Workspaces**: List of workspaces associated with this connection. Click to switch to a specific workspace.  \n\n<h4 id=\"create_new_database\"> Create new database </h4> \n\nAllows you to create a new database in the selected server.\nIt will use the first connection details from the group connection.\nIf no adequate account is found (no superuser or rolcreatedb), it will be greyed out with with an appropriate explanation tooltip text.\n\n### New database options\n  - **Database Name**: The name of the new database.  \n  - **Sample Schemas**: Select a sample schema to create the database with.  \n  - **Create database owner**: If checked, a new owner will be created for the database. Useful for ensuring the database is owned by a non-superuser account.  \n  - **New Owner Name**: The name of the new owner.  \n  - **New Owner Password**: The password of the new owner.  \n  - **New Owner Permission Type**: Apart from Owner it is possible to create a user with reduced permission types (SELECT/UPDATE/DELETE/INSERT).  \n  - **Create and connect**: Creates and connects to the new database.  \n\n<h4 id=\"connect_to_an_existing_database\"> Connect to an existing database </h4> \n\nAllows you to connect to an existing database in the selected server.\nIt will use the first connection details from the group connection. \n\n<img src=\"./screenshots/connect_existing_database.svg\" alt=\"Connect existing database popup screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\nAfter selecting the database, you can choose to create a new owner or user for the connection should you need to.\n\n  - **Select Database**: Choose a database from the server.  \n  - **Create database owner**: If checked, a new owner will be created for the database. Useful for ensuring the database is owned by a non-superuser account.  \n  - **New Owner Name**: The name of the new owner.  \n  - **New Owner Password**: The password of the new owner.  \n  - **New Owner Permission Type**: Apart from Owner it is possible to create a user with reduced permission types (SELECT/UPDATE/DELETE/INSERT).  \n  - **Save and connect**: Connects to the selected database with the new owner.  \n\n<h3 id=\"edit_connection_details\"> Edit connection details </h3> \n\nConnection settings, credentials, and other parameters to ensure your connection is configured correctly.\n\n  - **PostgreSQL Installation Instructions**: Instructions for installing PostgreSQL on your system.  \n  - **Connection Name**: The name of the connection.  \n  - **Connection Type**: Allows you change the connection details format: standard or connection string.  \n  - **Connection String**: The connection string for the database.   \n  - **Database Host**: The hostname or IP address of the database server.  \n  - **Database Port**: The port number on which the database server is listening.  \n  - **Database User**: The username used to connect to the database.  \n  - **Database Password**: The password for the database user.  \n  - **Database Name**: The name of the database to connect to.  \n  - **More Options**: Additional connection options.  \n    - **Schema Filter**: Controls which schemas are visible in the dashboard (public by default).  \n    - **Connection Timeout**: The maximum time to wait for a connection to the database before timing out.  \n    - **SSL Mode**: Configure SSL settings for the connection.  \n    - **Watch Schema**: Enabled by default. Enables schema change tracking. Any changes made to the database schema are reflected in the API and UI.  \n    - **Enable Realtime**: Enabled by default. Enables realtime data change tracking for tables and views. Requires trigger permissions to the underlying tables.  \n  - **Update Connection**: Save the changes made to the connection.  \n\n"
  },
  {
    "path": "docs/06_Connection_dashboard.md",
    "content": "<h1 id=\"connection_dashboard\"> Connection dashboard </h1> \n\nThe connection dashboard is your command center for exploring and managing your Postgres database. \nOpen tables, run SQL, visualize schema relationships, switch workspaces, and launch tools—all in one flexible, customizable workspace.\nWith quick search, saved queries, AI-powered assistance, and instant access to every database object, the dashboard gives you a fast, intuitive way to navigate your data and build the tools you need.\n\n## Features\n\n- **Unified workspace**: View tables, SQL editors, charts, and tools together in a flexible layout. Save and switch between different layouts and sets of opened views for different tasks or projects.\n- **AI Assistant**: Generate SQL, explore data, and get help directly within the dashboard.\n- **Flexible layout**: Drag, resize, and arrange views in a tiled layout to create a workspace that fits your needs.\n- **Global search**: Search across all tables, views, and functions in a single, fast search bar.\n- **Schema diagram**: Visualize relationships between tables and schemas to better understand your database structure.\n- **Import data**: Easily import data from CSV/JSON files into your database tables.\n\n<img src=\"./screenshots/dashboard.svgif.svg\" alt=\"Connection dashboard\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n## Components\n\n### Dashboard elements\n- <a href=\"#dashboard_menu\">Dashboard menu</a>: Allows opening tables and views, schema diagram, importing files, managing saved queries, and accessing dashboard settings.  \n- **Dashboard menu toggle**: Opens or closes the dashboard menu unless the menu is pinned.  \n- **Go to configuration**: Opens the configuration page for the selected connection.  \n- **Change connection**: Switch to a different database connection.  \n- **Workspaces**: List of available workspaces for the selected connection. Each workspace stores opened views and their layout.  \n- <a href=\"#workspaces_menu\">Workspaces menu</a>: Opens the workspaces menu, allowing you to create, manage, and switch between workspaces.  \n- <a href=\"#workspace_area\">Workspace area</a>: Main content area of the dashboard, where the tables, views, SQL editors and other visualisations are displayed.  \n- <a href=\"#ai_assistant\">AI Assistant</a>: Opens an AI assistant to help generate SQL queries, understand database schema, or perform other tasks.  \n- **Feedback**: Opens the feedback form, allowing you to provide feedback about the application.  \n- **Go to Connections**: Opens the connections list page.  \n\n<h2 id=\"dashboard_menu\"> Dashboard menu </h2> \n\nThe dashboard menu is the entry point in exploring your database.\nThe layout adapts to the screen size by pinning the menu to keep it open when there is enough space. \nFor wider screens the centered layout mode can be enabled through the settings.\n\n  - **Open an SQL editor**: Opens an SQL editor view in the workspace area.  \n  - **Quick search**: Opens the quick search menu for searching across all available tables and views from the current database.  \n  - **Settings**: Opens the settings menu for configuring dashboard layout preferences.  \n  - **Pin/Unpin menu**: Toggles the pinning of the dashboard menu. Pinned menus remain open until unpinned or accessing from a low width screen.  \n  - **Resize menu**: Allows resizing the dashboard menu. Drag to adjust the width of the menu.  \n  - **Resize centered layout**: Allows resizing the workspace area when centered layout is enabled. Drag to adjust the width of the centered layout.  \n  - **Saved queries**: List of saved queries of the current user from the current workspace. Click to open a saved query.  \n  - **Tables and views**: List of tables and views from the current database. Click to open a table or view. By default only the tables from the public schema are shown. Schema list from the connection settings controls which schemas are shown.  \n  - **Server-side functions**: List of server-side functions for the current database. Click to open a function.  \n  - <a href=\"#create/import\">Create/Import</a>: Opens the menu for creating new tables, server-side functions or importing csv/json files.  \n  - <a href=\"#schema_diagram\">Schema diagram</a>: Opens the schema diagram for visualizing the relationships between tables in the current database.  \n\n<h3 id=\"create/import\"> Create/Import </h3> \n\nCreate new tables, server-side functions or import files into the current database.\n\n  - **Create new table**: Opens the form to create a new table in the current database.  \n  - <a href=\"#import_file\">Import file</a>: Opens the form to import a file into the current database.  \n  - **Create TS Function**: Opens the form to create a new server-side TypeScript function for the current database.  \n\n"
  },
  {
    "path": "docs/07_Import_file.md",
    "content": "<h1 id=\"import_file\"> Import file </h1> \n\nImport files into the current database. Supported file types include CSV, GeoJSON, and JSON.\nThe import process allows you to specify the table name, infer column data types, and choose how to insert JSON/GeoJSON data into the table.  \n\n<img src=\"./screenshots/file_importer.svgif.svg\" alt=\"File Importer screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n### Import file options\n  - **Import file**: Input field for selecting a file to import. Supported types: csv/geojson/json.  \n  - **Table name**: New/existing table name into which data is to be imported.  \n  - **Try to infer and apply column data types**: Checkbox for inferring and applying column data types during import. If checked, the system will attempt to determine the appropriate data types for each column based on the imported file. If unchecked, TEXT data type will be used for all columns.  \n  - **Drop table if exists**: Checkbox for dropping the table if it already exists in the database. If checked, the existing table will be deleted before importing the new file.  \n  - **Insert as**: Select list for choosing the method of inserting JSON/GeoJSON data into the table. Options include: Single text value, JSONB rows, and Properties with geometry.  \n  - **Import**: Button to initiate the import process. Click to start importing the selected file into the specified table.  \n\n"
  },
  {
    "path": "docs/08_Schema_diagram.md",
    "content": "<h1 id=\"schema_diagram\"> Schema diagram </h1> \n\nExplore your database structure visually through the schema diagram. This tool lets you:\n- **Select schemas** — Choose one or multiple schemas to display\n- **Navigate freely** — Pan and zoom to focus on specific areas\n- **View table relationships** — See how tables connect through foreign keys\n- **Filter your view** — Show or hide tables and columns by relationship type\n- **Color links by root table** — Trace relationships back to their source at a glance. Links inherit the color of the table that defines the relationship (e.g., all user_id foreign keys match the users table color)\n- **Reset the layout** — Return to the default view which auto-arranges tables ensuring the most linked tables are central\n\nIt allows you to explore the schema structure, view table relationships, and manage the layout of the schema diagram.\nYou can pan and zoom the diagram, select schemas, filter tables and columns based on their relationship types, reset the layout.\nLink color modes allow you to better understand related tables and foreign key properties.\n\n<img src=\"./screenshots/schema_diagram.svgif.svg\" alt=\"Schema diagram screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n## Controls\n\n### Top controls\n  - **Table relationship filter**: Display tables based on their relationship type. Options include: all, linked (with relationships), orphaned (without relationships).  \n  - **Column relationship filter**: Display columns based on their relationship type. Options include: all, references (with relationships), none (no columns/only table names will be shown).  \n  - **Link colour mode**: Colour links by: default (fixed colour), root table (the colour of the table the relationship tree originates from), on-delete/on-update (colour based on constraint referential action).  \n  - **Reset layout**: Moving tables is persisted the state database. Clicking this resets the schema diagram layout to its initial state.  \n  - **Close schema diagram**: Closes the schema diagram and returns to the dashboard menu.  \n\n<h2 id=\"workspaces_menu\"> Workspaces menu </h2> \n\nWorkspaces are a powerful feature that allows you to organize your work within a connection.\nThe opened views and their layout is saved to the workspace, so you can switch between different sets of data and configurations without losing your progress.\n\nThe workspaces menu provides access to all available workspaces for the selected connection. You can create new workspaces, switch between existing ones, and manage workspace settings.\nEach workspace allows you to work with a separate set of data and configurations, making it easier to organize your work and collaborate with others.\nThe menu also includes options to clone existing workspaces and delete them if they are no longer needed.\n\n  - **Workspaces**: List of available workspaces. Click to switch to a different workspace.  \n    - **Delete workspace**: Opens the delete workspace confirmation dialog  \n      - **Delete workspace**: Confirms the deletion of the selected workspace.  \n    - **Clone workspace**: Creates a copy of the selected workspace with a new name.  \n    - **Workspace settings**: Opens the settings for the selected workspace, allowing you to manage its properties.  \n  - **Create new workspace**: Opens the form to create a new workspace for the selected connection.  \n    - **Workspace name**: Name of the new workspace.  \n    - **Create workspace**: Create and switch to the new workspace with the specified name.  \n  - **Toggle layout mode**: Switches between fixed and editable layout modes for the current workspace. Fixed mode locks the layout, preventing it from being changed by the user.  \n\n<h2 id=\"workspace_area\"> Workspace area </h2> \n\nThe workspace area is the main place for interacting with your data. \nIt includes the SQL editor, data tables, maps, and timecharts, allowing you to execute queries, visualize data, and manage database objects.\n\n  - <a href=\"#sql_editor\">SQL editor</a>: The SQL editor allows users to write and execute SQL queries against the selected database. It provides a user-friendly interface for interacting with the database.  \n  - <a href=\"#table_view\">Table view</a>: Allows interacting with a table/view from the database.  \n  - <a href=\"#map_view\">Map view</a>: Displays a map visualization based on the Table/SQL query results.  \n  - <a href=\"#timechart_view\">Timechart view</a>: Displays a timechart based on the Table/SQL query results.  \n\n"
  },
  {
    "path": "docs/09_SQL_editor.md",
    "content": "<h1 id=\"sql_editor\"> SQL editor </h1> \n\nThe SQL editor is a powerful tool for executing SQL queries against your PostgreSQL database. \n\n### Core Features\n- **Intelligent auto-completion** with context-aware suggestions based on your schema and data with JSONB property access support\n- **Rich suggestion details** with related objects, usage examples and documentation extracts, reducing the need to switch context\n- **Execute current statement** functionality to run only the SQL statement where the cursor is located\n- **Charting options** to visualize query results as timecharts or maps directly from the editor\n- **Multiple result display modes** including table, JSON, and CSV formats\n\nTo make it easier working with multiple queries, the default query execution behaviour is to execute the current statement.\nIt is highlighted by the blue vertical line to the left of the code. Press <kbd>Alt+E</kbd> or <kbd>Ctrl+Enter</kbd> or <kbd>F5</kbd> to execute it.\n\nThe editor is based on [Monaco Editor](https://microsoft.github.io/monaco-editor/), which powers VS Code. \nIt supports multi-cursor editing, find and replace, and many other advanced editing features.\n\nRealtime query resource usage can be enabled in the settings to monitor CPU and memory consumption.\n\n<img src=\"./screenshots/sql_editor.svgif.svg\" alt=\"SQL editor screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n## Components:\n\n  - **Header section**: Contains menu button, title and window minimise/fullscreen controls.  \n    - **Quick actions**: Quick actions for the view, providing easy access to charting and joins.  \n      - **Add timechart**: Adds a timechart visualization based on the current SQL statement. Visible only when the last executed statement returned at least one timestamp column.  \n      - **Add map**: Adds a map visualization based on the current SQL statement. Visible only when the last executed statement returned at least one geometry/geography column (postgis extension must be enabled).  \n    - **View title. Drag to re-arrange layout**: SQL editor query name, editable in the menu.  \n    - **Collapse the view**: Collapses the view, minimizing it to temporarily save space on the dashboard.   \n    - **Fullscreen**: Expands the view to fill the entire screen.  \n    - **Remove view**: Removes the SQL editor from the dashboard. If there are unsaved changes, a confirmation dialog will appear.  \n    - <a href=\"#sql_editor_menu\">SQL editor menu</a>: The SQL editor menu provides access to various options and settings for the SQL editor.  \n  - **SQL editor component**: The main component for the SQL editor. It contains the SQL editor and statement action buttons. Being bsed on Monaco editor, it supports syntax highlighting, auto-completion and other editing functionality like multi-cursor editing.  \n    - **SQL editor input**: The input field for writing SQL queries. It supports syntax highlighting and auto-completion.  \n    - **Execute current statement**: Executes the current SQL statement highlighted by the blue vertical line to the left of the code. This button is only visible when the cursor is within a statement.  \n    - **Add timechart**: Adds a timechart visualization based on the current SQL statement. This button is only visible when the cursor is within a statement that returns at least one timestamp column.  \n    - **Add map**: Adds a map visualization based on the current SQL statement. This button is only visible when the cursor is within a statement that returns at least one geometry/geography column (postgis extension must be enabled).  \n  - **SQL editor toolbar**: The toolbar provides various options for executing and managing SQL queries.  \n    - **Run query**: Executes the current SQL query. The result will be displayed in the query results section.  \n    - **Limit**: Sets the maximum number of rows to return in the query results. This is useful for limiting the amount of data returned, especially for large datasets.  \n    - **Cancel query**: Cancels the currently running query. This button is only visible when a query is running.  \n    - **Terminate query**: Forcefully terminates the currently running query. This is more aggressive than cancel and is only visible when a query is running.  \n    - **Stop LISTEN**: Stops the active LISTEN operation. This button is only visible when a LISTEN query is active.  \n    - **Row count**: Displays the number of rows fetched by the query and the total number of rows if available.  \n    - **Toggle table visibility**: Shows or hides the results table for the executed query.  \n    - **Toggle code editor**: Shows or hides the SQL code editor, allowing users to focus on query results when needed.  \n    - **Toggle notices**: Shows or hides database notices. When enabled, it displays notifications from the database system.  \n    - **Query duration**: Displays the execution time of the last completed query or the current running time for an active query.  \n    - **Copy results**: Copy/download query results as: CSV, TSV, JSON, Typescript definition, SQL SELECT INTO  \n    - **SQL error**: Displays any errors that occurred during the execution of the SQL query. This is useful for debugging and correcting SQL syntax.  \n  - **Query results**: Displays the results of the executed SQL query. Results can be displayed as a table (default), JSON or CSV. Users can interact with the results, such as sorting and filtering.  \n    - **Results table**: The results table displays the data returned by the executed SQL query. It supports sorting, filtering, and pagination.  \n    - **Chart**: Timechart/Map visualization of the SQL query results.  \n\n<h4 id=\"sql_editor_menu\"> SQL editor menu </h4> \n\nThe SQL editor menu provides access to various options and settings for the SQL editor.\n\n  - **General**: General settings for the SQL editor.  \n    - **Query name**: The name of the current SQL query. This is used for saving and managing queries.  \n    - **Result display mode**: The mode in which the results of the SQL query will be displayed. Options include table, JSON, and CSV.  \n    - **Save query as file**: Saves the current SQL query to a file. This can also be accomplished by pressing Ctrl+S.  \n    - **Open SQL file**: This allows loading the contents of an SQL file into the current query.  \n    - **Delete query**: Deletes the current SQL query. If it has contents a confirmation dialog will appear.  \n  - **Editor options**: Settings for the SQL editor's appearance and behavior.  \n    - **Editor settings**: Settings for the SQL editor's appearance and behavior. This includes font size, theme, and other preferences.  \n  - **Hotkeys**: Keyboard shortcuts for common actions in the SQL editor. This includes executing queries, saving files, and more.  \n\n"
  },
  {
    "path": "docs/10_Table_view.md",
    "content": "<h1 id=\"table_view\"> Table view </h1> \n\nThe table view allows you to explore, filter, and edit your Postgres data with ease.\nInstantly sort and search, build computed columns, pull in linked data through automatic joins, and create charts, maps, and cross-filtered views in a couple of clicks.\nWith smart forms for row editing, rich column controls, and deep schema-aware features, the table view turns your database into an interactive workspace for analysis, tooling, and rapid iteration.\n\n\n## Features\n\n- **Smart filtering**: Use the smart filter bar to quickly filter your data based on column types and values.\n- **Computed columns**: Add calculations or transformations of existing data.\n- **Automatic joins**: Show related data from linked tables with automatic joins and summarise if needed.\n- **Charts and maps**: Timechart and map visualisations with multi-layer support.\n- **Cross-filtered views**: Create additional table or chart views that are cross-filtered by the current table.\n- **Smart forms**: Edit rows using smart forms that adapt to your schema and data types.\n- **Conditional styling**: Style your columns based on row data.\n\n<img src=\"./screenshots/table.svgif.svg\" alt=\"Table view screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n## Components\n\n  - **Header section**: Contains menu button, title and window minimise/fullscreen controls.  \n    - **Quick actions**: Quick actions for the view, providing easy access to charting and joins.  \n      - **Toggle filter bar**: Shows or hides the filter bar, allowing users to filter the data displayed in the table.  \n      - **Add cross-filtered table**: Adds a new table view that is cross-filtered by the current table. This allows you to explore related data in a new table view.  \n      - **Add timechart**: Adds a timechart visualization based on the current table. Visible only when the current table (or any linked table) has a timestamp/date column.  \n      - **Add map**: Adds a map visualization based on the current data. Visible only when the current table (or any linked table) has a geometry/geography column (postgis extension must be enabled).  \n    - **View title. Drag to re-arrange layout**: The name of the table/view together with the number of records matching the current filters.   \n    - **Collapse the view**: Collapses the view, minimizing it to temporarily save space on the dashboard.   \n    - **Fullscreen**: Expands the view to fill the entire screen.  \n    - **Remove view**: Removes the view from the dashboard.  \n    - <a href=\"#table_menu\">Table menu</a>: Opens the table menu, allowing users to manage the table view.  \n  - <a href=\"#table_toolbar\">Table Toolbar</a>: Filtering and data add/edit interface for database tables and views.  \n  - **Table**: The main table view displaying the data from the database. It allows users to interact with the data, including sorting, filtering, and editing.  \n    - **Table header**: The header of the table, which contains the column names and allows users to sort the data by clicking on the column headers.  \n      - **Add column menu**: Opens the add column menu, allowing users to add computed columns, create new columns, add linked fields.  \n        - **Add Computed Field**: Opens a popup to create a computed column - calculations or transformations based on existing data using functions like aggregations, date formatting, string operations, etc.  \n          - **Function selector**: Choose a function to apply to the selected column (e.g., aggregates (min/max/avg/count), date formatting, string operations).  \n          - **Column selection**: List of applicable columns to apply functions to. Columns from foreign tables are also shown with the join path in the header.  \n          - **Column name**: Name for the new computed column. Auto-generated based on the function and column.  \n          - **Add to position**: Choose whether to add the computed column at the start or end of the column list.  \n          - **Add computed column**: Confirms and adds the new computed column to the table based on the selected function and parameters.  \n        - **Add Linked Data**: Opens a popup to display data from related tables via foreign key relationships. Disabled if no foreign keys exist or when using aggregates with nested columns.  \n          - **Join path selector**: Select which related table to join to via foreign key relationships. Shows available join paths from the current table.  \n          - **Column label**: Custom name/label for this linked column field in the table. Defaults to the related table name.  \n          - **Linked column selection**: Choose which columns from the related table to display in this linked field.  \n          - **Quick add computed column**: Add a single computed column from the linked table. E.g., count, sum, avg.  \n          - **More options**: Additional configuration for linked columns including layout, join type, filters, and limits.  \n            - **Layout mode**: Choose how to display the linked data: as rows, columns, or without headers.  \n            - **Join type**: Select between inner join (discards parent rows without matches) or left join (keeps all parent rows).  \n            - **Limit**: Maximum number of linked records to display (0-30). Optional.  \n            - **Filters and sorting**: Apply filters and sorting to the linked table data.  \n        - **Create New Column**: Opens a popup to create a new physical column in the database table. Disabled for views or users without SQL privileges.  \n          - **Column editor**: Configure column properties including name, data type, constraints, default values, and foreign key references.  \n          - **Foreign key reference**: Optionally set up a foreign key relationship to another table/column in the database.  \n          - **Data type selection**: Appears after typing the column name. Choose the data type for the new column (e.g., integer, text, date, boolean, etc.).  \n          - **Show create column SQL**: Generates and displays the SQL query that will be executed to create the new column in the database.  \n            - **Generated SQL query**: The generated ALTER TABLE SQL query to create the new column based on the specified properties.  \n            - **Execute create column**: Runs the generated ALTER TABLE query to create the new column in the database.  \n        - **Create New File Column**: Opens a popup to create a new column for handling file uploads and attachments. Requires file storage to be enabled.  \n          - **New column name**: Name for the new file column.  \n          - **Optional**: Whether the file column should allow NULL values (optional) or require a file (NOT NULL).  \n          - **File column configuration**: Configure accepted file types and other file handling options. Appears after entering the column name.  \n            - **Maximum file size in megabytes**: Set the maximum allowed file size for uploads in megabytes. Default is 1 MB. 0 means no limit.  \n            - **Content filter mode**: Choose how to filter accepted files: by file extension, basic content type (e.g., image, audio, vide), by specific content type (e.g., image/png), by specific extension (jpg, png, pdf).  \n          - **Create file column**: Confirms and creates the new file column.  \n      - **Column menu**: Opens the column menu, allowing users to change styling, render mode, view quick stats and other column related options.  \n        - **Sort**: Sort the table data based on the values in this column, either in ascending or descending order. Table can also be sorted by multiple columns by holding shift while clicking column headers.  \n        - **Style**: Customize the appearance of this column, including text color and cell background. You can also set conditional formatting rules to highlight specific data patterns.  \n        - **Display format**: Choose how the data in this column is displayed, such as date formats, number formats, or custom render modes.  \n        - **Filter**: Open the filter panel to set up filters based on this column's values, helping you to quickly narrow down the data displayed in the table.  \n        - **Quick stats**: View quick statistics about the data in this column, such as count, unique values, and distribution.  \n          - **Column Quick Stats**: The Column Quick Stats panel provides a summary of key statistics for the selected column, including distinct count, min/max values, and value distribution. You can also sort the distribution and add filters directly from this panel.  \n            - **Add filter**: Add a filter based on the selected value from top values list.  \n        - **Columns**: Shortcut to the column management panel to add, remove, or rearrange columns in the table.  \n        - **Add Computed Column**: Add a computed field based on calculations or transformations of existing data in this column.  \n        - **Apply function**: Apply a function to the data in this column, such as aggregations, string manipulations, or date transformations.  \n        - **Add Linked Columns**: Add linked data from related tables based on foreign key relationships.  \n        - **Alter**: Alter the column's properties, such as data type, default value, or constraints.  \n        - **Hide**: Hide this column from the table view without deleting it, allowing you to focus on the most relevant data.  \n        - **Hide Others**: Hide all other columns except this one, providing a focused view of the data in this column.  \n      - **Column header**: Pressing the header will toggle sorting state (if the column is sortable). Right clicking (or long press on mobile) will open column menu. Dragging the header will allow reordering the columns.  \n      - **Resize column**: Allows users to resize the column width by dragging the handle.  \n      - <a href=\"#view/edit_data\">View/edit data</a>: Opens the row card, allowing users to view/edit/delete the selected row.  \n      - **Insert row**: Opens the row insert menu, allowing users to add new rows to the table.  \n  - **Pagination Controls**: Navigation controls for paginated data.  \n    - **First Page**: Navigate to the first page of results.  \n    - **Previous Page**: Navigate to the previous page of results.  \n    - **Page Number**: Current page number. You can type a specific page number to jump directly to that page.  \n    - **Next Page**: Navigate to the next page of results.  \n    - **Last Page**: Navigate to the last page of results.  \n    - **Page Size**: Select how many rows to display per page. Changing this may adjust the current page if it would exceed the total number of pages.  \n    - **Page Count Information**: Displays the total number of pages and rows in the current dataset.  \n\n<h4 id=\"table_menu\"> Table menu </h4> \n\nThe table menu provides options for managing the table view, including viewing table info, editing columns, and managing data refresh rates.\n\n  - **Table info**: Postgres specific table/view details.  \n    - **Table name**: Displays the name of the table with option to rename it.  \n      - **Edit name**: Opens SQL editor to rename the table.  \n    - **Comment**: Displays and allows editing of the table comment.  \n      - **Edit comment**: Opens SQL editor to modify the table comment.  \n    - **OID**: Displays the object identifier of the table in the database.  \n    - **Type**: Shows whether this is a table or view.  \n    - **Owner**: Displays the database user who owns this table/view.  \n    - **Size information**: Provides details about the table's size and row count.  \n      - **Actual Size**: The physical size of the table data on disk.  \n      - **Index Size**: The size of all indexes associated with this table.  \n      - **Total Size**: The combined size of table data and indexes.  \n      - **Row count**: The total number of rows in the table.  \n    - **View definition**: Shows the SQL definition for views. Only visible for views, not tables.  \n    - **Vacuum**: Performs garbage collection and optionally analyzes the database. Only available for tables.  \n    - **Vacuum Full**: Performs a more thorough vacuum that can reclaim more space but takes longer and locks the table. Only available for tables.  \n    - **Drop**: Deletes the table or view from the database after confirmation.  \n  - **Columns**: Allows editing, reordering and toggling table columns.  \n    - **Columns list**: Allows editing, reordering and toggling table columns.  \n      - **Alter column**: Opens a popup to edit the column properties.  \n      - **Edit linked field**: Opens a popup to edit the linked field properties.  \n      - **Remove computed column**: Removes a computed column from the table. Only visible for computed columns.  \n  - **Data refresh**: Allows setting subscriptions or data refresh rates. By default every table subscribes to data changes.  \n  - **Triggers**: Allows managing triggers.   \n  - **Constraints**: Allows managing constraints.  \n  - **Indexes**: Allows managing indexes.  \n  - **Policies**: Allows managing policies.  \n  - **Access rules**: Allows managing prostgles access rules.  \n  - **Current query**: Allows viewing the SQL and data/layout info for the current table view.  \n  - **Display options**: Layout preferences.  \n\n<h4 id=\"table_toolbar\"> Table Toolbar </h4> \n\nTable toolbar can be toggled through the show/hide filtering button (top left corner). \nIt provides a user-friendly interface to add filters, search for data, and perform various actions on the table data.\n\n<img src=\"./screenshots/smart_filter_bar.svg\" alt=\"Smart Filter Bar screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n  - **Add Filter**: Allows adding a filter by chosing a column from the current table or from linked tables.  \n    - **Include Linked Columns**: Toggle to include columns from linked tables in the column list.  \n    - **Join To**: Toggles join view to specify which tables a join path to a specific table from which to select a column to filter by.  \n    - **Filterable Columns**: List of columns available for filtering. Click to add a filter.  \n  - **Search Bar**: Quick search functionality across the table data.  \n    - **Search**: Type to search across all searchable fields in the table. The search is a simple contains search. Selecting a result will add a filter to the table.  \n    - **Match Case**: Toggle to match the case of the search term with the data.  \n  - **Table actions**: Additional table data actions.  \n    - **Show additional actions**: Opens a menu with additional actions for the table data.  \n    - **Delete action**: Opens a menu to delete the selected rows from the table.  \n    - **Update action**: Opens a menu to update the selected rows in the table.  \n    - **Insert row**: Opens the row insert menu, allowing users to add new rows to the table.  \n  - **Filters**: Filters applied to the data  \n  - **Having Filters**: Filters applied after aggregation (HAVING clause in SQL).  \n\n<h1 id=\"view/edit_data\"> Row card </h1> \n\nSmart form is an intelligent, auto-generated form system that adapts to your database schema.\nIt provides a user-friendly interface for inserting and updating data with automatic validation,\nforeign key handling, and support for complex data types.\n\n<img src=\"./screenshots/smart_form.svgif.svg\" alt=\"SmartForm screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n## Features\n- **Auto-generated fields** based on table schema\n- **Data type validation** (text, numbers, dates, JSON, etc.)\n- **Foreign key support** with searchable dropdowns\n- **File upload** for media and document columns\n- **Linked data management** - insert related records inline\n- **JSON/JSONB editor** with syntax highlighting\n- **Geometry/Geography** support for spatial data\n- **Array types** with dynamic add/remove\n- **Default values** and constraints enforcement\n- **Required field** indicators\n- **Custom field rendering** based on column configuration\n\n## Field Types\n- Text inputs (single/multi-line)\n- Number inputs (integer, decimal)\n- Date/Time/Timestamp pickers\n- Boolean checkboxes\n- Select dropdowns (enums, foreign keys)\n- File upload fields\n- JSON/JSONB editors\n- Geometry/Geography mappers\n- Array editors\n\n  - **Smart Form**: The smart form displays the details of a single row from the table, allowing users to view and edit the data in a structured format.  \n    - **Table icon in header**: Table icon and table name. Table icon is configurable through the table menu settings.  \n    - **Previous row button**: Navigate to the previous row in the dataset. Only shown for tables with primary key. Disabled when there is no previous row available.  \n    - **Next row button**: Navigate to the next row in the dataset. Only shown for tables with primary key. Disabled when there is no next row available.  \n    - **Fullscreen toggle button**: Toggles the fullscreen mode of the SmartForm popup for an expanded view.  \n    - **Close button**: Closes the SmartForm popup without saving any data.  \n    - **Form field**: Each form field represents a column from the table, displaying the column name and the corresponding value for the selected row. Users can edit the value if the field is editable.  \n      - **Field label**: Displays the name of the column.  \n      - **Field input area**: The input area where users can view and edit the value of the column. The input type varies based on the column's data type.  \n      - **Field hint**: Additional information or guidance about the field, displayed below the input area.  \n      - **View more linked records**: For foreign key fields a 'View more' button appears to open a detailed list of all linked records in a separate popup. This is useful to browse and search all columns from the referenced table.  \n      - **Insert linked record**: If the column is a foreign key a 'Insert new record' button will appear on hover which allows inserting data into the referenced table. Useful when the desired value does not exist yet (foreign key columns only show existing values).  \n      - **Clear field value button**: Clear the current value of the field, resetting it to null. Only shown if the field is nullable and the user is allowed to update it.  \n    - **Joined records section**: If the current table has other tables referencing it via foreign keys, a section will appear at the bottom of the form showing lists of those related records. This allows users to view and manage data that is linked to the current row.  \n      - **Toggle joined records section**: Expand or collapse the joined records section to show or hide the list of related records.  \n      - **View more joined records**: Open a detailed list of all joined records in a separate popup. Useful to browse and search all columns from the joined table.  \n      - **Add joined record**: Open the SmartForm to insert a new joined record into the related table, automatically linking it to the current row.  \n      - **Toggle fullscreen mode**: Expand the section to fullscreen for better visibility and interaction with the joined records.  \n\n"
  },
  {
    "path": "docs/11_Map_view.md",
    "content": "<h1 id=\"map_view\"> Map view </h1> \n\nThe map view allows you to visualize geographical data from your database.\nIt requires the [PostGIS](https://postgis.net/) extension to be installed on your PostgreSQL database.\nIt can display points, lines, and polygons based on geometry or geography columns in your tables or views.\nIt supports multiple layers, custom basemaps, and various map controls for interaction.\n\n<img src=\"./screenshots/map.svgif.svg\" alt=\"Map view screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n  - **Header section**: Contains menu button, title and window minimise/fullscreen controls.  \n    - **View title. Drag to re-arrange layout**: Shows the table/view name together with the geometry/geography column name used for the map visualization.  \n    - **Collapse the view**: Collapses the view, minimizing it to temporarily save space on the dashboard.   \n    - **Fullscreen**: Expands the view to fill the entire screen.  \n    - **Remove view**: Removes the view from the dashboard.  \n    - **Chart toolbar**: Toolbar for the chart view if not detached. By default, newly charts added will appear over the originating table/sql editor view. They can be detached to a separate window.  \n      - **Chart menu**: Menu for the chart view.  \n      - **Collapse chart**: Collapses the chart window, minimizing it to save space on the dashboard. It can then be restored by clicking the chart icon in the SQL editor top left quick actions section.  \n      - **Detach chart**: Detaches the chart from the parent view, allowing it to be moved and resized independently. It keeps the connection the originating table view to cross filter it.  \n      - **Close chart**: Closes the chart view, returning to the originatine table/sql editor view.  \n    - <a href=\"#map_view_menu\">Map view menu</a>: Data refresh and display options.  \n  - <a href=\"#map_window_with_controls\">Map window with controls</a>: Map visualization and controls for interacting with the map.  \n\n<h4 id=\"map_view_menu\"> Map view menu </h4> \n\nThe map view menu provides options for configuring the map visualization, including data refresh, basemap settings, and layer management.\n\n  - **Data refresh**: Allows setting subscriptions or data refresh rates. By default every table subscribes to data changes.  \n  - **Basemap**: Allows setting the map tiles and projection.  \n    - **Projection**: Allows setting the map projection: Mercator (default) or Orthographic (allows a setting custom tile image for plan drawings).  \n  - **Layers**: Allows setting the map layers data source and style. The map supports multiple layers.  \n  - **Settings**: Allows setting the map layout options: aggregation limit, click behavior, etc.  \n\n<h4 id=\"map_window_with_controls\"> Map window with controls </h4> \n\nThe map window contains the map visualization and controls for interacting with the map.\nIt allows you to add layers, set the map extent behavior, and toggle cursor coordinates display.\n\n  - **Map layer manager**: Allows adding/removing layers to the map. Each layer can be configured with its own data source and style.  \n    - **Add layer**: Allows adding a new layer to the map. The available options are all the tables that have geometry or geography columns.  \n    - **Add OSM layer**: Allows adding a new layer based on OpenStreetMap data. This is useful for displaying additional map data like roads, restaurants, etc.  \n    - **Map basemap options**: Allows setting the map tiles and projection.  \n    - **Map opacity options**: Allows setting the opacity of the map layers.  \n  - **Show cursor coordinates**: Toggle displays the current cursor coordinates on the map.   \n  - **Map extent behavior**: Allows setting the map extent and auto-zoom behavior: Follow data, Follow map or Free roam.  \n  - **Zoom to data**: Zooms the map to fit the bounds of the data currently displayed.  \n  - **Add new feature**: Allows drawing and inserting a new feature: Point, Line or Polygon.  \n\n"
  },
  {
    "path": "docs/12_Timechart_view.md",
    "content": "<h1 id=\"timechart_view\"> Timechart view </h1> \n\nThe timechart view allows you to visualize time-series data from your database.\nIt supports multiple layers, each with its own data source and style.\nYou can add filters to the timechart to narrow down the data displayed.\n\n<img src=\"./screenshots/timechart.svgif.svg\" alt=\"Timechart view screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n## Components\n\n  - **Header section**: Contains menu button, title and window minimise/fullscreen controls.  \n    - **View title. Drag to re-arrange layout**: Shows the table/view name together with the number of records matching the current filters.  \n    - **Collapse the view**: Collapses the view, minimizing it to temporarily save space on the dashboard.   \n    - **Fullscreen**: Expands the view to fill the entire screen.  \n    - **Remove view**: Removes the view from the dashboard.  \n    - **Chart toolbar**: Toolbar for the chart view if not detached. By default, newly charts added will appear over the originating table/sql editor view. They can be detached to a separate window.  \n      - **Chart menu**: Menu for the chart view.  \n      - **Collapse chart**: Collapses the chart window, minimizing it to save space on the dashboard. It can then be restored by clicking the chart icon in the SQL editor top left quick actions section.  \n      - **Detach chart**: Detaches the chart from the parent view, allowing it to be moved and resized independently. It keeps the connection the originating table view to cross filter it.  \n      - **Close chart**: Closes the chart view, returning to the originatine table/sql editor view.  \n    - <a href=\"#timechart_view_menu\">Timechart view menu</a>: Timechart view menu  \n  - **Chart area with controls**: Timechart visualization.  \n    - **Layer manager**: Allows adding/removing layers from the chart. Each layer can be configured with its own data source and style.  \n      - **Layer list**: Displays the list of layers currently added to the chart.   \n        - **Layer color picker**: Allows setting the color for the layer. The color can be set for each column in the layer.  \n        - **Table name**: The name of the table used for the layer. This is the table that contains the data for the layer.  \n        - **Aggregation options**: Allows setting the y-axis options for the layer.  \n          - **Aggregation function**: Selects the aggregation function to be used for the layer. The available options are: Sum, Average, Min, Max, Count.  \n          - **Aggregation column**: Selects the numeric column to be used for the aggregation function.   \n          - **Group by**: Selects the column to group the data by. This is will create a line for each group by value.  \n          - **Close**: Closes the aggregation function popup.  \n        - **Toggle layer on/off**: Toggles the visibility of the layer on the chart. This allows you to hide or show the layer without removing it.  \n        - **Remove layer**: Removes the layer from the chart. This will delete the layer and its configuration.  \n      - **Add layer**: Allows adding a new layer to the chart. The available options are all the tables that have date or timestamp columns.  \n    - **Reset extent**: Resets the chart to the default extent, showing all data points. Visible when the chart was paned or zoomed.  \n    - **Layer legend**: Displays the layers currently added to the chart. Quick access to changing the layer color, aggregation type and group by.  \n    - **Add/Edit time filter**: Allows adding a time filter to the timechart. This will filter the data points based on the selected time range.  \n    - **Timechart canvas**: Zoomable and pannable canvas that displays the timechart. It shows the data points based on the selected layers and filters. Clicking on a point will add a filter with that time bucket.  \n\n<h4 id=\"timechart_view_menu\"> Timechart view menu </h4> \n\nThe timechart view menu provides options for configuring the timechart.\n\n\n"
  },
  {
    "path": "docs/13_AI_Assistant.md",
    "content": "<h1 id=\"ai_assistant\"> AI Assistant </h1> \n\nThe AI assistant is an intelligent companion that helps you work more efficiently with your PostgreSQL databases. \nIt can generate SQL queries, explain database schemas, analyze data patterns, and assist with various database-related tasks through a conversational interface.\nMCP Servers can be used to extend the AI capabilities with custom tools and integrations.\n\n<img src=\"./screenshots/ai_assistant.svgif.svg\" alt=\"AI assistant popup screenshot\" style=\"border: 1px solid; margin: 1em 0;\" />\n\nSupported AI Providers: OpenAI, Anthropic, Google Gemini, OpenRouter, and Local Models. \n\n*Note: AI providers are configured by administrators in Server Settings > LLM Providers*\n\n  - **Header actions**: Actions available in the header of the AI assistant popup.  \n    - **Chat settings**: Allows editing all chat settings and data as well deleting or cloning the current chat.  \n    - **Select chat**: Selects a chat from the list of available chats. Each chat represents a separate conversation with the AI assistant.  \n    - **New chat**: Creates a new chat with the AI assistant.  \n    - **Toggle fullscreen**: Toggles the fullscreen mode for the AI assistant popup.  \n    - **Close popup**: Closes the AI assistant popup.  \n  - **Chat messages**: List of messages in the current chat.  \n  - <a href=\"#message_input\">Message input</a>: Input field for entering messages to the AI assistant and quick actions.  \n\n<h3 id=\"message_input\"> Message input </h3> \n\nThe message input area allows you to write text, attach files and control other aspects of the AI assistant (change model, add/remove tools, speech to text).\n\n  - **Message input**: Input field for entering messages to the AI assistant. Pressing Shift+Enter creates a new line.  \n  - <a href=\"#mcp_tools_allowed\">MCP tools allowed</a>: Opens the MCP tools menu for the current chat. Default tools: filesystem, fetch, git, github, google-maps, memory, playwright, websearch, docker-sandbox, slack  \n  - **Database access**: Opens the database access settings for the current chat. This controls how the AI assistant can interact with the current database.  \n  - **Prompt Selector**: Opens the prompt details for the current chat, allowing you to manage the prompt template and other related settings.  \n    - **Prompt preview**: Preview of the prompt with context variables filled in.  \n  - **LLM Model**: Selects the LLM model to be used for the current chat. Different models may have different capabilities and performance.   \n    - **Select model**: Selects this LLM model for the current chat.  \n    - **Add model credentials**: Opens the form to add llm provider credentials for the selected LLM model.  \n  - **Attach files**: Attaches files to be sent to the AI assistant along with the message. Supported file types may vary depending on the AI model and configuration.  \n  - **Speech to Text**: Opens the speech-to-text input options, allowing you to send audio recordings or transcribe audio messages to send to the AI assistant. Right click to open speech-to-text settings.  \n  - **Send message**: Sends the entered message to the AI assistant.  \n\n<h4 id=\"mcp_tools_allowed\"> MCP tools allowed </h4> \n\nMCP Servers extend the capabilities of the AI assistant by providing custom tools and integrations.\n\n  - **Add MCP server**: Opens the form to add a new MCP server for the current chat.  \n    - **MCP tool json config**: JSON configuration for the MCP tool to be added.  \n    - **Add MCP server**: Adds the specified MCP server to the current chat.  \n  - **Stop/Start all MCP Servers**: Quick way to stop/restart all MCP servers.  \n  - **Search tools**: Searches for specific MCP tools in the list of available tools.  \n  - **MCP tools**: List of available MCP tools. To allow a tool to be used in the current chat it must be ticked. Each tool represents a specific functionality or integration.  \n    - **MCP server name**: Name of the parent MCP server associated with the tool.  \n    - **MCP server tools**: List of available tools for the selected MCP server. Click to enable or disable a specific tool for the current chat.  \n    - **MCP Server Logs**: Opens the logs for the selected MCP server, allowing you to view its activity and status.  \n    - **MCP Server Config**: Opens the configuration for the selected MCP server, allowing you to manage its settings.  \n      - **Save config**: Saves the configuration for the selected MCP server.  \n    - **Reload MCP tools**: Reloads the MCP tools for the selected MCP server, updating the list of available tools.  \n    - **Enable/Disable MCP server**: Enables or disables the selected MCP server for all chats. If configuration is required a popup will be shown.  \n\n"
  },
  {
    "path": "docs/14_Connection_configuration.md",
    "content": "<h1 id=\"connection_configuration\"> Connection configuration </h1> \n\nConfigure the selected database connection. Set connection details, manage users, and customize settings.\n<img src=\"./screenshots/connection_config.svgif.svg\" alt=\"Connection configuration\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n- <a href=\"#connection_details\">Connection details</a>: Edit connection parameters such as host, port, database name, and other connection settings.  \n- **Status monitor**: View real-time connection status, running queries, and system resource usage.  \n- <a href=\"#access_control\">Access control</a>: Manage user permissions and access rules for this database connection.  \n- <a href=\"#file_storage\">File storage</a>: Configure file upload and storage settings for this connection.  \n- <a href=\"#backup_and_restore\">Backup and Restore</a>: Manage database backups and restore operations.  \n- <a href=\"#api\">API</a>: Configure API access settings and view API documentation.  \n- **Table config**: Advanced table configuration using TypeScript (experimental feature).  \n- **Server-side functions**: Configure and manage server-side functions (experimental feature).  \n\n<h2 id=\"connection_details\"> Connection details </h2> \n\nConnection settings, credentials, and other parameters to ensure your connection is configured correctly.\n\n\n"
  },
  {
    "path": "docs/15_Access_control.md",
    "content": "<h1 id=\"access_control\"> Access control </h1> \n\n>  Not available on Desktop version\n  \nManage user permissions and access rules for this database connection.\n<img src=\"./screenshots/access_control.svgif.svg\" alt=\"Access control\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n  - **Create Access Rule**: Add a new access control rule to define user permissions.  \n  - **Save Access Rule**: Save changes to the current access control rule.  \n  - **Cancel Changes**: Cancel editing the current access control rule.  \n  - **Remove Rule**: Delete the selected access control rule.  \n\n"
  },
  {
    "path": "docs/16_File_storage.md",
    "content": "<h1 id=\"file_storage\"> File storage </h1> \n\nConfigure file upload and storage settings for this connection.\n\n<img src=\"./screenshots/file_storage.svg\" alt=\"File Storage Configuration\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n  - **Enable File Storage**: Enable file upload and storage capabilities for this connection.  \n\n"
  },
  {
    "path": "docs/17_Backup_and_Restore.md",
    "content": "<h1 id=\"backup_and_restore\"> Backup and Restore </h1> \n\nManage database backups and restore operations for this PostgreSQL connection. \nCreate reliable backups using PostgreSQL's native tools and restore \nyour data when needed.\n\nBackups can be saved to a local file system or to cloud storage to AWS S3.\nSimilarly, you can restore backups from local files or from AWS S3.\n\n<img src=\"./screenshots/backup_and_restore.svgif.svg\" alt=\"Backup and Restore\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n  - **Create Backup**: Start a new database backup operation.  \n    - **Backup Name**: Optional name for the backup to help identify it later.  \n    - **Backup Format**: Choose the backup format: Custom, Plain SQL, Tar, or Directory.  \n    - **Schema Only**: Backup only the database schema without data.  \n    - **Data Only**: Backup only the data without schema.  \n    - **Backup Destination**: Choose where to save the backup: Local filesystem or AWS S3.  \n    - **Number of Jobs**: Specify the number of parallel jobs to use for the backup process.  \n    - **Compression Level**: Set the compression level for the backup (0-9). Higher values mean better compression but slower performance.  \n    - **Exclude Schema**: Specify a schema to exclude from the backup.  \n    - **No Owner**: Do not output commands to set ownership of objects to match the original database. Useful when restoring to a different database user.  \n    - **Create**: Include commands to create the database in the backup.  \n    - **Globals Only**: Backup only global objects such as roles and tablespaces.  \n    - **Roles Only**: Backup only roles (users) from the database.  \n    - **Schema Only**: Backup only the database schema without data.  \n    - **Encoding**: Specify the character encoding to use in the backup.  \n    - **Clean**: Include commands to drop database objects before recreating them.  \n    - **Data Only**: Backup only the data without schema.  \n    - **If Exists**: Use IF EXISTS clauses in the backup to avoid errors when dropping objects that do not exist.  \n    - **Keep Logs**: Retain log files generated during the backup process.  \n    - **Start Backup**: Begin the backup process with the selected options.  \n  - **Automatic Backups**: Configure scheduled automatic backups for this database.  \n    - **Enable Automatic Backups**: Enable or disable automatic backup scheduling.  \n  - **Backups in Progress**: Monitor and manage ongoing backup operations.  \n    - **View Logs**: View real-time logs of the ongoing backup operation.  \n  - **Restore from File**: Initiate a database restore operation from a local backup file.  \n  - **Completed Backups**: View and manage completed backup operations.  \n    - **Delete Backup**: Delete the selected backup file from storage. Will ask for confirmation.  \n    - **Download Backup**: Download the selected backup file to your local system.  \n    - **Restore Backup**: Restore a database from a selected backup file. Will ask for confirmation.  \n\n"
  },
  {
    "path": "docs/18_API.md",
    "content": "<h1 id=\"api\"> API </h1> \n\nThe API section allows you to configure the API access settings for your application.\nThis enables programmatic access to your application's data and functionality via HTTP or WebSocket protocols.\nGenerated database types can be used to interact with the API in a type-safe manner through our TypeScript client library.\nCode snippets are provided to help you get started with using the API in your applications.\nYou can also manage API tokens for authentication and access control.\n\nYou can set the URL path for the API, manage allowed origins for CORS requests, and create or view API tokens for accessing the API. The API supports both WebSocket and HTTP protocols.\n\n  - **API URL Path**: Set the URL path for the API. This is the base path for all API endpoints.  \n  - **Allowed Origin Alert**: Will only appear if the allowed origin is not set.  \n    - **Allowed Origin**: Set the allowed origin for CORS requests. This controls which domains can make cross-origin requests to this app by setting the Access-Control-Allow-Origin header.  \n  - **Websocket API usage examples**: View examples of how to use the API using typescript and javascript  \n  - **HTTP API usage examples**: View examples of how to use the API using typescript and javascript  \n  - **API Tokens**: Shows existing API tokens and allows you to create new ones.  \n    - **Create API Token**: Create a new API token for accessing the API. Tokens can be used for both WebSocket and HTTP API access.  \n      - **Expires in (days)**: Set the number of days until the token expires. After expiration, the token will no longer be valid.  \n      - **Generate Token**: Click to generate a new API token. The token will be displayed once generated. After generation, the code examples will be updated to include the new token.  \n\n"
  },
  {
    "path": "docs/19_Server_Settings.md",
    "content": "<h1 id=\"server_settings\"> Server Settings </h1> \n\nManage server settings to enhance security, configure authentication methods, and set up LLM providers.\n<img src=\"./screenshots/server_settings.svg\" alt=\"Server Settings\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n- **Security**: Security. Configure domain access, IP restrictions, session duration, and login rate limits to enhance security.  \n  - **Settings form**: Configure server settings.  \n- **Authentication**: Manage user authentication methods, default user roles, and third-party login providers to control access.  \n  - **Website URL**: Website URL. Used for email and third-party login redirect URL. When first visiting the app as an admin user, it is automatically set to the current URL which will trigger a page refresh.  \n  - **Default user type**: The default user type assigned to new users. Defaults to 'default'.  \n  - <a href=\"#email_signup\">Email signup</a>: Email signup/magic-link authentication setup.  \n  - **Third-party login providers**: Third-party login providers (OAuth2)  \n- **MCP Servers**: Manage MCP servers and tools that can then be used in the Ask AI chat  \n- **LLM Providers**: Manage LLM providers, credentials and models to be used in the Ask AI chat  \n- **Services**: Manage services  \n\n<h3 id=\"email_signup\"> Email signup </h3> \n\nProvide SMTP or AWS SES credentials to enable email signup and magic-link authentication. \nBy default users authenticate using a password.\n\n  - **Enable/Disable email signup toggle**: Enable email signup. This will allow users to sign up and log in using their email address.  \n  - **Signup type**: Signup type. Choose between 'withPassword' or 'withMagicLink'.  \n  - **Email verification**: SMTP and email template setup.  \n    - **Email provider setup**: SMTP settings for sending registration/magic-link emails. Allowed providers: SMTP (host, port, username, password) or AWS SES (region, accessKeyId, secretAccessKey).  \n    - **Email Template setup**: Email template for registration/magic-link emails  \n    - **Test and save**: Test and Save SMTP and email template settings.  \n\n"
  },
  {
    "path": "docs/20_Account.md",
    "content": "<h1 id=\"account\"> Account </h1> \n\nManage your account settings, security preferences, and API access.\n\n<img src=\"./screenshots/account.svgif.svg\" alt=\"Account Page\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n- **Account details**: View and update your account information.  \n  - **Account information**: View all account details and associated data (workspaces, dashboards, views, etc.)  \n- **Security**: Manage your account security settings.  \n  - **Two-factor authentication**: Set up and manage two-factor authentication for enhanced security.  \n    - **Generate QR Code**: Generate a QR code to set up 2FA with your authenticator app.  \n    - **Can't scan QR code**: View manual setup instructions if you can't scan the QR code.  \n    - **Confirm code**: Enter the code from your authenticator app to enable 2FA.  \n    - **Enable 2FA**: Complete the 2FA setup process by confirming the code.  \n    - **Base64 secret**: Manual setup secret key for your authenticator app.  \n  - **Disable 2FA**: Turn off two-factor authentication for your account.  \n  - **Change password**: Change your account password.  \n  - **Active sessions**: View and manage your active web sessions.  \n- **API**: View and manage your API access settings.  \n  - **API Details**: View your API credentials and configuration.  \n\n"
  },
  {
    "path": "docs/21_Command_Palette.md",
    "content": "<h1 id=\"command_palette\"> Command Palette </h1> \n\nKeyboard-driven navigation to different parts of the application without having to browse through menus or panels. \n\nPress <kbd>Ctrl+K</kbd> to open the command palette popup. Type to search through the documentation for functionality, settings, and other sections.\n<img src=\"./screenshots/command_palette.svgif.svg\" alt=\"Command Palette\" style=\"border: 1px solid; margin: 1em 0;\" />\n\n- **Search commands input**: Type to search for commands and actions  \n- **Search results**: List of matching commands and actions. Press Enter to execute/go to the selected command.  \n\n"
  },
  {
    "path": "e2e/.gitignore",
    "content": "node_modules/\n/test-results/\n/electron-report/\n/playwright-report/\n/playwright/.cache/\ndemo"
  },
  {
    "path": "e2e/package.json",
    "content": "{\n  \"name\": \"prostgles-ui-e2e\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"setup\": \"cp ../client/src/Testing.ts ./tests\",\n    \"test\": \"npm run setup && playwright test main.spec.ts && playwright test demo_video_setup.spec.ts && playwright test demo_video.spec.ts && playwright test create_docs.spec.ts && playwright test command_palette.spec.ts\",\n    \"create-docs\": \"npm run setup && npx playwright test create_docs.spec.ts \",\n    \"command-palette\": \"npm run setup && npx playwright test command_palette.spec.ts \",\n    \"test-local\": \"bash test-local.sh\",\n    \"test-debug\": \"npm run setup && playwright test --debug\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.56.1\",\n    \"@types/node\": \"^20.4.4\",\n    \"@types/smtp-server\": \"^3.5.12\",\n    \"otplib\": \"^12.0.1\"\n  },\n  \"dependencies\": {\n    \"smtp-server\": \"^3.16.1\"\n  }\n}\n"
  },
  {
    "path": "e2e/playwright.config.js",
    "content": "\"use strict\";\nvar __assign = (this && this.__assign) || function () {\n    __assign = Object.assign || function(t) {\n        for (var s, i = 1, n = arguments.length; i < n; i++) {\n            s = arguments[i];\n            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))\n                t[p] = s[p];\n        }\n        return t;\n    };\n    return __assign.apply(this, arguments);\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nvar test_1 = require(\"@playwright/test\");\nvar timeoutMinutes = 6;\nexports.default = (0, test_1.defineConfig)({\n    timeout: timeoutMinutes * 6e4,\n    testDir: \"./tests\",\n    fullyParallel: false,\n    forbidOnly: !!process.env.CI,\n    retries: 0,\n    workers: 100,\n    reporter: \"html\",\n    use: {\n        baseURL: \"http://localhost:3004\",\n        trace: \"retain-on-failure\",\n        video: \"retain-on-failure\",\n        // video: \"on\",\n        testIdAttribute: \"data-command\",\n        actionTimeout: 5e3,\n    },\n    maxFailures: 0,\n    projects: [\n        {\n            name: \"chromium\",\n            use: __assign({}, test_1.devices[\"Desktop Chrome\"]),\n        },\n        // {\n        //   name: 'firefox',\n        //   use: { ...devices['Desktop Firefox'] },\n        // },\n        // {\n        //   name: 'webkit',\n        //   use: { ...devices['Desktop Safari'] },\n        // },\n        /* Test against mobile viewports. */\n        // {\n        //   name: 'Mobile Chrome',\n        //   use: { ...devices['Pixel 5'] },\n        // },\n        // {\n        //   name: 'Mobile Safari',\n        //   use: { ...devices['iPhone 12'] },\n        // },\n        /* Test against branded browsers. */\n        // {\n        //   name: 'Microsoft Edge',\n        //   use: { ...devices['Desktop Edge'], channel: 'msedge' },\n        // },\n        // {\n        //   name: 'Google Chrome',\n        //   use: { ..devices['Desktop Chrome'], channel: 'chrome' },\n        // },\n    ],\n});\n"
  },
  {
    "path": "e2e/playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\n\nconst timeoutMinutes = 6;\n\nexport default defineConfig({\n  timeout: timeoutMinutes * 6e4,\n  testDir: \"./tests\",\n  fullyParallel: false,\n\n  forbidOnly: !!process.env.CI,\n\n  retries: 0,\n  workers: process.env.CI ? 1 : 4,\n  reporter: [[\"html\", { noCopyPrompt: true }]],\n  use: {\n    baseURL: \"http://localhost:3004\",\n    trace: \"retain-on-failure\",\n    video: \"retain-on-failure\",\n    // video: \"on\",\n    testIdAttribute: \"data-command\",\n    actionTimeout: 5e3,\n  },\n  maxFailures: 0,\n  projects: [\n    {\n      name: \"chromium\",\n      use: { ...devices[\"Desktop Chrome\"] },\n    },\n  ],\n});\n"
  },
  {
    "path": "e2e/test-local.sh",
    "content": "# export PRGL_DEV_ENV=true && \\\nnpm i && \\\nnpx playwright install && \\\nnpm run setup && \\\nnpx playwright test main.spec.ts --headed"
  },
  {
    "path": "e2e/tests/Testing.ts",
    "content": "export const COMMANDS = {\n  \"NewConnectionForm.connectionName\": \"Connection name input field\",\n  \"NewConnectionForm.connectionType\": \"Connection type select field\",\n  \"NewConnectionForm.db_conn\": \"Database connection input field\",\n  \"NewConnectionForm.db_host\": \"Database host input field\",\n  \"NewConnectionForm.db_port\": \"Database port input field\",\n  \"NewConnectionForm.db_user\": \"Database user input field\",\n  \"NewConnectionForm.db_pass\": \"Database password input field\",\n  \"NewConnectionForm.db_name\": \"Database name input field\",\n  \"NewConnectionForm.MoreOptionsToggle\": \"\",\n  \"NewConnectionForm.schemaFilter\": \"\",\n  \"NewConnectionForm.connectionTimeout\": \"Connection timeout input field\",\n  \"NewConnectionForm.sslMode\": \"SSL mode select field\",\n  \"NewConnectionForm.watchSchema\": \"Watch schema toggle\",\n  \"NewConnectionForm.realtime\": \"Realtime toggle\",\n  \"NewConnectionForm.testConnection\": \"Test connection button\",\n\n  \"config.goToConnDashboard\": \"Go to connection workspace \",\n  \"config.details\": \"\",\n  \"config.bkp\": \"\",\n  \"config.tableConfig\": \"\",\n  \"config.bkp.create\": \"\",\n  \"config.bkp.create.name\": \"Backup name input field\",\n  \"config.bkp.create.start\": \"\",\n  \"config.bkp.AutomaticBackups\": \"\",\n  \"config.bkp.AutomaticBackups.toggle\": \"\",\n  \"config.ac\": { desc: \"\", uiOnly: true },\n  \"config.status\": \"\",\n  \"config.ac.create\": \"\",\n  \"config.ac.save\": \"\",\n  \"config.ac.removeRule\": \"\",\n  \"config.ac.cancel\": \"\",\n  \"config.ac.createDefault\": \"\",\n  \"config.ac.edit.user\": \"Opens select/edit access rule user types\",\n  \"config.ac.edit.user.select\": \"User type select search box\",\n  \"config.ac.edit.user.select.create\":\n    \"Creates a user type when the search did not yield any results\",\n  \"config.ac.edit.user.select.done\": \"Closes create user popup\",\n\n  \"config.ac.edit.type\":\n    \"Rule type button group. Each button.value will contain the type\",\n\n  \"config.ac.edit.dataAccess\": \"Data access section with tables/runsql\",\n\n  \"config.ac.edit.createWorkspaces\": \"\",\n  \"config.ac.edit.publishedWorkspaces\": \"\",\n\n  \"config.ac.edit.typeCustom.tables\": \"\",\n  \"config.ac.edit.typeAll\": \"\",\n  \"config.ac.edit.typeSQL\": \"\",\n  \"config.files\": \"\",\n  \"config.files.toggle\": \"\",\n  \"config.files.toggle.confirm\": \"\",\n  \"config.api\": { desc: \"\", uiOnly: true },\n  \"config.methods\": \"\",\n\n  \"dashboard.window.rowInsert\": \"Open row insert panel\",\n  \"dashboard.window.rowInsertTop\": \"Open row insert panel from top filter bar\",\n\n  \"W_SQLMenu.name\": \"\",\n  \"W_SQLMenu.renderDisplayMode\": \"\",\n  \"W_SQLMenu.saveQuery\": \"\",\n  \"W_SQLMenu.openSQLFile\": \"\",\n  \"W_SQLMenu.deleteQuery\": \"\",\n\n  \"W_SQLEditor.executeStatement\": \"Executes the current SQL statement\",\n  W_SQLEditor: \"\",\n  W_SQLBottomBar: \"\",\n  \"W_SQLBottomBar.runQuery\":\n    \"Executes the current query (selected, block or full)\",\n  \"W_SQLBottomBar.limit\": \"\",\n  \"W_SQLBottomBar.queryDuration\": \"\",\n  \"W_SQLBottomBar.cancelQuery\": \"Cancels the current query\",\n  \"W_SQLBottomBar.terminateQuery\": \"Terminates the current query\",\n  \"W_SQLBottomBar.stopListen\": \"Terminates the current LISTEN query\",\n  \"W_SQLBottomBar.toggleTable\": \"\",\n  \"W_SQLBottomBar.toggleCodeEditor\": \"\",\n  \"W_SQLBottomBar.toggleNotices\": \"\",\n  \"W_SQLBottomBar.stopLoopQuery\": \"Stop loop query\",\n  \"W_SQLBottomBar.copyResults\": \"Copy results\",\n  \"W_SQLBottomBar.rowCount\": \"Row count\",\n  \"W_SQLBottomBar.sqlError\": \"SQL error\",\n\n  W_SQLResults: \"\",\n  \"Window.W_QuickMenu\": \"Quick menu for the current window\",\n  \"Window.ChildChart\": \"\",\n  \"Window.ChildChart.toolbar\": \"\",\n\n  \"dashboard.window.fullscreen\": \"fullscreen\",\n  \"dashboard.window.close\": \"close\",\n  \"dashboard.window.collapse\": \"collapse\",\n  \"dashboard.window.viewEditRow\": \"\",\n  \"dashboard.window.toggleFilterBar\": \"\",\n  \"dashboard.window.menu\": \"\",\n\n  \"dashboard.window.detachChart\": \"\",\n  \"dashboard.window.collapseChart\": \"\",\n  \"dashboard.window.closeChart\": \"\",\n  \"dashboard.window.chartMenu\": \"\",\n  \"dashboard.window.restoreMinimisedCharts\": \"\",\n\n  \"dashboard.goToConnConfig\": \"Go to connection config\",\n  \"dashboard.menu.settingsToggle\": \"\",\n  \"dashboard.menu.settings.defaultLayoutType\": \"\",\n  \"dashboard.menu.settings\": \"\",\n  \"dashboard.menu\": \"\",\n  \"dashboard.menu.sqlEditor\": \"\",\n  \"dashboard.menu.quickSearch\": \"\",\n  \"dashboard.menu.resize\": \"\",\n  \"dashboard.centered-layout.resize\": \"\",\n  \"dashboard.menu.fileTable\": \"\",\n  \"dashboard.menu.savedQueriesList\": \"\",\n  \"dashboard.menu.tablesSearchList\": \"\",\n  \"dashboard.menu.tablesSearchListInput\": \"\",\n  \"dashboard.menu.serverSideFunctionsList\": \"\",\n  \"dashboard.menu.create\": \"\",\n  \"dashboard.menu.createTable\": \"\",\n  \"dashboard.menu.createTable.tableName\": \"\",\n  \"dashboard.menu.createTable.addColumn\": \"\",\n  \"dashboard.menu.createTable.addColumn.confirm\": \"\",\n  \"dashboard.menu.createTable.confirm\": \"\",\n\n  \"W_TableMenu_TableInfo.name\": \"\",\n  \"W_TableMenu_TableInfo.comment\": \"\",\n  \"W_TableMenu_TableInfo.oid\": \"\",\n  \"W_TableMenu_TableInfo.type\": \"\",\n  \"W_TableMenu_TableInfo.owner\": \"\",\n  \"W_TableMenu_TableInfo.sizeInfo\": \"\",\n  \"W_TableMenu_TableInfo.viewDefinition\": \"\",\n  \"W_TableMenu_TableInfo.vacuum\": \"\",\n  \"W_TableMenu_TableInfo.vacuumFull\": \"\",\n  \"W_TableMenu_TableInfo.drop\": \"\",\n\n  \"W_TableMenu_ColumnList.alter\": \"\",\n  \"W_TableMenu_ColumnList.linkedColumnOptions\": \"\",\n  \"W_TableMenu_ColumnList.removeComputedColumn\": \"\",\n\n  TableHeader: \"\",\n  \"TableHeader.resizeHandle\": \"\",\n\n  \"FormField.clear\": \"Clear a FormField\",\n\n  SmartForm: \"\",\n  \"SmartForm.header.tableIconAndName\": \"\",\n  \"SmartForm.header.previousRow\": \"\",\n  \"SmartForm.header.nextRow\": \"\",\n\n  \"SmartForm.close\": \"Close dialog\",\n  \"SmartForm.delete\": \"Deletes row\",\n  \"SmartForm.delete.confirm\": \"Confirms Deleting a row\",\n  \"SmartForm.update\": \"update row\",\n  \"SmartForm.update.confirm\": \"Confirms update a row\",\n  \"SmartForm.insert\": \"Confirms Deleting a row\",\n  \"SmartForm.clone\": \"Confirms Deleting a row\",\n\n  \"SQLSmartEditor.Run\": \"Run the sql statement\",\n\n  \"SearchList.toggleAll\": \"\",\n  \"SearchList.List\": \"\",\n  \"SearchList.Input\": \"\",\n  ViewMoreSmartCardList: \"\",\n  \"Section.toggleFullscreen\": \"\",\n  FieldFilterControl: \"\",\n  \"FieldFilterControl.type\": \"\",\n  \"FieldFilterControl.type.custom\": \"\",\n  \"FieldFilterControl.type.except\": \"\",\n  \"FieldFilterControl.select\": \"\",\n  \"RenderFilter.edit\": \"\",\n  \"RenderFilter.done\": \"\",\n\n  ForcedFilterControl: \"\",\n  \"ForcedFilterControl.type\": \"\",\n  \"ForcedFilterControl.type.disabled\": \"\",\n  \"ForcedFilterControl.type.enabled\": \"\",\n\n  CheckFilterControl: \"\",\n  \"CheckFilterControl.type\": \"\",\n  \"CheckFilterControl.type.disabled\": \"\",\n  \"CheckFilterControl.type.enabled\": \"\",\n\n  selectRule: \"\",\n  selectRuleAdvanced: \"\",\n  updateRule: \"\",\n  updateRuleAdvanced: \"\",\n  deleteRule: \"\",\n  deleteRuleAdvanced: \"\",\n  insertRule: \"\",\n  insertRuleAdvanced: \"\",\n  syncRule: \"\",\n  syncRuleAdvanced: \"\",\n\n  SearchAll: \"\",\n  SmartAddFilter: \"\",\n  FilterWrapper: \"\",\n  \"FilterWrapper.typeSelect\": \"\",\n  FileBtn: \"\",\n\n  \"ForcedDataControl.toggle\": \"\",\n  \"ForcedDataControl.addColumn\": \"\",\n  \"TablePermissionControls.close\": \"\",\n  \"TablePermissionControls.done\": \"\",\n\n  Connections: \"\",\n  \"Connections.add\": \"add\",\n  \"Connections.new\": \" \",\n  \"Connection.openConnection\": \"Open connection\",\n  \"Connection.workspaceList\": \"Connection workspace list\",\n\n  \"Connection.closeAllWindows\": \"\",\n  \"Connection.statusMonitor\": \"\",\n  \"Connection.configure\": \"\",\n  \"Connection.edit\": \"\",\n  \"Connection.edit.updateOrCreateConfirm\": \"\",\n  \"Connection.edit.delete\": \"\",\n  \"Connection.edit.delete.dropDatabase\": \"\",\n  \"Connection.edit.delete.confirm\": \"\",\n\n  \"Connection.disconnect\": \"\",\n\n  \"ConnectionServer.add\": \"\",\n  \"ConnectionServer.add.newDatabase\": \"\",\n  \"ConnectionServer.add.existingDatabase\": \"\",\n  \"ConnectionServer.NewDbName\": \"\",\n  \"ConnectionServer.add.confirm\": \"\",\n\n  \"SmartFilterBar.toggle\": \"\",\n  \"SmartFilterBar.rightOptions.show\": \"\",\n  \"SmartFilterBar.rightOptions.update\": \"\",\n  \"SmartFilterBar.rightOptions.delete\": \"\",\n\n  \"ColumnEditor.name\": \"\",\n  \"ColumnEditor.dataType\": \"\",\n\n  AddColumnMenu: \"\",\n\n  WorkspaceAddBtn: \"\",\n  WorkspaceMenuDropDown: \"\",\n  \"WorkspaceMenuDropDown.WorkspaceAddBtn\": \"\",\n  \"WorkspaceMenu.SearchList\": \"\",\n  \"WorkspaceMenu.CloneWorkspace\": \"\",\n  \"WorkspaceMenu.toggleWorkspaceLayoutMode\": \"\",\n  WorkspaceDeleteBtn: \"\",\n  \"WorkspaceDeleteBtn.Confirm\": \"\",\n\n  JoinPathSelectorV2: \"\",\n  \"LinkedColumn.Add\": \"\",\n  \"LinkedColumn.ColumnList.toggle\": \"\",\n\n  \"SummariseColumn.toggle\": \"\",\n  FunctionSelector: \"\",\n  \"SummariseColumn.apply\": \"\",\n  \"Popup.header\": \"\",\n  \"Popup.close\": \"\",\n  \"Popup.content\": \"\",\n  \"Popup.footer\": \"\",\n  \"Popup.toggleFullscreen\": \"\",\n  \"PopupSection.fullscreen\": \"\",\n  \"PopupSection.content\": \"\",\n  \"LinkedColumn.ColumnListMenu\": \"\",\n  \"AddChartMenu.Map\": \"\",\n  \"AddChartMenu.Timechart\": \"\",\n  W_TimeChart: \"\",\n  \"W_TimeChart.ActiveRow\": \"\",\n  \"W_TimeChart.AddTimeChartFilter\": \"\",\n  SmartFormField: \"\",\n\n  \"CloseSaveSQLPopup.delete\": \"\",\n\n  SchemaGraph: \"\",\n  \"SchemaGraph.TopControls\": \"\",\n  \"SchemaGraph.TopControls.tableRelationsFilter\": \"\",\n  \"SchemaGraph.TopControls.columnRelationsFilter\": \"\",\n  \"SchemaGraph.TopControls.linkColorMode\": \"\",\n  \"SchemaGraph.TopControls.resetLayout\": \"\",\n  AddColumnReference: \"\",\n  \"SmartFormFieldOptions.AttachFile\": \"\",\n  RuleToggle: \"\",\n  \"table.options.displayMode\": \"\",\n  \"table.options.cardView.groupBy\": \"\",\n  \"table.options.cardView.orderBy\": \"\",\n  \"CardView.row\": \"\",\n  \"CardView.group\": \"\",\n  \"CardView.DragHeader\": \"\",\n\n  \"CreateFileColumn.confirm\": \"\",\n  \"AppDemo.start\": \"\",\n  MenuList: \"\",\n  ComparablePGPolicies: \"\",\n  \"ConnectionServer.SampleSchemas\": \"\",\n  \"dashboard.goToConnections\": \"\",\n  QuickAddComputedColumn: \"\",\n  \"SmartSelect.Done\": \"\",\n  \"SmartAddFilter.JoinTo\": \"\",\n  \"SmartAddFilter.toggleIncludeLinkedColumns\": \"\",\n  ContextDataSelector: \"\",\n  ClickCatchOverlay: \"\",\n  \"BackupControls.Restore\": \"\",\n  ChartLayerManager: \"\",\n  \"App.colorScheme\": \"\",\n  \"ElectronSetup.Next\": \"\",\n  \"ElectronSetup.Back\": \"\",\n  PostgresInstallationInstructions: \"\",\n  \"PostgresInstallationInstructions.Close\": \"\",\n  \"ElectronSetup.Done\": \"\",\n  SmartCardList: \"\",\n  \"AutomaticBackups.destination\": \"\",\n  \"AutomaticBackups.frequency\": \"\",\n  \"AutomaticBackups.hourOfDay\": \"\",\n  \"WorkspaceAddBtn.Create\": \"\",\n  MapOpacityMenu: \"\",\n  MapBasemapOptions: \"\",\n  \"InMapControls.goToDataBounds\": \"\",\n  \"InMapControls.showCursorCoords\": \"\",\n  LayerColorPicker: \"\",\n  \"W_TimeChart.resetExtent\": \"\",\n  TimeChartFilter: \"\",\n  \"ChartLayerManager.toggleLayer\": \"\",\n  \"ChartLayerManager.removeLayer\": \"\",\n  \"ChartLayerManager.AddChartLayer.addLayer\": \"\",\n  \"ChartLayerManager.AddChartLayer.addOSMLayer\": \"\",\n  ConnectionSelector: \"\",\n  \"Setup2FA.Enable\": \"\",\n  \"Setup2FA.Enable.GenerateQR\": \"\",\n  \"Setup2FA.Enable.CantScanQR\": \"\",\n  \"Setup2FA.Enable.Base64Secret\": \"\",\n  \"Setup2FA.Enable.ConfirmCode\": \"\",\n  \"Setup2FA.Enable.Confirm\": \"\",\n  \"Setup2FA.Disable\": \"\",\n  \"Setup2FA.error\": \"\",\n  \"DashboardMenuHeader.togglePinned\": \"\",\n  \"BackupControls.DeleteAll\": \"\",\n  \"BackupControls.DeleteAll.Confirm\": \"\",\n  \"ProjectConnection.error\": \"\",\n  NotFound: \"\",\n  \"NotFound.goHome\": \"\",\n  \"ConnectionServer.NewUserName\": \"\",\n  \"ConnectionServer.NewUserPassword\": \"\",\n  \"ConnectionServer.NewUserPermissionType\": \"\",\n  \"ConnectionServer.withNewOwnerToggle\": \"\",\n  \"W_Table.TableNotFound\": \"\",\n  JoinedRecords: \"\",\n  \"JoinedRecords.AddRow\": \"\",\n  \"JoinedRecords.SectionToggle\": \"\",\n  \"JoinedRecords.Section\": \"\",\n  \"SmartCard.viewEditRow\": \"\",\n  \"TimeChartLayerOptions.yAxis\": \"\",\n  \"TimeChartLayerOptions.aggFunc\": \"\",\n  \"TimeChartLayerOptions.aggFunc.select\": \"\",\n  \"TimeChartLayerOptions.groupBy\": \"\",\n  \"TimeChartLayerOptions.numericColumn\": \"\",\n  \"AgeFilter.comparator\": \"\",\n  \"AgeFilter.argsLeftToRight\": \"\",\n  \"Login.error\": \"\",\n  AskLLMAccessControl: \"\",\n  \"AskLLMAccessControl.AllowAll\": \"\",\n  \"Chat.messageList\": \"\",\n  \"Chat.sendWrapper\": \"\",\n  \"Chat.send\": \"\",\n  \"Chat.sendStop\": \"\",\n  \"Chat.addFiles\": \"\",\n  \"Chat.textarea\": \"\",\n  \"Chat.speech\": \"\",\n  AskLLM: \"\",\n  \"AskLLM.popup\": \"\",\n  SetupLLMCredentials: \"\",\n  \"SetupLLMCredentials.free\": \"\",\n  \"SetupLLMCredentials.api\": \"\",\n  \"AskLLMAccessControl.llm_daily_limit\": \"\",\n\n  DeckGLFeatureEditor: \"\",\n  \"MapBasemapOptions.Projection\": \"\",\n\n  SmartFilterBar: \"\",\n  SearchList: \"\",\n  \"SearchList.MatchCase\": \"\",\n  AddJoinFilter: \"\",\n  Pagination: \"\",\n  \"Pagination.page\": \"\",\n  \"Pagination.lastPage\": \"\",\n  \"Pagination.nextPage\": \"\",\n  \"Pagination.prevPage\": \"\",\n  \"Pagination.firstPage\": \"\",\n  \"Pagination.pageCountInfo\": \"\",\n  \"Pagination.pageSize\": \"\",\n  MapExtentBehavior: \"\",\n  AddLLMCredentialForm: \"\",\n  \"AddLLMCredentialForm.Save\": \"\",\n  \"AddLLMCredentialForm.Provider\": \"\",\n  \"App.LanguageSelector\": \"\",\n  \"EmailAuthSetup.SignupType\": \"\",\n  EmailAuthSetup: \"\",\n  \"EmailAuthSetup.error\": \"\",\n  \"EmailAuthSetup.toggle\": \"\",\n  EmailSMTPAndTemplateSetup: \"\",\n  \"EmailSMTPAndTemplateSetup.save\": \"\",\n  \"Login.toggle\": \"\",\n  AuthNotifPopup: \"\",\n  \"ProstglesSignup.continue\": \"\",\n  \"PublishedMethods.deleteFunction\": \"\",\n  \"SmartFormFieldOptions.NestedInsert\": \"\",\n\n  \"Btn.ClickConfirmation\": \"\",\n  \"Btn.ClickConfirmation.Confirm\": \"\",\n  AddMCPServer: \"\",\n  \"AddMCPServer.Open\": \"\",\n  \"AddMCPServer.Add\": \"\",\n  \"LLMChatOptions.MCPTools\": \"\",\n  \"LLMChatOptions.DatabaseAccess\": \"\",\n  \"MCPServersToolbar.stopAllToggle\": \"\",\n  \"MCPServersToolbar.searchTools\": \"\",\n  ConnectionsOptions: \"\",\n  \"ConnectionsOptions.showStateDatabase\": \"\",\n  \"ConnectionsOptions.showDatabaseNames\": \"\",\n  \"AuthProviderSetup.websiteURL\": \"\",\n  \"AuthProviderSetup.defaultUserType\": \"\",\n  \"AuthProviders.list\": \"\",\n  EmailSMTPSetup: \"\",\n  EmailTemplateSetup: \"\",\n  \"WorkspaceMenu.list\": \"\",\n  WorkspaceSettings: \"\",\n  \"LLMChatOptions.toggle\": \"\",\n  \"LLMChat.select\": \"\",\n  \"LLMChatOptions.Prompt\": \"\",\n  \"LLMChatOptions.Model\": \"\",\n  \"AskLLMChat.NewChat\": \"\",\n  \"AskLLMChat.LoadSuggestedToolsAndPrompt\": \"\",\n  \"AskLLMChat.LoadSuggestedDashboards\": \"\",\n  \"AskLLMChat.UnloadSuggestedDashboards\": \"\",\n  \"AskLLMToolApprover.AllowAlways\": \"\",\n  \"AskLLMToolApprover.AllowOnce\": \"\",\n  \"AskLLMToolApprover.Deny\": \"\",\n  MonacoEditor: \"\",\n  MCPServerTools: \"\",\n  \"MCPServerFooterActions.logs\": \"\",\n  \"MCPServerFooterActions.config\": \"\",\n  \"MCPServerFooterActions.enableToggle\": \"\",\n  \"MCPServerFooterActions.refreshTools\": \"\",\n  MCPServerConfigButton: \"\",\n  MCPServerConfig: \"\",\n  \"MCPServerConfig.save\": \"\",\n  \"MCPServers.toggleAutoApprove\": \"\",\n  Feedback: \"\",\n  \"FileImporterFooter.import\": \"\",\n\n  Sessions: \"Active sessions list\",\n  \"Account.ChangePassword\": \"Change password\",\n  NavBar: \"\",\n  \"NavBar.mobileMenuToggle\": \"\",\n  \"NavBar.logout\": \"\",\n  CommandPalette: \"\",\n  \"Chat.attachedFiles\": \"\",\n  \"Window.W_QuickMenu.addCrossFilteredTable\": \"Add cross-filtered table\",\n  Alert: \"Alert popup\",\n  ErrorComponent: \"\",\n  ToolUseMessage: \"\",\n  \"ToolUseMessage.toggle\": \"\",\n  \"ToolUseMessage.Popup\": \"\",\n  MarkdownMonacoCode: \"\",\n  \"MCPServersInstall.install\": \"\",\n  NewConnectionForm: \"\",\n  \"BackupsControls.Completed\": \"Completed backups list\",\n  \"AllowedOriginCheck.FormField\": \"\",\n  \"APIDetailsWs.Examples\": \"\",\n  \"APIDetailsHttp.Examples\": \"\",\n  AllowedOriginCheck: \"\",\n  \"APIDetailsTokens.CreateToken\": \"\",\n  \"APIDetailsTokens.CreateToken.daysUntilExpiration\": \"\",\n  \"APIDetailsTokens.CreateToken.generate\": \"\",\n  APIDetailsTokens: \"\",\n  \"AskLLM.DeleteMessage\": \"\",\n  \"DockerSandboxCreateContainer.Logs\": \"\",\n  TableBody: \"\",\n  \"ServerSideFunctions.onMountEnabled\": \"\",\n  DashboardMenu: \"\",\n  \"SearchAll.Popup\": \"\",\n\n  AddComputedColMenu: \"\",\n  \"AddComputedColMenu.countOfAllRows\": \"\",\n  \"AddComputedColMenu.addBtn\": \"\",\n  \"AddComputedColMenu.name\": \"\",\n  \"AddComputedColMenu.addTo\": \"\",\n  \"LinkedColumn.joinType\": \"\",\n  \"LinkedColumn.layoutType\": \"\",\n  \"CreateColumn.next\": \"\",\n  FileColumnConfigEditor: \"\",\n  \"FileColumnConfigEditor.maxFileSizeMB\": \"\",\n  \"FileColumnConfigEditor.contentMode\": \"\",\n  CreateFileColumn: \"\",\n  ColumnQuickStats: \"\",\n  \"ColumnQuickStats.addFilter\": \"\",\n  \"QuickAddComputedColumn.Add\": \"\",\n  \"QuickAddComputedColumn.name\": \"\",\n  \"LLMChatOptions.Prompt.Preview\": \"\",\n  \"FunctionColumnList.SearchInput\": \"\",\n  \"LLMChatOptions.Model.AddCredentials\": \"\",\n  QuickFilterGroupsControl: \"\",\n  LinkedColumn: \"\",\n  CreateColumn: \"\",\n  \"ToolUseMessage.toggleGroup\": \"\",\n  \"PGDumpOptions.format\": \"\",\n  \"PGDumpOptions.destination\": \"\",\n  \"PGDumpOptions.numberOfJobs\": \"\",\n  \"PGDumpOptions.compressionLevel\": \"\",\n  \"PGDumpOptions.excludeSchema\": \"\",\n  \"PGDumpOptions.noOwner\": \"\",\n  \"PGDumpOptions.create\": \"\",\n  \"PGDumpOptions.globalsOnly\": \"\",\n  \"PGDumpOptions.rolesOnly\": \"\",\n  \"PGDumpOptions.schemaOnly\": \"\",\n  \"PGDumpOptions.encoding\": \"\",\n  \"PGDumpOptions.clean\": \"\",\n  \"PGDumpOptions.dataOnly\": \"\",\n  \"PGDumpOptions.ifExists\": \"\",\n  \"PGDumpOptions.keepLogs\": \"\",\n  \"BackupControls.backupsInProgress\": \"\",\n  \"BackupsControls.Completed.delete\": \"\",\n  \"BackupsControls.Completed.download\": \"\",\n  \"BackupsControls.Completed.restore\": \"\",\n  \"BackupsControls.Completed.deleteAll\": \"\",\n  \"BackupsControls.restoreFromFile\": \"\",\n  BackupLogs: \"\",\n  FilterWrapper_FieldName: \"\",\n  FilterWrapper_Field: \"\",\n  \"CloudStorageCredentialSelector.selectCredential\": \"\",\n  DashboardMenuContent: \"\",\n} as const satisfies Record<\n  string,\n  | string\n  | {\n      desc: string;\n      uiOnly?: true;\n    }\n>;\nexport type Command = keyof typeof COMMANDS;\n\nexport type TestSelectors = {\n  \"data-command\"?: Command;\n  \"data-key\"?: string;\n  id?: string;\n};\n\nexport const dataCommand = (cmd: Command): { \"data-command\": Command } => ({\n  \"data-command\": cmd,\n});\nexport const getCommandElemSelector = (cmd: Command) => {\n  return `[data-command=${JSON.stringify(cmd)}]`;\n};\nexport const getDataKeyElemSelector = (key: string) => {\n  return `[data-key=${JSON.stringify(key)}]`;\n};\nexport const getDataLabelElemSelector = (key: string) => {\n  return `[data-label=${JSON.stringify(key)}]`;\n};\n\nexport const COMMAND_SEARCH_ATTRIBUTE_NAME = \"data-command-search-ended\";\n\nexport const MOCK_ELECTRON_WINDOW_ATTR = \"MOCK_ELECTRON_WINDOW_ATTR\" as const;\n\ndeclare module \"react\" {\n  interface HTMLAttributes<T> {\n    \"data-command\"?: Command;\n  }\n}\n\nexport declare namespace SVGif {\n  export type CursorAnimation =\n    | {\n        elementSelector: string;\n        offset?: { x: number; y: number };\n        duration: number;\n        type: \"click\" | \"clickAppearOnHover\";\n\n        /**\n         * Time to wait before clicking after reaching the final position\n         * */\n        waitBeforeClick?: number;\n        /**\n         * Time to stay on the final position after clicking\n         */\n        lingerMs?: number;\n      }\n    | {\n        type: \"moveTo\";\n        xy: [number, number];\n        duration: number;\n      };\n  export type Animation =\n    | CursorAnimation\n    | {\n        elementSelector: string;\n        duration: number;\n        type: \"type\";\n        extraAnimation?:\n          | { type: \"zoomToElement\" }\n          | { type: \"bringToFront\"; elementSelector: string };\n        /**\n         * Maximum scale to zoom in while typing\n         */\n        maxScale?: number;\n      }\n    | {\n        elementSelector: string;\n        duration: number;\n        type: \"zoomToElement\" | \"bringToFront\";\n        bringToFrontSelector?: string;\n        /**\n         * Maximum scale to zoom in\n         */\n        maxScale?: number;\n      }\n    | {\n        elementSelector: string;\n        duration: number;\n        type: \"fadeIn\" | \"growIn\";\n      }\n    | {\n        type: \"wait\";\n        duration: number;\n      };\n  export type Scene = {\n    svgFileName: string;\n    caption?: string;\n    animations: Animation[];\n  };\n}\n\n/**\n * TODO: Forbid imports to ensure this file is portable\n */\n"
  },
  {
    "path": "e2e/tests/command_palette.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\nimport { COMMAND_SEARCH_ATTRIBUTE_NAME } from \"./Testing\";\nimport { login, MINUTE, PageWIds } from \"./utils/utils\";\nimport { goTo } from \"utils/goTo\";\nimport { USERS } from \"utils/constants\";\n\ntest.use({\n  viewport: {\n    width: 900,\n    height: 600,\n  },\n  trace: \"retain-on-failure\",\n  launchOptions: {\n    args: [\"--start-maximized\"],\n  },\n});\n\nconst IS_PIPELINE = process.env.CI === \"true\";\n\ntest.describe(\"Test command palette\", () => {\n  test.describe.configure({\n    retries: 0,\n    mode: \"parallel\",\n    timeout: 15 * MINUTE,\n  });\n\n  let flatUIDocs: any[];\n  test.beforeEach(async ({ page: p }) => {\n    const page = p as PageWIds;\n    page.on(\"console\", console.log);\n    page.on(\"pageerror\", console.error);\n\n    if (!flatUIDocs) {\n      if (IS_PIPELINE) {\n        // Takes too long. Run locally only\n        return;\n      }\n      await goTo(page, \"/\");\n      await page.waitForTimeout(500);\n      flatUIDocs = await page.evaluate(() => {\n        //@ts-ignore\n        return window.flatUIDocs;\n      });\n      if (!flatUIDocs || !flatUIDocs.length) {\n        throw new Error(\"No docs found in the command search\");\n      }\n    }\n\n    await page.waitForTimeout(100);\n  });\n\n  const workers = 2;\n  for (let i = 0; i < workers; i++) {\n    test(`Test command search worker: ${i}`, async ({ page: p }) => {\n      const page = p as PageWIds;\n      if (IS_PIPELINE) {\n        // Takes too long. Run locally only\n        return;\n      }\n      await login(page, USERS.test_user, \"/login\");\n\n      const workerFlatDocsBatchSize = Math.ceil(flatUIDocs.length / workers);\n      const startIndex = i * workerFlatDocsBatchSize;\n      const workerFlatDocs = flatUIDocs.slice(\n        startIndex,\n        startIndex + workerFlatDocsBatchSize,\n      ) as {\n        title: string;\n        parentTitles: string[];\n      }[];\n      console.log(\n        `Worker ${i} has ${workerFlatDocs.length} docs to test`,\n        flatUIDocs.length,\n      );\n      await page.waitForTimeout(500);\n\n      if (!workerFlatDocs.length) {\n        throw new Error(\"No docs found in the command search\");\n      }\n\n      const batchSize = 50;\n      do {\n        const batchItems = workerFlatDocs.splice(0, batchSize);\n        await page.reload();\n        await page.waitForTimeout(1500);\n        console.log(`Remaining docs: ${workerFlatDocs.length}`);\n        for (const [indx, doc] of batchItems.entries()) {\n          if (doc.title === \"Logout\") {\n            continue; // Skip logout as it will close the session\n          }\n\n          console.log(doc.parentTitles.join(\" > \") + \" -\", doc.title);\n          await page.keyboard.press(\"Control+KeyK\", { delay: 100 });\n          await page\n            .getByTestId(\"CommandPalette\")\n            .locator(\"input\")\n            .fill(doc.title);\n          await page.waitForTimeout(200);\n          await page.keyboard.press(\"Enter\");\n          await page.waitForTimeout(200);\n\n          await expect(page.locator(\"body\")).toHaveAttribute(\n            COMMAND_SEARCH_ATTRIBUTE_NAME,\n            doc.title,\n            { timeout: 15_000 },\n          );\n          await page.evaluate((COMMAND_SEARCH_ATTRIBUTE_NAME) => {\n            document.body.removeAttribute(COMMAND_SEARCH_ATTRIBUTE_NAME);\n          }, COMMAND_SEARCH_ATTRIBUTE_NAME);\n\n          /** Close any popups */\n          await page.keyboard.press(\"Escape\", { delay: 100 });\n          await page.keyboard.press(\"Escape\", { delay: 100 });\n          await page.waitForTimeout(100);\n        }\n      } while (workerFlatDocs.length > 0);\n    });\n  }\n});\n"
  },
  {
    "path": "e2e/tests/createReceipt.ts",
    "content": "import type { PageWIds } from \"utils/utils\";\nimport * as path from \"path\";\n\nexport const createReceipt = async (page1: PageWIds) => {\n  const context = await page1.context();\n  const page = await context.newPage();\n  const width = 500;\n  const height = 600;\n  await page.setViewportSize({ width, height });\n  const receiptData = {\n    hotelName: \"Grand Ocean Hotel\",\n    guestName: \"John Doe\",\n    roomNumber: \"305\",\n    checkIn: \"2025-09-10\",\n    checkOut: \"2025-09-12\",\n    amount: \"$450.00\",\n    receiptNumber: \"RCPT-20250911-001\",\n  };\n\n  const receiptHTML = `\n    <html>\n      <head>\n        <style>\n          body {\n            font-family: 'Arial', sans-serif;\n            width: 500px;\n            margin: 0;\n            padding: 20px;\n            border: 2px solid #333;\n            border-radius: 10px;\n            background: #fdfdfd;\n          }\n          .header {\n            text-align: center;\n            margin-bottom: 20px;\n          }\n          .logo {\n            width: 80px;\n            height: 80px;\n            background: #ccc;\n            border-radius: 50%;\n            display: inline-block;\n            margin-bottom: 10px;\n          }\n          h1 {\n            margin: 0;\n            font-size: 24px;\n            color: #2c3e50;\n          }\n          table {\n            width: 100%;\n            border-collapse: collapse;\n            margin: 20px 0;\n          }\n          td {\n            padding: 8px 5px;\n          }\n          tr:nth-child(even) {\n            background: #f0f0f0;\n          }\n          .amount {\n            font-weight: bold;\n            font-size: 18px;\n            text-align: right;\n          }\n          .footer {\n            text-align: center;\n            margin-top: 20px;\n            font-size: 12px;\n            color: #555;\n          }\n          .qr {\n            display: block;\n            margin: 10px auto;\n            width: 80px;\n            height: 80px;\n            background: #eee;\n            text-align: center;\n            line-height: 80px;\n            color: #999;\n            font-size: 10px;\n            font-weight: bold;\n          }\n        </style>\n      </head>\n      <body>\n        <div class=\"header\">\n          <div class=\"logo\">Logo</div>\n          <h1>${receiptData.hotelName}</h1>\n        </div>\n\n        <table>\n          <tr><td><strong>Guest:</strong></td><td>${receiptData.guestName}</td></tr>\n          <tr><td><strong>Room:</strong></td><td>${receiptData.roomNumber}</td></tr>\n          <tr><td><strong>Check-in:</strong></td><td>${receiptData.checkIn}</td></tr>\n          <tr><td><strong>Check-out:</strong></td><td>${receiptData.checkOut}</td></tr>\n          <tr><td><strong>Amount:</strong></td><td class=\"amount\">${receiptData.amount}</td></tr>\n        </table>\n\n        <div class=\"footer\">\n          Receipt #: ${receiptData.receiptNumber}\n          <div class=\"qr\">QR</div>\n        </div>\n      </body>\n    </html>\n  `;\n\n  await page.setContent(receiptHTML, { waitUntil: \"domcontentloaded\" });\n\n  const fileName = \"hotel_receipt.png\";\n  // Take screenshot\n  const filePath = path.join(__dirname, \"../demo\", fileName);\n  await page.screenshot({\n    path: filePath,\n    fullPage: true,\n  });\n\n  await page.close();\n  return { filePath };\n};\n"
  },
  {
    "path": "e2e/tests/create_docs.spec.ts",
    "content": "import { test } from \"@playwright/test\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { saveSVGs } from \"./svgScreenshots/utils/saveSVGs\";\nimport {\n  login,\n  MINUTE,\n  openConnection,\n  PageWIds,\n  restoreFromBackup,\n  runDbsSql,\n} from \"./utils/utils\";\nimport { DOCS_DIR } from \"svgScreenshots/utils/constants\";\nimport { svgScreenshotsCompleteReferenced } from \"svgScreenshots/utils/svgScreenshotsCompleteReferenced\";\nimport { USERS } from \"utils/constants\";\nimport { goTo } from \"utils/goTo\";\n\ntest.use({\n  viewport: {\n    width: 900,\n    height: 900,\n  },\n  trace: \"retain-on-failure\",\n  launchOptions: {\n    args: [\"--start-maximized\"],\n  },\n});\n\nconst IS_PIPELINE = process.env.CI === \"true\";\n\ntest.describe(\"Create docs and screenshots\", () => {\n  test.describe.configure({\n    retries: 0,\n    mode: \"serial\",\n    timeout: 18 * MINUTE,\n  });\n\n  test(`Restore databases`, async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await login(page, USERS.test_user, \"/login\");\n    // await saveSVGifs(page); throw \"hehe\"; // For debugging\n    await openConnection(page, \"prostgles_video_demo\");\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.bkp\").click();\n    await restoreFromBackup(page, \"Demo\");\n\n    // Disable onMount\n    await goTo(page, \"/connections\");\n    await openConnection(page, \"food_delivery\");\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.methods\").click();\n    const onMountToggle = await page.getByTestId(\n      \"ServerSideFunctions.onMountEnabled\",\n    );\n    if ((await onMountToggle.getAttribute(\"aria-checked\")) === \"true\") {\n      await onMountToggle.click();\n      await page.waitForTimeout(1000);\n    }\n  });\n\n  test(\"Create docs\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await login(page, USERS.test_user, \"/login\");\n\n    if (!IS_PIPELINE) {\n      /** Delete existing markdown docs */\n      if (fs.existsSync(DOCS_DIR)) {\n        fs.rmSync(DOCS_DIR, { force: true, recursive: true });\n      }\n      fs.mkdirSync(DOCS_DIR, { recursive: true });\n    }\n\n    const files: { fileName: string; text: string }[] = await page.evaluate(\n      async () => {\n        //@ts-ignore\n        return window.documentation;\n      },\n    );\n    await page.waitForTimeout(100);\n    for (const file of files) {\n      const filePath = path.join(DOCS_DIR, file.fileName);\n\n      const preparedFileContent = getDocWithDarkModeImgTags(file.text);\n      if (IS_PIPELINE) {\n        const existingFile = fs.readFileSync(filePath, \"utf-8\");\n        if (existingFile !== preparedFileContent) {\n          console.error(existingFile, preparedFileContent);\n          throw new Error(\n            `File ${file.fileName} has changed. Please update the docs. Existing ${existingFile} Expected ${preparedFileContent}`,\n          );\n        }\n      } else {\n        fs.writeFileSync(filePath, preparedFileContent, \"utf-8\");\n      }\n      await page.waitForTimeout(100);\n    }\n\n    /** Ensure all scripts exist in the readme to ensure we don't show non-tested scripts */\n    const uiInstallationFile = fs.readFileSync(\n      path.join(DOCS_DIR, \"02_Installation.md\"),\n      \"utf-8\",\n    );\n    const mainReadmeFile = fs.readFileSync(\n      path.join(__dirname, \"../../\", \"README.md\"),\n      \"utf-8\",\n    );\n    const getScripts = (fileContent: string) => {\n      const scripts = fileContent\n        .split(\"```\")\n        .slice(1)\n        .filter((_, index) => index % 2 === 0)\n        .map((script) => {\n          return script.split(\"```\")[0].trim();\n        });\n      return scripts;\n    };\n    const docsScripts = getScripts(uiInstallationFile);\n    const readmeScripts = getScripts(mainReadmeFile);\n    if (!docsScripts.length) {\n      throw new Error(\"No scripts found in the installation file\");\n    }\n\n    for (const script of docsScripts) {\n      if (!readmeScripts.includes(script)) {\n        throw new Error(\n          `Script \"${script}\" not found in the main README file. Please ensure all scripts are included.`,\n        );\n      }\n    }\n  });\n\n  test(\"Create screenshots\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await login(page, USERS.test_user, \"/login\");\n    if (!IS_PIPELINE) {\n      await page.waitForTimeout(1100);\n\n      await prepare(page);\n      const { svgifSpecs, overviewSvgifSpecs, svgifCovers } =\n        await saveSVGs(page);\n      const svgFilesUsedExternally = [\n        ...overviewSvgifSpecs\n          .filter((s) => s.usedExternally)\n          .map((s) => s.fileName + \".svgif\"),\n        ...svgifCovers.map((c) => c.fileName),\n      ];\n      await svgScreenshotsCompleteReferenced(\n        svgifSpecs.flatMap((s) => s.scenes),\n        svgFilesUsedExternally,\n      );\n    }\n  });\n});\n\nconst getDocWithDarkModeImgTags = (fileContent: string) => {\n  const imgTags = fileContent.split(\"<img\");\n  // Must replace all img tags with theme aware src\n  if (imgTags.length > 1) {\n    imgTags.slice(1).forEach((imgTag, index) => {\n      const tagText = \"<img\" + imgTag.split(\"/>\")[0] + \"/>\";\n      fileContent = fileContent.replaceAll(\n        tagText,\n        tagText.replace(\"/>\", `style=\"border: 1px solid; margin: 1em 0;\" />`),\n      );\n    });\n  }\n  return fileContent;\n};\n\nconst prepare = async (page: PageWIds) => {\n  await runDbsSql(\n    page,\n    \"UPDATE database_configs SET table_schema_transform = $1, table_schema_positions = $2 WHERE db_name = 'prostgles_video_demo';\",\n    [\n      {\n        scale: 0.6054127940141592,\n        translate: {\n          x: 255.48757732436974,\n          y: 222.24872020228725,\n        },\n      },\n      {\n        chats: {\n          x: 56.65461492027448,\n          y: -48.61370734626732,\n        },\n        users: {\n          x: -386.5771496372805,\n          y: -94.50849696823889,\n        },\n        orders: {\n          x: -24.605404166298356,\n          y: 149.52211472623705,\n        },\n        contacts: {\n          x: -37.987816516202955,\n          y: -274.46819244071696,\n        },\n        messages: {\n          x: 264.9505194760658,\n          y: -214.05139083148245,\n        },\n        chat_members: {\n          x: 279.98574299550825,\n          y: 50.72883570617583,\n        },\n      },\n    ],\n  );\n};\n"
  },
  {
    "path": "e2e/tests/create_load_test_users.spec.ts",
    "content": "import { test } from \"@playwright/test\";\nimport { PageWIds, login } from \"./utils/utils\";\nimport { USERS } from \"utils/constants\";\ntest.use({\n  viewport: { width: 1280, height: 1080 },\n  video: {\n    mode: \"on\",\n    size: { width: 1280, height: 1080 },\n  },\n});\n\nconst loadUsers = new Array(100).fill(0).map((_, i) => `load_user_${i}`);\ntest.describe(\"Create load test users\", () => {\n  test.beforeEach(async ({ page }) => {\n    page.on(\"console\", console.log);\n    page.on(\"pageerror\", console.error);\n\n    await page.waitForTimeout(100);\n  });\n\n  test(\"Create load test users\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await login(page, USERS.test_user, \"http://localhost:3004/login\");\n    await page.evaluate(async (loadUsers) => {\n      try {\n        const existingLoadUsers = await (window as any).dbs.users.count({\n          username: { $in: loadUsers },\n        });\n        if (+existingLoadUsers > 10) {\n          return;\n        }\n        await (window as any).dbs.users.insert(\n          loadUsers.map((username) => ({\n            username,\n            password: username,\n            type: \"admin\",\n            status: \"active\",\n          })),\n        );\n      } catch (err: any) {\n        document.body.innerText = JSON.stringify(err.message ?? err);\n        throw err;\n      }\n    }, loadUsers);\n    await page.waitForTimeout(2100);\n  });\n});\n"
  },
  {
    "path": "e2e/tests/demo_video.spec.ts",
    "content": "import { test } from \"@playwright/test\";\nimport {\n  PageWIds,\n  createDatabase,\n  login,\n  openConnection,\n  runDbsSql,\n  setupProstglesLLMProvider,\n} from \"./utils/utils\";\nimport { USERS } from \"utils/constants\";\nimport { goTo } from \"utils/goTo\";\n// const viewPortSize = { width: 1920, height: 1080 };\nconst viewPortSize = { width: 1280, height: 1080 };\ntest.use({\n  viewport: viewPortSize,\n  video: {\n    mode: \"on\",\n    size: viewPortSize,\n  },\n  trace: \"on\",\n  launchOptions: {\n    args: [\"--start-maximized\"],\n  },\n});\n\nconst videoTestDuration = 10 * 60e3;\ntest.describe(\"Demo video\", () => {\n  test.setTimeout(videoTestDuration);\n\n  test(\"Video demo\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await login(page, USERS.test_user, \"/login\");\n    await page.waitForTimeout(2000);\n    // await page.getByTestId(\"App.colorScheme\").click();\n    // // await page.getByTestId(\"App.colorScheme\").locator(`[data-key=light]`).click();\n    // await page.getByTestId(\"App.colorScheme\").locator(`[data-key=dark]`).click();\n    const getVideoDemoConnection = async () => {\n      // await goTo(page, \"/connections\");\n      const videoDemoConnection = await page.getByRole(\"link\", {\n        name: \"prostgles_video_demo\",\n        exact: true,\n      });\n      return videoDemoConnection;\n    };\n\n    const localVideoDemoConnection = await getVideoDemoConnection();\n    if (await localVideoDemoConnection.isVisible()) {\n      await localVideoDemoConnection.click();\n    } else {\n      await createDatabase(\"prostgles_video_demo\", page);\n      await goTo(page, \"/connections\");\n      await createDatabase(\"food_delivery\", page, true);\n    }\n\n    await setupProstglesLLMProvider(page);\n\n    const startDemo = async (theme: \"dark\" | \"light\") => {\n      await goTo(page, \"/connections\");\n      await page.getByTestId(\"App.colorScheme\").click();\n      await page\n        .getByTestId(\"App.colorScheme\")\n        .locator(`[data-key=${theme}]`)\n        .click();\n      await page.waitForTimeout(1000);\n      const videoDemoConnection = await getVideoDemoConnection();\n\n      await videoDemoConnection.click();\n      await page.waitForTimeout(2e3);\n      await page\n        .getByTestId(\"AppDemo.start\")\n        .evaluate(async (node: HTMLButtonElement) => {\n          try {\n            await (node as any).start();\n          } catch (e) {\n            console.error(e);\n            console.error(JSON.stringify(e));\n            throw JSON.stringify(e);\n          }\n        });\n      await page.waitForTimeout(1e3);\n    };\n    await startDemo(\"light\");\n  });\n\n  test(`Backup databases`, async ({ page: p }) => {\n    const page = p as PageWIds;\n    await login(page, USERS.test_user, \"/login\");\n    await openConnection(page, \"prostgles_video_demo\");\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.bkp\").click();\n    await page.getByTestId(\"config.bkp.create\").click();\n    await page.getByTestId(\"config.bkp.create.name\").fill(\"Demo\");\n    await page.getByTestId(\"config.bkp.create.start\").click();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/demo_video_setup.spec.ts",
    "content": "import { test } from \"@playwright/test\";\nimport { PageWIds, createDatabase, login } from \"./utils/utils\";\nimport { USERS } from \"utils/constants\";\nimport { goTo } from \"utils/goTo\";\n\nconst videoTestDuration = 10 * 60e3;\ntest.describe(\"Demo video setup\", () => {\n  test.setTimeout(videoTestDuration);\n\n  test(\"Demo\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await login(page, USERS.test_user, \"http://localhost:3004/login\");\n    await page.waitForTimeout(2000);\n    const getVideoDemoConnection = async () => {\n      await page.getByRole(\"link\", { name: \"Connections\" }).click();\n      const videoDemoConnection = await page.getByRole(\"link\", {\n        name: \"prostgles_video_demo\",\n        exact: true,\n      });\n      return videoDemoConnection;\n    };\n    const localVideoDemoConnection = await getVideoDemoConnection();\n    if (await localVideoDemoConnection.isVisible()) {\n      await localVideoDemoConnection.click();\n    } else {\n      await createDatabase(\"prostgles_video_demo\", page);\n      await goTo(page, \"http://localhost:3004/connections\");\n      await createDatabase(\"food_delivery\", page, true);\n      await page.waitForTimeout(2000);\n      await goTo(page, \"http://localhost:3004/connections\");\n      await createDatabase(\"crypto\", page, true);\n    }\n  });\n});\n"
  },
  {
    "path": "e2e/tests/load_test.spec.ts",
    "content": "import { test } from \"@playwright/test\";\nimport { PageWIds, login } from \"./utils/utils\";\ntest.use({\n  viewport: { width: 1280, height: 1080 },\n  video: {\n    mode: \"on\",\n    size: { width: 1280, height: 1080 },\n  },\n});\n\ntest.describe.serial(\"Load test first cache\", () => {\n  test(\"Load test first cache\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await login(page, \"load_user_0\", \"http://localhost:3004/login\");\n  });\n});\nconst loadUsers = new Array(22).fill(0).map((_, i) => `load_user_${i}`);\ntest.describe.configure({ mode: \"parallel\" });\nfor (const [idx, user] of loadUsers.entries()) {\n  test.describe(\"Load test_\" + idx, () => {\n    test(\"Load test\", async ({ page: p }) => {\n      const page = p as PageWIds;\n      await login(page, user, \"http://localhost:3004/login\");\n      await page.waitForTimeout(2000);\n      const getVideoDemoConnection = async () => {\n        await page\n          .getByRole(\"link\", { name: \"Connections\" })\n          .click({ timeout: 10e3 });\n        const videoDemoConnection = await page.getByRole(\"link\", {\n          name: \"prostgles_video_demo\",\n          exact: true,\n        });\n        return videoDemoConnection;\n      };\n      const localVideoDemoConnection = await getVideoDemoConnection();\n      if (await localVideoDemoConnection.isVisible()) {\n        await localVideoDemoConnection.click();\n      }\n\n      // await page.getByTestId(\"AppDemo.start\")\n      //   .evaluate(async (node: HTMLButtonElement) => {\n      //     try {\n      //       await (node as any).start();\n      //     } catch (e) {\n      //       console.error(e);\n      //       console.error(JSON.stringify(e));\n      //       throw JSON.stringify(e);\n      //     }\n      //   });\n      await page.waitForTimeout(20e3);\n    });\n  });\n}\n"
  },
  {
    "path": "e2e/tests/main.spec.ts",
    "content": "import { chromium, expect, test } from \"@playwright/test\";\nimport { authenticator } from \"otplib\";\nimport { speechToTextTest } from \"speechToTextTest\";\nimport {\n  localNoAuthSetup,\n  QUERIES,\n  TEST_DB_NAME,\n  USERS,\n} from \"utils/constants\";\nimport { goTo } from \"utils/goTo\";\nimport { isPortFree } from \"utils/isPortFree\";\nimport { startMockSMTPServer } from \"./mockSMTPServer\";\nimport { testAskLLMCode } from \"./testAskLLM\";\nimport { getCommandElemSelector, getDataKeyElemSelector } from \"./Testing\";\nimport {\n  clickInsertRow,\n  closeWorkspaceWindows,\n  createAccessRule,\n  createAccessRuleForTestDB,\n  createDatabase,\n  deleteExistingLLMChat,\n  disablePwdlessAdminAndCreateUser,\n  dropConnectionAndDatabase,\n  enableAskLLM,\n  fileName,\n  fillLoginFormAndSubmit,\n  fillSmartForm,\n  forEachLocator,\n  getAskLLMLastMessage,\n  getDataKey,\n  getLLMResponses,\n  getMonacoEditorBySelector,\n  getMonacoValue,\n  getSearchListItem,\n  getSelector,\n  getTableWindow,\n  insertRow,\n  login,\n  loginWhenSignupIsEnabled,\n  monacoType,\n  openConnection,\n  openTable,\n  PageWIds,\n  restoreFromBackup,\n  runDbSql,\n  runDbsSql,\n  runSql,\n  selectAndUpsertFile,\n  sendAskLLMMessage,\n  setModelByText,\n  setPromptByText,\n  setTableRule,\n  setWspColLayout,\n  typeConfirmationCode,\n  uploadFile,\n} from \"./utils/utils\";\nimport exp = require(\"constants\");\n\nconst DB_NAMES = {\n  test: TEST_DB_NAME,\n  food_delivery: \"food_delivery\",\n  // sample_database: \"sample_database\",\n  video_demo_database: \"prostgles_video_demo\",\n};\n\ntest.describe.configure({ mode: \"serial\" });\ntest.describe(\"Main test\", () => {\n  if (process.env.ONLY_VIDEO) {\n    test.skip();\n  }\n  let getEmails: () => any[];\n\n  test.beforeAll(async () => {\n    ({ getEmails } = startMockSMTPServer());\n    console.log(\"getEmails\", getEmails());\n    return { getEmails };\n  });\n\n  test.beforeEach(async ({ page }) => {\n    page.on(\"console\", console.log);\n    page.on(\"pageerror\", console.error);\n    await page.waitForTimeout(200);\n  });\n\n  const deleteAllBackups = async (page: PageWIds) => {\n    await page.getByTestId(\"config.bkp\").click();\n    await page\n      .getByTestId(\"BackupsControls.Completed\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n    const canDelete = await page.getByRole(\"button\", { name: \"Delete all...\" });\n    if (await canDelete.count()) {\n      await canDelete.click();\n      await typeConfirmationCode(page);\n      await page\n        .getByRole(\"button\", { name: \"Force delete backups\", exact: true })\n        .click();\n      await page.waitForTimeout(1e3);\n    }\n  };\n\n  test(\"port 3009 must be available for mcpsandbox test\", async () => {\n    const free = await isPortFree(3009);\n    expect(free).toBe(true);\n  });\n\n  test(\"Connecting with an existing sid to a fresh instance will ignore the sid IF passwordless admin did not claim a session yet\", async ({\n    page: p,\n  }) => {\n    const page = p as PageWIds;\n    const url = new URL(\"http://localhost:3004\");\n    await page.context().addCookies([\n      {\n        name: \"sid_token\",\n        value: \"1random-sid\",\n        domain: url.hostname,\n        path: \"/\",\n        httpOnly: true,\n        secure: url.protocol.startsWith(\"https\"),\n        sameSite: \"Lax\",\n      },\n    ]);\n\n    await goTo(page);\n\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: \"Prostgles UI state\" }).click();\n\n    const browser2 = await chromium.launch();\n    const newPage = await browser2.newPage();\n\n    await newPage.context().addCookies([\n      {\n        name: \"sid_token\",\n        value: \"2random-sid\",\n        domain: url.hostname,\n        path: \"/\",\n        httpOnly: true,\n        secure: url.protocol.startsWith(\"https\"),\n        sameSite: \"Lax\",\n      },\n    ]);\n    await goTo(newPage);\n    await expect(await newPage.textContent(\"body\")).toContain(\n      \"Only 1 session is allowed for the passwordless admin\",\n    );\n    await browser2.close();\n    await newPage.close();\n\n    await runDbsSql(\n      page,\n      `\n      DELETE FROM magic_links; \n      DELETE FROM sessions; \n      DELETE FROM login_attempts`,\n    ).catch(async (e) => {\n      if (e && e.code === \"40P01\") {\n        const info = await runDbsSql(\n          page,\n          `-- Get detailed information about locks on the specific relations\n          SELECT \n            pid,\n            sa.usename,\n            sa.application_name,\n            sa.client_addr,\n            sa.query_start,\n            sa.state,\n            sa.query\n          FROM pg_stat_activity sa WHERE query <> ''`,\n          {},\n          { returnType: \"rows\" },\n        );\n        console.error(\"Deadlock detected:\", info);\n      }\n      return Promise.reject(e);\n    });\n  });\n\n  test(\"Can disable passwordless admin by creating a new admin user. User data is reassigned and accessible to the new user\", async ({\n    page: p,\n  }) => {\n    const page = p as PageWIds;\n\n    const goToWorkspace = async (openUsersTable = true) => {\n      await page\n        .getByRole(\"link\", { name: \"Connections\" })\n        .click({ timeout: 6e3 });\n      await page.getByRole(\"link\", { name: \"Prostgles UI state\" }).click();\n      if (openUsersTable) {\n        await openTable(page, \"users\");\n      }\n      await page\n        .getByTestId(\"dashboard.window.rowInsert\")\n        .and(page.locator(`[data-key=\"users\"]`))\n        .waitFor({ state: \"visible\", timeout: 20e3 });\n    };\n\n    await goTo(page);\n    await goToWorkspace();\n\n    await page.waitForTimeout(500);\n\n    if (!localNoAuthSetup) {\n      await disablePwdlessAdminAndCreateUser(page);\n    }\n    await login(page);\n    await page.waitForTimeout(500);\n\n    await goTo(page, \"/new-connection\");\n    await page.waitForTimeout(500);\n    await goTo(page);\n\n    /** Expect the workspace to have users table open */\n    await goToWorkspace(false);\n\n    /** SmartForm view mode shows correct data */\n    await openTable(page, \"login_attempts\");\n    await page.getByTestId(\"dashboard.window.viewEditRow\").last().click();\n    await page.waitForTimeout(2e3);\n    const formField = await page.locator(\n      getSelector({ testid: \"SmartFormField\", dataKey: \"username\" }),\n    );\n\n    const userTypeField = await formField.textContent();\n    await expect(userTypeField).toEqual(`Username${USERS.test_user}`);\n    await page.getByTestId(\"SmartForm.close\").waitFor({ state: \"visible\" });\n    await page.getByTestId(\"SmartForm.close\").click();\n\n    await closeWorkspaceWindows(page);\n\n    /** SmartForm view mode shows correct buttons */\n    await openTable(page, \"user_types\");\n    await page.getByTestId(\"dashboard.window.viewEditRow\").last().click();\n    await page.getByTestId(\"SmartForm.close\").waitFor({ state: \"visible\" });\n    await page.getByTestId(\"SmartForm.delete\").waitFor({ state: \"visible\" });\n    await page.getByTestId(\"SmartForm.close\").click();\n\n    /** Schema diagram works */\n    await page.getByTestId(\"SchemaGraph\").click();\n    await page.waitForTimeout(1e3);\n    await page\n      .getByText(\"Reset layout\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n  });\n\n  test(\"Email password registration setup\", async ({ page: p, browser }) => {\n    const page = p as PageWIds;\n\n    await login(page);\n\n    await goTo(page, \"/component-list\");\n    await expect(page.locator(\"body\")).toContainText(\n      \"All button heights match for loading state.\",\n    );\n\n    await goTo(page, \"/server-settings\");\n\n    /** SmartForm onLoaded bug */\n    await page.locator(`[data-key=\"security\"]`).click();\n    await page\n      .getByText(\"Allowed IPs and subnets\", { exact: true })\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n\n    await page.locator(`[data-key=\"auth\"]`).click();\n    /** The url will be updated automatically and this will page trigger a reload */\n    await page.waitForTimeout(1e3);\n    const input = await page.getByLabel(\"Website URL\");\n    await expect(await input.getAttribute(\"value\")).toEqual(\n      \"http://localhost:3004\",\n    );\n    await page.getByTestId(\"EmailAuthSetup\").locator(\"button\").click();\n    await page.getByTestId(\"EmailAuthSetup.SignupType\").click();\n    await page.locator(`[data-key=\"withPassword\"]`).click();\n    const fillHostAndTest = async (hostVal: string) => {\n      await page.getByTestId(\"EmailSMTPAndTemplateSetup\").click();\n      await page.getByText(\"Email Provider\").click();\n      await page.locator(`[data-label=\"Host\"] input`).fill(hostVal);\n      await page\n        .getByTestId(\"EmailSMTPAndTemplateSetup\")\n        .getByText(\"Save\")\n        .click();\n    };\n    await fillHostAndTest(\"invalid___prostgles-test-mock\");\n    await page.getByText(\"Enabled\").click();\n    await page.getByText(\"Save\").click({ timeout: 10e3 });\n    const errNode = await page.getByTestId(\"EmailAuthSetup.error\");\n    await expect(await errNode.textContent()).toContain(\n      // \"getaddrinfo ENOTFOUND invalid___prostgles-test-mock\", // or EAI_AGAIN\n      \"getaddrinfo E\",\n    );\n    await expect(await errNode.textContent()).toContain(\n      \"invalid___prostgles-test-mock\",\n    );\n    await fillHostAndTest(\"prostgles-test-mock\");\n    await page.waitForTimeout(1500);\n    const errNode1 = await page.getByTestId(\"EmailAuthSetup.error\");\n    await expect(await errNode1.count()).toBe(0);\n  });\n\n  test(\"Email password registration\", async ({ page: p, browser }) => {\n    const page = await browser.newPage();\n    const newPage = p as PageWIds;\n    await login(page);\n\n    await goTo(newPage, \"/login\");\n\n    /** Test failed login throttle */\n    await newPage.locator(\"#username\").fill(USERS.new_user);\n    await newPage.locator(\"#password\").fill(USERS.new_user);\n    const start = Date.now();\n    await newPage.getByRole(\"button\", { name: \"Sign in\" }).click();\n    await newPage.getByTestId(\"Login.error\").waitFor({ state: \"visible\" });\n    await expect(Date.now() - start).toBeGreaterThan(499);\n    await newPage.reload();\n\n    /** Passwords do not match registration check */\n    await newPage.getByTestId(\"Login.toggle\").click();\n    await newPage.locator(\"#username\").fill(USERS.new_user);\n    await newPage.locator(\"#password\").fill(USERS.new_user);\n    await newPage.getByRole(\"button\", { name: \"Sign up\" }).click();\n    await expect(\n      await newPage.getByTestId(\"Login.error\").textContent(),\n    ).toContain(\"Passwords do not match\");\n    await newPage.locator(\"#new-password\").fill(USERS.new_user);\n    await newPage.getByRole(\"button\", { name: \"Sign up\" }).click();\n    await expect(\n      await newPage\n        .getByTestId(\"AuthNotifPopup\")\n        .getByTestId(\"Popup.content\")\n        .textContent(),\n    ).toBe(\n      \"Email verification sent. Open the verification url or enter the code to confirm your email\",\n    );\n    await newPage.getByRole(\"button\", { name: \"Ok\" }).click();\n\n    const newUser = await runDbsSql(\n      page,\n      `SELECT * FROM users WHERE username = $1`,\n      [USERS.new_user],\n      { returnType: \"row\" },\n    );\n    const code = newUser?.registration?.email_confirmation?.confirmation_code;\n    await expect(typeof code).toBe(\"string\");\n    await expect(code.length).toBe(6);\n    await newPage.locator(\"#email-verification-code\").fill(code);\n    await newPage.getByRole(\"button\", { name: \"Confirm email\" }).click();\n    await expect(\n      await newPage\n        .getByTestId(\"AuthNotifPopup\")\n        .getByTestId(\"Popup.content\")\n        .textContent(),\n    ).toBe(\"Your email has been confirmed. You can now sign in\");\n    await newPage.getByRole(\"button\", { name: \"Ok\" }).click();\n\n    await newPage.locator(\"#username\").fill(USERS.new_user);\n    await newPage.locator(\"#password\").fill(USERS.new_user);\n    await newPage.getByRole(\"button\", { name: \"Sign in\" }).click();\n\n    await newPage.getByTestId(\"App.colorScheme\").waitFor({ state: \"visible\" });\n  });\n\n  test(\"Enable email magic link registrations & Translations\", async ({\n    page: p,\n  }) => {\n    const page = p as PageWIds;\n\n    await login(page);\n    await goTo(page, \"/server-settings\");\n    await page.locator(`[data-key=\"auth\"]`).click();\n    await page.getByTestId(\"EmailAuthSetup\").locator(\"button\").click();\n    await page.getByTestId(\"EmailAuthSetup.SignupType\").click();\n    await page.locator(`[data-key=\"withMagicLink\"]`).click();\n    await page.getByText(\"Save\").click();\n    await page.waitForTimeout(1500);\n    const errNodeCount = await page.getByTestId(\"EmailAuthSetup.error\").count();\n    await expect(errNodeCount).toBe(0);\n\n    await goTo(page, \"/connections\");\n    await page.getByTestId(\"App.LanguageSelector\").click();\n    await page.locator(`[data-key=\"es\"]`).click();\n    await page.waitForLoadState(\"networkidle\");\n    await page.waitForTimeout(500);\n    await page.getByText(\"Nueva conexión\").waitFor({ state: \"visible\" });\n    await page.getByTestId(\"App.LanguageSelector\").click();\n    await page.locator(`[data-key=\"en\"]`).click();\n    await page.waitForLoadState(\"networkidle\");\n    await page.waitForTimeout(500);\n  });\n\n  test(\"Email magic link signup\", async ({ page: p, browser }) => {\n    const newPage = p as PageWIds;\n    const page: PageWIds = await browser.newPage();\n\n    /**\n     * Can still login with password with email magic link registrations\n     */\n    await goTo(page, \"/login\");\n    await page.locator(\"#username\").fill(USERS.test_user);\n    await page.getByRole(\"button\", { name: \"Continue\" }).click();\n    await page.locator(\"#password\").waitFor({ state: \"visible\" });\n    await page.locator(\"#password\").fill(USERS.test_user);\n    await page.getByRole(\"button\", { name: \"Continue\" }).click();\n    await page.getByTestId(\"App.colorScheme\").waitFor({ state: \"visible\" });\n\n    await goTo(newPage, \"/login\");\n\n    await newPage.locator(\"#username\").fill(USERS.new_user1);\n    await newPage.getByRole(\"button\", { name: \"Continue\" }).click();\n\n    await expect(\n      await newPage\n        .getByTestId(\"AuthNotifPopup\")\n        .getByTestId(\"Popup.content\")\n        .textContent(),\n    ).toBe(\"Magic link sent. Open the url from your email to login\");\n    await newPage.getByRole(\"button\", { name: \"Ok\" }).click();\n\n    const newUser = await runDbsSql(\n      page,\n      `\n      SELECT * \n      FROM users \n      WHERE username = $1\n      `,\n      [USERS.new_user1],\n      { returnType: \"row\" },\n    );\n    const failedAttempts = await runDbsSql(\n      page,\n      `\n      DELETE -- SELECT * \n      FROM login_attempts\n      `,\n      undefined,\n      { returnType: \"rows\" },\n    );\n    console.log(\"failedAttempts\", failedAttempts);\n    const code = newUser?.registration.otp_code;\n    await expect(typeof code).toBe(\"string\");\n    await goTo(newPage, `/magic-link?code=${code}&email=${USERS.new_user1}`);\n\n    await newPage.getByTestId(\"App.colorScheme\").waitFor({ state: \"visible\" });\n\n    await runDbsSql(\n      page,\n      `\n      DELETE FROM login_attempts;\n      `,\n      undefined,\n      { returnType: \"row\" },\n    );\n  });\n\n  test(\"System theme works as expected\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await page.emulateMedia({ colorScheme: \"dark\" });\n    await goTo(page);\n    await page.locator(\"html.dark-theme\").waitFor({ state: \"visible\" });\n    const darkBackgroundColor = await page.evaluate(() => {\n      return getComputedStyle(document.body).backgroundColor;\n    });\n    await expect(darkBackgroundColor).toBe(\"rgb(26, 25, 25)\");\n\n    await page.emulateMedia({ colorScheme: \"light\" });\n    await goTo(page);\n    await page.locator(\"html.light-theme\").waitFor({ state: \"visible\" });\n    const lightBackgroundColor = await page.evaluate(() => {\n      return getComputedStyle(document.body).backgroundColor;\n    });\n    await expect(lightBackgroundColor).toBe(\"rgb(244, 245, 247)\");\n  });\n\n  test(\"Setup Free LLM assistant signup\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await loginWhenSignupIsEnabled(page);\n    const existingCloudDb = await page\n      .getByRole(\"link\", {\n        name: \"cloud\",\n        exact: true,\n      })\n      .count();\n\n    /** Create cloud db */\n    if (!existingCloudDb) {\n      const dbName = \"cloud\";\n      await createDatabase(dbName, page, false);\n      await page.getByTestId(\"dashboard.goToConnConfig\").click();\n      await page.getByTestId(\"config.api\").click();\n      await page.locator(\"input#url_path\").fill(dbName);\n      /** Enable http api */\n      await page.getByText(\"Enabled\").click();\n      await page.waitForTimeout(1500);\n    } else {\n      await page.getByRole(\"link\", { name: \"Cloud\" }).click();\n      await page.getByTestId(\"dashboard.goToConnConfig\").click();\n      await page.getByTestId(\"config.api\").click();\n    }\n\n    /** Add server-side func */\n    await page.getByTestId(\"config.methods\").click();\n\n    /** This timeout is crucial in ensuring monaco editor shows suggestions */\n    await page.getByText(\"Create function\").click({ timeout: 10e3 });\n    await page.locator(\"input#function_name\").fill(\"askLLM\");\n    await page.waitForTimeout(1e3);\n    await monacoType(page, \".MethodDefinition\", \"dbo.t\", {\n      deleteAll: false,\n      /** This helps with flaky tests done on workers */\n      pressAfterTyping: [\n        \"Backspace\",\n        \"Backspace\",\n        \"Backspace\",\n        \"Backspace\",\n        \"Backspace\",\n      ],\n    });\n    await monacoType(page, \".MethodDefinition\", \"dbo.t\", {\n      deleteAll: false,\n    });\n    await page.keyboard.press(\"Tab\");\n\n    /** Ensure db schema suggestions work */\n    const initialCode =\n      \"export const run: ProstglesMethod = async (args, { db, dbo, user, callMCPServerTool }) => {\\n  dbo.tx\\n}\";\n    const funcCode = await getMonacoValue(page, \".MethodDefinition\");\n    await expect(funcCode).toEqual(initialCode);\n\n    /** Add llm server side func */\n    const fullCode = initialCode.replace(\"dbo.tx\", testAskLLMCode + \"dbo.tx\");\n    await monacoType(page, \".MethodDefinition\", fullCode, {\n      deleteAll: true,\n      keyPressDelay: 0,\n    });\n    const funcCode2 = await getMonacoValue(page, \".MethodDefinition\");\n    const allWhiteSpaceAsSingleSpace = (v: string) => {\n      const res = v.replace(/\\s+/g, \" \");\n      return res;\n    };\n    await expect(allWhiteSpaceAsSingleSpace(funcCode2)).toEqual(\n      allWhiteSpaceAsSingleSpace(fullCode),\n    );\n\n    /** Add askLLM func args */\n    await page.getByTitle(\"Add new item\").click();\n    await page.getByLabel(\"Argument name\").fill(\"messages\");\n    await page.getByLabel(\"Data type\").click();\n    await page.locator(`[data-key=\"any\"]`).click();\n    await page.getByRole(\"button\", { name: \"Add function\" }).click();\n\n    /** Page will reload after func is added */\n    await page.waitForLoadState(\"networkidle\");\n    await page.waitForTimeout(1e3);\n    /** JSONBSchema localValue bugs. Argument must show */\n    await page.getByTitle(\"Edit function\").click();\n    await page.waitForTimeout(1e3);\n    await page.getByLabel(\"Argument name\").waitFor({ state: \"visible\" });\n    await page.getByTestId(\"Popup.close\").click();\n\n    /**\n     * Publish functions for user\n     */\n    await page.getByTestId(\"config.ac\").click();\n    await createAccessRule(page, \"default\");\n    await page.getByText(\"askLLM\").click();\n    await page.getByText(\"Create rule\").click();\n    await page.waitForTimeout(1e3);\n\n    /** Signup for free LLM assistant */\n    await goTo(page, \"/connections\");\n    await page.getByRole(\"link\", { name: \"Prostgles UI state\" }).click();\n    await page.getByTestId(\"AskLLM\").click();\n    await page.getByTestId(\"SetupLLMCredentials.free\").click({ timeout: 10e3 });\n    await page.locator(\"input#email\").fill(USERS.free_llm_user1);\n    await page.getByTestId(\"ProstglesSignup.continue\").click();\n    await page.waitForTimeout(1e3);\n    const llmUser = await runDbsSql(\n      page,\n      `\n      SELECT * \n      FROM users \n      WHERE username = $1\n      `,\n      [USERS.free_llm_user1],\n      { returnType: \"row\" },\n    );\n    const freeLLMCode = llmUser?.registration.otp_code;\n    await expect(typeof freeLLMCode).toBe(\"string\");\n    await page.locator(\"input#otp-code\").fill(freeLLMCode);\n    await page.getByTestId(\"ProstglesSignup.continue\").click();\n    await page.waitForTimeout(1e3);\n    await page.locator(\".ProstglesSignup\").waitFor({ state: \"detached\" });\n  });\n\n  test(\"Test LLM responses and tools\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await loginWhenSignupIsEnabled(page);\n\n    await openConnection(page, \"cloud\");\n\n    await runDbsSql(\n      page,\n      `\n      UPDATE mcp_servers SET enabled = false WHERE name IN ('playwright', 'docker-sandbox');\n      DELETE FROM mcp_server_tools WHERE server_name IN ('playwright', 'docker-sandbox');\n      `,\n    );\n    await runDbSql(\n      page,\n      `\n      CREATE TABLE IF NOT EXISTS receipts (\n        id SERIAL PRIMARY KEY,\n        company_name TEXT,\n        amount NUMERIC,\n        currency TEXT,\n        date TIMESTAMP,\n        created_at TIMESTAMP DEFAULT NOW()\n      );\n      `,\n    );\n    /* Wait for schema change */\n    await page.waitForTimeout(2e3);\n\n    await deleteExistingLLMChat(page);\n\n    await page.getByTestId(\"Popup.close\").click();\n\n    const userMessage = \"hey\";\n    const responses = await getLLMResponses(page, [userMessage]);\n    const messageCost = \"$0\";\n    await expect(responses).toEqual([\n      {\n        isOk: false,\n        response: messageCost + \"free ai assistant\" + userMessage,\n      },\n    ]);\n\n    await page.getByTestId(\"AskLLM\").click();\n    await setModelByText(page, \"son\");\n\n    await setPromptByText(page, \"Create task\");\n    await sendAskLLMMessage(page, \" task \");\n\n    /** Refresh tools */\n    await runDbsSql(\n      page,\n      `UPDATE mcp_servers SET enabled = true WHERE name = 'fetch';`,\n    );\n    await page.waitForTimeout(5e3);\n    await runDbsSql(\n      page,\n      `UPDATE mcp_servers SET enabled = false WHERE name = 'fetch';`,\n    );\n\n    await page\n      .getByTestId(\"AskLLMChat.LoadSuggestedToolsAndPrompt\")\n      .click({ timeout: 10e3 });\n    await page.getByText(\"OK\", { exact: true }).click();\n\n    const mcpToolsBtn = await page.getByTestId(\"LLMChatOptions.MCPTools\");\n    await expect(mcpToolsBtn).toContainText(\"1\");\n\n    const dbToolsBtn = await page\n      .getByTestId(\"LLMChatOptions.DatabaseAccess\")\n      .locator(\"button\");\n    await expect(await dbToolsBtn.getAttribute(\"class\")).toContain(\n      \"btn-color-action\",\n    );\n\n    await setPromptByText(page, \"dashboard\");\n\n    await sendAskLLMMessage(\n      page,\n      \"I need some useful dashboards to track performance\",\n    );\n    await page\n      .getByTestId(\"AskLLMChat.LoadSuggestedDashboards\")\n      .click({ timeout: 18e3 });\n\n    const workspaceBtn = await page.getByTestId(\"WorkspaceMenu.list\");\n    await expect(workspaceBtn).toContainText(\"Customer Insights\");\n\n    await page.waitForTimeout(1e3);\n    await page.getByTestId(\"AskLLM\").click();\n\n    await page.getByTestId(\"AskLLMChat.UnloadSuggestedDashboards\").click();\n    await expect(workspaceBtn).not.toContainText(\"Customer Insights\");\n\n    await page.waitForTimeout(2e3);\n    await page.getByTestId(\"AskLLM\").click();\n    await sendAskLLMMessage(page, \" mcp \", true);\n\n    await page.waitForTimeout(1e3);\n    const mcpToolUse = await getAskLLMLastMessage(page);\n    await expect(mcpToolUse).toContain(\"successfully fetched the login page\");\n\n    await page.waitForTimeout(1e3);\n    await sendAskLLMMessage(page, \" mcpplaywright \");\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `Tool name \"playwright--browser_navigate\" is invalid. Try enabling and reloading the tools`,\n    );\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `Tool name \"playwright--browser_snapshot\" is invalid. Try enabling and reloading the tools`,\n    );\n\n    const enableMCPServers = async (serverNames: string[]) => {\n      await page\n        .getByTestId(\"LLMChatOptions.MCPTools\")\n        .click({ timeout: 10e3 });\n      for (const serverName of serverNames) {\n        const toggleBtn = await page\n          .locator(getDataKeyElemSelector(serverName))\n          .getByTestId(\"MCPServerFooterActions.enableToggle\");\n\n        await toggleBtn.scrollIntoViewIfNeeded();\n        await page.waitForTimeout(500);\n        await toggleBtn.click();\n        await page.waitForTimeout(500);\n      }\n    };\n\n    await enableMCPServers([\"playwright\"]);\n    await page\n      .getByText(\"browser_tabs\")\n      .waitFor({ state: \"visible\", timeout: 10e3 }); // wait for tools list to refresh\n    await page.getByTestId(\"Popup.close\").last().click();\n\n    await page.waitForTimeout(2e3);\n    await sendAskLLMMessage(page, \" mcpplaywright \");\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `Tool name \"playwright--browser_navigate\" is not allowed`,\n    );\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `Tool name \"playwright--browser_snapshot\" is not allowed`,\n    );\n    const toggleMCPTools = async (\n      toolNames: string[],\n      toggleAutoApprove?: boolean,\n    ) => {\n      await page\n        .getByTestId(\"LLMChatOptions.MCPTools\")\n        .click({ timeout: 10e3 });\n      await page.waitForTimeout(1000);\n      for (const toolName of toolNames) {\n        await page\n          .getByTestId(\"LLMChatOptions.MCPTools\")\n          .getByTestId(\"MCPServerTools\")\n          .getByText(toolName, { exact: true })\n          .click({ timeout: 30e3 }); //force: true,???????\n        await page.waitForTimeout(1500);\n      }\n      if (toggleAutoApprove) {\n        await page.getByTestId(\"MCPServers.toggleAutoApprove\").click();\n      }\n      await page.getByTestId(\"Popup.close\").last().click();\n      await page.waitForTimeout(500);\n    };\n\n    await toggleMCPTools([\"browser_navigate\", \"browser_snapshot\"]);\n\n    await sendAskLLMMessage(page, \" mcpplaywright \");\n    await page.waitForTimeout(2e3);\n    await page.getByTestId(\"AskLLMToolApprover.AllowOnce\").click();\n    await page.waitForTimeout(200);\n    await page.getByTestId(\"AskLLMToolApprover.AllowOnce\").click();\n    await page.waitForTimeout(10e3);\n    const lastToolUseBtn = await page\n      .getByTestId(\"Chat.messageList\")\n      .getByText(\"browser_snapshot\")\n      .last();\n    await lastToolUseBtn.scrollIntoViewIfNeeded();\n    await page.waitForTimeout(1e3);\n    await lastToolUseBtn.click();\n    await page.waitForTimeout(1e3);\n    /** Can be flaky */\n    await expect(page.getByTestId(\"MarkdownMonacoCode\").last()).toContainText(\n      `Page Title: Prostgles`,\n      { timeout: 15e3 },\n    );\n    await lastToolUseBtn.click();\n\n    await page.waitForTimeout(2e3);\n    /** Test max consecutive tool call fails */\n    await sendAskLLMMessage(page, \" mcpfail \");\n    await page.getByTestId(\"ToolUseMessage.toggleGroup\").last().click();\n    await expect(\n      page\n        .getByTestId(\"Chat.messageList\")\n        .getByText(`Tool name \"fetch--invalidfetch\" is invalid`),\n    ).toHaveCount(5, { timeout: 30e3 });\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `failed consecutive tool requests reached`,\n    );\n\n    /** Test max consecutive tool call fails */\n    for (let step = 0; step < 5; step++) {\n      await sendAskLLMMessage(page, \" cost \");\n    }\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `Maximum number (5) of failed consecutive tool requests reached`,\n    );\n\n    const newChat = async () => {\n      await page.getByTestId(\"AskLLMChat.NewChat\").click();\n      await page.waitForTimeout(1e3);\n    };\n\n    /** Test max chat cost */\n    const defaultMaxCost = 5;\n    const costPerMsg = 1.8;\n    await newChat();\n    for (\n      let step = 0;\n      step < Math.ceil(defaultMaxCost / costPerMsg) + 1;\n      step++\n    ) {\n      await sendAskLLMMessage(page, \"cost\");\n    }\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `Maximum total cost of the chat (5) reached. Current cost: 5.4`,\n    );\n\n    const maxCost = 4;\n    await page.getByTestId(\"LLMChatOptions.toggle\").click();\n    await fillSmartForm(page, \"llm_chats\", {\n      max_total_cost_usd: maxCost.toString(),\n    });\n    await page.getByTestId(\"Popup.close\").last().click();\n    /** Test max speculative chat cost */\n    await newChat();\n    await enableMCPServers([\"filesystem\"]);\n    const githubWorkerPath = [\"work\", \"ui\"] as const;\n    const path = [\n      ...(process.env.CI === \"true\" ? githubWorkerPath : []),\n      \"ui\",\n      \"client\",\n      \"node_modules\",\n    ] as const;\n    for (const segment of path) {\n      await page.locator(`[data-label=${JSON.stringify(segment)}]`).click();\n      await page.waitForTimeout(1e3);\n    }\n\n    await page.getByTestId(\"MCPServerConfig.save\").click();\n    await page.getByTestId(\"Popup.close\").last().click();\n    await toggleMCPTools([\"directory_tree\"], true);\n    for (let step = 0; step < Math.floor(maxCost / costPerMsg); step++) {\n      await sendAskLLMMessage(page, \"cost\");\n    }\n    await sendAskLLMMessage(page, \" estimated_cost \");\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `Maximum total cost of the chat (5) will be reached after sending this message`,\n      { timeout: 15e3 },\n    );\n\n    /* MCP Docker sandbox */\n    await newChat();\n    /* Prompt persists from the prev chat */\n    await expect(page.getByTestId(\"LLMChatOptions.Prompt\")).toContainText(\n      \"Create dashboards\",\n    );\n\n    await enableMCPServers([\"docker-sandbox\"]);\n    await page.waitForTimeout(2e3);\n    /** Tools are loaded after enabling */\n    await page\n      .locator(getDataKeyElemSelector(\"docker-sandbox\"))\n      .getByText(\"create_container\", { exact: true })\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n    await page\n      .locator(getDataKeyElemSelector(\"docker-sandbox\"))\n      .getByTestId(\"MCPServerFooterActions.refreshTools\")\n      .click();\n    await expect(page.getByTestId(\"Popup.content\").last()).toContainText(\n      `Reloaded 1 tool for \"docker-sandbox\" server`,\n    );\n    await page.getByText(\"OK\", { exact: true }).click();\n    await page\n      .locator(getDataKeyElemSelector(\"docker-sandbox\"))\n      .getByText(\"create_container\", { exact: true })\n      .click();\n    await page.waitForTimeout(1e3);\n    await page.getByTestId(\"Popup.close\").last().click();\n\n    const dockerRunAndExpect = async (result: string, inFullscreen = false) => {\n      await sendAskLLMMessage(page, \" mcpsandbox \");\n      await page.getByTestId(\"AskLLMToolApprover.AllowOnce\").click();\n      await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n        \"create a container that runs\",\n        { timeout: 60e3 },\n      );\n      await page.waitForTimeout(3e3);\n      await page\n        .getByTestId(\"Chat.messageList\")\n        .locator(\".Loading\")\n        .waitFor({ state: \"detached\", timeout: 40e3 });\n      await page.getByTestId(\"ToolUseMessage.toggle\").last().click();\n      if (inFullscreen) {\n        await page.getByTestId(\"PopupSection.fullscreen\").last().click();\n      }\n      await expect(\n        inFullscreen ?\n          page.getByTestId(\"PopupSection.content\").last()\n        : page.getByTestId(\"ToolUseMessage\").last(),\n      ).toContainText(result, {\n        timeout: 10e3,\n      });\n    };\n    /** Must test to see if port 3009 is free to avoid confusing errors */\n\n    await dockerRunAndExpect(`Tool \"execute_sql_with_rollback\" not found`);\n\n    await page.getByTestId(\"LLMChatOptions.DatabaseAccess\").click();\n\n    await runDbSql(\n      page,\n      `CREATE TABLE IF NOT EXISTS users (\n        id SERIAL PRIMARY KEY,\n        username TEXT NOT NULL UNIQUE, \n        created_at TIMESTAMP DEFAULT NOW()\n      );\n      INSERT INTO users (username) \n      VALUES ('fresh_user') ON CONFLICT DO NOTHING;\n      `,\n    );\n    await page\n      .getByTestId(\"Popup.content\")\n      .last()\n      .getByLabel(\"Mode\", { exact: true })\n      .click();\n\n    await page.getByRole(\"option\", { name: \"Run readonly SQL\" }).click();\n    await page.getByTestId(\"Popup.close\").last().click();\n    await page.waitForTimeout(4e3); // wait for askLLM publish method forked process to restart after schema change\n    await dockerRunAndExpect(`username: 'fresh_user'`, true);\n\n    /** Test stopping chat */\n    await page.getByTestId(\"Popup.close\").last().click();\n    await newChat();\n    await sendAskLLMMessage(page, \" longresponse \", {\n      onAfterSend: async () => {\n        await page.getByTestId(\"Chat.sendStop\").click();\n      },\n    });\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `longresponse`,\n    );\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      `aborted by user`,\n    );\n\n    /** Test parallel tool use single auto-approve */\n    await newChat();\n    await sendAskLLMMessage(page, \" parallel_calls \");\n\n    await expect(\n      page.getByTestId(\"ToolUseMessage.toggleGroup\").last(),\n    ).toContainText(\"3 tool calls\");\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      \"Tool call failed. Will not retry\",\n      {\n        timeout: 30e3,\n      },\n    );\n    await page.getByTestId(\"ToolUseMessage.toggleGroup\").last().click();\n    await expect(\n      page\n        .getByTestId(\"Chat.messageList\")\n        .getByText(\n          'Tool name \"fetch--fetch\" is not allowed. Must enable it for this chat',\n        ),\n    ).toHaveCount(3, { timeout: 30e3 });\n\n    await newChat();\n    await toggleMCPTools([\"fetch\"]);\n    await sendAskLLMMessage(page, \" parallel_calls \");\n\n    /** Should not request approval for the other 2 requests */\n    await page.getByTestId(\"AskLLMToolApprover.AllowAlways\").click();\n\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      \"Fetched in parallel successfully\",\n      {\n        timeout: 30e3,\n      },\n    );\n    await page.getByTestId(\"ToolUseMessage.toggleGroup\").last().click();\n\n    await expect(\n      page.locator(\n        getCommandElemSelector(\"ToolUseMessage.toggle\") +\n          `[data-color=\"default\"]`,\n      ),\n    ).toHaveCount(3, {\n      timeout: 30e3,\n    });\n\n    await newChat();\n    await toggleMCPTools([\"websearch\", \"get_snapshot\"]);\n    await page.waitForTimeout(7e3); // wait for the server to start\n    await sendAskLLMMessage(page, \" websearch \");\n    await page.getByTestId(\"AskLLMToolApprover.AllowOnce\").click();\n    await page.getByTestId(\"AskLLMToolApprover.AllowOnce\").click();\n    await expect(page.getByTestId(\"Chat.messageList\")).toContainText(\n      \"Search done.\",\n      {\n        timeout: 30e3,\n      },\n    );\n\n    await page.getByTestId(\"ToolUseMessage.toggle\").first().click();\n    const text = await page.getByTestId(\"ToolUseMessage\").first().textContent();\n    if (text?.includes(\"No results found.\")) {\n      // Probably due to engines limiting searches\n    } else {\n      await expect(page.getByTestId(\"ToolUseMessage\").first()).toContainText(\n        \"https://www.postgresql.org/\",\n      );\n    }\n    await page.getByTestId(\"ToolUseMessage.toggle\").first().click();\n    await page.getByTestId(\"ToolUseMessage.toggle\").last().click();\n    await expect(page.getByTestId(\"ToolUseMessage\").last()).toContainText(\n      \"Page Title: Prostgles UI\",\n    );\n\n    /** Speech to text */\n    await speechToTextTest(page);\n  });\n\n  test(\"Disable signups\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await loginWhenSignupIsEnabled(page);\n\n    /** Disable signups */\n    await goTo(page, \"/server-settings\");\n    await page.locator(`[data-key=\"auth\"]`).click();\n    await page.getByTestId(\"EmailAuthSetup\").locator(\"button\").click();\n    await page.getByText(\"Enable\").click();\n    await page.getByText(\"Save\").click();\n\n    /** Revert LLM signup */\n    await runDbsSql(\n      page,\n      `\n      UPDATE global_settings\n      SET prostgles_registration = null\n      `,\n    );\n    await runDbsSql(\n      page,\n      `\n      DELETE FROM llm_credentials;\n      TRUNCATE llm_chats CASCADE;\n      `,\n    );\n  });\n\n  test(\"Limit login attempts max failed limit\", async ({ browser }) => {\n    const page: PageWIds = await browser.newPage({\n      extraHTTPHeaders: {\n        \"x-real-ip\": \"1.1.1.1\",\n      },\n    });\n\n    await login(page, USERS.test_user, \"/login\");\n    await page.waitForTimeout(1500);\n\n    await runDbsSql(\n      page,\n      `DELETE FROM login_attempts; UPDATE global_settings SET login_rate_limit = '{\"groupBy\": \"x-real-ip\", \"maxAttemptsPerHour\": 5}'`,\n    );\n    await page.request.post(\"/logout\");\n    await goTo(page, \"/login\");\n    const loginAndExpectError = async (\n      errorMessage: string,\n      user: string,\n      lpage: PageWIds,\n    ) => {\n      await lpage.waitForTimeout(1e3);\n      await fillLoginFormAndSubmit(lpage, user);\n      await lpage\n        .getByTestId(\"Login.error\")\n        .waitFor({ state: \"visible\", timeout: 15e3 });\n      await expect(\n        await lpage.getByTestId(\"Login.error\").textContent(),\n      ).toContain(errorMessage);\n    };\n    for (let i = 0; i < 5; i++) {\n      await page.reload();\n      await loginAndExpectError(\"Invalid credentials\", \"invalid\", page);\n    }\n    await loginAndExpectError(\"Too many failed \", \"invalid\", page);\n    await loginAndExpectError(\"Too many failed \", USERS.default_user, page);\n\n    /** TODO: finish cookie rate test after playwright ws headers fix\n     * https://github.com/microsoft/playwright/issues/28948\n     */\n    // const newPageC: PageWIds = await browser.newPage({\n    //   extraHTTPHeaders: {\n    //     'x-real-ip': '1.1.1.2'\n    //   }\n    // });\n    // await login(newPageC, USERS.test_user, \"/\");\n    // await newPageC.waitForTimeout(1e3);\n    // await newPageC.getByRole('link', { name: 'Connections' }).click();\n    // await goTo(newPageC, \"/logout\");\n    // for(let i = 0; i < 5; i++){\n    //   const newPageCc: PageWIds = await browser.newPage({\n    //     extraHTTPHeaders: {\n    //       'x-real-ip': '1.1.1.22'\n    //     },\n    //     storageState: {\n    //       cookies: [\n    //         { name: 'sid_token', value: \"random\"+i, path: \"/\", domain: \"localhost\", httpOnly: true, sameSite: \"Lax\", expires: Math.round((Date.now() + 36e4)/1e3), secure: false }\n    //       ],\n    //       origins: []\n    //     }\n    //   });\n    //   // await newPageCc.context().addCookies([\n    //   //   { name: 'sid_token', value: \"random\"+i, path: \"/\", domain: \"localhost\", httpOnly: true, sameSite: \"Lax\" }\n    //   // ]);\n    //   await goTo(newPageCc, \"/\");\n    //   await newPageCc.waitForTimeout(1e3);\n    //   await newPageCc.close();\n    // }\n    // await loginAndExpectError(\"Too many failed attempts\", USERS.default_user, newPageC);\n\n    /** Revert */\n    const newPage: PageWIds = await browser.newPage({\n      extraHTTPHeaders: {\n        \"x-real-ip\": \"1.1.1.3\",\n      },\n    });\n    await login(newPage, USERS.test_user, \"/login\");\n    await newPage.waitForTimeout(1500);\n    await runDbsSql(\n      newPage,\n      `DELETE FROM login_attempts; UPDATE global_settings SET login_rate_limit = '{\"groupBy\": \"ip\", \"maxAttemptsPerHour\": 5}'`,\n    );\n  });\n\n  test(\"Create db with owner\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await login(page);\n    const dbName = \"db_with_owner\";\n    await createDatabase(dbName, page, false, { name: dbName, pass: dbName });\n    const currUser = await runDbSql(\n      page,\n      `SELECT current_user`,\n      {},\n      { returnType: \"value\" },\n    );\n    await expect(currUser).toEqual(dbName);\n\n    /** Ensure realtime works and is resilient to schema change */\n    const createTableQuery = `CREATE TABLE \"table_name\" ( id SERIAL PRIMARY KEY, title  VARCHAR(250), gencol TEXT GENERATED ALWAYS AS ( title || id::TEXT) stored);`;\n    await runDbSql(page, createTableQuery);\n    await page\n      .getByTestId(\"dashboard.menu.tablesSearchList\")\n      .locator(`[data-key=\"table_name\"]`)\n      .click();\n    await page.locator(`[role=\"columnheader\"]`).nth(1).click();\n    await page.locator(`[role=\"columnheader\"]`).nth(1).click();\n\n    await runDbSql(\n      page,\n      `INSERT INTO table_name (title) VALUES('my_new_value_')`,\n    );\n    await runDbSql(\n      page,\n      `INSERT INTO table_name (title) VALUES('my_new_value_')`,\n    );\n    await page\n      .getByText(\"my_new_value_1\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n\n    await runDbSql(page, `DROP TABLE table_name;`);\n    await page\n      .getByTestId(\"W_Table.TableNotFound\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n\n    await runDbSql(page, createTableQuery);\n    await runDbSql(\n      page,\n      `INSERT INTO table_name (title) VALUES('my_new_value_')`,\n    );\n    await runDbSql(\n      page,\n      `INSERT INTO table_name (title) VALUES('my_new_value_')`,\n    );\n    await runDbSql(\n      page,\n      `INSERT INTO table_name (title) VALUES('my_new_value_')`,\n    );\n    await runDbSql(\n      page,\n      `INSERT INTO table_name (title) VALUES('my_new_value_')`,\n    );\n    await page\n      .getByText(\"my_new_value_1\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n\n    /**\n     * Flaky test. Last failure logs\n     * \n      {stack: Array(1), message: Invalid or disallowed table: table_name}\n      There was an issue reconnecting old subscriptions {stack: Array(1), message: Invalid or disallowed table: table_name} {lastData: Array(0), tableName: table_name, command: subscribe, param1: Object, param2: Object}\n      Uncaught error within running subscription \n      _psqlWS_..table_name.{}.{\"select\":{\"*\":1},\"limit\":0}.m.sub {stack: Array(1), message: Invalid or disallowed table: table_name}\n      1751466146652 onDebug schemaChanged []\n      1751466146652 onDebug onReady.call [sql] \n      Table not found: table_name \n      Table not found: table_name\n      {message: Unexpected empty object select}\n      Subscribe failed {message: Unexpected empty object select} \n      Table not found: table_name\n      Table not found: table_name\n      {message: Unexpected empty object select}\n      Subscribe failed {message: Unexpected empty object select}\n    */\n\n    await page\n      .getByText(\"my_new_value_3\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n    await page.waitForTimeout(4e3);\n\n    /** Test schema select */\n    await runDbSql(\n      page,\n      `\n      CREATE SCHEMA \"MySchema\";\n      CREATE TABLE \"MySchema\".\"MyTable\" (\n        \"MyColumn\" TEXT\n      );\n    `,\n    );\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"NewConnectionForm.MoreOptionsToggle\").click();\n    await page.getByTestId(\"NewConnectionForm.schemaFilter\").click();\n    await page\n      .getByTestId(\"NewConnectionForm.schemaFilter\")\n      .locator(`[data-key=\"MySchema\"]`)\n      .click();\n    await page.keyboard.press(\"Escape\");\n    await page.getByTestId(\"Connection.edit.updateOrCreateConfirm\").click();\n    await page\n      .getByTestId(\"dashboard.menu.tablesSearchList\")\n      .locator(`[data-key=${JSON.stringify(`\"MySchema\".\"MyTable\"`)}]`)\n      .click();\n    await insertRow(page, `\"MySchema\".\"MyTable\"`, { MyColumn: \"some value\" });\n    await page\n      .getByText(\"some value\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n\n    await goTo(page, \"localhost:3004/connections\");\n    await page.waitForTimeout(4e3);\n    await dropConnectionAndDatabase(dbName, page);\n    await page.waitForTimeout(4e3);\n    await runDbsSql(page, `DROP USER db_with_owner;`);\n  });\n\n  test(\"Login returnUrl\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    const requestedUrl =\n      \"http://localhost:3004/connections/some-connection?a=b\";\n    await login(page, undefined, requestedUrl);\n    await page\n      .getByTestId(\"ProjectConnection.error\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n    const currentUrl = await page.url();\n    await expect(currentUrl).toEqual(requestedUrl);\n  });\n\n  test(\"Open redirect returnUrl\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    const requestedUrl =\n      \"http://localhost:3004/login?returnURL=/%2F%2Fwikipedia.org\";\n    await login(page, undefined, requestedUrl);\n    await goTo(page, requestedUrl);\n    await goTo(page, requestedUrl);\n    const currentUrl = await page.url();\n    await expect(currentUrl.startsWith(\"http://localhost:3004/\")).toBe(true);\n  });\n\n  test(\"Test 2FA\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await login(page);\n\n    await page.getByRole(\"link\", { name: \"test_user\" }).click();\n    await page.getByTestId(\"MenuList\").locator(`[data-key=\"security\"]`).click();\n    await page.getByTestId(\"Setup2FA.Enable\").click();\n    await page.getByTestId(\"Setup2FA.Enable.GenerateQR\").click();\n    await page.getByTestId(\"Setup2FA.Enable.CantScanQR\").click();\n    const Base64Secret = (\n      await page.getByTestId(\"Setup2FA.Enable.Base64Secret\").textContent()\n    )\n      ?.split(\" \")\n      .at(-1);\n    const recoveryCode = await page\n      .locator(\"#totp_recovery_code\")\n      .textContent();\n    const createAndFillCode = async () => {\n      await page.waitForTimeout(1200);\n      const code = authenticator.generate(Base64Secret ?? \"\");\n      await page\n        .getByTestId(\"Setup2FA.Enable.ConfirmCode\")\n        .locator(\"input\")\n        .fill(code);\n      await page.getByTestId(\"Setup2FA.Enable.Confirm\").click();\n    };\n    await createAndFillCode();\n\n    /** Allow 1 invalid code */\n    const INVALID_CODE = \"Invalid code\";\n    await page.waitForTimeout(300);\n    const setupErrorNode = await page.getByTestId(\"Setup2FA.error\");\n    if (\n      (await setupErrorNode.count()) &&\n      (await setupErrorNode.textContent())?.includes(INVALID_CODE)\n    ) {\n      await createAndFillCode();\n    }\n\n    /** Using token */\n    await login(page);\n    const fillTokenAndSignIn = async () => {\n      const newCode = authenticator.generate(Base64Secret ?? \"\");\n      await page.locator(\"#totp_token\").fill(newCode);\n      await page.getByRole(\"button\", { name: \"Sign in\", exact: true }).click();\n    };\n    await fillTokenAndSignIn();\n    await page.waitForTimeout(1e3);\n\n    /** Retry once when it sometimes fails */\n    const errorNode = await page.getByTestId(\"Login.error\");\n    if (\n      (await errorNode.count()) &&\n      (await errorNode.textContent())?.includes(INVALID_CODE)\n    ) {\n      await page.waitForTimeout(1e3);\n      await fillTokenAndSignIn();\n    }\n    await page\n      .getByRole(\"link\", { name: \"Connections\" })\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n\n    /** Using recovery code */\n    await page.request.post(\"/logout\");\n    await login(page);\n    await page\n      .getByRole(\"button\", { name: \"Enter recovery code\", exact: true })\n      .click();\n    await page.locator(\"#totp_recovery_code\").fill(recoveryCode ?? \"\");\n    await page.getByRole(\"button\", { name: \"Sign in\", exact: true }).click();\n    await page.waitForTimeout(3e3);\n    await page\n      .getByRole(\"link\", { name: \"Connections\" })\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n\n    await page.getByRole(\"link\", { name: \"test_user\" }).click();\n    await page.getByTestId(\"MenuList\").locator(`[data-key=\"security\"]`).click();\n    await page.getByTestId(\"Setup2FA.Disable\").click();\n  });\n\n  test(\"Sample database backups\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await login(page);\n\n    /** Create Sample database */\n    await createDatabase(\"sample_database\", page, false);\n\n    await page\n      .getByTestId(\"dashboard.goToConnConfig\")\n      .waitFor({ state: \"visible\", timeout: 10e3 });\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.details\").click();\n    await page.getByTestId(\"config.status\").click();\n    await page.getByTestId(\"config.ac\").click();\n    await page.getByTestId(\"config.methods\").click();\n    await page.getByTestId(\"config.files\").click();\n    await page.getByTestId(\"config.bkp\").click();\n    await page.getByTestId(\"config.api\").click();\n\n    await page.getByTestId(\"config.bkp\").click();\n    /** Delete previous backups (when testing locally) */\n    await deleteAllBackups(page);\n\n    /** Test automatic backups */\n    const toggleAutomaticBackups = async () => {\n      await page.getByTestId(\"config.bkp.AutomaticBackups\").click();\n      await page\n        .getByTestId(\"config.bkp.AutomaticBackups.toggle\")\n        .click({ timeout: 5e3 });\n      await page.waitForTimeout(1e3);\n      /** If it was enabled already enabled then re-enable it */\n      const text = await page\n        .getByTestId(\"config.bkp.AutomaticBackups\")\n        .textContent();\n      if (text?.includes(\"Enable automatic\")) {\n        await toggleAutomaticBackups();\n      }\n    };\n    await toggleAutomaticBackups();\n    await page\n      .getByRole(\"button\", { name: \"Restore...\", exact: true })\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n    await deleteAllBackups(page);\n\n    /** Disable automatic backups */\n    await toggleAutomaticBackups();\n  });\n\n  test(\"Delete previous users and data (to improve local tests)\", async ({\n    page: p,\n  }) => {\n    const page = p as PageWIds;\n\n    await login(page);\n    /** Delete previous user */\n    await page.goto(\"localhost:3004/users\", { waitUntil: \"networkidle\" });\n    for (const username of [USERS.default_user, USERS.public_user]) {\n      await page.locator(`#search-all`).fill(username);\n      await page.waitForTimeout(1e3);\n      // await page.keyboard.press(\"ArrowDown\");\n      await page.keyboard.press(\"Enter\");\n      await page.waitForTimeout(1e3);\n      await forEachLocator(\n        page,\n        async () => page.getByTestId(\"dashboard.window.viewEditRow\"),\n        async (editBtn) => {\n          await editBtn.click();\n          await page.getByTestId(\"SmartForm.delete\").click();\n          await page.getByTestId(\"SmartForm.delete.confirm\").click();\n          await page.waitForTimeout(1e3);\n        },\n      );\n      await page.reload();\n      await page.waitForTimeout(1e3);\n    }\n\n    /** Delete stt_mode  */\n    await runDbsSql(page, `UPDATE users SET options = null;`);\n\n    await page.goto(\"localhost:3004/account\", { waitUntil: \"networkidle\" });\n    await monacoType(page, `[data-label=\"Options\"]`, `{ `, {\n      // moveCursorAfterTyping: [\"Right\"],\n    });\n    await page.keyboard.type(\"vst\", { delay: 100 });\n    await page.keyboard.press(\"Tab\");\n    await page.keyboard.type(\"fa\", { delay: 100 });\n    await page.keyboard.press(\"Tab\");\n    await page.waitForTimeout(200);\n    const monacoEditor = await getMonacoEditorBySelector(\n      page,\n      `[data-label=\"Options\"]`,\n    );\n    const text = await monacoEditor.innerText();\n    /** Space charCode=32 but here we get 160 */\n    if (!text.includes(`\"viewedSQLTips\":${String.fromCharCode(160)}false`)) {\n      throw `Expected \"viewedSQLTips\": false in ${text}`;\n    }\n    await page.getByRole(\"button\", { name: \"Update\", exact: true }).click();\n    await page\n      .getByRole(\"button\", { name: \"Update row!\", exact: true })\n      .click();\n    await page.getByRole(\"link\", { name: \"Connections\", exact: true }).click();\n    await page.waitForTimeout(1e3);\n    const existingTestConn = await page.getByRole(\"link\", {\n      name: TEST_DB_NAME,\n      exact: true,\n    });\n    if (await existingTestConn.isVisible()) {\n      await dropConnectionAndDatabase(TEST_DB_NAME, page);\n    }\n    await page.waitForTimeout(1200);\n    for (const dbName of Object.values(DB_NAMES)) {\n      await runDbsSql(\n        page,\n        `DROP DATABASE IF EXISTS ${JSON.stringify(dbName)}`,\n      );\n    }\n  });\n\n  test(\"Create and prepare test database\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await login(page);\n    await page.getByRole(\"link\", { name: \"Connections\", exact: true }).click();\n    await createDatabase(TEST_DB_NAME, page);\n    await page\n      .getByTestId(\"dashboard.menu.sqlEditor\")\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n    const editors = await page.locator(\".ProstglesSQL\").count();\n    if (!editors) {\n      await page.getByTestId(\"dashboard.menu.sqlEditor\").click();\n    }\n    if (editors > 1) {\n      throw \"Not ok\";\n    }\n    await page\n      .getByRole(\"button\", { name: \"Ok, don't show again\", exact: true })\n      .click();\n\n    /** Test sql key bindings */\n    await page.keyboard.press(\"Alt+KeyE\");\n    const keybindings = [\"Alt+KeyE\", \"Control+KeyE\", \"Control+Enter\", \"F5\"];\n    for (const key of keybindings) {\n      const text = `hello${key}`;\n      await monacoType(page, `.ProstglesSQL`, `SELECT '${text}'`);\n      await page.keyboard.press(key);\n      await page\n        .locator(\".W_SQLResults\")\n        .getByText(text, { exact: true })\n        .waitFor({ state: \"visible\", timeout: 5e3 });\n    }\n    await monacoType(page, `.ProstglesSQL`, `SELECT pg_sleep(22)`);\n    await page.keyboard.press(\"Alt+KeyE\");\n    await page.waitForTimeout(2e3);\n    await page.keyboard.press(\"Escape\");\n    await page\n      .locator(\".W_SQLBottomBar .ErrorComponent\")\n      .getByText(\"canceling statement due to user request\", { exact: true })\n      .waitFor({ state: \"visible\", timeout: 5e3 });\n\n    /** Create schema */\n    await runSql(page, QUERIES.orders);\n    await page.waitForTimeout(1e3);\n    await page\n      .getByTestId(\"dashboard.menu.tablesSearchList\")\n      .locator(`[data-key=${JSON.stringify(\"orders\")}]`)\n      .waitFor({ state: \"visible\", timeout: 5e3 });\n    await closeWorkspaceWindows(page);\n\n    /** Enable file storage */\n    await page\n      .getByTestId(\"dashboard.goToConnConfig\")\n      .waitFor({ state: \"visible\", timeout: 10e3 });\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.files\").click();\n    await page.getByTestId(\"config.files.toggle\").click();\n    await page.getByTestId(\"config.files.toggle.confirm\").click();\n    await page.getByTestId(\"config.goToConnDashboard\").click();\n\n    /** Create table */\n    await page.getByTestId(\"dashboard.menu.create\").click();\n    await page.getByText(\"Create table\").click();\n    await page\n      .getByTestId(\"dashboard.menu.createTable.tableName\")\n      .getByRole(\"textbox\")\n      .fill(\"my_table\");\n    const addColumn = async (\n      colName: string,\n      dataType: string,\n      isPkey?: boolean,\n    ) => {\n      await page.getByTestId(\"dashboard.menu.createTable.addColumn\").click();\n      const colEditor = await page.getByTestId(\"ColumnEditor.name\");\n      await colEditor.getByRole(\"textbox\").fill(colName);\n      await page\n        .getByTestId(\"ColumnEditor.dataType\")\n        .waitFor({ state: \"visible\" });\n      await page.keyboard.press(\"Tab\");\n      await page.keyboard.type(dataType, { delay: 300 });\n      await page.keyboard.press(\"Enter\");\n      if (isPkey) {\n        await page.getByText(\"Primary key\").click();\n      }\n      await page\n        .getByTestId(\"dashboard.menu.createTable.addColumn.confirm\")\n        .click();\n    };\n    await addColumn(\"id\", \"ser\", true);\n    await addColumn(\"name\", \"text\");\n    await addColumn(\"content\", \"text\");\n    await addColumn(\"secret\", \"text\");\n\n    /** Add icon field */\n    await page.getByTestId(\"dashboard.menu.createTable.addColumn\").click();\n    await page.getByTestId(\"AddColumnReference\").click();\n    await page\n      .getByTestId(\"SearchList.List\")\n      .locator(`[data-key=\"files.id\"]`)\n      .click();\n    await page\n      .getByTestId(\"dashboard.menu.createTable.addColumn.confirm\")\n      .click();\n\n    /** Create table */\n    await page.getByTestId(\"dashboard.menu.createTable.confirm\").click();\n    await page.getByTestId(\"SQLSmartEditor.Run\").click();\n\n    /** Insert records into new table */\n    await page.getByRole(\"option\").getByText(\"My Table\").click({ delay: 200 });\n    await insertRow(page, \"my_table\", { name: \"some text\" });\n    const deletedRowName = \"some more text\";\n    await insertRow(page, \"my_table\", { name: deletedRowName });\n\n    /** Search row */\n    await page.getByTestId(\"dashboard.window.toggleFilterBar\").click();\n    await page.locator(\"input#search-all\").fill(\"2\");\n    await page.getByRole(\"option\", { name: \"2\" }).click();\n    /** This should work as well!!! */\n    // await page.keyboard.press(\"ArrowDown\", { delay: 300 });\n    // await page.keyboard.press(\"Enter\", { delay: 300 });\n\n    /** Backup db */\n    await page\n      .getByTestId(\"dashboard.goToConnConfig\")\n      .waitFor({ state: \"visible\", timeout: 10e3 });\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.bkp\").click();\n    await page\n      .getByRole(\"button\", { name: \"Create backup\", exact: true })\n      .click();\n    await page\n      .getByRole(\"button\", { name: \"Start backup\", exact: true })\n      .click();\n    await page.getByText(\"Completed\");\n\n    /** Delete row */\n    await page.getByTestId(\"config.goToConnDashboard\").click();\n    await page.getByTestId(\"dashboard.window.viewEditRow\").click();\n    await page.getByRole(\"button\", { name: \"Delete\", exact: true }).click();\n    await page.getByRole(\"button\", { name: \"Delete!\", exact: true }).click();\n    await page.waitForTimeout(200);\n\n    /** Restore db */\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.bkp\").click();\n    await restoreFromBackup(page);\n\n    /** Go to dashboard. Deleted row should be there */\n    await page.waitForTimeout(2200);\n    await page.getByTestId(\"config.goToConnDashboard\").click();\n    await page\n      .getByText(deletedRowName)\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n    await page.waitForTimeout(1200);\n\n    /** SearchAll for that row as well */\n    await page.keyboard.press(\"Control+Shift+KeyF\");\n    await page.getByTestId(\"SearchAll\").fill(deletedRowName);\n    await page.waitForTimeout(1100);\n    await page\n      .getByText(`name: ${deletedRowName}`)\n      .waitFor({ state: \"visible\", timeout: 15e3 });\n    await page.keyboard.press(\"Escape\");\n\n    /** Test Access control */\n    await page.goto(\"localhost:3004/users\", { waitUntil: \"networkidle\" });\n    await page.waitForTimeout(1000);\n    await insertRow(\n      page,\n      \"users\",\n      { username: USERS.default_user, password: USERS.default_user },\n      true,\n    );\n    await insertRow(\n      page,\n      \"users\",\n      { username: USERS.default_user1, password: USERS.default_user1 },\n      true,\n    );\n    // await insertRow(page, \"users\", { username: USERS.public_user, password: USERS.public_user, type: \"public\" }, true);\n  });\n\n  test(\"Set access rules\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await createAccessRuleForTestDB(page, \"default\");\n\n    await setTableRule(\n      page,\n      \"my_table\",\n      {\n        select: { excludedFields: [\"secret\"] },\n        insert: { forcedData: { name: \"abc\" }, excludedFields: [\"secret\"] },\n        update: { forcedData: { name: \"abc\" }, excludedFields: [\"secret\"] },\n        delete: { forcedFilter: [{ fieldName: \"name\", value: \"abc\" }] },\n      },\n      false,\n    );\n    await setTableRule(\n      page,\n      \"orders\",\n      { select: {}, update: {}, insert: {}, delete: {} },\n      false,\n    );\n    await setTableRule(\n      page,\n      \"files\",\n      { select: {}, update: {}, insert: {}, delete: {} },\n      true,\n    );\n\n    /** Expect LLM to ask for API credentials */\n    await page.getByTestId(\"AskLLM\").click();\n    await page.getByTestId(\"AskLLM.popup\").waitFor({ state: \"visible\" });\n    await page.getByTestId(\"SetupLLMCredentials\").waitFor({ state: \"visible\" });\n    const chatSend = await page\n      .getByTestId(\"AskLLM.popup\")\n      .getByTestId(\"Chat.send\")\n      .count();\n    await expect(chatSend).toBe(0);\n    await page.getByTestId(\"Popup.close\").click();\n\n    /** Setup LLM */\n    await enableAskLLM(page, 0);\n\n    /** Expect LLM to work */\n    const [{ isOk }] = await getLLMResponses(page, [\"hehhehey\"]);\n    expect(isOk).toBe(true);\n\n    /** Clear prev chats to ensure llm limit test works */\n    await runDbsSql(page, `TRUNCATE llm_chats CASCADE;`);\n\n    /** Save access rule  */\n    await page.getByTestId(\"config.ac.save\").click();\n    await page.waitForTimeout(2e3);\n  });\n\n  test(\"Default user has correct permissions\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await page.request.post(\"/logout\");\n    await login(page, USERS.default_user);\n\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME }).click();\n    await page.waitForTimeout(1000);\n    await page.getByTestId(\"dashboard.menu\").waitFor({ state: \"visible\" });\n    await setWspColLayout(page);\n    /** Open table using ctrl+P */\n    await openTable(page, \"my_tabl\");\n    await openTable(page, \"ord\");\n    await openTable(page, \"fil\");\n    await page.waitForTimeout(1000);\n\n    await uploadFile(page);\n\n    await insertRow(page, \"my_table\", { content: USERS.default_user });\n    await insertRow(page, \"orders\", { status: \"incomplete\" });\n    await expect(await page.getByText(\"secret\").count()).toBe(0);\n  });\n\n  test(\"Default user1 has correct permissions\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await page.request.post(\"/logout\");\n    await login(page, USERS.default_user1);\n\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME }).click();\n    await page.waitForTimeout(1000);\n    await setWspColLayout(page);\n\n    /** Open table using ctrl+P */\n    await openTable(page, \"my_tabl\");\n    await openTable(page, \"ord\");\n    await openTable(page, \"fil\");\n    await page.waitForTimeout(1000);\n\n    await uploadFile(page);\n\n    await insertRow(page, \"my_table\", { content: USERS.default_user1 });\n    await insertRow(page, \"orders\", { status: \"incomplete\" });\n    await expect(await page.getByText(\"secret\").count()).toBe(0);\n  });\n\n  test(\"Admin user (test_user) can see all data\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await page.request.post(\"/logout\");\n    await login(page, USERS.test_user);\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME }).click();\n\n    await setWspColLayout(page);\n    await page.waitForTimeout(1e3);\n\n    await closeWorkspaceWindows(page);\n\n    /** Open table using ctrl+P */\n    await openTable(page, \"my_tabl\");\n    await openTable(page, \"ord\");\n    await openTable(page, \"file\");\n\n    await expect(await page.getByText(\"abc\", { exact: true }).count()).toBe(2);\n    await expect(await page.getByText(\"incomplete\").count()).toBe(2);\n    await expect(await page.getByText(\"secret\").count()).toBe(1);\n    await expect(await page.getByText(fileName).count()).toBe(2);\n\n    /** Disable files permissions to test direct insert */\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.ac\").click();\n    await page.locator(`.ExistingAccessRules_Item_Header`).click();\n    await setTableRule(\n      page,\n      \"files\",\n      { select: {}, update: {}, insert: {}, delete: {} },\n      true,\n    );\n    await page.getByTestId(\"config.ac.save\").click();\n    await page.waitForTimeout(2e3);\n  });\n\n  test(\"User can insert and view files through allowed reference columns\", async ({\n    page: p,\n  }) => {\n    const page = p as PageWIds;\n\n    await login(page, USERS.default_user);\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME, exact: true }).click();\n    await page.waitForTimeout(1e3);\n    await clickInsertRow(page, \"my_table\");\n    await selectAndUpsertFile(\n      page,\n      (page) => page.getByTestId(\"SmartFormFieldOptions.AttachFile\").click(),\n      () =>\n        expect(\n          page.getByTestId(\"Popup.content\").locator(\"img\"),\n          // Must have blob src attribute\n        ).toHaveAttribute(\"src\", /^\\s*blob:/),\n    );\n    const imgSrc = await page\n      .getByTestId(\"Popup.content\")\n      .locator(\"img\")\n      .getAttribute(\"src\");\n    await expect(imgSrc).toContain(\"blob:\");\n    const nodes = await page.getByText(fileName).innerHTML();\n    console.log({ nodes });\n    await expect(page.getByText(fileName)).toHaveCount(1);\n    await page.waitForTimeout(1e3);\n    // Required to ensure it is not obscured by the insert btn\n    await page.getByTestId(\"dashboard.window.fullscreen\").last().click();\n    await page.getByTestId(\"dashboard.window.viewEditRow\").last().click();\n\n    await expect(\n      page.getByTestId(\"Popup.content\").locator(\"img\"),\n    ).toHaveAttribute(\n      \"src\",\n      // Inserted url starts with /prostgles_storage\n\n      /^\\/prostgles_storage/,\n    );\n\n    // Deleting works\n    await page\n      .locator(getDataKey(\"files_id\"))\n      .getByTestId(\"FormField.clear\")\n      .click();\n    await page.getByTestId(\"SmartForm.update\").click();\n    await page\n      .getByRole(\"button\", { name: \"Update row!\", exact: true })\n      .click();\n    await expect(page.getByText(fileName)).toHaveCount(0);\n\n    // Updating row with file works\n    await page.getByTestId(\"dashboard.window.viewEditRow\").last().click();\n    await selectAndUpsertFile(\n      page,\n      (page) => page.getByTestId(\"SmartFormFieldOptions.AttachFile\").click(),\n      () =>\n        expect(\n          page.getByTestId(\"Popup.content\").locator(\"img\"),\n        ).toHaveAttribute(\"src\", /^\\s*blob:/),\n      true,\n    );\n    await expect(page.getByText(fileName)).toHaveCount(1);\n  });\n\n  test(\"Table tests\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await login(page, USERS.test_user);\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME, exact: true }).click();\n\n    const wspName = \"Table tests\";\n    /** Delete existing duplicate workspace */\n    await page.getByTestId(\"WorkspaceMenuDropDown\").click();\n    await page.waitForTimeout(1e3);\n    let deleted = false;\n    await forEachLocator(\n      page,\n      async () => {\n        //.getByText(\"WorkspaceMenu.SearchList\")\n        const res = await page\n          .getByTestId(\"WorkspaceMenu.SearchList\")\n          .locator(`[data-key=${JSON.stringify(wspName)}]`);\n        return res;\n      },\n      async (item) => {\n        const deleteWspBtn = await item.getByTestId(\"WorkspaceDeleteBtn\");\n        deleted = true;\n        await deleteWspBtn.click();\n        await page.waitForTimeout(555);\n        await page.getByTestId(\"WorkspaceDeleteBtn.Confirm\").click();\n        await page.waitForTimeout(555);\n      },\n    );\n    if (!deleted) {\n      await page.locator(`[data-close-popup=\"true\"]`).click();\n    }\n    await page.getByTestId(\"WorkspaceMenuDropDown\").click();\n    await page.getByTestId(\"WorkspaceMenuDropDown.WorkspaceAddBtn\").click();\n    await page.waitForTimeout(1e3);\n    await page.keyboard.insertText(wspName);\n    await page.keyboard.press(\"Enter\");\n    await page.waitForTimeout(1e3);\n    await setWspColLayout(page);\n\n    const query = `\n      CREATE EXTENSION IF NOT EXISTS postgis;\n      DROP TABLE IF EXISTS orders CASCADE;\n      DROP TABLE IF EXISTS users CASCADE;\n      CREATE TABLE users (id UUID  PRIMARY KEY, first_name text, last_name text, email text, created_at timestamp default now(), type TEXT DEFAULT 'default', position numeric);\n      ${QUERIES.orders}\n      ALTER TABLE orders \n      ADD COLUMN total_cost NUMERIC NOT NULL DEFAULT random() * 100;\n      ALTER TABLE orders ADD COLUMN delivery_address GEOGRAPHY DEFAULT st_point(-0.08 + random()/10, 51.5 + random()/10)::GEOGRAPHY;\n      ALTER TABLE orders\n      ADD COLUMN created_at timestamp default now() + (random() * 10 * '1day'::interval);\n      ALTER TABLE orders ADD FOREIGN KEY (user_id) REFERENCES users;\n      INSERT INTO users (id, first_name, last_name, email, type, position)\n      SELECT \n        user_id, \n        \"left\"(user_id::TEXT, 8), \n        \"right\"(user_id::TEXT, 8), \n        \"left\"(user_id::TEXT, 8) || \"right\"(user_id::TEXT, 8) || '@gmail.com',  \n        CASE WHEN rownumid = 1 THEN 'default' WHEN rownumid = 2 THEN 'admin' WHEN random() < .5 THEN 'default' ELSE 'admin' END, \n        row_number() over()\n      FROM (\n        SELECT *, row_number() over( PARTITION BY user_id) as dpid, row_number() over() as rownumid\n        FROM (\n          SELECT rownum, gen_random_uuid() as user_id\n          FROM generate_series(1, 1e2) as rownum\n        ) t\n      ) t1\n      WHERE dpid = 1;\n      \n      INSERT INTO orders (user_id, status)\n      SELECT u.id, 'completed'\n      FROM (\n        SELECT rownum \n        FROM generate_series(1, 1e2) as rownum\n      ) t\n      LEFT JOIN users u ON true;\n      `;\n    await page.evaluate(async (query) => {\n      try {\n        await (window as any).db.sql(query);\n      } catch (err) {\n        document.body.innerText = JSON.stringify(err);\n      }\n    }, query);\n\n    await getSearchListItem(page, { dataKey: \"users\" }).click();\n    const usersTable = await getTableWindow(page, \"users\");\n\n    /** Test pagination */\n    const pageInput = await usersTable.getByTestId(\"Pagination.page\");\n    await expect(await pageInput.inputValue()).toBe(\"1\");\n    await expect(await pageInput.getAttribute(\"min\")).toBe(\"1\");\n    await expect(await pageInput.getAttribute(\"max\")).toBe(\"7\");\n    await expect(\n      await usersTable.getByTestId(\"Pagination.pageCountInfo\").textContent(),\n    ).toBe(`7 pages  (100 rows)`);\n    await usersTable.getByTestId(\"Pagination.lastPage\").click();\n    await pageInput.scrollIntoViewIfNeeded();\n    await expect(await pageInput.inputValue()).toBe(\"7\");\n    await usersTable.getByTestId(\"Pagination.firstPage\").click();\n    await expect(await pageInput.inputValue()).toBe(\"1\");\n    await usersTable.getByTestId(\"Pagination.nextPage\").click();\n    await expect(await pageInput.inputValue()).toBe(\"2\");\n    await usersTable.getByTestId(\"Pagination.lastPage\").click();\n    await usersTable.getByTestId(\"Pagination.pageSize\").click();\n    await page\n      .getByTestId(\"Pagination.pageSize\")\n      .locator(getDataKey(\"50\"))\n      .click();\n    await expect(await pageInput.inputValue()).toBe(\"2\");\n    await expect(\n      await usersTable.getByTestId(\"Pagination.pageCountInfo\").textContent(),\n    ).toBe(`2 pages  (100 rows)`);\n\n    await usersTable.getByTestId(\"AddColumnMenu\").click();\n    await getSearchListItem(page.getByTestId(\"AddColumnMenu\"), {\n      dataKey: \"Referenced\",\n    }).click();\n    await page.getByTestId(\"JoinPathSelectorV2\").click();\n    await page.waitForTimeout(1e3);\n    await getSearchListItem(page.getByTestId(\"JoinPathSelectorV2\"), {\n      dataKey: \"orders\",\n    }).click();\n    await page.getByTestId(\"LinkedColumn.ColumnList.toggle\").click();\n    await page.waitForTimeout(1e3);\n    await getSearchListItem(page, { dataKey: \"total_cost\" })\n      .getByTestId(\"SummariseColumn.toggle\")\n      .click();\n    await page.waitForTimeout(1e3);\n    await getSearchListItem(page.getByTestId(\"FunctionSelector\"), {\n      dataKey: \"$sum\",\n    }).click();\n    await page.getByTestId(\"SummariseColumn.apply\").click();\n    await page\n      .getByTestId(\"LinkedColumn.ColumnListMenu\")\n      .getByTestId(\"Popup.close\")\n      .click();\n    await page.getByTestId(\"LinkedColumn.Add\").click();\n\n    await page.waitForTimeout(1e3);\n    await usersTable.getByTestId(\"AddChartMenu.Map\").click();\n    await page\n      .getByTestId(\"AddChartMenu.Map\")\n      .getByRole(\"option\")\n      .filter({ hasText: \"> orders (delivery_address)\" })\n      .click();\n    await page.waitForTimeout(3e3);\n    await page.getByTestId(\"dashboard.window.detachChart\").click();\n    await page.waitForTimeout(1e3);\n    await usersTable.getByTestId(\"AddChartMenu.Timechart\").click();\n    await page\n      .getByTestId(\"AddChartMenu.Timechart\")\n      .getByRole(\"option\")\n      .getByText(\"> orders (created_at)\", { exact: true })\n      .click();\n    await page.waitForTimeout(3e3);\n    await page.getByTestId(\"dashboard.window.detachChart\").click();\n\n    /** Set count all to ensure the W_TimeChart.ActiveRow below works */\n    // await page.getByTestId(\"TimeChartLayerOptions.aggFunc\").click();\n    // await page.getByTestId(\"TimeChartLayerOptions.aggFunc.select\").click();\n    // await page.getByTestId(\"TimeChartLayerOptions.aggFunc.select\").locator(`[data-key=\"$countAll\"]`).click();\n    // await page.getByTestId(\"Popup.close\").click();\n\n    await page.waitForTimeout(5e3);\n    /** Mouse move is needed to show tooltip and to trigger re-render so that _renderedData.x,y are defined */\n    await page.mouse.move(700, 111);\n    await page.mouse.move(700, 150);\n    await page.mouse.click(700, 150);\n    await page\n      .getByTestId(\"W_TimeChart.ActiveRow\")\n      .waitFor({ state: \"visible\", timeout: 5e3 });\n    /** Test W_TimeChart activeRow crossfilter */\n    const {\n      data = [],\n      x = -99,\n      y = -99,\n    } = await page.evaluate(async () => {\n      try {\n        const timeChartRef = document.querySelector(\".W_TimeChart\")!;\n        const bbox = timeChartRef.getBoundingClientRect();\n        const data = (timeChartRef as any)._renderedData as {\n          x: number;\n          y: number;\n          value: number;\n        }[];\n        return { data, x: bbox.x, y: bbox.y };\n      } catch (err: any) {\n        document.body.innerText = err.toString() + JSON.stringify(err);\n        return {};\n      }\n    });\n    const [firstPoint, secondPoint] = data;\n    if (\n      typeof firstPoint?.x !== \"number\" ||\n      typeof secondPoint?.x !== \"number\"\n    ) {\n      console.error(\"firstPoint or secondPoint missing:\", {\n        firstPoint,\n        secondPoint,\n      });\n    } else {\n      await page.mouse.click(firstPoint.x + x, firstPoint.y + y);\n      await page.waitForTimeout(1e3);\n      await page.mouse.click(firstPoint.x + x, firstPoint.y + y); // to close it\n      await page.waitForTimeout(1e3);\n      await page.mouse.click(secondPoint.x + x, secondPoint.y + y);\n      await page.waitForTimeout(1e3);\n    }\n\n    // Close active row\n    await page.waitForTimeout(2e3);\n    const brush = await page.getByTestId(\"W_TimeChart.ActiveRow\");\n    if (await brush.isVisible()) {\n      await brush.locator(\"button\").click();\n    }\n\n    /**\n     * Ensure card view works\n     */\n    await usersTable.getByTestId(\"dashboard.window.menu\").click();\n    await page.waitForTimeout(1e3);\n    await page.getByText(\"Display options\").click();\n    await page.getByTestId(\"table.options.displayMode\").click();\n    await page\n      .getByTestId(\"SearchList.List\")\n      .locator(`[data-key=\"card\"]`)\n      .click();\n    await page.getByTestId(\"table.options.cardView.groupBy\").click();\n    await page\n      .getByTestId(\"SearchList.List\")\n      .locator(`[data-key=\"type\"]`)\n      .click();\n    await page.getByTestId(\"table.options.cardView.orderBy\").click();\n    await page\n      .getByTestId(\"SearchList.List\")\n      .locator(`[data-key=\"position\"]`)\n      .click();\n    await page.waitForTimeout(1e3);\n    await page.keyboard.press(\"Escape\");\n    await usersTable.getByTestId(\"dashboard.window.fullscreen\").click();\n    await page.waitForTimeout(3e3);\n    await expect(await page.getByText(\"first_name\").first()).toBeVisible();\n\n    /** Test groupBy */\n    const swapFirstTwoRows = async () => {\n      const groups = await page.getByTestId(\"CardView.group\").all();\n      await expect(groups.length).toBe(2);\n      const [firstGroup, secondGroup] = groups;\n      await page.waitForTimeout(2e3);\n      const firstGroupFirstRow = await firstGroup\n        .getByTestId(\"CardView.row\")\n        .first();\n      const secondGroupFirstRow = await secondGroup\n        .getByTestId(\"CardView.row\")\n        .first();\n      const draggedText = await firstGroupFirstRow\n        .getByTitle(\"id\")\n        .textContent();\n      const targetText = await secondGroupFirstRow\n        .getByTitle(\"id\")\n        .textContent();\n      const firstDragHandle = await firstGroupFirstRow.getByTestId(\n        \"CardView.DragHeader\",\n      );\n      const secondDragHandle = await secondGroupFirstRow.getByTestId(\n        \"CardView.DragHeader\",\n      );\n\n      const box1 = await firstDragHandle.boundingBox();\n      const box2 = await secondDragHandle.boundingBox();\n      if (!box1 || !box2) {\n        throw \"box1 or box2 missing\";\n      }\n      await page.mouse.move(box1.x + box1.width / 2, box1.y + box1.height / 2, {\n        steps: 22,\n      });\n      await firstDragHandle.hover();\n      await page.waitForTimeout(1e3);\n      await page.mouse.down({ button: \"left\" });\n      await page.mouse.move(box2.x + box2.width / 2, box2.y + box2.height / 2, {\n        steps: 111,\n      });\n      await page.mouse.up({ button: \"left\" });\n      await page.waitForTimeout(5e3);\n\n      const newTargetText = await secondGroup\n        .getByTestId(\"CardView.row\")\n        .first()\n        .getByTitle(\"id\")\n        .textContent();\n      return { draggedText, newTargetText };\n    };\n\n    await swapFirstTwoRows();\n    await page.waitForTimeout(1e3);\n    const { draggedText, newTargetText } = await swapFirstTwoRows();\n    await page.waitForTimeout(1e3);\n    await expect(draggedText).toEqual(newTargetText);\n    await page.waitForTimeout(2e3);\n  });\n\n  test(\"API tests\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await login(page, USERS.test_user);\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME, exact: true }).click();\n    await page\n      .getByTestId(\"dashboard.goToConnConfig\")\n      .waitFor({ state: \"visible\", timeout: 10e3 });\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.api\").click();\n\n    await page\n      .getByRole(\"button\", { name: \"Create token\", exact: true })\n      .click();\n    await page.getByRole(\"button\", { name: \"Generate\", exact: true }).click();\n    await page.getByTestId(\"Popup.close\").click();\n    await page.getByRole(\"button\", { name: \"Examples\", exact: true }).click();\n    await page\n      .getByRole(\"button\", { name: \"Download code sample\", exact: true })\n      .click();\n  });\n\n  test(\"SQL Autocomplete\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    const sqlTestTimeout = { total: 9 * 6e4, sql: 8 * 6e4 };\n    test.setTimeout(sqlTestTimeout.total);\n\n    await page.request.post(\"/logout\");\n    await login(page, USERS.test_user);\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME }).click();\n\n    await page.getByTestId(\"dashboard.menu\").waitFor({ state: \"visible\" });\n    await page.waitForTimeout(3000);\n\n    /** Ensure SQL Autocomplete works */\n    await closeWorkspaceWindows(page);\n    await page.getByTestId(\"dashboard.menu.sqlEditor\").click();\n    await page.getByTestId(\"dashboard.window.menu\").click();\n    await page.getByText(\"General\").click();\n\n    for (const _ of new Array(5).fill(1)) {\n      await page.getByText(\"TEST\", { exact: true }).click();\n      await page.waitForTimeout(100);\n    }\n\n    const startSqlTest = async () =>\n      new Promise(async (resolve, reject) => {\n        page.on(\"dialog\", (dialog) => {\n          const msg = dialog.message();\n          console.log(\"1\", msg);\n          if (msg.includes(\"does not match the expected\")) {\n            reject(msg);\n            throw msg;\n          }\n          if (msg === \"Demo finished successfully\") {\n            return resolve(1);\n          }\n          return dialog.accept();\n        });\n      });\n    await startSqlTest();\n  });\n\n  test(\"Admin can create and drop connections\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await login(page, USERS.test_user);\n    const dbName = \"my_new_db\";\n    await createDatabase(dbName, page);\n    await page.getByTestId(\"dashboard.menu\").waitFor({ state: \"visible\" });\n    await page.goBack();\n\n    const { connectionSelector } = await dropConnectionAndDatabase(\n      dbName,\n      page,\n    );\n    await goTo(page, \"localhost:3004/connections\");\n    await page.reload();\n    const deletedConnection = await page.locator(connectionSelector);\n    await expect(await deletedConnection.count()).toEqual(0);\n  });\n\n  test(\"Set public user access rules\", async ({ page: p }) => {\n    const page = p as PageWIds;\n\n    await createAccessRuleForTestDB(page, \"public\");\n    await setTableRule(\n      page,\n      \"my_table\",\n      {\n        select: {},\n      },\n      false,\n    );\n    await setTableRule(\n      page,\n      \"orders\",\n      { select: {}, update: {}, insert: {}, delete: {} },\n      false,\n    );\n    await setTableRule(\n      page,\n      \"files\",\n      { select: {}, update: {}, insert: {}, delete: {} },\n      true,\n    );\n    await enableAskLLM(page, 4, true);\n    await page.getByTestId(\"config.ac.save\").click();\n    await page.waitForTimeout(2e3);\n  });\n\n  test(\"Public user can access all allowed sections without issues\", async ({\n    page: p,\n  }) => {\n    const page = p as PageWIds;\n    await goTo(page, \"localhost:3004/connections\");\n    await page.reload();\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME }).click();\n    await page\n      .getByTestId(\"dashboard.menu.tablesSearchList\")\n      .waitFor({ state: \"visible\", timeout: 10e3 });\n    await openTable(page, \"my_tabl\");\n\n    /** Ask LLM limit */\n    const [response1, response2, response3] = await getLLMResponses(page, [\n      \"hey\",\n      \"hey\",\n      \"hey\",\n    ]);\n    await expect(response1.isOk).toBe(true);\n    await expect(response2.isOk).toBe(true);\n    await expect(response3.isOk).toBe(true);\n  });\n\n  test(\"Public user ask llm limit\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    await goTo(page, \"localhost:3004/connections\");\n    await page.reload();\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME }).click();\n    await page\n      .getByTestId(\"dashboard.menu.tablesSearchList\")\n      .waitFor({ state: \"visible\", timeout: 10e3 });\n    const [response1, response2] = await getLLMResponses(page, [\"hey\", \"hey\"]);\n    await expect(response1.isOk).toBe(true);\n    await expect(response2.isOk).toBe(false);\n  });\n\n  test(\"MCP Servers\", async ({ page: p }) => {\n    const page = p as PageWIds;\n    // await goTo(page, \"localhost:3004/login\");\n    await login(page, USERS.test_user, \"localhost:3004/login\");\n    await page.getByRole(\"link\", { name: \"Connections\" }).click();\n    await page.getByRole(\"link\", { name: TEST_DB_NAME }).click();\n\n    await page.getByTestId(\"AskLLM\").click();\n    await page.getByTestId(\"AskLLM.popup\").waitFor({ state: \"visible\" });\n    await page.getByTestId(\"LLMChatOptions.MCPTools\").click();\n    await page.getByTestId(\"AddMCPServer.Open\").click();\n    await monacoType(\n      page,\n      \".SmartCodeEditor\",\n      JSON.stringify(\n        {\n          mcpServers: {\n            myServer: {\n              command: \"npx\",\n              args: [\"@modelcontextprotocol/server-filesystem\", \"ALLOWED_DIR\"],\n              env: {\n                GITHUB_PERSONAL_ACCESS_TOKEN: \"<YOUR_TOKEN>\",\n              },\n            },\n          },\n        },\n        null,\n        2,\n      ),\n      {\n        deleteAllAndFill: true,\n        /** For some reason an extra bracket is inserted */\n        pressAfterTyping: [\"Backspace\"],\n      },\n    );\n\n    await page.locator(\".SwitchToggle \" + getDataKey(\"ALLOWED_DIR\")).click();\n    await page.getByTestId(\"AddMCPServer.Add\").click();\n    await page.getByTestId(\"AddMCPServer.Add\").waitFor({ state: \"detached\" });\n    await page\n      .getByTestId(\"SmartCardList\")\n      .locator(getDataKey(\"myServer\"))\n      .getByTitle(\"Press to enable\")\n      .click();\n\n    await page.getByLabel(\"ALLOWED_DIR\").fill(\"/prostgles-mcp-test\");\n    await page.getByText(\"Enable\", { exact: true }).click();\n  });\n});\n"
  },
  {
    "path": "e2e/tests/mockSMTPServer.ts",
    "content": "import { SMTPServer, SMTPServerOptions } from \"smtp-server\";\n\nexport const startMockSMTPServer = () => {\n  const emails: string[] = [];\n  const getEmails = () => emails;\n\n  const serverOptions: SMTPServerOptions = {\n    authMethods: [\"PLAIN\", \"LOGIN\", \"CRAM-MD5\"],\n    onAuth(auth, session, callback) {\n      // Accept any username/password\n      console.log(\"Auth attempt:\", auth.username, auth.password, auth);\n      callback(null, { user: auth.username });\n\n      /*\n      if (auth.username === 'testuser' && auth.password === 'testpass') {\n        callback(null, { user: auth.username });\n      } else {\n        callback(new Error('Invalid username or password'));\n      }\n      */\n    },\n    authOptional: true,\n    secure: false,\n    onConnect(session, callback) {\n      console.log(\"Client connected:\", session.remoteAddress);\n      callback();\n    },\n\n    // Handler for handling mail from (sender)\n    onMailFrom(address, session, callback) {\n      console.log(\"Mail from:\", address.address);\n      callback();\n    },\n\n    // Handler for handling rcpt to (recipient)\n    onRcptTo(address, session, callback) {\n      console.log(\"Recipient:\", address.address);\n      callback();\n    },\n\n    // Handler for handling incoming mail data\n    onData(stream, session, callback) {\n      let mailData = \"\";\n\n      stream.on(\"data\", (chunk) => {\n        mailData += chunk.toString();\n      });\n\n      stream.on(\"end\", () => {\n        console.log(\"Received mail:\");\n        console.log(\"------------------------\");\n        console.log(mailData.replaceAll(\"=3D\", \"=\"));\n        emails.push(mailData.replaceAll(\"=3D\", \"=\"));\n        console.log(\"------------------------\");\n        callback();\n      });\n    },\n    onClose(session) {\n      console.log(\"Session closed\", session.id);\n    },\n  };\n\n  const server = new SMTPServer(serverOptions);\n\n  const PORT = 3017;\n  server.listen(PORT, \"127.0.0.1\", () => {\n    console.log(`Mock SMTP Server running on port ${PORT}`);\n  });\n\n  server.on(\"error\", (err) => {\n    console.error(\"Server error:\", err);\n  });\n\n  server.on(\"close\", () => {\n    console.log(\"Server closed\");\n  });\n\n  return {\n    getEmails,\n  };\n};\n"
  },
  {
    "path": "e2e/tests/sampleToolUseData.ts",
    "content": "export const prostglesUIDashboardSample = {\n  prostglesWorkspaces: [\n    {\n      icon: \"ClipboardList\",\n      name: \"Order Management\",\n      layout: {\n        id: \"order-root\",\n        size: 1,\n        type: \"col\",\n        items: [\n          {\n            id: \"order-top-row\",\n            size: 0.6,\n            type: \"row\",\n            items: [\n              {\n                id: \"orders-table\",\n                size: 0.7,\n                type: \"item\",\n                viewType: \"table\",\n                tableName: \"orders\",\n              },\n              {\n                id: \"order-status-chart\",\n                size: 0.3,\n                type: \"item\",\n                viewType: \"timechart\",\n                tableName: \"orders\",\n              },\n            ],\n          },\n          {\n            id: \"order-items-table\",\n            size: 0.4,\n            type: \"item\",\n            viewType: \"table\",\n            tableName: \"order_items\",\n          },\n        ],\n        isRoot: true,\n      },\n      windows: [\n        {\n          id: \"orders-table\",\n          sort: [\n            {\n              asc: false,\n              key: \"created_at\",\n              nulls: \"last\",\n            },\n          ],\n          type: \"table\",\n          columns: [\n            {\n              name: \"id\",\n              width: 80,\n            },\n            {\n              name: \"restaurant_id\",\n              width: 120,\n              nested: {\n                path: [\n                  {\n                    on: [\n                      {\n                        restaurant_id: \"id\",\n                      },\n                    ],\n                    table: \"restaurants\",\n                  },\n                ],\n                limit: 1,\n                columns: [\n                  {\n                    name: \"name\",\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n            {\n              name: \"customer_id\",\n              width: 120,\n              nested: {\n                path: [\n                  {\n                    on: [\n                      {\n                        customer_id: \"id\",\n                      },\n                    ],\n                    table: \"users\",\n                  },\n                ],\n                limit: 1,\n                columns: [\n                  {\n                    name: \"first_name\",\n                  },\n                  {\n                    name: \"last_name\",\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n            {\n              name: \"status\",\n              width: 120,\n              styling: {\n                type: \"conditional\",\n                conditions: [\n                  {\n                    value: \"pending\",\n                    operator: \"=\",\n                    chipColor: \"yellow\",\n                  },\n                  {\n                    value: \"confirmed\",\n                    operator: \"=\",\n                    chipColor: \"blue\",\n                  },\n                  {\n                    value: \"preparing\",\n                    operator: \"=\",\n                    chipColor: \"indigo\",\n                  },\n                  {\n                    value: \"ready\",\n                    operator: \"=\",\n                    chipColor: \"purple\",\n                  },\n                  {\n                    value: \"picked_up\",\n                    operator: \"=\",\n                    chipColor: \"gray\",\n                  },\n                  {\n                    value: \"delivered\",\n                    operator: \"=\",\n                    chipColor: \"green\",\n                  },\n                  {\n                    value: \"cancelled\",\n                    operator: \"=\",\n                    chipColor: \"red\",\n                  },\n                ],\n              },\n            },\n            {\n              name: \"total_price\",\n              width: 100,\n            },\n            {\n              name: \"created_at\",\n              width: 180,\n            },\n          ],\n          table_name: \"orders\",\n        },\n        {\n          id: \"order-status-chart\",\n          type: \"timechart\",\n          layers: [\n            {\n              yAxis: \"count(*)\",\n              table_name: \"orders\",\n              dateColumn: \"created_at\",\n            },\n          ],\n        },\n        {\n          id: \"order-items-table\",\n          type: \"table\",\n          columns: [\n            {\n              name: \"order_id\",\n              width: 100,\n            },\n            {\n              name: \"menu_item_id\",\n              width: 120,\n              nested: {\n                path: [\n                  {\n                    on: [\n                      {\n                        menu_item_id: \"id\",\n                      },\n                    ],\n                    table: \"menu_items\",\n                  },\n                ],\n                limit: 1,\n                columns: [\n                  {\n                    name: \"name\",\n                  },\n                  {\n                    name: \"category\",\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n            {\n              name: \"quantity\",\n              width: 80,\n            },\n            {\n              name: \"price\",\n              width: 100,\n            },\n          ],\n          table_name: \"order_items\",\n        },\n      ],\n    },\n    {\n      icon: \"StorefrontOutline\",\n      name: \"Restaurant Analytics\",\n      layout: {\n        id: \"restaurant-root\",\n        size: 1,\n        type: \"col\",\n        items: [\n          {\n            id: \"restaurant-top-row\",\n            size: 0.5,\n            type: \"row\",\n            items: [\n              {\n                id: \"restaurants-table\",\n                size: 0.6,\n                type: \"item\",\n                viewType: \"table\",\n                tableName: \"restaurants\",\n              },\n              {\n                id: \"restaurant-ratings\",\n                size: 0.4,\n                type: \"item\",\n                viewType: \"table\",\n                tableName: \"ratings\",\n              },\n            ],\n          },\n          {\n            id: \"restaurant-bottom-row\",\n            size: 0.5,\n            type: \"row\",\n            items: [\n              {\n                id: \"menu-items-table\",\n                size: 0.7,\n                type: \"item\",\n                viewType: \"table\",\n                tableName: \"menu_items\",\n              },\n              {\n                id: \"revenue-chart\",\n                size: 0.3,\n                type: \"item\",\n                viewType: \"timechart\",\n                tableName: \"orders\",\n              },\n            ],\n          },\n        ],\n        isRoot: true,\n      },\n      windows: [\n        {\n          id: \"restaurants-table\",\n          type: \"table\",\n          columns: [\n            {\n              name: \"id\",\n              width: 80,\n            },\n            {\n              name: \"name\",\n              width: 200,\n            },\n            {\n              name: \"address\",\n              width: 250,\n            },\n            {\n              name: \"created_at\",\n              width: 180,\n            },\n          ],\n          table_name: \"restaurants\",\n        },\n        {\n          id: \"restaurant-ratings\",\n          sort: [\n            {\n              asc: false,\n              key: \"created_at\",\n              nulls: \"last\",\n            },\n          ],\n          type: \"table\",\n          columns: [\n            {\n              name: \"restaurant_id\",\n              width: 120,\n              nested: {\n                path: [\n                  {\n                    on: [\n                      {\n                        restaurant_id: \"id\",\n                      },\n                    ],\n                    table: \"restaurants\",\n                  },\n                ],\n                limit: 1,\n                columns: [\n                  {\n                    name: \"name\",\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n            {\n              name: \"rating\",\n              width: 80,\n              styling: {\n                type: \"conditional\",\n                conditions: [\n                  {\n                    value: \"2\",\n                    operator: \"<=\",\n                    chipColor: \"red\",\n                  },\n                  {\n                    value: \"3\",\n                    operator: \"=\",\n                    chipColor: \"yellow\",\n                  },\n                  {\n                    value: \"4\",\n                    operator: \"=\",\n                    chipColor: \"blue\",\n                  },\n                  {\n                    value: \"5\",\n                    operator: \"=\",\n                    chipColor: \"green\",\n                  },\n                ],\n              },\n            },\n            {\n              name: \"review\",\n              width: 300,\n            },\n            {\n              name: \"created_at\",\n              width: 150,\n            },\n          ],\n          table_name: \"ratings\",\n        },\n        {\n          id: \"menu-items-table\",\n          type: \"table\",\n          columns: [\n            {\n              name: \"restaurant_id\",\n              width: 120,\n              nested: {\n                path: [\n                  {\n                    on: [\n                      {\n                        restaurant_id: \"id\",\n                      },\n                    ],\n                    table: \"restaurants\",\n                  },\n                ],\n                limit: 1,\n                columns: [\n                  {\n                    name: \"name\",\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n            {\n              name: \"name\",\n              width: 200,\n            },\n            {\n              name: \"category\",\n              width: 120,\n            },\n            {\n              name: \"price\",\n              width: 100,\n            },\n            {\n              name: \"description\",\n              width: 300,\n            },\n          ],\n          table_name: \"menu_items\",\n        },\n        {\n          id: \"revenue-chart\",\n          type: \"timechart\",\n          layers: [\n            {\n              yAxis: {\n                column: \"total_price\",\n                aggregation: \"sum\",\n              },\n              table_name: \"orders\",\n              dateColumn: \"created_at\",\n            },\n          ],\n        },\n      ],\n    },\n    {\n      icon: \"TruckDelivery\",\n      name: \"Delivery Operations\",\n      layout: {\n        id: \"delivery-root\",\n        size: 1,\n        type: \"col\",\n        items: [\n          {\n            id: \"delivery-top-row\",\n            size: 0.6,\n            type: \"row\",\n            items: [\n              {\n                id: \"active-deliveries\",\n                size: 0.5,\n                type: \"item\",\n                viewType: \"table\",\n                tableName: \"orders\",\n              },\n              {\n                id: \"delivery-map\",\n                size: 0.5,\n                type: \"item\",\n                viewType: \"map\",\n                tableName: \"v_riders\",\n              },\n            ],\n          },\n          {\n            id: \"delivery-status-changes\",\n            size: 0.4,\n            type: \"item\",\n            viewType: \"table\",\n            tableName: \"delivery_status_changes\",\n          },\n        ],\n        isRoot: true,\n      },\n      windows: [\n        {\n          id: \"active-deliveries\",\n          type: \"table\",\n          filter: [\n            {\n              type: \"$in\",\n              value: [\"confirmed\", \"preparing\", \"ready\", \"out_for_delivery\"],\n              fieldName: \"status\",\n            },\n          ],\n          columns: [\n            {\n              name: \"id\",\n              width: 80,\n            },\n            {\n              name: \"deliverer_id\",\n              width: 120,\n              nested: {\n                path: [\n                  {\n                    on: [\n                      {\n                        deliverer_id: \"id\",\n                      },\n                    ],\n                    table: \"users\",\n                  },\n                ],\n                limit: 1,\n                columns: [\n                  {\n                    name: \"first_name\",\n                  },\n                  {\n                    name: \"last_name\",\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n            {\n              name: \"status\",\n              width: 120,\n              styling: {\n                type: \"conditional\",\n                conditions: [\n                  {\n                    value: \"confirmed\",\n                    operator: \"=\",\n                    chipColor: \"yellow\",\n                  },\n                  {\n                    value: \"preparing\",\n                    operator: \"=\",\n                    chipColor: \"blue\",\n                  },\n                  {\n                    value: \"ready\",\n                    operator: \"=\",\n                    chipColor: \"purple\",\n                  },\n                  {\n                    value: \"out_for_delivery\",\n                    operator: \"=\",\n                    chipColor: \"indigo\",\n                  },\n                ],\n              },\n            },\n            {\n              name: \"created_at\",\n              width: 150,\n            },\n            {\n              name: \"updated_at\",\n              width: 150,\n            },\n          ],\n          table_name: \"orders\",\n        },\n        {\n          id: \"delivery-map\",\n          type: \"map\",\n          layers: [\n            {\n              geoColumn: \"location\",\n              table_name: \"v_riders\",\n            },\n          ],\n        },\n        {\n          id: \"delivery-status-changes\",\n          sort: [\n            {\n              asc: false,\n              key: \"created_at\",\n              nulls: \"last\",\n            },\n          ],\n          type: \"table\",\n          columns: [\n            {\n              name: \"order_id\",\n              width: 100,\n            },\n            {\n              name: \"delivery_status\",\n              width: 150,\n              styling: {\n                type: \"conditional\",\n                conditions: [\n                  {\n                    value: \"assigned\",\n                    operator: \"=\",\n                    chipColor: \"blue\",\n                  },\n                  {\n                    value: \"picked_up\",\n                    operator: \"=\",\n                    chipColor: \"yellow\",\n                  },\n                  {\n                    value: \"in_transit\",\n                    operator: \"=\",\n                    chipColor: \"purple\",\n                  },\n                  {\n                    value: \"delivered\",\n                    operator: \"=\",\n                    chipColor: \"green\",\n                  },\n                ],\n              },\n            },\n            {\n              name: \"created_at\",\n              width: 180,\n            },\n          ],\n          table_name: \"delivery_status_changes\",\n        },\n      ],\n    },\n    {\n      icon: \"MapMarker\",\n      name: \"Geographic Overview\",\n      layout: {\n        id: \"geo-root\",\n        size: 1,\n        type: \"row\",\n        items: [\n          {\n            id: \"restaurant-map\",\n            size: 0.5,\n            type: \"item\",\n            viewType: \"map\",\n            tableName: \"london_restaurants.geojson\",\n          },\n          {\n            id: \"user-map\",\n            size: 0.5,\n            type: \"item\",\n            viewType: \"map\",\n            tableName: \"customers\",\n          },\n        ],\n        isRoot: true,\n      },\n      windows: [\n        {\n          id: \"restaurant-map\",\n          type: \"map\",\n          layers: [\n            {\n              geoColumn: \"geometry\",\n              table_name: \"london_restaurants.geojson\",\n            },\n          ],\n        },\n        {\n          id: \"user-map\",\n          type: \"map\",\n          layers: [\n            {\n              geoColumn: \"location\",\n              table_name: \"customers\",\n            },\n          ],\n        },\n      ],\n    },\n    {\n      icon: \"AccountGroup\",\n      name: \"Customer Insights\",\n      layout: {\n        id: \"customer-root\",\n        size: 1,\n        type: \"col\",\n        items: [\n          {\n            id: \"customer-top-row\",\n            size: 0.5,\n            type: \"row\",\n            items: [\n              {\n                id: \"customers-table\",\n                size: 0.6,\n                type: \"item\",\n                viewType: \"table\",\n                tableName: \"customers\",\n              },\n              {\n                id: \"customer-orders-chart\",\n                size: 0.4,\n                type: \"item\",\n                viewType: \"timechart\",\n                tableName: \"customers\",\n              },\n            ],\n          },\n          {\n            id: \"customer-addresses\",\n            size: 0.5,\n            type: \"item\",\n            viewType: \"table\",\n            tableName: \"user_addresses\",\n          },\n        ],\n        isRoot: true,\n      },\n      windows: [\n        {\n          id: \"customers-table\",\n          sort: [\n            {\n              asc: false,\n              key: \"total_orders\",\n              nulls: \"last\",\n            },\n          ],\n          type: \"table\",\n          filter: [\n            {\n              type: \"$eq\",\n              value: \"customer\",\n              fieldName: \"type\",\n            },\n          ],\n          columns: [\n            {\n              name: \"id\",\n              width: 80,\n            },\n            {\n              name: \"first_name\",\n              width: 120,\n            },\n            {\n              name: \"last_name\",\n              width: 120,\n            },\n            {\n              name: \"email\",\n              width: 200,\n            },\n            {\n              name: \"phone_number\",\n              width: 150,\n            },\n            {\n              name: \"total_orders\",\n              width: 120,\n            },\n            {\n              name: \"last_order\",\n              width: 180,\n            },\n            {\n              name: \"created_at\",\n              width: 180,\n            },\n          ],\n          table_name: \"customers\",\n        },\n        {\n          id: \"customer-orders-chart\",\n          type: \"timechart\",\n          layers: [\n            {\n              yAxis: \"count(*)\",\n              table_name: \"customers\",\n              dateColumn: \"created_at\",\n            },\n          ],\n        },\n        {\n          id: \"customer-addresses\",\n          type: \"table\",\n          columns: [\n            {\n              name: \"user_id\",\n              width: 100,\n              nested: {\n                path: [\n                  {\n                    on: [\n                      {\n                        user_id: \"id\",\n                      },\n                    ],\n                    table: \"users\",\n                  },\n                ],\n                limit: 1,\n                columns: [\n                  {\n                    name: \"first_name\",\n                  },\n                  {\n                    name: \"last_name\",\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n            {\n              name: \"address_id\",\n              width: 120,\n              nested: {\n                path: [\n                  {\n                    on: [\n                      {\n                        address_id: \"id\",\n                      },\n                    ],\n                    table: \"addresses\",\n                  },\n                ],\n                limit: 1,\n                columns: [\n                  {\n                    name: \"street\",\n                  },\n                  {\n                    name: \"city\",\n                  },\n                  {\n                    name: \"postal_code\",\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n          ],\n          table_name: \"user_addresses\",\n        },\n      ],\n    },\n  ],\n};\n\nexport const dockerWeatherToolUse = {\n  files: {\n    Dockerfile:\n      'FROM node:18-alpine\\nWORKDIR /app\\nCOPY package.json .\\nRUN npm install\\nCOPY . .\\nCMD [\"node\", \"fetch_weather.js\"]',\n    \"package.json\":\n      '{\\n  \"name\": \"weather-fetcher\",\\n  \"version\": \"1.0.0\",\\n  \"dependencies\": {\\n    \"axios\": \"^1.6.0\"\\n  }\\n}',\n    \"fetch_weather.js\":\n      \"const axios = require('axios');\\n\\nconst DB_URL = 'http://172.17.0.1:3009/db/execute_sql_with_commit';\\n// Alternative: Use Open-Meteo (completely free, no API key needed)\\nasync function fetchFromOpenMeteo() {\\n  const lat = 51.5074; // London latitude\\n  const lon = -0.1278; // London longitude\\n  \\n  // Get last 4 years of data\\n  const endDate = new Date();\\n  const startDate = new Date();\\n  startDate.setFullYear(endDate.getFullYear() - 4);\\n  \\n  const startDateStr = startDate.toISOString().split('T')[0];\\n  const endDateStr = endDate.toISOString().split('T')[0];\\n  \\n  console.log(`Fetching data from ${startDateStr} to ${endDateStr}...`);\\n  \\n  try {\\n    const response = await axios.get('https://archive-api.open-meteo.com/v1/era5', {\\n      params: {\\n        latitude: lat,\\n        longitude: lon,\\n        start_date: startDateStr,\\n        end_date: endDateStr,\\n        daily: 'temperature_2m_mean,temperature_2m_min,temperature_2m_max,precipitation_sum,windspeed_10m_max,relative_humidity_2m_mean',\\n        timezone: 'Europe/London'\\n      }\\n    });\\n    \\n    const data = response.data.daily;\\n    \\n    for (let i = 10e10; i < data.time.length; i++) {\\n      const sql = `\\n        INSERT INTO weather_data (\\n          city, country, date, temperature_avg, temperature_min, temperature_max,\\n          humidity, precipitation, wind_speed, weather_condition\\n        ) VALUES (\\n          $1, $2, $3, $4, $5, $6, $7, $8, $9, $10\\n        ) ON CONFLICT (city, country, date) DO NOTHING\\n      `;\\n      \\n      await axios.post(DB_URL, {\\n        sql: sql,\\n        query_params: [\\n          'London',\\n          'United Kingdom',\\n          data.time[i],\\n          data.temperature_2m_mean[i],\\n          data.temperature_2m_min[i],\\n          data.temperature_2m_max[i],\\n          data.relative_humidity_2m_mean[i],\\n          data.precipitation_sum[i],\\n          data.windspeed_10m_max[i],\\n          'Historical Data'\\n        ]\\n      });\\n      \\n      if (i % 100 === 0) {\\n        console.log(`✓ Processed ${i + 1}/${data.time.length} records`);\\n      }\\n    }\\n    \\n    console.log(`✓ Successfully inserted ${data.time.length} weather records for London`);\\n    \\n  } catch (error) {\\n    console.error('Error fetching from Open-Meteo:', error.message);\\n  }\\n}\\n\\n// Run the Open-Meteo version (free, no API key needed)\\nfetchFromOpenMeteo().catch(console.error);\",\n  },\n  timeout: 120000,\n  networkMode: \"bridge\",\n};\n"
  },
  {
    "path": "e2e/tests/speechToTextTest.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport { readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getDataKey, type PageWIds } from \"utils/utils\";\n\nexport const speechToTextTest = async (page: PageWIds) => {\n  await grantMicrophonePermission(page);\n  await mockMediaDevicesWithAudioFile(page);\n  await page.reload();\n  await page.getByTestId(\"AskLLM\").click();\n  const newChat = async () => {\n    await page.getByTestId(\"AskLLMChat.NewChat\").click();\n    await page.waitForTimeout(1e3);\n  };\n\n  await newChat();\n  await page.getByTestId(\"Chat.speech\").click();\n  await page.locator(getDataKey(\"stt-local\")).click();\n  await page\n    .locator(getDataKey(\"speechToText\") + \" \" + getDataKey(\"model\"))\n    .click();\n  await page.locator(getDataKey(\"tiny\")).click();\n  await page\n    .locator(getDataKey(\"speechToText\") + \" \" + getDataKey(\"service-toggle\"))\n    .click();\n  await expect(\n    page.locator(\n      getDataKey(\"speechToText\") + \" \" + getDataKey(\"service-status\"),\n    ),\n  ).toContainText(\"running\", { timeout: 5 * 60_000 });\n\n  // Used to debug\n  await page.locator(getDataKey(\"audio\")).click();\n  await page.getByTestId(\"Popup.close\").last().click();\n\n  await page.getByTestId(\"Chat.speech\").click();\n  await page.waitForTimeout(2000);\n  await page\n    .getByTestId(\"Chat.sendWrapper\")\n    .locator(\"audio\")\n    .waitFor({ state: \"visible\" });\n  await page.getByTestId(\"Chat.send\").click();\n  await page\n    .getByTestId(\"Chat.messageList\")\n    .locator(\"audio\")\n    .waitFor({ state: \"visible\" });\n\n  await page\n    .getByTestId(\"Chat.speech\")\n    .click({ button: \"right\", timeout: 10_000 });\n  await page.locator(getDataKey(\"stt-local\")).click();\n  await page.getByTestId(\"Popup.close\").last().click();\n\n  await page.getByTestId(\"Chat.speech\").click();\n  await page.waitForTimeout(2000);\n  await page.waitForTimeout(2000);\n  await expect(page.getByTestId(\"Chat.textarea\")).toHaveValue(\"Hello.\", {\n    timeout: 10000,\n  });\n};\n\n// Helper to grant microphone permissions\nconst grantMicrophonePermission = async (page: PageWIds): Promise<void> => {\n  await page.context().grantPermissions([\"microphone\"]);\n};\nconst mockMediaDevicesWithAudioFile = async (page: PageWIds): Promise<void> => {\n  // Read file in Node.js context BEFORE injecting\n  const buffer = readFileSync(join(__dirname, \"hello.mp3\"));\n  const base64Audio = buffer.toString(\"base64\");\n\n  await page.addInitScript((audioBase64: string) => {\n    navigator.mediaDevices.getUserMedia = async (constraints) => {\n      console.log(\"[Mock] getUserMedia called with:\", constraints);\n      // simulate real user delay\n      await new Promise((resolve) => setTimeout(resolve, 2000));\n      if (constraints?.audio) {\n        // Decode base64 to ArrayBuffer in browser\n        const binaryString = atob(audioBase64);\n        const bytes = new Uint8Array(binaryString.length);\n        for (let i = 0; i < binaryString.length; i++) {\n          bytes[i] = binaryString.charCodeAt(i);\n        }\n\n        const audioContext = new AudioContext();\n        if (audioContext.state === \"suspended\") {\n          await audioContext.resume();\n        }\n        const audioBuffer = await audioContext.decodeAudioData(bytes.buffer);\n\n        const source = audioContext.createBufferSource();\n        const destination = audioContext.createMediaStreamDestination();\n\n        source.buffer = audioBuffer;\n        source.connect(destination);\n        source.start();\n\n        console.log(\"[Mock] Returning mock stream\");\n        return destination.stream;\n      }\n      throw new Error(\"No audio constraints\");\n    };\n  }, base64Audio);\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/SVG_SCREENSHOT_DETAILS.ts",
    "content": "import { dashboardSvgif } from \"svgScreenshots/dashboard.svgif\";\nimport { fileImporter } from \"svgScreenshots/fileImporter.svgif\";\nimport { schemaDiagramSvgif } from \"svgScreenshots/schemaDiagram.svgif\";\nimport { goTo } from \"utils/goTo\";\nimport { getCommandElemSelector, getDataKeyElemSelector } from \"../Testing\";\nimport { getDashboardUtils, getDataKey, type PageWIds } from \"../utils/utils\";\nimport { accountSvgif } from \"./account.svgif\";\nimport { aiAssistantSvgif } from \"./aiAssistant.svgif\";\nimport { backupAndRestoreSvgif } from \"./backupAndRestore.svgif\";\nimport { commandPaletteSvgif } from \"./commandPalette.svgif\";\nimport { electronSetupSvgif } from \"./electronSetup.svgif\";\nimport { mapSvgif } from \"./map.svgif\";\nimport { navbarSvgif } from \"./navbar.svgif\";\nimport { newConnectionSvgif } from \"./newConnection.svgif\";\nimport { smartFormSvgif } from \"./smartForm.svgif\";\nimport { sqlEditorSvgif } from \"./sqlEditor.svgif\";\nimport { tableSvgif } from \"./table.svgif\";\nimport { timechartSvgif } from \"./timechart.svgif\";\nimport type { getSceneUtils } from \"./utils/getSceneUtils\";\n\nexport type OnBeforeScreenshot = (\n  page: PageWIds,\n  utils: ReturnType<typeof getDashboardUtils>,\n  svgOpts: Omit<ReturnType<typeof getSceneUtils>, \"svgifScenes\">,\n) => Promise<void>;\n\nexport const SVG_SCREENSHOT_DETAILS = {\n  command_palette: commandPaletteSvgif,\n  ai_assistant: aiAssistantSvgif,\n  dashboard: dashboardSvgif,\n  timechart: timechartSvgif,\n  smart_form: smartFormSvgif,\n  backup_and_restore: backupAndRestoreSvgif,\n  table: tableSvgif,\n  map: mapSvgif,\n  account: accountSvgif,\n  navbar: navbarSvgif,\n  electron_setup: electronSetupSvgif,\n  schema_diagram: schemaDiagramSvgif,\n  sql_editor: sqlEditorSvgif,\n  file_importer: fileImporter,\n  new_connection: newConnectionSvgif,\n  connections: async (page) => {\n    await goTo(page, \"/connections\");\n  },\n  smart_filter_bar: async (page, { openConnection }) => {\n    await openConnection(\"prostgles_video_demo\");\n    await page.waitForTimeout(1500);\n  },\n  file_storage: async (page, { openConnection }) => {\n    await openConnection(\"prostgles_video_demo\");\n    await page.getByTestId(\"dashboard.goToConnConfig\").click();\n    await page.getByTestId(\"config.files\").click();\n    await page.mouse.move(0, 0);\n    await page.waitForTimeout(1500);\n  },\n  access_control: async (page, { openConnection }, { addSceneAnimation }) => {\n    await openConnection(\"prostgles_video_demo\");\n    await addSceneAnimation(getCommandElemSelector(\"dashboard.goToConnConfig\"));\n    await addSceneAnimation(getCommandElemSelector(\"config.ac\"));\n    await addSceneAnimation(getDataKey(\"default\"));\n    await page.mouse.move(0, 0);\n    await page.waitForTimeout(1500);\n  },\n  server_settings: async (page) => {\n    await page.reload();\n    await goTo(page, \"/server-settings\");\n    await page.waitForTimeout(1500);\n  },\n  connect_existing_database: async (page) => {\n    await goTo(page, \"/connections\");\n    await page.getByTestId(\"ConnectionServer.add\").click();\n    await page.locator(getDataKeyElemSelector(\"existing\")).click();\n    await page.getByTestId(\"ConnectionServer.add.existingDatabase\").click();\n    await page\n      .getByTestId(\"ConnectionServer.add.existingDatabase\")\n      .locator(getDataKeyElemSelector(\"postgres\"))\n      .click();\n    await page.waitForTimeout(1500);\n  },\n  connection_config: async (\n    page,\n    { openConnection },\n    { addScene, addSceneAnimation },\n  ) => {\n    await openConnection(\"prostgles_video_demo\");\n    await addSceneAnimation(getCommandElemSelector(\"dashboard.goToConnConfig\"));\n    await addSceneAnimation(getCommandElemSelector(\"config.status\"));\n    await addSceneAnimation(getCommandElemSelector(\"config.ac\"));\n    await addSceneAnimation(getCommandElemSelector(\"config.files\"));\n    await addSceneAnimation(getCommandElemSelector(\"config.bkp\"));\n    await addSceneAnimation(getCommandElemSelector(\"config.api\"));\n    await addSceneAnimation(getCommandElemSelector(\"config.tableConfig\"));\n    await addSceneAnimation(getCommandElemSelector(\"config.methods\"));\n    await addScene();\n    await page.waitForTimeout(1500);\n  },\n} satisfies Record<\n  string,\n  OnBeforeScreenshot | Record<string, OnBeforeScreenshot>\n>;\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/account.svgif.ts",
    "content": "import { getDataKeyElemSelector } from \"Testing\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { goTo } from \"utils/goTo\";\n\nexport const accountSvgif: OnBeforeScreenshot = async (\n  page,\n  _,\n  { addSceneAnimation },\n) => {\n  await goTo(page, \"/account\");\n  await page.waitForTimeout(1500);\n  await addSceneAnimation(getDataKeyElemSelector(\"security\"));\n  await addSceneAnimation(getDataKeyElemSelector(\"api\"));\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/aiAssistant.svgif.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport { getCommandElemSelector, getDataKeyElemSelector } from \"Testing\";\nimport { createReceipt } from \"createReceipt\";\nimport {\n  closeWorkspaceWindows,\n  deleteExistingLLMChat,\n  getDataKey,\n  runDbSql,\n  setModelByText,\n  setPromptByText,\n} from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { typeSendAddScenes } from \"./utils/typeSendAddScenes\";\n\nexport const aiAssistantSvgif: OnBeforeScreenshot = async (\n  page,\n  { openConnection },\n  { addScene, addSceneAnimation },\n) => {\n  // await goTo(page, \"/server-settings?section=llmProviders\");\n  // await page.getByTestId(\"dashboard.window.rowInsertTop\").click();\n  // await page.getByTestId(\"Popup.content\").waitFor({ state: \"visible\" });\n  // await page.waitForTimeout(1500);\n  // await page.keyboard.press(\"Enter\");\n  // await page.waitForTimeout(1500);\n\n  // await addScene({\n  //   svgFileName: \"supported_providers\",\n  //   caption: \"Supported providers\",\n  // });\n  await openConnection(\"food_delivery\");\n  await page.getByTestId(\"AskLLM\").click();\n  await page.waitForTimeout(1000);\n  const UnloadSuggestedDashboards = await page.getByTestId(\n    \"AskLLMChat.UnloadSuggestedDashboards\",\n  );\n  if (await UnloadSuggestedDashboards.count()) {\n    await UnloadSuggestedDashboards.click();\n    await page.waitForTimeout(1000);\n  } else {\n    await page.getByTestId(\"Popup.close\").last().click();\n  }\n  await deleteExistingLLMChat(page);\n  await page.getByTestId(\"Popup.close\").last().click();\n  await closeWorkspaceWindows(page);\n  await addSceneAnimation(getCommandElemSelector(\"AskLLM\"));\n\n  await setModelByText(page, \"pros\");\n  await setPromptByText(page, \"dashboard\");\n  const deletePreviousMessages = async () => {\n    const firstMessage = await page.getByTestId(\"AskLLM.DeleteMessage\").first();\n    if (await firstMessage.count()) {\n      await firstMessage.click();\n      await page.locator(getDataKeyElemSelector(\"allToBottom\")).click();\n    }\n  };\n  await addScene({\n    svgFileName: \"focus_textarea\",\n    animations: [\n      { type: \"wait\", duration: 1000 },\n      {\n        type: \"click\",\n        elementSelector: getCommandElemSelector(\"Chat.textarea\"),\n        offset: { x: 20, y: 10 },\n        duration: 1e3,\n      },\n    ],\n  });\n\n  await typeSendAddScenes(\n    page,\n    addScene,\n    \"I need some dashboards with useful insights and metrics\",\n    [\n      {\n        type: \"click\",\n        elementSelector: getCommandElemSelector(\n          \"AskLLMChat.LoadSuggestedDashboards\",\n        ),\n        duration: 1000,\n      },\n    ],\n  );\n  await page.getByTestId(\"AskLLMChat.LoadSuggestedDashboards\").click();\n  await page.waitForTimeout(4000);\n  await addScene({ svgFileName: \"dashboards_loaded\" });\n\n  await page.getByTestId(\"AskLLM\").click();\n  await page.getByTestId(\"AskLLMChat.UnloadSuggestedDashboards\").click();\n\n  await openConnection(\"prostgles_video_demo\");\n  await closeWorkspaceWindows(page);\n  await runDbSql(\n    page,\n    `\n      CREATE TABLE IF NOT EXISTS receipts (\n        id SERIAL PRIMARY KEY,\n        company TEXT,\n        extracted_text TEXT,\n        amount NUMERIC,\n        currency TEXT,\n        date TIMESTAMP,\n        created_at TIMESTAMP DEFAULT NOW()\n      );\n      `,\n  );\n  await page.getByTestId(\"AskLLM\").click();\n  await deletePreviousMessages();\n  await setPromptByText(page, \"chat\");\n  await setModelByText(page, \"pros\");\n\n  await deletePreviousMessages();\n  await setPromptByText(page, \"create task\");\n  await typeSendAddScenes(\n    page,\n    addScene,\n    \"The task involves importing data from receipt images I will paste in this chat\",\n  );\n  const loadTaskBtn = await page\n    .getByTestId(\"AskLLMChat.LoadSuggestedToolsAndPrompt\")\n    .last();\n\n  await loadTaskBtn.waitFor({ state: \"visible\", timeout: 15000 });\n  await addSceneAnimation(\n    getCommandElemSelector(\"AskLLMChat.LoadSuggestedToolsAndPrompt\"),\n  );\n\n  await page.getByTestId(\"Alert\").getByText(\"OK\").waitFor({ state: \"visible\" });\n  await page.waitForTimeout(1000);\n  await addScene({\n    svgFileName: \"tasks\",\n    animations: [{ type: \"wait\", duration: 1000 }],\n  });\n  await page.getByTestId(\"Alert\").getByText(\"OK\").click();\n  await page.waitForTimeout(4000);\n  const { filePath } = await createReceipt(page);\n  const fileChooserPromise = page.waitForEvent(\"filechooser\");\n  await page.getByTestId(\"Chat.addFiles\").click();\n  const fileChooser = await fileChooserPromise;\n  await fileChooser.setFiles(filePath);\n\n  await typeSendAddScenes(\n    page,\n    addScene,\n    ``,\n    //   [\n    //   { type: \"wait\", duration: 500 },\n    //   {\n    //     type: \"click\",\n    //     elementSelector:\n    //       getCommandElemSelector(\"ToolUseMessage.toggle\") + \":last-child\",\n    //     duration: 1000,\n    //   },\n    // ]\n  );\n\n  await page.waitForTimeout(2000);\n  await addScene({\n    svgFileName: \"vision_ocr\",\n    animations: [{ type: \"wait\", duration: 1000 }],\n  });\n\n  await page.getByTestId(\"ToolUseMessage.toggle\").last().click();\n  await expect(page.getByTestId(\"Popup.content\").last()).toContainText(\n    \"Grand Ocean Hotel\",\n  );\n\n  await deletePreviousMessages();\n  await setPromptByText(page, \"chat\");\n  await page.getByTestId(\"LLMChatOptions.MCPTools\").click();\n  await page\n    .getByTestId(\"MCPServerTools\")\n    .getByText(\"create_container\")\n    .click();\n  await page.getByText(\"Auto-approve: ON\").click();\n  await page.waitForTimeout(1000);\n  await page.getByText(\"Auto-approve: OFF\").click();\n  await page.waitForTimeout(1000);\n  await page.getByTestId(\"Popup.close\").last().click();\n\n  await typeSendAddScenes(\n    page,\n    addScene,\n    \"Upload some weather data for London for the last 4 years\",\n    [\n      { type: \"wait\", duration: 1000 },\n      {\n        type: \"click\",\n        elementSelector: getCommandElemSelector(\"ToolUseMessage.toggle\"),\n        duration: 1000,\n      },\n    ],\n  );\n  await page.getByTestId(\"ToolUseMessage.toggle\").last().click();\n  await page.waitForTimeout(2500);\n  await expect(page.getByTestId(\"ToolUseMessage\").last()).toContainText(\n    \"Fetching data from\",\n  );\n\n  await addSceneAnimation(getDataKeyElemSelector(\"fetch_weather.js\"));\n\n  // await page.getByTestId(\"ToolUseMessage\").last().scrollIntoViewIfNeeded();\n  // await addScene({\n  //   svgFileName: \"docker_js\",\n  //   animations: [\n  //     { type: \"wait\", duration: 1000 },\n  //     {\n  //       type: \"click\",\n  //       elementSelector: getDataKeyElemSelector(\"fetch_weather.js\"),\n  //       duration: 1000,\n  //     },\n  //   ],\n  // });\n\n  await addScene({\n    svgFileName: \"docker\",\n    animations: [{ type: \"wait\", duration: 1000 }],\n  });\n\n  await page.getByTestId(\"Popup.close\").last().click();\n  await deleteExistingLLMChat(page);\n  await page.getByTestId(\"LLMChatOptions.DatabaseAccess\").click();\n  await page\n    .getByTestId(\"Popup.content\")\n    .last()\n    .getByLabel(\"Mode\", { exact: true })\n    .click();\n\n  await page.getByRole(\"option\", { name: \"Run readonly SQL\" }).click();\n  await page.getByTestId(\"Popup.close\").last().click();\n\n  await setPromptByText(page, \"chat\");\n\n  const allowOnce = async (doClick = true) => {\n    const allowOnceBtn = await page\n      .getByTestId(\"AskLLMToolApprover.AllowOnce\")\n      .last();\n    await allowOnceBtn.waitFor({ state: \"visible\", timeout: 15000 });\n    doClick && (await allowOnceBtn.click());\n    await page.waitForTimeout(2500);\n  };\n  await typeSendAddScenes(\n    page,\n    addScene,\n    \"Show a list of orders from the last 30 days\",\n    [\n      { type: \"wait\", duration: 1000 },\n      {\n        type: \"click\",\n        elementSelector: getCommandElemSelector(\"AskLLMToolApprover.AllowOnce\"),\n        duration: 1000,\n      },\n    ],\n    () => allowOnce(false),\n  );\n  await allowOnce();\n  await page.getByTestId(\"ToolUseMessage.toggle\").last().click();\n  await expect(page.getByTestId(\"MarkdownMonacoCode\").last()).toContainText(\n    \"SELECT * FROM orders\",\n  );\n  await page.waitForTimeout(2000);\n  await addScene({ svgFileName: \"sql_result\" });\n\n  await deletePreviousMessages();\n  await addSceneAnimation(getCommandElemSelector(\"Chat.speech\"), \"rightClick\");\n  await addSceneAnimation(getDataKey(\"stt-local\"));\n  await addScene({ svgFileName: \"stt\" });\n  await page.keyboard.press(\"Escape\");\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/backupAndRestore.svgif.ts",
    "content": "import { getCommandElemSelector } from \"Testing\";\nimport { closeWorkspaceWindows, getDataKey, runDbsSql } from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\n\nexport const backupAndRestoreSvgif: OnBeforeScreenshot = async (\n  page,\n  { openConnection },\n  { addSceneAnimation, addScene },\n) => {\n  await openConnection(\"crypto\");\n  await closeWorkspaceWindows(page);\n  await addSceneAnimation(getCommandElemSelector(\"dashboard.goToConnConfig\"));\n  await addSceneAnimation(getCommandElemSelector(\"config.bkp\"));\n\n  /** Delete existing */\n  const deleteAllBtn = page.getByTestId(\"BackupControls.DeleteAll\");\n  if (await deleteAllBtn.count()) {\n    await deleteAllBtn.click();\n    const codeConfirmation = await page\n      .getByTitle(\"confirmation-code\")\n      .textContent();\n\n    if (!codeConfirmation) {\n      throw new Error(\"Expected confirmation code to be present\");\n    }\n    const input = page.locator('input[name=\"confirmation\"]');\n    await input.fill(codeConfirmation);\n    await page.getByTestId(\"BackupControls.DeleteAll.Confirm\").click();\n    await page.waitForTimeout(1000);\n  }\n\n  /** Add random credentials */\n  await runDbsSql(\n    page,\n    `\n        DELETE FROM credentials;\n        INSERT INTO credentials (\n         --name, \n        type, key_id, key_secret, region, bucket, endpoint_url)\n        VALUES \n          (\n           -- 'Demo S3 Creds',\n            'AWS',\n            'AKIAIOSFODNN7EXAMPLE',\n            'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n            'us-east-1',\n            'demo-app-assets',\n            DEFAULT\n          ), (\n           -- 'Demo S3 Creds',\n            'Cloudflare',\n            '260f4c3d8a1e4ab5b9f7d2e1a9c0f123',\n            'b32c9f4c7e8d1a2f9c0b4e6d7a1f2c3e9d4b7a0c1e2f3',\n            'auto',\n            'demo-app-assets',\n            'https://11112222333344445555666677778888.r2.cloudflarestorage.com'\n          ) \n      `,\n  );\n  await addSceneAnimation(getCommandElemSelector(\"config.bkp.create\"));\n  const backupName = \"Test\";\n\n  await addSceneAnimation(getDataKey(\"Cloud\"));\n  await addSceneAnimation(\n    getCommandElemSelector(\"CloudStorageCredentialSelector.selectCredential\"),\n    undefined,\n    \"fast\",\n  );\n  await addScene({ animations: [{ type: \"wait\", duration: 1500 }] });\n  await page.keyboard.press(\"Escape\");\n  await addSceneAnimation(getDataKey(\"Local\"), undefined, \"fast\");\n  await addSceneAnimation(\n    getCommandElemSelector(\"config.bkp.create.name\"),\n    {\n      action: \"type\",\n      text: backupName,\n    },\n    \"fast\",\n  );\n  await addSceneAnimation(\n    getCommandElemSelector(\"config.bkp.create.start\"),\n    undefined,\n    \"fast\",\n  );\n  await page\n    .getByTestId(\"BackupControls.DeleteAll\")\n    .waitFor({ state: \"visible\", timeout: 20_000 });\n\n  const {\n    rows: [existingDemoBackup],\n  } = await runDbsSql(\n    page,\n    \"SELECT * FROM backups WHERE name = ${backupName} \",\n    { backupName },\n  );\n  if (!existingDemoBackup) {\n    throw new Error(\"Expected existing backup named \" + backupName);\n  }\n\n  const logLines = (existingDemoBackup.dump_logs as string).split(\"\\n\");\n  const totalSize = Number(existingDemoBackup.dbSizeInBytes);\n  const originalCreated = existingDemoBackup.created;\n  const created = new Date(originalCreated).toISOString();\n  const steps = 3;\n  const query = `\n        UPDATE backups\n        SET status = \\${status},\n            dump_logs = \\${dump_logs},\n            created = \\${created}\n        WHERE name = \\${backupName}\n      `;\n  for (const [indexFromEnd] of logLines.slice(-steps).entries()) {\n    const index = logLines.length - 1 - indexFromEnd;\n    const currentPercentage =\n      !indexFromEnd ? 1 : (\n        -0.6 + (logLines.length - steps + indexFromEnd + 1) / logLines.length\n      );\n    await runDbsSql(page, query, {\n      dump_logs: logLines.slice(0, index).join(\"\\n\"),\n      created,\n      status: {\n        loading: { loaded: currentPercentage * totalSize, total: totalSize },\n      },\n      backupName,\n    });\n    await page.waitForTimeout(1500);\n    await addScene({ animations: [{ type: \"wait\", duration: 1500 }] });\n  }\n\n  await runDbsSql(page, query, {\n    dump_logs: logLines.join(\"\\n\"),\n    created: originalCreated,\n    status: {\n      ok: \"1\",\n    },\n    backupName,\n  });\n\n  await page.mouse.move(0, 0);\n  await page.waitForTimeout(1500);\n  await addScene();\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/commandPalette.svgif.ts",
    "content": "import { getCommandElemSelector } from \"Testing\";\nimport { closeWorkspaceWindows, openConnection } from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport type { SVGifScene } from \"./utils/constants\";\n\nconst showBriefly = {\n  animations: [{ type: \"wait\", duration: 600 }],\n} satisfies Partial<SVGifScene>;\n\nexport const commandPaletteSvgif: OnBeforeScreenshot = async (\n  page,\n  { toggleMenuPinned },\n  { addScene },\n) => {\n  await openConnection(page, \"Prostgles UI automated tests database\");\n  await closeWorkspaceWindows(page);\n  await toggleMenuPinned(false);\n  await page.keyboard.press(\"Control+KeyK\");\n  await page.getByTestId(\"CommandPalette\").waitFor({ state: \"visible\" });\n  await page.waitForTimeout(1000);\n  // await addScene({\n  //   svgFileName: \"empty\",\n  //   caption: \"Command palette (Ctrl+K)...\",\n  // });\n  await addScene({\n    animations: [\n      {\n        type: \"growIn\",\n        duration: 500,\n        elementSelector: getCommandElemSelector(\"CommandPalette\"),\n      },\n      ...showBriefly.animations,\n    ],\n    caption: \"Command palette (Ctrl+K)...\",\n  });\n  for (const char of \"add mc\") {\n    await page.keyboard.press(char);\n    await page.waitForTimeout(200);\n    await addScene({\n      animations: [\n        {\n          type: \"wait\",\n          duration: 100,\n        },\n      ],\n    });\n  }\n  await page.keyboard.press(\"ArrowDown\");\n  await addScene(showBriefly);\n  // await page.keyboard.press(\"ArrowDown\");\n  // await addScene(showBriefly);\n  await page.waitForTimeout(500);\n  await addScene({\n    caption: \"Enter opens the selected UI view\",\n    ...showBriefly,\n  });\n  await page.keyboard.press(\"Enter\");\n  await page.waitForTimeout(6500);\n  await addScene({\n    animations: [\n      {\n        type: \"growIn\",\n        duration: 500,\n        elementSelector: getCommandElemSelector(\"AddMCPServer\"),\n      },\n      { type: \"wait\", duration: 1000 },\n    ],\n  });\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/dashboard.svgif.ts",
    "content": "import { getCommandElemSelector } from \"Testing\";\nimport { goTo } from \"utils/goTo\";\nimport {\n  closeWorkspaceWindows,\n  deleteAllWorkspaces,\n  getDataKey,\n} from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { expect } from \"@playwright/test\";\nimport { clickTableRow } from \"./table.svgif\";\n\nexport const dashboardSvgif: OnBeforeScreenshot = async (\n  page,\n  { openConnection, openMenuIfClosed, toggleMenuPinned },\n  { addScene, addSceneAnimation },\n) => {\n  await goTo(page, \"/connections\");\n\n  await openConnection(\"food_delivery\");\n\n  const toggleMenuBtn = await page.getByTestId(\n    \"DashboardMenuHeader.togglePinned\",\n  );\n  await page.getByTestId(\"WorkspaceMenuDropDown\").waitFor({ state: \"visible\" });\n  if (await toggleMenuBtn.count()) {\n    await toggleMenuBtn.click();\n  }\n\n  await expect(\n    page.getByTestId(\"DashboardMenuHeader.togglePinned\"),\n  ).toHaveCount(0);\n\n  await deleteAllWorkspaces(page);\n  await closeWorkspaceWindows(page);\n\n  await openMenuIfClosed();\n  await page.getByTestId(\"dashboard.menu.settingsToggle\").click();\n  await page.getByLabel(\"Default layout type\").click();\n  await page.locator(getDataKey(\"col\")).click();\n  await page.getByTestId(\"Popup.close\").last().click();\n\n  // await setOrAddWorkspace(page, \"Default Grid Layout\");\n  await openMenuIfClosed(true);\n\n  /** Search all */\n  await addScene({ caption: \"Search all tables (Ctrl+Shift+F)\" });\n  await page.keyboard.press(\"Control+Shift+KeyF\");\n  await addScene();\n  const searchAllInput = page.getByTestId(\"SearchAll\");\n  /** To prevent searching */\n  await searchAllInput.evaluate(\n    (el: HTMLInputElement) => (el.value = \"bengal tiger\"),\n  );\n  await addScene({\n    animations: [\n      {\n        type: \"growIn\",\n        elementSelector: getCommandElemSelector(\"SearchAll.Popup\"),\n        duration: 500,\n      },\n      {\n        type: \"type\",\n        elementSelector: getCommandElemSelector(\"SearchAll\"),\n        duration: 2000,\n      },\n    ],\n  });\n  await searchAllInput.evaluate((el: HTMLInputElement) => (el.value = \"\"));\n  await searchAllInput.fill(\"bengal tiger\");\n  await page.waitForTimeout(1000);\n  await addScene({ animations: [{ type: \"wait\", duration: 1500 }] });\n  await page.waitForTimeout(5500);\n  await addScene({ animations: [{ type: \"wait\", duration: 1500 }] });\n  await page.keyboard.press(\"Enter\");\n  await page.getByTestId(\"dashboard.window.menu\").waitFor({ state: \"visible\" });\n  await page.waitForTimeout(2000);\n  await addScene({ animations: [{ type: \"wait\", duration: 1500 }] });\n\n  // Table\n  await closeWorkspaceWindows(page);\n  await openMenuIfClosed();\n  await addSceneAnimation(getDataKey(\"orders\"));\n\n  const pageParams = { page, addSceneAnimation, addScene };\n  await clickTableRow(pageParams, 1, undefined, 1);\n\n  await addSceneAnimation(\n    getCommandElemSelector(\"JoinedRecords.SectionToggle\") +\n      '[data-key=\"order_items\"]',\n  );\n\n  await page.waitForTimeout(2000);\n  await page\n    .locator(\n      getCommandElemSelector(\"JoinedRecords.Section\") +\n        '[data-key=\"order_items\"]',\n    )\n    .scrollIntoViewIfNeeded();\n  await addScene();\n  await page.getByTestId(\"Popup.close\").click();\n\n  /* Ensure location is populated */\n  await addSceneAnimation(\n    getCommandElemSelector(\"dashboard.window.toggleFilterBar\"),\n  );\n  await page.getByTestId(\"SearchList.Input\").fill(\"picked\");\n  await page.locator(`[data-label=\"picked_up\"]`).waitFor({ state: \"visible\" });\n  await page.keyboard.press(\"ArrowDown\");\n  await page.keyboard.press(\"Enter\");\n  await page.getByTestId(\"dashboard.window.toggleFilterBar\").click();\n  await page.reload();\n  await page.getByTestId(\"dashboard.window.menu\").waitFor({ state: \"visible\" });\n\n  await addSceneAnimation(getCommandElemSelector(\"AddChartMenu.Map\"));\n\n  await addSceneAnimation(getDataKey(\"(deliverer_id = id) users\"));\n  await page.waitForTimeout(3000);\n  await addSceneAnimation(\n    getCommandElemSelector(\"dashboard.window.detachChart\"),\n  );\n\n  await page.waitForTimeout(3000);\n\n  await page.getByTestId(\"MapExtentBehavior\").click();\n  await page.waitForTimeout(2000);\n  await page.locator(getDataKey(\"autoZoomToData\")).click();\n  await clickTableRow(pageParams, 2);\n\n  await clickTableRow(pageParams, 3);\n\n  await clickTableRow(pageParams, 1);\n\n  await addScene({ animations: [{ type: \"wait\", duration: 1000 }] });\n\n  await clickTableRow(pageParams, 1);\n\n  await addSceneAnimation(getCommandElemSelector(\"AddChartMenu.Timechart\"));\n  await addSceneAnimation(\n    getCommandElemSelector(\"AddChartMenu.Timechart\") +\n      \" \" +\n      getDataKey(\"created_at\"),\n  );\n  await addScene({ animations: [{ type: \"wait\", duration: 3000 }] });\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/electronSetup.svgif.ts",
    "content": "import { goTo } from \"utils/goTo\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { getCommandElemSelector } from \"Testing\";\n\nexport const electronSetupSvgif: OnBeforeScreenshot = async (\n  page,\n  _,\n  { addSceneAnimation, addScene },\n) => {\n  await page.addInitScript(() => {\n    //@ts-ignore\n    window.MOCK_ELECTRON_WINDOW_ATTR = true;\n  });\n  await goTo(page, \"/\");\n  await addSceneAnimation(getCommandElemSelector(\"ElectronSetup.Next\"));\n  await page.waitForTimeout(2500);\n  await addScene({\n    animations: [{ type: \"wait\", duration: 5000 }],\n  });\n  await page.addInitScript(() => {\n    //@ts-ignore\n    delete window.MOCK_ELECTRON_WINDOW_ATTR;\n  });\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/fileImporter.svgif.ts",
    "content": "import { getCommandElemSelector, getDataKeyElemSelector } from \"Testing\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { closeWorkspaceWindows, runDbSql } from \"utils/utils\";\n\nexport const fileImporter: OnBeforeScreenshot = async (\n  page,\n  { openConnection, openMenuIfClosed },\n  { addScene, addSceneAnimation },\n) => {\n  await openConnection(\"prostgles_video_demo\");\n  await closeWorkspaceWindows(page);\n  await openMenuIfClosed();\n  await addSceneAnimation(getCommandElemSelector(\"dashboard.menu.create\"));\n\n  await addSceneAnimation(getDataKeyElemSelector(\"import file\"));\n\n  const tableName = \"contacts.csv\";\n  await runDbSql(page, \"DROP TABLE IF EXISTS ${tableName:name}\", { tableName });\n  await page.waitForTimeout(500);\n  const csvContent = `Name,Email,Age,Department\n    Alice Wilson,alice@example.com,28,Engineering\n    Charlie Brown,charlie@example.com,32,Marketing\n    Diana Prince,diana@example.com,29,Sales\n    Edward Norton,edward@example.com,31,HR`;\n  await page.waitForTimeout(500);\n\n  await addSceneAnimation(getCommandElemSelector(\"FileBtn\"));\n\n  await page.getByTestId(\"FileBtn\").setInputFiles({\n    name: tableName,\n    mimeType: \"text/plain\",\n    buffer: Buffer.from(csvContent),\n  });\n  await page.waitForTimeout(1500);\n  await addScene({ animations: [{ type: \"wait\", duration: 2000 }] });\n  await addSceneAnimation(getCommandElemSelector(\"FileImporterFooter.import\"));\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/getOverviewSvgifSpecs.svgif.ts",
    "content": "import type { SVG_SCREENSHOT_DETAILS } from \"./SVG_SCREENSHOT_DETAILS\";\nimport type { SVGifScene } from \"./utils/constants\";\n\nexport const getOverviewSvgifSpecs = async (\n  existing: Record<keyof typeof SVG_SCREENSHOT_DETAILS, SVGifScene[]>,\n) => {\n  const sliceScenes = (\n    fileName: keyof typeof SVG_SCREENSHOT_DETAILS,\n    start: number,\n    end = existing[fileName].length,\n  ) => {\n    const scenes = structuredClone(existing[fileName].slice(start, end));\n    if (scenes.length !== end - start) {\n      throw new Error(\n        `Not enough scenes in ${fileName}: expected ${end - start}, got ${scenes.length}`,\n      );\n    }\n    const lastScene = scenes[scenes.length - 1];\n    lastScene.animations.push({ type: \"wait\", duration: 4000 });\n    return scenes;\n  };\n  const overviewSvgifSpecs = [\n    /** Overview section */\n    {\n      fileName: \"linked_data\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"table\", 2)],\n    },\n    {\n      fileName: \"interactive_dashboards\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"dashboard\", 15)],\n    },\n    {\n      fileName: \"ai_assistant_overview\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"ai_assistant\", 0)],\n    },\n    {\n      fileName: \"sql_editor_overview\",\n      usedExternally: true,\n      scenes: [\n        ...sliceScenes(\"sql_editor\", 1, 10),\n        ...sliceScenes(\"sql_editor\", 18, 19),\n      ],\n    },\n\n    {\n      fileName: \"ai_assistant_questions\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"ai_assistant\", 20, 25)],\n    },\n\n    {\n      fileName: \"ai_assistant_dashboards\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"ai_assistant\", 1, 6)],\n    },\n    {\n      fileName: \"ai_assistant_mcp_tools\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"ai_assistant\", 6, 13)],\n    },\n    {\n      fileName: \"ai_assistant_stt\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"ai_assistant\", 24)],\n    },\n\n    /** SQL section */\n    {\n      fileName: \"sql_editor_suggestions\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"sql_editor\", 1, 8)],\n    },\n    {\n      fileName: \"sql_editor_charts\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"sql_editor\", 12)],\n    },\n    {\n      fileName: \"sql_editor_jsonb\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"sql_editor\", 7, 10)],\n    },\n\n    /** Backup and restore section */\n    {\n      fileName: \"backup_and_restore_overview\",\n      usedExternally: true,\n      scenes: [...sliceScenes(\"backup_and_restore\", 2)],\n    },\n\n    {\n      fileName: \"overview\",\n      scenes: [\n        ...sliceScenes(\"command_palette\", 1, 8),\n        ...sliceScenes(\"schema_diagram\", 1, 6),\n        ...sliceScenes(\"dashboard\", 6),\n        ...sliceScenes(\"ai_assistant\", 0),\n\n        ...sliceScenes(\"sql_editor\", 8, 10),\n        ...sliceScenes(\"sql_editor\", 18, 19),\n        ...sliceScenes(\"file_importer\", 0),\n      ],\n    },\n  ];\n\n  const svgifCovers: { fileName: string; svgSceneFileName: string }[] = [\n    {\n      fileName: \"linked_data\",\n      svgSceneFileName: existing.dashboard[20]!.svgFileName,\n    },\n    {\n      fileName: \"sql_editor\",\n      svgSceneFileName: existing.sql_editor[13]!.svgFileName,\n    },\n    {\n      fileName: \"sql_editor1\",\n      svgSceneFileName: existing.sql_editor[18]!.svgFileName,\n    },\n    {\n      fileName: \"backups\",\n      svgSceneFileName: existing.backup_and_restore[16]!.svgFileName,\n    },\n\n    {\n      fileName: \"ai_assistant\",\n      svgSceneFileName: existing.ai_assistant[4]!.svgFileName,\n    },\n\n    {\n      fileName: \"timechart_cover\",\n      svgSceneFileName: existing.timechart[19]!.svgFileName,\n    },\n  ];\n\n  return { svgifCovers, overviewSvgifSpecs };\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/map.svgif.ts",
    "content": "import { getCommandElemSelector } from \"Testing\";\nimport { getDataKey, openConnection } from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { clickTableRow } from \"./table.svgif\";\n\nexport const mapSvgif: OnBeforeScreenshot = async (\n  page,\n  { toggleMenuPinned },\n  { addScene, addSceneAnimation },\n) => {\n  await openConnection(page, \"food_delivery\");\n  await page.waitForTimeout(1500);\n\n  await addSceneAnimation(getCommandElemSelector(\"AddChartMenu.Map\"));\n  await addSceneAnimation(\n    getCommandElemSelector(\"AddChartMenu.Map\") + \" \" + getDataKey(\"location\"),\n  );\n  await addSceneAnimation(\n    getCommandElemSelector(\"dashboard.window.detachChart\"),\n  );\n  await page.waitForTimeout(3000);\n  await addSceneAnimation(getCommandElemSelector(\"MapExtentBehavior\"));\n  await addSceneAnimation(getDataKey(\"autoZoomToData\"));\n\n  /** Ensure only records with location are at the top */\n  await page.locator(getDataKey(\"location\")).click();\n\n  const pageParams = { page, addSceneAnimation, addScene };\n  await clickTableRow(pageParams, 1, undefined, 2);\n  await clickTableRow(pageParams, 2, undefined, 2);\n  await clickTableRow(pageParams, 3, undefined, 2);\n  await addScene();\n\n  // let getUsersTableView = await page.locator(`[data-table-name=\"users\"]`);\n\n  // if (!(await getUsersTableView.count())) {\n  //   await openTable(page, \"users\");\n  //   await page.getByTestId(\"AddChartMenu.Map\").click();\n  //   await page.waitForTimeout(1500);\n  //   await page.keyboard.press(\"Enter\");\n  //   await page.waitForTimeout(1500);\n  //   getUsersTableView = await page.locator(`[data-table-name=\"users\"]`);\n  // }\n  // await toggleMenuPinned();\n\n  // const chartDetachBtn = await getUsersTableView.getByTestId(\n  //   \"dashboard.window.detachChart\",\n  // );\n\n  // if (await chartDetachBtn.count()) {\n  //   await chartDetachBtn.click();\n  //   await page.waitForTimeout(1500);\n  // }\n\n  await page\n    .locator(`[data-view-type=\"map\"]`)\n    .getByTestId(\"dashboard.window.fullscreen\")\n    .click();\n  await page.waitForTimeout(1500);\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/navbar.svgif.ts",
    "content": "import { getCommandElemSelector, getDataKeyElemSelector } from \"Testing\";\nimport { goTo } from \"utils/goTo\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\n\nexport const navbarSvgif: OnBeforeScreenshot = async (\n  page,\n  _,\n  { addSceneAnimation },\n) => {\n  await goTo(page, \"/\");\n\n  for (const to of [\"/connections\", \"/users\", \"/server-settings\", \"/account\"]) {\n    await addSceneAnimation(getDataKeyElemSelector(to));\n    await page.waitForTimeout(2000);\n  }\n\n  await addSceneAnimation(getCommandElemSelector(\"App.colorScheme\"));\n  await page.keyboard.press(\"Escape\");\n  await page.waitForTimeout(2000);\n  await addSceneAnimation(getCommandElemSelector(\"App.LanguageSelector\"));\n  await page.waitForTimeout(2000);\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/newConnection.svgif.ts",
    "content": "import { goTo } from \"utils/goTo\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\n\nexport const newConnectionSvgif: OnBeforeScreenshot = async (\n  page,\n  _,\n  { addScene },\n) => {\n  await goTo(page, \"/connections\");\n  await addScene({\n    animations: [\n      {\n        type: \"wait\",\n        duration: 1000,\n      },\n      {\n        elementSelector: '[data-command=\"Connections.new\"]',\n        duration: 1000,\n        type: \"click\",\n      },\n    ],\n  });\n  await page.getByTestId(\"Connections.new\").click();\n  await page.waitForTimeout(1500);\n  await addScene({ svgFileName: \"new_connection\" });\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/schemaDiagram.svgif.ts",
    "content": "import { getCommandElemSelector } from \"Testing\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { closeWorkspaceWindows } from \"utils/utils\";\n\nexport const schemaDiagramSvgif: OnBeforeScreenshot = async (\n  page,\n  { openMenuIfClosed, openConnection },\n  { addScene },\n) => {\n  await openConnection(\"prostgles_video_demo\");\n  await closeWorkspaceWindows(page);\n  await openMenuIfClosed();\n  await addScene({\n    animations: [\n      { type: \"wait\", duration: 1000 },\n      {\n        type: \"click\",\n        elementSelector: getCommandElemSelector(\"SchemaGraph\"),\n        duration: 1000,\n      },\n    ],\n  });\n  await page.getByTestId(\"SchemaGraph\").click();\n  await page.waitForTimeout(1500);\n  await addScene({\n    animations: [\n      {\n        type: \"fadeIn\",\n        elementSelector: getCommandElemSelector(\"SchemaGraph\"),\n        duration: 500,\n      },\n      { type: \"wait\", duration: 1000 },\n    ],\n  });\n\n  await page\n    .getByTestId(\"SchemaGraph\")\n    .locator(\"canvas\")\n    .waitFor({ state: \"visible\" });\n\n  for (const point of [\n    [390, 430],\n    [350, 440],\n  ] satisfies [number, number][]) {\n    await page.mouse.move(...point, { steps: 10 });\n    await page.waitForTimeout(400);\n    await addScene({\n      animations: [\n        {\n          type: \"moveTo\",\n          xy: point,\n          duration: 500,\n        },\n        { type: \"wait\", duration: 1000 },\n      ],\n    });\n  }\n\n  await addScene({\n    animations: [{ type: \"moveTo\", xy: [350, 460], duration: 200 }],\n  });\n  await addScene({\n    animations: [{ type: \"wait\", duration: 4000 }],\n  });\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/smartForm.svgif.ts",
    "content": "import { closeWorkspaceWindows, getDataKey, openTable } from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { getCommandElemSelector } from \"Testing\";\n\nexport const smartFormSvgif: OnBeforeScreenshot = async (\n  page,\n  { openConnection, toggleMenuPinned },\n  { addSceneAnimation, addScene },\n) => {\n  await openConnection(\"food_delivery\");\n  await closeWorkspaceWindows(page);\n  await openTable(page, \"orders\");\n  await toggleMenuPinned(false);\n  await addSceneAnimation({\n    selector: getCommandElemSelector(\"dashboard.window.viewEditRow\"),\n    nth: 0,\n  });\n  await page.waitForTimeout(1500);\n  await addSceneAnimation(\n    getCommandElemSelector(\"JoinedRecords.SectionToggle\") +\n      getDataKey(\"order_items\"),\n  );\n  await page.waitForTimeout(1500);\n  await addSceneAnimation({\n    selector: getCommandElemSelector(\"SmartCard.viewEditRow\"),\n    nth: 0,\n  });\n  await addScene();\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/sqlEditor.svgif.ts",
    "content": "import { getCommandElemSelector, type SVGif } from \"Testing\";\nimport {\n  closeWorkspaceWindows,\n  getDataKey,\n  monacoType,\n  runDbSql,\n} from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { expect } from \"@playwright/test\";\n\nexport const sqlEditorSvgif: OnBeforeScreenshot = async (\n  page,\n  { openConnection, openMenuIfClosed, toggleMenuPinned },\n  { addScene, addSceneAnimation },\n) => {\n  await openConnection(\"prostgles_video_demo\");\n  await page.getByTestId(\"WorkspaceMenu.list\").getByText(\"default\").click();\n  await toggleMenuPinned();\n  await closeWorkspaceWindows(page);\n  await openMenuIfClosed();\n\n  const sqlSuggestionsScene = async ({\n    query,\n    svgFileName,\n    caption,\n  }: {\n    query: string;\n    svgFileName: string;\n    caption?: string;\n    animations?: SVGif.Animation[];\n  }) => {\n    await monacoType(page, `.ProstglesSQL`, query, {\n      deleteAllAndFill: true,\n    });\n    await addScene({\n      svgFileName,\n      caption,\n      animations: [{ type: \"wait\", duration: 1000 }],\n    });\n  };\n\n  await page.waitForTimeout(500);\n  await addSceneAnimation(getCommandElemSelector(\"dashboard.menu.sqlEditor\"));\n  await page.waitForTimeout(500);\n  await toggleMenuPinned();\n\n  await sqlSuggestionsScene({\n    svgFileName: \"keywords\",\n    caption: \"Keywords details and documentation\",\n    query: \"se\",\n  });\n\n  await sqlSuggestionsScene({\n    query: \"EXPLAIN ( \",\n    svgFileName: \"explain_options\",\n    animations: [{ type: \"wait\", duration: 2000 }],\n    // caption: \"EXPLAIN command options\",\n  });\n\n  await sqlSuggestionsScene({\n    query: \"CREATE INDEX idx_messages_sent ON messages \\nUSING \",\n    svgFileName: \"index_types\",\n    animations: [{ type: \"wait\", duration: 2000 }],\n    // caption: \"Index type suggestions\",\n  });\n\n  await sqlSuggestionsScene({\n    query: \"SELECT jsonb_agg\",\n    svgFileName: \"functions\",\n    caption: \"Function argument details and documentation\",\n  });\n\n  await sqlSuggestionsScene({\n    query: \"SELECT *\\nFROM me\",\n    svgFileName: \"tables\",\n    caption: \"Table details with related data\",\n  });\n\n  await sqlSuggestionsScene({\n    query: \"SELECT * \\nFROM messages m\\nJOIN us\",\n    svgFileName: \"joins\",\n    caption: \"JOIN suggestions\",\n  });\n\n  await sqlSuggestionsScene({\n    query:\n      \"SELECT * \\nFROM messages m \\nJOIN users u\\n ON u.id = m.sender_id\\nWHERE u.options \",\n    svgFileName: \"jsonb_properties\",\n    caption: \"JSONB property suggestions\",\n  });\n\n  /** Insert data */\n  await runDbSql(\n    page,\n    `\n    DELETE FROM users WHERE username IN ('user1', 'user2', 'user3');\n    WITH user_inserts AS (\n      INSERT INTO users (id, username, options) VALUES\n      (gen_random_uuid(), 'user1', '{\"timeZone\": \"UTC\"}'),\n      (gen_random_uuid(), 'user2', '{\"timeZone\": \"PST\"}'),\n      (gen_random_uuid(), 'user3', '{\"timeZone\": \"America/New_York\"}')\n      RETURNING *\n    )\n    INSERT INTO messages (sender_id, message_text, \"timestamp\") VALUES\n    ((SELECT id FROM user_inserts WHERE username = 'user1'), 'Hello from user1', now() - '1day'::interval),\n    ((SELECT id FROM user_inserts WHERE username = 'user2'), 'Hello from user2', now() - '2day'::interval),\n    ((SELECT id FROM user_inserts WHERE username = 'user2'), 'Hello from user2', now() - '2day'::interval),\n    ((SELECT id FROM user_inserts WHERE username = 'user2'), 'Hello from user2', now() - '4day'::interval),\n    ((SELECT id FROM user_inserts WHERE username = 'user2'), 'Hello from user2', now() - '4day'::interval),\n    ((SELECT id FROM user_inserts WHERE username = 'user3'), 'Hello from user3', now() - '5day'::interval);\n  `,\n  );\n  await monacoType(\n    page,\n    `.ProstglesSQL`,\n    \"SELECT * \\nFROM messages m \\nJOIN users u\\n ON u.id = m.sender_id\\nWHERE u.options ->>'timeZone' = ''\",\n    {\n      deleteAllAndFill: true,\n      pressAfterTyping: [\"ArrowLeft\", \"Control+Space\"],\n    },\n  );\n  await addScene({\n    svgFileName: \"values\",\n    caption: \"Column value suggestions\",\n  });\n  await page.keyboard.press(\"Tab\");\n  await page.keyboard.press(\"Alt+KeyE\");\n  await page.waitForTimeout(1500);\n  await addScene({\n    svgFileName: \"values_result\",\n    caption: \"Results table with sorting\",\n  });\n\n  await page.reload();\n  await page.waitForTimeout(1500);\n\n  await sqlSuggestionsScene({\n    query: \"SELECT max() over( \",\n    svgFileName: \"window_functions\",\n    caption: \"Window functions\",\n  });\n\n  await page.getByTestId(\"dashboard.window.menu\").click();\n  await page.getByText(\"Editor options\").click();\n  await monacoType(page, `.CodeEditor`, \", show\", {\n    pressBeforeTyping: [\"ArrowLeft\"],\n    pressAfterTyping: [\"Tab\", \"Space\", \"ArrowDown\", \"Enter\"],\n  });\n  await page.getByText(\"Update options\").click();\n  await page.waitForTimeout(1000);\n  await page.keyboard.press(\"Escape\");\n  await page.keyboard.press(\"Escape\");\n  await monacoType(page, `.ProstglesSQL`, intensiveQuery, {\n    deleteAllAndFill: true,\n    keyPressDelay: 0,\n  });\n  await page.keyboard.press(\"Alt+KeyE\");\n  await expect(page.getByTestId(\"W_SQLBottomBar\")).toContainText(\"Mhz\");\n  await addScene({\n    svgFileName: \"cpu_usage\",\n    animations: [{ type: \"wait\", duration: 2000 }],\n    caption: \"Runtime statistics\",\n  });\n\n  await page.reload();\n\n  await monacoType(\n    page,\n    `.ProstglesSQL`,\n    \"WITH recent_messages AS (\\n  SELECT * FROM messages\\n  WHERE \\\"timestamp\\\" > NOW() - INTERVAL '7 days'\\n)\\nSELECT * FROM \",\n    {\n      deleteAllAndFill: true,\n      /** Sets value to avoid extra parens inserted while typing */\n      keyPressDelay: 0,\n      pressAfterTyping: [\n        \"ArrowDown\",\n        \"ArrowDown\",\n        \"ArrowDown\",\n        \"Control+ArrowRight\",\n        \"Control+ArrowRight\",\n        \"Control+ArrowRight\",\n        \"Control+ArrowRight\",\n        \"Control+ArrowRight\",\n        \"Control+Space\",\n      ],\n    },\n  );\n  await addScene({\n    svgFileName: \"cte\",\n    caption: \"Common Table Expression (CTE) completion\",\n  });\n\n  await page.keyboard.press(\"Tab\");\n  await page.waitForTimeout(1500);\n\n  await addScene({\n    svgFileName: \"timechart_btn\",\n    animations: [\n      {\n        type: \"click\",\n        elementSelector: getCommandElemSelector(\"AddChartMenu.Timechart\"),\n        duration: 500,\n      },\n    ],\n  });\n\n  await page.getByTestId(\"AddChartMenu.Timechart\").first().click();\n  await page.waitForTimeout(1500);\n  await addScene({\n    svgFileName: \"timechart_btn2\",\n    animations: [\n      {\n        type: \"click\",\n        elementSelector: '[data-key=\"timestamp\"]',\n        duration: 500,\n      },\n    ],\n  });\n  await page.locator(getDataKey(\"timestamp\")).click();\n  await page.waitForTimeout(1500);\n  await addScene({\n    svgFileName: \"timechart\",\n    caption: \"Timechart visualization\",\n  });\n  await page.getByTestId(\"dashboard.window.closeChart\").click();\n\n  await runDbSql(page, `CREATE EXTENSION IF NOT EXISTS \"postgis\"`);\n  await page.waitForTimeout(1500);\n  /** Map */\n  const mapQuery = [\n    `SELECT id,`,\n    `ST_SetSRID(`,\n    `  ST_MakePoint(`,\n    `    CAST( 144.9 + (random() - 0.5) * 0.5 AS double precision),  `,\n    `    CAST( -37.8 + (random() - 0.5) * 0.5 AS double precision)  `,\n    `  ),`,\n    `  4326`,\n    `) AS geom`,\n    `FROM generate_series(1, 100) AS id`,\n  ].join(\"\\n\");\n  await monacoType(page, `.ProstglesSQL`, mapQuery, {\n    deleteAllAndFill: true,\n    keyPressDelay: 0,\n  });\n  await addScene({\n    svgFileName: \"map_btn\",\n    animations: [\n      {\n        type: \"click\",\n        elementSelector: getCommandElemSelector(\"AddChartMenu.Map\"),\n        duration: 500,\n      },\n    ],\n  });\n\n  await page.getByTestId(\"AddChartMenu.Map\").first().click();\n  await page.waitForTimeout(2500);\n  await addScene({\n    svgFileName: \"map\",\n    caption: \"Map visualization\",\n  });\n  await monacoType(page, `.ProstglesSQL`, `${mapQuery}\\nINNER JOIN `, {\n    deleteAllAndFill: false,\n    keyPressDelay: 0,\n    pressAfterTyping: [\"Control+Space\"],\n  });\n  await addScene({\n    svgFileName: \"map_with_suggestions\",\n    caption: \"Map visualization\",\n  });\n  await page.getByTestId(\"dashboard.window.closeChart\").click();\n};\n\nconst intensiveQuery = `\nWITH RECURSIVE\n  -- Generate pixel grid and map to complex plane\n  pixels AS (\n    SELECT\n      x, y,\n      -2.5 + (x * 3.5 / 900) AS cx,\n      -1.0 + (y * 2.0 / 600) AS cy\n    FROM generate_series(0, 900-1) AS x,\n         generate_series(0, 600-1) AS y \n  ),\n  -- Recursively iterate z = z² + c\n  mandelbrot_iterations AS (\n    SELECT x, y, cx, cy, 0.0 AS zx, 0.0 AS zy, 0 AS iteration\n    FROM pixels \n    UNION ALL \n    SELECT\n      x, y, cx, cy,\n      zx * zx - zy * zy + cx AS zx,\n      2.0 * zx * zy + cy AS zy,\n      iteration + 1\n    FROM mandelbrot_iterations\n    WHERE iteration < 5 -- max_iterations\n      AND (zx * zx + zy * zy) <= 4.0\n  )\nSELECT x, y, MAX(iteration) AS depth\nFROM mandelbrot_iterations\nGROUP BY x, y;\n\n `;\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/table.svgif.ts",
    "content": "import { getCommandElemSelector } from \"Testing\";\nimport {\n  closeWorkspaceWindows,\n  deleteAllWorkspaces,\n  getDataKey,\n  type PageWIds,\n} from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\n\nexport const tableSvgif: OnBeforeScreenshot = async (\n  page,\n  { openConnection, toggleMenuPinned, openMenuIfClosed },\n  { addScene, addSceneAnimation },\n) => {\n  await openConnection(\"food_delivery\");\n  await deleteAllWorkspaces(page);\n  await closeWorkspaceWindows(page);\n  await toggleMenuPinned(false);\n\n  await page.getByTestId(\"dashboard.menu\").click();\n\n  // await addSceneAnimation(\n  //   getCommandElemSelector(\"dashboard.menu.tablesSearchListInput\"),\n  //   { action: \"type\", text: \"users\", mode: \"fill\" },\n  // );\n  // return;\n  await openMenuIfClosed();\n  await addSceneAnimation(getDataKey(\"users\"));\n\n  /** Show linked computed column */\n  await addSceneAnimation(getCommandElemSelector(\"AddColumnMenu\"));\n\n  await addSceneAnimation(\n    getCommandElemSelector(\"AddColumnMenu\") + \" \" + getDataKey(\"Computed\"),\n    undefined,\n    \"fast\",\n  );\n  await addSceneAnimation(getDataKey(\"$sum\"));\n  await addSceneAnimation(\n    getCommandElemSelector(\"FunctionColumnList.SearchInput\"),\n    { action: \"type\", text: \"total\", mode: \"fill\" },\n  );\n\n  await addSceneAnimation(getDataKey(\"(id = customer_id) orders.Total Price\"));\n  await addSceneAnimation(\n    getCommandElemSelector(\"QuickAddComputedColumn.name\"),\n    { action: \"type\", text: \"Total Spent\", mode: \"fill\" },\n  );\n  await addSceneAnimation(\n    getCommandElemSelector(\"QuickAddComputedColumn.Add\"),\n    undefined,\n    \"fast\",\n  );\n  await page.waitForTimeout(2000);\n\n  /** Sort by computed column */\n  await addSceneAnimation(getDataKey(\"Total Spent\"), undefined, \"fast\");\n  await addSceneAnimation(getDataKey(\"Total Spent\"), undefined, \"fast\");\n\n  /** Show card joined records */\n  const pageParams = { page, addSceneAnimation, addScene };\n  await clickTableRow(pageParams, 1, undefined, 1);\n  const openJoinedSection = async (joinedTable: string) => {\n    await addSceneAnimation(\n      getCommandElemSelector(\"JoinedRecords.SectionToggle\") +\n        getDataKey(joinedTable),\n    );\n    await page.waitForTimeout(2000);\n    await page\n      .locator(\n        getCommandElemSelector(\"JoinedRecords.Section\") +\n          getDataKey(joinedTable),\n      )\n      .scrollIntoViewIfNeeded();\n    await addScene({ animations: [{ type: \"wait\", duration: 500 }] });\n  };\n  // was ok up to here\n  await openJoinedSection(\"orders\");\n  await addSceneAnimation({\n    selector: getCommandElemSelector(\"SmartCard.viewEditRow\"),\n    nth: 0,\n  });\n  await openJoinedSection(\"order_items\");\n\n  await page.getByTestId(\"Popup.close\").last().click();\n  await page.getByTestId(\"Popup.close\").last().click();\n\n  /** Show quick stats filter and map */\n  await addSceneAnimation(\n    `[role=\"columnheader\"]` + getDataKey(\"type\"),\n    \"rightClick\",\n  );\n  await addSceneAnimation(getDataKey(\"Quick Stats\"));\n  await addSceneAnimation(getDataKey(\"rider\"));\n  await page.getByTestId(\"Popup.close\").click();\n\n  return;\n  // await addSceneAnimation(getDataKey(\"orders\"));\n  // await addSceneAnimation(\n  //   getCommandElemSelector(\"JoinedRecords.SectionToggle\") +\n  //     '[data-key=\"order_items\"]',\n  // );\n  // await page.waitForTimeout(2000);\n  // await page\n  //   .locator(\n  //     getCommandElemSelector(\"JoinedRecords.Section\") +\n  //       '[data-key=\"order_items\"]',\n  //   )\n  //   .scrollIntoViewIfNeeded();\n  // await addScene();\n  // await page.getByTestId(\"Popup.close\").click();\n  // await addSceneAnimation(\n  //   getCommandElemSelector(\"dashboard.window.toggleFilterBar\"),\n  // );\n\n  // await page.getByTestId(\"SmartFilterBar\").locator(\"input\").fill(\"picked\");\n  // await page.waitForTimeout(500);\n  // await addScene({\n  //   animations: [\n  //     {\n  //       type: \"wait\",\n  //       duration: 1000,\n  //     },\n  //     {\n  //       type: \"type\",\n  //       elementSelector: getCommandElemSelector(\"SmartFilterBar\"),\n  //       duration: 2000,\n  //     },\n  //   ],\n  // });\n  // await page.keyboard.press(\"ArrowDown\");\n  // await page.keyboard.press(\"Enter\");\n  // await page.waitForTimeout(500);\n  // await addScene();\n\n  // await addSceneAnimation(getCommandElemSelector(\"AddChartMenu.Map\"));\n  // await addSceneAnimation(getDataKey(\"(deliverer_id = id) users\"));\n  // await page.waitForTimeout(1000);\n  // await addScene();\n};\n\nexport const clickTableRow = async (\n  {\n    page,\n    addSceneAnimation,\n  }: { page: PageWIds } & Parameters<typeof tableSvgif>[2],\n  rowIndex: number,\n  recordScene = true,\n  columnIndex: 1 | 2 = 2,\n) => {\n  const commonSelector = `${getCommandElemSelector(\"TableBody\")} [role=\"row\"]:nth-of-type(${rowIndex}) [role=cell]:nth-of-type(${columnIndex})`;\n  const svgif = commonSelector;\n  const playwright = `${commonSelector} ${columnIndex === 1 ? \"button\" : \"\"}`;\n  if (!recordScene) {\n    await page.locator(playwright).click();\n    return;\n  }\n  await addSceneAnimation({ svgif, playwright });\n  await page.waitForTimeout(2000);\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/timechart.svgif.ts",
    "content": "import { closeWorkspaceWindows, getDataKey, openTable } from \"utils/utils\";\nimport type { OnBeforeScreenshot } from \"./SVG_SCREENSHOT_DETAILS\";\nimport { getCommandElemSelector } from \"Testing\";\n\nexport const timechartSvgif: OnBeforeScreenshot = async (\n  page,\n  { openConnection, toggleMenuPinned },\n  { addScene, addSceneAnimation },\n) => {\n  await openConnection(\"crypto\");\n  await closeWorkspaceWindows(page);\n  await openTable(page, \"futures\");\n  await addSceneAnimation(\n    getCommandElemSelector(\"dashboard.window.toggleFilterBar\"),\n  );\n  await addSceneAnimation(getCommandElemSelector(\"SearchList.Input\"), {\n    action: \"type\",\n    text: \"btc\",\n  });\n  // await page.keyboard.press(\"ArrowDown\");\n  // await addScene({ animations: [{ type: \"wait\", duration: 500 }] });\n  // await page.keyboard.press(\"ArrowDown\");\n  // await addScene({ animations: [{ type: \"wait\", duration: 500 }] });\n  // await page.keyboard.press(\"Enter\");\n  // await addScene({ animations: [{ type: \"wait\", duration: 500 }] });\n  await addSceneAnimation(`[data-label=\"BTCUSDC\"]`);\n  await addSceneAnimation(getCommandElemSelector(\"FilterWrapper_FieldName\"));\n  await addSceneAnimation(\n    getCommandElemSelector(\"FilterWrapper\") +\n      \" \" +\n      getCommandElemSelector(\"SearchList.Input\"),\n    {\n      action: \"type\",\n      text: \"xrp\",\n    },\n  );\n  await addSceneAnimation(getDataKey(\"XRPUSDT\"));\n  await addSceneAnimation(getCommandElemSelector(\"FilterWrapper_Field\"));\n\n  await addSceneAnimation(getCommandElemSelector(\"AddChartMenu.Timechart\"));\n  await addSceneAnimation(\n    getCommandElemSelector(\"AddChartMenu.Timechart\") +\n      \" \" +\n      getDataKey(\"timestamp\"),\n  );\n  await page.getByTestId(\"LayerColorPicker\").click();\n  await page.locator(getDataKey(\"#CB11F0\")).click();\n\n  await page.getByTestId(\"dashboard.window.detachChart\");\n  await addSceneAnimation(\n    getCommandElemSelector(\"TimeChartLayerOptions.aggFunc\"),\n  );\n  await addSceneAnimation(\n    getCommandElemSelector(\"TimeChartLayerOptions.groupBy\"),\n  );\n  await addSceneAnimation(getDataKey(\"symbol\"));\n  await addSceneAnimation(getCommandElemSelector(\"Popup.close\"));\n  await addScene();\n  await toggleMenuPinned(false);\n  await page.waitForTimeout(1500);\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/utils/constants.ts",
    "content": "import * as path from \"path\";\nimport type { SVGif } from \"Testing\";\n\nexport const DOCS_DIR = path.join(__dirname, \"../../../../docs/\");\nif (!DOCS_DIR.endsWith(\"ui/docs/\")) {\n  throw new Error(\"DOCS_DIR is not set correctly: \" + DOCS_DIR);\n}\n\nexport const SCREENSHOTS_PATH = \"/screenshots\";\nexport const SVGIF_SCENES_PATH = \"/svgif-scenes\";\n\nexport const SVG_SCREENSHOT_DIR = path.join(DOCS_DIR, SCREENSHOTS_PATH);\nexport const SVGIF_SCENES_DIR = path.join(\n  DOCS_DIR,\n  SCREENSHOTS_PATH,\n  SVGIF_SCENES_PATH,\n);\n\nexport type SVGifScene = SVGif.Scene;\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/utils/getFilesFromDir.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { MINUTE } from \"../../utils/utils\";\n\nexport const getFilesFromDir = (\n  dir: string,\n  endWith: string,\n  checkAge = true,\n) => {\n  const files = fs\n    .readdirSync(dir)\n    .filter((file) => file.endsWith(endWith))\n    .map((fileName) => {\n      const filePath = path.join(dir, fileName);\n      const content = fs.readFileSync(filePath, { encoding: \"utf-8\" });\n      return { fileName, filePath, stat: fs.statSync(filePath), content };\n    });\n\n  if (checkAge) {\n    const filesThatAreNotRecent = files.filter(\n      (file) => file.stat.mtimeMs < Date.now() - 120 * MINUTE,\n    );\n    if (filesThatAreNotRecent.length) {\n      throw `${JSON.stringify(endWith)} files are not recent: ${filesThatAreNotRecent\n        .map((file) => file.fileName)\n        .join(\", \")}`;\n    }\n  }\n  return files;\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/utils/getSceneUtils.ts",
    "content": "import type { PageWIds } from \"utils/utils\";\nimport type { SVGifScene } from \"./constants\";\nimport { saveSVGScreenshot } from \"./saveSVGScreenshot\";\nimport type { Locator } from \"@playwright/test\";\n\nexport const getSceneUtils = (\n  page: PageWIds,\n  fileName: string,\n  svgifScenes: SVGifScene[],\n) => {\n  const addScene = async (partialScene?: Partial<SVGifScene>) => {\n    svgifScenes ??= [];\n    const sceneFileName = [\n      fileName,\n      svgifScenes.length.toString().padStart(2, \"0\"),\n      partialScene?.svgFileName,\n    ]\n      .filter(Boolean)\n      .join(\"_\");\n    const animations = partialScene?.animations ?? [\n      {\n        type: \"wait\",\n        duration: 3000,\n      },\n    ];\n    const scene: SVGifScene = {\n      ...partialScene,\n      svgFileName: sceneFileName,\n      animations,\n    };\n    svgifScenes.push(scene);\n    await saveSVGScreenshot(page, sceneFileName, scene);\n  };\n\n  const addSceneAnimation = async (\n    selector:\n      | string\n      | { svgif: string; playwright: string; nth?: number }\n      | { selector: string; nth?: number },\n    action:\n      | \"click\"\n      | \"rightClick\"\n      | {\n          action: \"type\";\n          text: string;\n          /** Defaults to charByChar */\n          mode?: \"charByChar\" | \"fill\" | \"fillZoomTo\";\n        } = \"click\",\n    duration: \"auto\" | \"fast\" = \"auto\",\n  ) => {\n    const {\n      svgif: svgifSelector,\n      playwright: playwrightSelector,\n      nth,\n    } = typeof selector === \"string\" ?\n        { svgif: selector, playwright: selector, nth: undefined }\n      : \"selector\" in selector ?\n        {\n          svgif: selector.selector,\n          playwright: selector.selector,\n          nth: selector.nth,\n        }\n      : selector;\n\n    const playwrightLocator =\n      Number.isFinite(nth) ?\n        page.locator(playwrightSelector).nth(nth!)\n      : page.locator(playwrightSelector);\n    await playwrightLocator.scrollIntoViewIfNeeded();\n\n    const elementIsVisible = await playwrightLocator.evaluate((n) => {\n      const hoverParent = n.closest(`[class*=\"hover\"]`) as HTMLElement | null;\n      const parentIsNotVisible =\n        hoverParent && getComputedStyle(hoverParent).opacity == \"0\";\n      const isVisible =\n        getComputedStyle(n).opacity != \"0\" && !parentIsNotVisible;\n\n      return isVisible;\n    });\n\n    if (!elementIsVisible) {\n      await moveCursorToElement(page, playwrightLocator);\n    }\n\n    await addScene({\n      animations: [\n        {\n          type: \"wait\",\n          duration: duration === \"fast\" ? 800 : 1000,\n        },\n        {\n          type: elementIsVisible ? \"click\" : \"clickAppearOnHover\",\n          elementSelector: svgifSelector,\n          duration: duration === \"fast\" ? 700 : 1000,\n          waitBeforeClick: duration === \"fast\" ? 200 : 500,\n          lingerMs: duration === \"fast\" ? 200 : 500,\n        },\n      ],\n    });\n    if (action === \"click\" || action === \"rightClick\") {\n      await playwrightLocator.click({\n        button: action === \"rightClick\" ? \"right\" : \"left\",\n      });\n    } else {\n      const { mode = \"charByChar\" } = action;\n\n      /** This way we can correctly show any filtered items */\n      if (mode === \"charByChar\") {\n        await playwrightLocator.fill(\"\");\n\n        for (const char of action.text) {\n          await page.keyboard.press(char);\n          await page.waitForTimeout(200);\n          await addScene({ animations: [{ type: \"wait\", duration: 50 }] });\n        }\n      } else {\n        /** Set value without triggering search */\n        await playwrightLocator.evaluate((n, val) => {\n          (n as HTMLInputElement).value = val;\n        }, action.text);\n\n        await page.waitForTimeout(100);\n        const msPerChar = 30;\n        const zoomDurations = mode === \"fillZoomTo\" ? 500 + 500 + 300 : 0; // zoom in + zoom out + wait before zoom out\n\n        await addScene({\n          animations: [\n            // { type: \"wait\", duration: duration === \"fast\" ? 700 : 1000 },\n            {\n              type: \"type\",\n              elementSelector: svgifSelector,\n              zoomToElement: mode === \"fillZoomTo\",\n              duration:\n                zoomDurations + Math.max(500, action.text.length * msPerChar),\n            },\n          ],\n        });\n        await playwrightLocator.fill(action.text);\n        await addScene({ animations: [{ type: \"wait\", duration: 500 }] });\n      }\n    }\n    /** prevent hover from showing hidden elements */\n    await page.mouse.move(-100, -100);\n\n    await page.waitForTimeout(1000);\n  };\n\n  return {\n    addScene,\n    addSceneAnimation,\n    svgifScenes,\n  };\n};\n\nconst moveCursorToElement = async (page: PageWIds, el: Locator) => {\n  const box = await el.boundingBox();\n  if (!box)\n    throw new Error(\"Element has no bounding box (possibly off-screen).\");\n\n  const x = box.x + box.width / 2;\n  const y = box.y + box.height / 2;\n\n  await page.mouse.move(x, y);\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/utils/saveSVGScreenshot.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { type PageWIds } from \"../../utils/utils\";\nimport {\n  SVG_SCREENSHOT_DIR,\n  SVGIF_SCENES_DIR,\n  type SVGifScene,\n} from \"./constants\";\n\nexport const saveSVGScreenshot = async (\n  page: PageWIds,\n  fileName: string,\n  svgifScene: SVGifScene | undefined,\n) => {\n  const start = Date.now();\n  const svgStrings: { light: string; dark: string } = await page.evaluate(\n    async () => {\n      //@ts-ignore\n      const result = await window.toSVG(document.body);\n      return result as { light: string; dark: string };\n    },\n  );\n\n  const { light, dark } = svgStrings;\n  if (!light || !dark) throw \"SVG missing\";\n  const dir = svgifScene ? SVGIF_SCENES_DIR : SVG_SCREENSHOT_DIR;\n  fs.mkdirSync(dir, { recursive: true });\n  /**\n   * Fallback to png screenshot for ios\n   */\n  await page.screenshot({\n    path: path.join(dir, fileName + \".png\"),\n    fullPage: true,\n  });\n  const filePath = path.join(dir, fileName + \".svg\");\n  const filePathDark = path.join(dir, fileName + \".dark.svg\");\n\n  fs.writeFileSync(filePath, light, {\n    encoding: \"utf8\",\n  });\n  fs.writeFileSync(filePathDark, dark, {\n    encoding: \"utf8\",\n  });\n  console.log(\n    `Saved SVG screenshot (${(Date.now() - start).toLocaleString()}ms): ${fileName}.svg`,\n  );\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/utils/saveSVGifs.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { type PageWIds } from \"utils/utils\";\nimport {\n  SVG_SCREENSHOT_DIR,\n  SVGIF_SCENES_DIR,\n  type SVGifScene,\n} from \"./constants\";\nimport { getFilesFromDir } from \"./getFilesFromDir\";\nimport { goTo } from \"utils/goTo\";\nexport type SVGIfSpec = {\n  fileName: string;\n  scenes: SVGifScene[];\n};\nexport const saveSVGifs = async (\n  page: PageWIds,\n  svgifSpecs: { fileName: string; scenes: SVGifScene[] }[],\n  svgifCovers: { fileName: string; svgSceneFileName: string }[],\n) => {\n  await goTo(page, \"/invalid-url-to-avoid-loading-anything\");\n  const svgSceneFiles = getFilesFromDir(SVGIF_SCENES_DIR, \".svg\", false);\n\n  const svgifSpecsDark = svgifSpecs.map(({ fileName, scenes }) => ({\n    fileName: fileName + \".dark\",\n    scenes: scenes.map((scene) => ({\n      ...scene,\n      svgFileName: scene.svgFileName + \".dark\",\n    })),\n  }));\n  const svgifs = await page.evaluate(\n    async ({ svgFiles, svgifSpecs }) => {\n      const filesMap = new Map<string, string>(\n        svgFiles.map(({ fileName, content }) => [fileName, content]),\n      );\n\n      const result = await Promise.all(\n        svgifSpecs.map(async ({ fileName, scenes }) => {\n          //@ts-ignore\n          const content = await window.getSVGif(scenes, filesMap);\n          return { fileName: `${fileName}.svgif.svg`, content, scenes };\n        }),\n      );\n\n      return result;\n    },\n    { svgFiles: svgSceneFiles, svgifSpecs: [...svgifSpecs, ...svgifSpecsDark] },\n  );\n\n  const savePath = SVG_SCREENSHOT_DIR;\n  if (!fs.existsSync(savePath)) {\n    fs.mkdirSync(savePath, { recursive: true });\n  }\n  svgifs.forEach(({ fileName, content, scenes }) => {\n    fs.writeFileSync(path.join(savePath, fileName), content);\n  });\n\n  const svgifCoversDark = svgifCovers.map(({ fileName, svgSceneFileName }) => ({\n    fileName: fileName + \".dark\",\n    svgSceneFileName: svgSceneFileName + \".dark\",\n  }));\n  [...svgifCovers, ...svgifCoversDark].forEach(\n    ({ fileName, svgSceneFileName }) => {\n      const svgFile = svgSceneFiles.find(\n        (f) => f.fileName === svgSceneFileName + \".svg\",\n      );\n      if (!svgFile) {\n        throw new Error(\n          `SVG scene file not found: ${svgSceneFileName}. Expecting one of ${svgSceneFiles.map((f) => f.fileName).join(\", \")}`,\n        );\n      }\n      fs.writeFileSync(path.join(savePath, fileName + \".svg\"), svgFile.content);\n    },\n  );\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/utils/saveSVGs.ts",
    "content": "import * as fs from \"fs\";\nimport { getOverviewSvgifSpecs } from \"svgScreenshots/getOverviewSvgifSpecs.svgif\";\nimport { getDashboardUtils, type PageWIds } from \"../../utils/utils\";\nimport { SVG_SCREENSHOT_DETAILS } from \"../SVG_SCREENSHOT_DETAILS\";\nimport { SVG_SCREENSHOT_DIR, type SVGifScene } from \"./constants\";\nimport { saveSVGifs } from \"./saveSVGifs\";\nimport { saveSVGScreenshot } from \"./saveSVGScreenshot\";\nimport { getSceneUtils } from \"./getSceneUtils\";\n\nexport const saveSVGs = async (page: PageWIds) => {\n  /** Delete existing markdown docs */\n  if (fs.existsSync(SVG_SCREENSHOT_DIR)) {\n    fs.rmSync(SVG_SCREENSHOT_DIR, { recursive: true, force: true });\n  }\n\n  const utils = getDashboardUtils(page);\n  const svgifSpecs: { fileName: string; scenes: SVGifScene[] }[] = [];\n  for (const [fileName, onBefore] of Object.entries(SVG_SCREENSHOT_DETAILS)) {\n    const svgifScenes: SVGifScene[] = [];\n    const { addScene, addSceneAnimation } = getSceneUtils(\n      page,\n      fileName,\n      svgifScenes,\n    );\n    await onBefore(page, utils, { addScene, addSceneAnimation });\n    if (svgifScenes.length) {\n      const svgifSpec = {\n        fileName,\n        scenes: svgifScenes,\n      };\n      svgifSpecs.push(svgifSpec);\n      console.time(`Generated SVGif: ${fileName}.svgif.svg`);\n\n      await saveSVGifs(page, [svgifSpec], []);\n      console.timeEnd(`Generated SVGif: ${fileName}.svgif.svg`);\n    } else {\n      await saveSVGScreenshot(page, fileName, undefined);\n    }\n  }\n  const svgifSpecsObj = fromEntriesTyped(\n    svgifSpecs.map((s) => [s.fileName, s.scenes]),\n  );\n  const { overviewSvgifSpecs, svgifCovers } =\n    await getOverviewSvgifSpecs(svgifSpecsObj);\n  await saveSVGifs(page, overviewSvgifSpecs, svgifCovers);\n  await page.waitForTimeout(100);\n  return { svgifSpecs, overviewSvgifSpecs, svgifCovers };\n};\n\nconst fromEntriesTyped = <K extends string, V>(\n  entries: [K, V][],\n): Record<K, V> => {\n  return Object.fromEntries(entries) as Record<K, V>;\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/utils/svgScreenshotsCompleteReferenced.ts",
    "content": "import { SVG_SCREENSHOT_DETAILS } from \"svgScreenshots/SVG_SCREENSHOT_DETAILS\";\nimport {\n  DOCS_DIR,\n  SCREENSHOTS_PATH,\n  SVG_SCREENSHOT_DIR,\n  type SVGifScene,\n} from \"./constants\";\nimport { getFilesFromDir } from \"./getFilesFromDir\";\n\nconst getSavedSVGFiles = () => {\n  const savedSVGFiles = getFilesFromDir(SVG_SCREENSHOT_DIR, \".svg\");\n\n  const svgFileNames = Object.entries(SVG_SCREENSHOT_DETAILS).flatMap(\n    ([key, val]) =>\n      typeof val === \"function\" ?\n        [key]\n      : Object.keys(val).map((v) => `${key}_${v}`),\n  );\n\n  return { savedSVGFiles, svgFileNames };\n};\n\nexport const svgScreenshotsCompleteReferenced = async (\n  svgifScenes: SVGifScene[],\n  svgifFilesUsedExternally: string[],\n) => {\n  const { savedSVGFiles } = getSavedSVGFiles();\n  const docMarkdownFiles = getFilesFromDir(DOCS_DIR, \".md\");\n\n  const usedSrcValuesWithInfo = docMarkdownFiles.flatMap((f) =>\n    f.content\n      .split(`src=\"`)\n      .slice(1)\n      .map((v) => {\n        const startsWithDot = v.startsWith(\".\");\n        const src = v\n          .split(`\"`)[0]\n          .slice(SCREENSHOTS_PATH.length + (startsWithDot ? 2 : 1));\n        return {\n          docName: f.fileName,\n          srcWithFragment: src.includes(\"#\") ? src : undefined,\n          src: src.split(\"#\")[0].slice(0, -4),\n        };\n      }),\n  );\n\n  const usedSrcValues = Array.from(\n    new Set(usedSrcValuesWithInfo.map((v) => v.src)),\n  );\n\n  const deadSrcValues = usedSrcValues.filter(\n    (v) => !savedSVGFiles.find((f) => f.fileName === v + \".svg\"),\n  );\n\n  if (deadSrcValues.length) {\n    throw `The following SVG image src tags from docs do not have a matching saved svg file: ${deadSrcValues.join(\", \")}`;\n  }\n\n  const svgifFileNames = new Set(svgifScenes.map((s) => s.svgFileName));\n  const svgFilesNotUsedAnywhere = savedSVGFiles\n    .map((f) => f.fileName.slice(0, -4))\n    .filter((fileName) => {\n      /** Exclude dark variants */\n      if (fileName.includes(\".dark\")) return false;\n      const isUsed =\n        usedSrcValues.includes(fileName) ||\n        svgifFilesUsedExternally.includes(fileName);\n      const isSVGif = fileName.includes(\".svgif\");\n      if (!isUsed && !isSVGif) {\n        const usedInSvgif = svgifFileNames.has(fileName);\n        if (!usedInSvgif) {\n          console.log(\n            `SVG file not used in docs or svgif: ${fileName} .SVGif scenes: ${svgifScenes.map((s) => s.svgFileName).join(\", \")} `,\n          );\n        }\n        return !usedInSvgif;\n      }\n      return !isUsed;\n    });\n\n  if (svgFilesNotUsedAnywhere.length) {\n    throw `The following saved svg files are not referenced in any doc or svgif: ${svgFilesNotUsedAnywhere.join(\", \")}`;\n  }\n\n  // Ensure fragments are valid\n  for (const { srcWithFragment } of usedSrcValuesWithInfo) {\n    const [src, fragment] = srcWithFragment?.split(\"#\") ?? [];\n    if (src && fragment) {\n      const svgFile = savedSVGFiles.find((f) => f.fileName === src);\n      if (!svgFile) {\n        throw `SVG file not found/missing: ${src}`;\n      }\n      if (!svgFile.content.includes(`<view id=\"${fragment}\"`)) {\n        throw `SVG file ${src} does not contain fragment id: ${fragment}`;\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "e2e/tests/svgScreenshots/utils/typeSendAddScenes.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport type { OnBeforeScreenshot } from \"svgScreenshots/SVG_SCREENSHOT_DETAILS\";\nimport { getCommandElemSelector, type SVGif } from \"Testing\";\nimport type { PageWIds } from \"utils/utils\";\n\nexport const typeSendAddScenes = async (\n  page: PageWIds,\n  addScene: Parameters<OnBeforeScreenshot>[2][\"addScene\"],\n  text: string,\n  endAnimations: SVGif.Animation[] = [],\n  waitFor?: () => Promise<void>,\n) => {\n  await page.getByTestId(\"Chat.textarea\").fill(text);\n  await page.waitForTimeout(1000);\n  const msPerChar = 30;\n  const zoomDurations = 500 + 500 + 300; // zoom in + zoom out + wait before zoom out\n  const typeAnimation: SVGif.Animation | undefined =\n    !text ? undefined : (\n      {\n        type: \"type\",\n        elementSelector: getCommandElemSelector(\"Chat.textarea\"),\n        duration: zoomDurations + text.length * msPerChar,\n        extraAnimation: {\n          type: \"bringToFront\",\n          elementSelector: getCommandElemSelector(\"Chat.sendWrapper\"),\n        },\n      }\n    );\n  const waitAnimation: SVGif.Animation = {\n    type: \"wait\",\n    duration: 500,\n  };\n  await addScene({\n    animations:\n      typeAnimation ? [typeAnimation, waitAnimation] : [waitAnimation],\n  });\n  await page.getByTestId(\"Chat.send\").click();\n  await page.waitForTimeout(2000);\n  await addScene(); // LLM response loading\n  const lastMessage = page\n    .getByTestId(\"Chat.messageList\")\n    .locator(\".message\")\n    .last();\n\n  await expect(lastMessage).toContainClass(\"incoming\", { timeout: 15000 });\n\n  for await (const animation of endAnimations) {\n    if (animation.type !== \"wait\" && animation.type !== \"moveTo\") {\n      await page\n        .locator(animation.elementSelector)\n        .waitFor({ state: \"visible\", timeout: 15000 });\n    }\n  }\n\n  await waitFor?.();\n  await addScene({\n    animations: [\n      {\n        type: \"fadeIn\",\n        duration: 500,\n        elementSelector:\n          getCommandElemSelector(\"Chat.messageList\") + \" > g:last-of-type\",\n      },\n      { type: \"wait\", duration: 1500 },\n      ...endAnimations,\n    ],\n  });\n};\n"
  },
  {
    "path": "e2e/tests/testAskLLM.ts",
    "content": "import { join } from \"path\";\nimport { prostglesUIDashboardSample } from \"sampleToolUseData\";\nimport { dockerWeatherToolUse } from \"sampleToolUseData\";\n\nconst stringify = (obj: any) => JSON.stringify(obj, null, 2);\nexport const clientNodeModulesDirectory = join(\n  __dirname,\n  \"../../client/node_modules\",\n);\nconsole.log(\"Client node modules dir:\", clientNodeModulesDirectory);\n\nconst taskToolArguments = {\n  suggested_prompt:\n    \"I will paste receipt images in this chat. Please extract the following information from each receipt:\\n- Company/merchant name\\n- Total amount\\n- Currency\\n- Date of purchase\\n- Full extracted text\\n\\nAfter extracting the data, insert it into the receipts table.\",\n  suggested_database_access: {\n    Mode: \"Custom\",\n    tables: [\n      {\n        tableName: \"receipts\",\n        select: true,\n        insert: true,\n        delete: false,\n        update: true,\n      },\n    ],\n  },\n  suggested_database_tool_names: [],\n  suggested_mcp_tool_names: [\"fetch--fetch\"],\n};\n\ntype ToolUse = {\n  content?: string;\n  tool: {\n    id: string;\n    type: \"function\";\n    function: {\n      name: string;\n      arguments: string;\n    };\n  }[];\n  duration?: number;\n  result_content?: string;\n};\n\nconst taskToolUse: ToolUse = {\n  content:\n    \"Based on your requirements, I suggest the following prompt and database access settings to help you get started effectively.\",\n  tool: [\n    {\n      id: \"task-tool-use\",\n      type: \"function\",\n      function: {\n        name: \"prostgles-ui--suggest_tools_and_prompt\",\n        arguments: stringify(taskToolArguments),\n      },\n    },\n  ],\n};\n\nconst webSearchToolUse: ToolUse = {\n  content: `To provide you with the most accurate and up-to-date information, I'll use the web search tool to look up recent data related to your query.`,\n  tool: [\n    {\n      id: \"websearch-tool-use\",\n      type: \"function\",\n      function: {\n        name: \"websearch--websearch\",\n        arguments: stringify({\n          q: '\"prostgles websearch\"',\n        }),\n      },\n    },\n    {\n      id: \"websearch-tool-use-snapshot\",\n      type: \"function\",\n      function: {\n        name: \"websearch--get_snapshot\",\n        arguments: stringify({\n          url: \"http://127.0.0.1:3004/login\",\n        }),\n      },\n    },\n  ],\n  result_content: `Search done.`,\n};\n\nconst dashboardToolUse: ToolUse = {\n  content: `I analyzed your schema for what appears to be a food delivery platform. Let me suggest several workspaces that would provide valuable insights into different aspects of your business.`,\n  tool: [\n    {\n      id: \"dashboard-tool-use\",\n      type: \"function\",\n      function: {\n        name: \"prostgles-ui--suggest_dashboards\",\n        arguments: stringify(prostglesUIDashboardSample),\n      },\n    },\n  ],\n};\n\nconst mcpToolUse: ToolUse = {\n  content: `To assist you further, I'll use the fetch tool to access the  application.`,\n  tool: [\n    {\n      id: \"mcp-tool-use\",\n      type: \"function\",\n      function: {\n        name: \"fetch--fetch\",\n        arguments: stringify({\n          url: \"http://localhost:3004/login\",\n        }),\n      },\n    },\n  ],\n  result_content: `I've successfully fetched the login page of the application. Let me know if you need any specific information or actions performed on this page.`,\n};\nconst playwrightMCPToolUse: ToolUse = {\n  content: `I'll use Playwright to navigate to the login page and take a snapshot of it. This will help us verify that the page loads correctly and looks as expected.`,\n  tool: [\n    {\n      id: \"mcp-tool-use-playwright1\",\n      type: \"function\",\n      function: {\n        name: \"playwright--browser_navigate\",\n        arguments: stringify({\n          url: \"http://localhost:3004/login\",\n        }),\n      },\n    },\n    {\n      id: \"mcp-tool-use-playwright2\",\n      type: \"function\",\n      function: {\n        name: \"playwright--browser_snapshot\",\n        arguments: stringify({\n          url: \"http://localhost:3004/login\",\n        }),\n      },\n    },\n  ],\n};\nconst isDocker = Boolean(process.env.IS_DOCKER);\nconst mcpSandboxToolUse: ToolUse = {\n  content: `I'll create a container that runs a simple Node.js application.`,\n  tool: [\n    {\n      id: \"mcp-tool-use-sandbox1\",\n      type: \"function\",\n      function: {\n        name: \"docker-sandbox--create_container\",\n        arguments: stringify({\n          files: {\n            Dockerfile: `FROM node:20 \\nWORKDIR /app \\nCOPY . . \\nRUN npm install \\nCMD [\"npm\", \"start\"]`,\n            \"package.json\": JSON.stringify({\n              name: \"test-app\",\n              version: \"1.0.0\",\n              scripts: {\n                start: \"node index.js\",\n              },\n              depenencies: {\n                \"node-fetch\": \"^3.3.0\",\n              },\n            }),\n            \"index.js\": dedent(`\n            fetch(\n              \"http://${isDocker ? \"prostgles-ui-docker-mcp\" : \"172.17.0.1\"}:3009/db/execute_sql_with_rollback\", \n              { headers: { \"Content-Type\": \"application/json\" }, \n              method: \"POST\", \n              body: JSON.stringify({ sql: \"SELECT * FROM users\" }) \n            }).then(res => res.json()).then(console.log).catch(console.error);`),\n          },\n          networkMode: \"bridge\",\n          timeout: 30_000,\n        }),\n      },\n    },\n  ],\n};\n\nconst toolResponses: Record<string, ToolUse> = {\n  task: taskToolUse,\n  dashboards: dashboardToolUse,\n  mcp: mcpToolUse,\n  mcpfail: {\n    content: \"Hmm, the fetch tool encountered an error. Let's try again...\",\n    tool: mcpToolUse.tool.map((t) => ({\n      ...t,\n      function: { ...t.function, name: \"fetch--invalidfetch\" },\n    })),\n    duration: 1000,\n    result_content: \"... let's retry the failed tool\",\n  },\n  mcpplaywright: playwrightMCPToolUse,\n  mcpsandbox: mcpSandboxToolUse,\n  parallel_calls: {\n    content: \"I'll fetch in parallel \",\n    tool: [mcpToolUse.tool[0], mcpToolUse.tool[0], mcpToolUse.tool[0]].map(\n      (t, i) => ({\n        ...t,\n        id: t.id + \"_\" + (i + 1),\n      }),\n    ),\n    duration: 2000,\n    result_content: \"Fetched in parallel successfully\",\n  },\n  websearch: webSearchToolUse,\n  weather: {\n    content:\n      \"I'll create a container with a script that fetches real historical weather data from a free API source.\",\n    tool: [\n      {\n        id: \"weather-tool-use\",\n        type: \"function\",\n        function: {\n          name: \"docker-sandbox--create_container\",\n          arguments: stringify(dockerWeatherToolUse),\n        },\n      },\n    ],\n    result_content:\n      \"The container has fetched the historical weather data for London for the last 4 years.\",\n  },\n  last: {\n    content:\n      \"To get the information you need, I'll run a SQL query against your database to fetch the relevant data.\",\n    tool: [\n      {\n        id: \"sql-tool-use\",\n        type: \"function\",\n        function: {\n          name: \"prostgles-db--execute_sql_with_rollback\",\n          arguments: stringify({\n            sql: \"SELECT * FROM orders WHERE created_at >= NOW() - INTERVAL '30 days';\",\n          }),\n        },\n      },\n    ],\n    result_content:\n      \"Here is the list of orders from the last 30 days that you requested:  \\n\\n- OrderID: 101, Customer: John Doe, Amount: $250.00, Date: 2025-09-10 \\n- OrderID: 102, Customer: Jane Smith, Amount: $150.00, Date: 2025-09-11\",\n  },\n  receipt: {\n    content:\n      \"Great! I've extracted the text from the receipt image. Now, I'll insert the relevant details into the receipts table in your database.\",\n    tool: [\n      {\n        id: \"db-tool-use\",\n        type: \"function\",\n        function: {\n          name: \"prostgles-db--insert\",\n          arguments: stringify({\n            tableName: \"receipts\",\n            data: [\n              {\n                extracted_text: \"Item1 $10.00\\nItem2 $15.00\\nTotal $25.00\",\n                amount: 450,\n                currency: \"USD\",\n                company: \"Grand Ocean Hotel\",\n                date: \"2025-09-12\",\n                created_at: new Date().toISOString(),\n              },\n            ],\n          }),\n        },\n      },\n    ],\n    result_content:\n      \"Inserted receipt data for Item1 $10.00, Item2 $15.00, Total $25.00 into the receipts table at Grand Ocean Hotel.\",\n  },\n  estimated_cost: {\n    tool: [\n      {\n        id: \"filesystem-tool-use\",\n        type: \"function\",\n        function: {\n          name: \"filesystem--directory_tree\",\n          arguments: stringify({\n            path: clientNodeModulesDirectory,\n          }),\n        },\n      },\n    ],\n  },\n};\n\nexport const testAskLLMCode = `\n\nconst toolResponses = ${stringify(toolResponses)};\n\nconst lastMsg = args.messages.at(-1);\nconst lastMsgText = lastMsg?.content?.[0]?.type === \"image_url\"? \" receipt \" : lastMsg?.content?.[0]?.text;\nconst { tool_call_id, is_error } = lastMsg ?? {};\nconst toolCallKeyResult = typeof tool_call_id === \"string\"? tool_call_id.split(\"#\")[0] : undefined;\nconst toolResult = toolCallKeyResult && toolResponses[toolCallKeyResult];\nconst failedToolResult = toolCallKeyResult === \"mcpfail\";// typeof lastMsg.tool_call_id === \"string\" && lastMsg.tool_call_id.includes(\"fetch--invalidfetch\");\nconst msg = failedToolResult ? \" mcpfail \" : lastMsgText;\n\nconst toolResponseKey = Object.keys(toolResponses).find(k => msg && msg.includes(\" \" + k + \" \")); \nconst toolResponse = toolResponses[toolResponseKey];\n\nconst defaultContent = !msg && !failedToolResult? undefined : (\"free ai assistant\" + (msg ?? \" empty message\") + (failedToolResult ? \"... let's retry the failed tool\" : \"\"));\nconst content = is_error? \"Tool call failed. Will not retry\" : toolResult?.result_content ?? toolResponse?.content ?? defaultContent;\nconst tool_calls = toolResponse?.tool.map(tc => ({ ...tc, id: [toolResponseKey + \"#\", tc.id, tc[\"function\"].name, Math.random(), Date.now()].join(\"_\") })); \n\nconst duration = toolResponse?.duration ?? (3000 + Math.random() * 2000);\nawait new Promise(res => setTimeout(res, duration));\n\nconst choicesItem = { \n  type: \"text\", \n  message: {\n    content,\n    tool_calls \n  }\n};\n\nreturn { \n  choices: [\n    choicesItem\n  ],\n  type: \"Anthropic\",\n  usage: {\n    completion_tokens: msg === \"cost\"? 1e5 : 0, \n    prompt_tokens: msg === \"cost\"? 1e5 : 0, \n    total_tokens: 0, \n  },\n};//`;\n\nfunction dedent(str: string) {\n  const lines = str.replace(/^\\n/, \"\").split(\"\\n\");\n  const indent = Math.min(\n    ...lines\n      .filter((line) => line.trim().length > 0)\n      .map((line) => line.match(/^(\\s*)/)![1].length),\n  );\n  return lines.map((line) => line.slice(indent)).join(\"\\n\");\n}\n"
  },
  {
    "path": "e2e/tests/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"Node\",\n    \"sourceMap\": true,\n    \"outDir\": \"../tests-out\",\n    \"strict\": true,\n    \"strictNullChecks\": true\n  }\n}\n"
  },
  {
    "path": "e2e/tests/utils/constants.ts",
    "content": "export enum USERS {\n  test_user = \"test_user\",\n  default_user = \"default_user\",\n  default_user1 = \"default_user1\",\n  public_user = \"public_user\",\n  new_user = \"new_user\",\n  new_user1 = \"new_user1\",\n  free_llm_user1 = \"free_llm_user1\",\n}\nexport const TEST_DB_NAME = \"Prostgles UI automated tests database\";\n\nexport const localNoAuthSetup = !!process.env.PRGL_DEV_ENV;\nexport const QUERIES = {\n  orders: `CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id UUID NOT NULL, status TEXT );`,\n};\n"
  },
  {
    "path": "e2e/tests/utils/goTo.ts",
    "content": "import type { Request } from \"@playwright/test\";\nimport { type PageWIds } from \"./utils\";\nimport { localNoAuthSetup, USERS } from \"./constants\";\n\nexport const goTo = async (page: PageWIds, url = \"localhost:3004\") => {\n  const pendingRequests = new Set<Request>();\n\n  // Store listener references for cleanup\n  const requestListener = (request: Request) => {\n    pendingRequests.add(request);\n    // console.log(`→ REQUEST: ${request.method()} ${request.url()}`);\n  };\n\n  const requestFinishedListener = (request: Request) => {\n    pendingRequests.delete(request);\n    // console.log(`✓ FINISHED: ${request.url()}`);\n  };\n\n  const requestFailedListener = (request: Request) => {\n    pendingRequests.delete(request);\n    // console.log(`✗ FAILED: ${request.url()} - ${request.failure()?.errorText}`);\n  };\n\n  // const responseListener = (response: Response) => {\n  //   console.log(`← RESPONSE: ${response.status()} ${response.url()}`);\n  // };\n\n  page.on(\"request\", requestListener);\n  page.on(\"requestfinished\", requestFinishedListener);\n  page.on(\"requestfailed\", requestFailedListener);\n  // page.on(\"response\", responseListener);\n  try {\n    const resp = await page.goto(url, {\n      waitUntil: \"networkidle\",\n      timeout: 30e3,\n    });\n    if (resp && resp.status() >= 400) {\n      console.error(`page.goto failed:`, await resp.text());\n    }\n    if (!resp) {\n      console.warn(`page.goto ${url}: no response`);\n    }\n  } catch (error) {\n    const requestInfo = Array.from(pendingRequests)\n      .map((request) => {\n        let reqMethod = \"??\";\n        let reqUrl = \"??\";\n        try {\n          reqUrl = request.url();\n        } catch {}\n        try {\n          reqMethod = request.method();\n        } catch {}\n        return `\\n - ${reqMethod}: ${reqUrl}`;\n      })\n      .join(\"\");\n    console.error(requestInfo);\n    /** It's usually  http://localhost:3004/json.worker.js */\n    throw new Error(\"\\n⚠️ TIMEOUT! Pending requests: \" + requestInfo, {\n      cause: error,\n    });\n    // throw error;\n  } finally {\n    page.off(\"request\", requestListener);\n    page.off(\"requestfinished\", requestFinishedListener);\n    page.off(\"requestfailed\", requestFailedListener);\n    // page.off(\"response\", responseListener);\n  }\n\n  await page.waitForTimeout(500);\n  pendingRequests.clear();\n  const errorCompSelector = \"div.ErrorComponent\";\n  if (await page.isVisible(errorCompSelector)) {\n    const pageText = await page.innerText(errorCompSelector);\n    if (pageText.includes(\"connectionError\")) {\n      if (localNoAuthSetup && pageText.includes(\"passwordless admin\")) {\n        throw `For local testing you must disable passwordless admin and \\ncreate a prostgles admin account for user: ${USERS.test_user} with password: ${USERS.test_user}`;\n      }\n      throw pageText;\n    }\n  }\n};\n"
  },
  {
    "path": "e2e/tests/utils/isPortFree.ts",
    "content": "import * as net from \"net\";\n\nexport const isPortFree = async (\n  port: number,\n  host = \"127.0.0.1\",\n): Promise<boolean> => {\n  return new Promise((resolve) => {\n    const server = net\n      .createServer()\n      .once(\"error\", (err: NodeJS.ErrnoException) => {\n        if (err.code === \"EADDRINUSE\") resolve(false);\n        else resolve(false);\n      })\n      .once(\"listening\", () => {\n        server.close(() => resolve(true));\n      })\n      .listen(port, host);\n  });\n};\n"
  },
  {
    "path": "e2e/tests/utils/utils.ts",
    "content": "import { Locator, Page as PG, expect } from \"@playwright/test\";\nimport * as path from \"path\";\nimport {\n  Command,\n  getCommandElemSelector,\n  getDataKeyElemSelector,\n} from \"../Testing\";\nimport { goTo } from \"./goTo\";\nimport { TEST_DB_NAME, USERS } from \"./constants\";\n\ntype FuncNamesReturningLocatorObj = {\n  [prop in keyof PG as PG[prop] extends (...args: any) => any ?\n    prop extends \"expect\" ? never\n    : prop extends \"getByTestId\" ? never\n    : prop extends \"evaluateHandle\" ? never\n    : prop extends \"waitForFunction\" ? never\n    : ReturnType<PG[prop]> extends Promise<any> ? never\n    : ReturnType<PG[prop]> extends Locator ? prop\n    : never\n  : never]: 1;\n};\ntype FuncNames = keyof FuncNamesReturningLocatorObj;\ntype LocatorFuncNamesReturningLocatorObj = {\n  [prop in keyof Locator as Locator[prop] extends (...args: any) => any ?\n    prop extends \"expect\" ? never\n    : prop extends \"getByTestId\" ? never\n    : prop extends \"evaluateHandle\" ? never\n    : prop extends \"waitForFunction\" ? never\n    : ReturnType<Locator[prop]> extends Promise<any> ? never\n    : ReturnType<Locator[prop]> extends Locator ? prop\n    : // ReturnType<Locator[prop]> extends Promise<Locator>? prop :\n      never\n  : never]: 1;\n};\ntype LocatorFuncNames = keyof LocatorFuncNamesReturningLocatorObj;\nexport type PageWIds = Omit<PG, FuncNames | \"getByTestId\"> & {\n  getByTestId: (command: Command) => LocatorWIds;\n} & {\n  [funcName in FuncNames]: (\n    ...args: Parameters<PG[funcName]>\n  ) => Omit<ReturnType<PG[funcName]>, \"getByTestId\"> &\n    Pick<PageWIds, \"getByTestId\">;\n};\n\ntype LocatorOverridenFuncNames = \"getByTestId\" | \"all\" | \"last\";\nexport type LocatorWIds = Omit<\n  Locator,\n  LocatorFuncNames | LocatorOverridenFuncNames\n> & {\n  getByTestId: (command: Command) => LocatorWIds;\n  all: () => Promise<LocatorWIds[]>;\n  first: () => LocatorWIds;\n  last: () => LocatorWIds;\n} & {\n  [funcName in LocatorFuncNames]: (\n    ...args: Parameters<Locator[funcName]>\n  ) => Omit<ReturnType<Locator[funcName]>, LocatorOverridenFuncNames> &\n    Pick<LocatorWIds, LocatorOverridenFuncNames>;\n};\nexport const getMonacoEditorBySelector = async (\n  page: PageWIds,\n  parentSelector: string,\n) => {\n  const monacoEditor = await page\n    .locator(`${parentSelector} .monaco-editor`)\n    .first();\n  return monacoEditor;\n};\n\nexport const getMonacoValue = async (\n  page: PageWIds,\n  parentSelector: string,\n) => {\n  await page.keyboard.press(\"Control+A\");\n  await page.waitForTimeout(1500);\n  const monacoNode = await getMonacoEditorBySelector(page, parentSelector);\n  const text = await monacoNode.evaluate((node) => {\n    //@ts-ignore\n    return node.parentElement!._getValue() as string;\n  });\n  // const text = await page.evaluate(\n  //   () => window.getSelection()?.toString() ?? \"\",\n  // );\n  const normalizedText = text?.replace(/\\u00A0/g, \" \"); // Replace char 160 with char 32\n  return normalizedText;\n};\n\ntype KeyPress = \"Control\" | \"Shift\";\ntype InputKey =\n  | KeyPress\n  | \"Enter\"\n  | \"Escape\"\n  | \"Tab\"\n  | \"Backspace\"\n  | \"Delete\"\n  | \"Space\";\ntype ArrowKey = \"ArrowUp\" | \"ArrowDown\" | \"ArrowLeft\" | \"ArrowRight\";\ntype ArrowKeyCombinations = `${KeyPress}+${ArrowKey | InputKey}`;\ntype KeyPressOrCombination = InputKey | ArrowKeyCombinations | ArrowKey;\n\n/**\n * Will overwrite all previous content\n */\nexport const monacoType = async (\n  page: PageWIds,\n  parentSelector: string,\n  text: string,\n  {\n    deleteAll,\n    deleteAllAndFill,\n    pressBeforeTyping,\n    pressAfterTyping,\n    keyPressDelay = 100,\n  }: {\n    deleteAll?: boolean;\n    deleteAllAndFill?: boolean;\n    pressBeforeTyping?: KeyPressOrCombination[];\n    pressAfterTyping?: KeyPressOrCombination[];\n    keyPressDelay?: number;\n  } = { deleteAll: true },\n) => {\n  const monacoEditor = await getMonacoEditorBySelector(page, parentSelector);\n  await monacoEditor.click();\n  await page.waitForTimeout(500);\n\n  if (deleteAll || deleteAllAndFill) {\n    await page.keyboard.press(\"Control+A\");\n    await page.waitForTimeout(500);\n    await page.keyboard.press(\"Delete\");\n  }\n  await page.waitForTimeout(500);\n  await monacoEditor.click();\n  await monacoEditor.blur();\n  await page.waitForTimeout(500);\n  await monacoEditor.click();\n  await page.waitForTimeout(500);\n\n  for (const key of pressBeforeTyping ?? []) {\n    await page.keyboard.press(key);\n    await page.waitForTimeout(50);\n  }\n  if (!keyPressDelay) {\n    await monacoEditor.evaluate((node, text) => {\n      //@ts-ignore\n      console.log(node.parentElement!.editorRef);\n      //@ts-ignore\n      node.parentElement.editorRef.setValue(text);\n    }, text);\n    // await monacoEditor.fill(text, { force: true });\n  } else if (deleteAllAndFill) {\n    await page.keyboard.insertText(text);\n  } else {\n    await page.keyboard.type(text, { delay: keyPressDelay });\n  }\n  for (const key of pressAfterTyping ?? []) {\n    await page.keyboard.press(key);\n    await page.waitForTimeout(50);\n  }\n  await page.waitForTimeout(500);\n};\n\nexport const runSql = async (page: PageWIds, query: string) => {\n  await monacoType(page, `.ProstglesSQL`, query);\n  await page.waitForTimeout(300);\n  await page.getByTestId(\"W_SQLBottomBar.runQuery\").click();\n  await page.waitForTimeout(200);\n  await page.getByTestId(\"W_SQLBottomBar.runQuery\").isEnabled({ timeout: 5e3 });\n  await page.waitForTimeout(1e3);\n};\n\nexport const fillSmartForm = async (\n  page: PageWIds,\n  tableName: string,\n  values: Record<string, string | Record<string, any> | Record<string, any>[]>,\n) => {\n  const smartFormLocator = `${getTestId(\"SmartForm\")}${getDataKey(tableName)}`;\n  for (const [key, valueOrRowOrRows] of Object.entries(values)) {\n    const unescapedSelector = `${tableName}-${key}`;\n    const escapedSelector = await page.evaluate(\n      (unescapedSelector) => CSS.escape(unescapedSelector),\n      unescapedSelector,\n    );\n    if (typeof valueOrRowOrRows === \"string\") {\n      const value = valueOrRowOrRows;\n      const elem = await page.locator(\"input#\" + escapedSelector);\n      await elem.fill(value);\n      const selectElem = await page.locator(\n        `[data-key=${JSON.stringify(key)}] .FormField_Select`,\n      );\n      if (await selectElem.isVisible()) {\n        await selectElem.click();\n        await page\n          .getByTestId(\"SearchList.List\")\n          .locator(`[data-key=${JSON.stringify(value)}]`)\n          .click();\n      }\n      /** Joined records */\n    } else if (Array.isArray(valueOrRowOrRows)) {\n      const nestedTableName = key;\n\n      // const tabItem = await page.locator(\n      //   `${smartFormLocator} ${getTestId(\"JoinedRecords\")} ${getDataKey(nestedTableName)}`,\n      // );\n      // const isActive = await tabItem.getAttribute(\"aria-current\");\n      // if (isActive !== \"true\") {\n      //   await tabItem.click();\n      // }\n      for (const nestedRow of valueOrRowOrRows) {\n        await page\n          .locator(\n            `${getTestId(\"JoinedRecords.AddRow\")}${getDataKey(nestedTableName)}`,\n          )\n          .click();\n        await fillSmartFormAndInsert(page, nestedTableName, nestedRow);\n      }\n\n      /** Nested insert into fkey */\n    } else if (typeof valueOrRowOrRows === \"object\") {\n      const nestedInsertBtn = await page\n        .locator(\n          `${getTestId(\"SmartFormField\")}${getDataKey(key)} ${getTestId(\"SmartFormFieldOptions.NestedInsert\")}`,\n        )\n        .first();\n      const nestedTableName = await nestedInsertBtn.getAttribute(\"data-key\");\n      if (!nestedTableName) throw `nestedTableName not found for ${key}`;\n      await nestedInsertBtn.click();\n      await fillSmartFormAndInsert(page, nestedTableName, valueOrRowOrRows);\n    }\n  }\n\n  return page.locator(smartFormLocator);\n};\n\nexport const fillSmartFormAndInsert = async (\n  page: PageWIds,\n  tableName: string,\n  values: Record<string, string | Record<string, any> | Record<string, any>[]>,\n) => {\n  const form = await fillSmartForm(page, tableName, values);\n  await page.waitForTimeout(200);\n  await form.getByTestId(\"SmartForm.insert\").click();\n  await page.waitForTimeout(200);\n};\n\nexport const clickInsertRow = async (\n  page: PageWIds,\n  tableName: string,\n  useTopBtn = false,\n) => {\n  await page\n    .getByTestId(\n      useTopBtn ?\n        \"dashboard.window.rowInsertTop\"\n      : \"dashboard.window.rowInsert\",\n    )\n    .and(page.locator(`[data-key=${JSON.stringify(tableName)}]`))\n    .click();\n  await page.waitForTimeout(200);\n};\n\nexport const insertRow = async (\n  page: PageWIds,\n  tableName: string,\n  row: Record<string, string>,\n  useTopBtn = false,\n) => {\n  await clickInsertRow(page, tableName, useTopBtn);\n  await fillSmartFormAndInsert(page, tableName, row);\n  await page.waitForTimeout(2200);\n};\n\nexport const fillLoginFormAndSubmit = async (\n  page: PageWIds,\n  userNameAndPassword = \"test_user\",\n) => {\n  await page.locator(\"#username\").waitFor({ state: \"visible\", timeout: 30e3 });\n  await page.locator(\"#username\").fill(\"\");\n  await page.locator(\"#username\").fill(userNameAndPassword);\n  await page.locator(\"#password\").fill(userNameAndPassword);\n  await page.getByRole(\"button\", { name: \"Sign in\", exact: true }).click();\n};\n\nexport const login = async (\n  page: PageWIds,\n  userNameAndPassword = \"test_user\",\n  url = \"localhost:3004\",\n) => {\n  await goTo(page, url);\n  await fillLoginFormAndSubmit(page, userNameAndPassword);\n  await page.locator(\"#username\").waitFor({ state: \"detached\", timeout: 30e3 });\n};\n\nexport const typeConfirmationCode = async (page: PageWIds) => {\n  await page.waitForTimeout(200);\n  const code = await page.getByTitle(\"confirmation-code\").textContent();\n  await page.waitForTimeout(200);\n  await page\n    .locator(`input[name=\"confirmation\"]`)\n    .fill(code ?? \"code not found on the page\");\n  await page.waitForTimeout(200);\n};\n\nexport const forEachLocator = async (\n  page: PageWIds,\n  match: () => Promise<LocatorWIds> | LocatorWIds,\n  onMatch: (locator: LocatorWIds) => Promise<void>,\n) => {\n  let items: LocatorWIds[] = [];\n  do {\n    items = await (await match()).all();\n    const firstItem = items[0];\n    if (firstItem) {\n      await onMatch(firstItem);\n      await page.waitForTimeout(220);\n    }\n  } while (items.length);\n};\n\nexport const closeWorkspaceWindows = async (page: PageWIds) => {\n  await forEachLocator(\n    page,\n    () => page.getByTestId(\"dashboard.window.close\"),\n    async (closeBtn) => {\n      await closeBtn.click();\n      /** SQL windows need save or delete confirmation */\n      const deleteSqlBtn = await page.getByRole(\"button\", {\n        name: \"Delete\",\n        exact: true,\n      });\n      if (await deleteSqlBtn.count()) {\n        await deleteSqlBtn.click();\n      }\n      await page.waitForTimeout(220);\n    },\n  );\n\n  const closeBtnsCount = await page\n    .getByTestId(\"dashboard.window.close\")\n    .count();\n  if (closeBtnsCount) {\n    throw `${closeBtnsCount} windows are still opened`;\n  }\n  await page.waitForTimeout(100);\n};\n\nexport const getDataKey = (key: string) =>\n  `[data-key=${JSON.stringify(key)}]` as const;\nexport const getTestId = (testid: Command) =>\n  `[data-command=${JSON.stringify(testid)}]` as const;\nexport const getSelector = ({\n  testid,\n  dataKey,\n}: {\n  testid: Command | undefined;\n  dataKey: string | undefined;\n}) =>\n  [\n    testid && `[data-command=${JSON.stringify(testid)}]`,\n    dataKey && `[data-key=${JSON.stringify(dataKey)}]`,\n  ]\n    .filter(Boolean)\n    .join(\"\");\n\ntype FFilter = { fieldName: string; value: string }[];\ntype FData = Record<string, string>;\nexport const setTableRule = async (\n  page: PageWIds,\n  tableName: string,\n  rule: {\n    select?: { excludedFields?: string[]; forcedFilter?: FFilter };\n    insert?: { excludedFields?: string[]; forcedData?: FData };\n    update?: {\n      excludedFields?: string[];\n      forcedFilter?: FFilter;\n      forcedData?: FData;\n    };\n    delete?: { forcedFilter?: FFilter };\n  },\n  isFileTable: boolean,\n) => {\n  const tableRow = await page.locator(getDataKey(tableName));\n\n  const addSmartFilter = async ({\n    fieldName,\n    value,\n  }: {\n    fieldName: string;\n    value: string;\n  }) => {\n    await page.getByTestId(\"SmartAddFilter\").click();\n    await page.locator(getDataKey(fieldName)).click();\n    const filterWrapperSelector = `${getTestId(\"FilterWrapper\")}${getDataKey(fieldName)}`;\n    await page\n      .locator(`${filterWrapperSelector} input[type=\"text\"]`)\n      .fill(value);\n    await page.waitForTimeout(500);\n    await page.keyboard.press(\"Enter\");\n    await page.waitForTimeout(200);\n  };\n\n  const setForcedFilter = async (forcedFilter?: FFilter) => {\n    if (!forcedFilter) return;\n\n    await page.getByTestId(\"ForcedFilterControl.type\").click();\n    await page.getByTestId(\"ForcedFilterControl.type.enabled\").click();\n\n    for (const { fieldName, value } of forcedFilter) {\n      await addSmartFilter({ fieldName, value });\n    }\n  };\n\n  const setExcludedFields = async (excludedFields?: string[]) => {\n    if (!excludedFields?.length) return;\n\n    await page.getByTestId(\"FieldFilterControl.type\").click();\n    await page.getByTestId(\"FieldFilterControl.type.except\").click();\n    await page.getByTestId(\"FieldFilterControl.select\").click();\n    await page.getByTestId(\"SearchList.toggleAll\").click();\n    for (const field of excludedFields) {\n      await page.locator(`[data-key=${JSON.stringify(field)}]`).click();\n      await page.waitForTimeout(500);\n    }\n    await page.keyboard.press(\"Escape\");\n  };\n\n  const setForcedData = async (forcedData?: FData) => {\n    if (!forcedData) return;\n\n    // await page.getByTestId(\"ForcedDataControl.toggle\").click();\n    // for(const [fieldName, value] of Object.entries(forcedData)){\n    //   await page.getByTestId(\"ForcedDataControl.addColumn\").click();\n    //   await page.locator(`[data-key=${JSON.stringify(fieldName)}]`).click();\n    //   await page.locator(`input#${tableName}-${fieldName}`).type(value);\n    //   await page.waitForTimeout(500);\n    // }\n\n    await page.getByTestId(\"CheckFilterControl.type\").click();\n    await page.getByTestId(\"CheckFilterControl.type.enabled\").click();\n\n    for (const [fieldName, value] of Object.entries(forcedData)) {\n      await addSmartFilter({ fieldName, value });\n    }\n  };\n\n  const toggleFileRule = async () => {\n    const ruleToggle = await page.getByTestId(\"RuleToggle\");\n    if (isFileTable) {\n      await ruleToggle.click();\n      await page.getByTestId(\"TablePermissionControls.close\").click();\n    }\n  };\n\n  if (rule.select) {\n    await tableRow.getByTestId(\"selectRule\").click();\n    await toggleFileRule();\n    await page.waitForTimeout(500);\n\n    if (!isEmpty(rule.select)) {\n      await tableRow.getByTestId(\"selectRuleAdvanced\").click();\n\n      await setExcludedFields(rule.select.excludedFields);\n      await setForcedFilter(rule.select.forcedFilter);\n\n      await page.getByTestId(\"TablePermissionControls.close\").click();\n      await page.waitForTimeout(500);\n    }\n  }\n\n  if (rule.insert) {\n    await tableRow.getByTestId(\"insertRule\").click();\n    await toggleFileRule();\n    await page.waitForTimeout(500);\n\n    if (!isEmpty(rule.select)) {\n      await tableRow.getByTestId(\"insertRuleAdvanced\").click();\n\n      await setExcludedFields(rule.insert.excludedFields);\n      await setForcedData(rule.insert.forcedData);\n\n      await page.getByTestId(\"TablePermissionControls.close\").click();\n      await page.waitForTimeout(500);\n    }\n  }\n\n  if (rule.update) {\n    await tableRow.getByTestId(\"updateRule\").click();\n    await toggleFileRule();\n    await page.waitForTimeout(500);\n\n    if (!isEmpty(rule.select)) {\n      await tableRow.getByTestId(\"updateRuleAdvanced\").click();\n\n      await setExcludedFields(rule.update.excludedFields);\n      await setForcedFilter(rule.update.forcedFilter);\n      await setForcedData(rule.update.forcedData);\n\n      await page.getByTestId(\"TablePermissionControls.close\").click();\n      await page.waitForTimeout(500);\n    }\n  }\n\n  if (rule.delete) {\n    await tableRow.getByTestId(\"deleteRule\").click();\n    await toggleFileRule();\n    await page.waitForTimeout(500);\n\n    if (!isEmpty(rule.select)) {\n      await tableRow.getByTestId(\"deleteRuleAdvanced\").click();\n\n      await setForcedFilter(rule.delete.forcedFilter);\n\n      await page.getByTestId(\"TablePermissionControls.close\").click();\n      await page.waitForTimeout(500);\n    }\n  }\n  await page.waitForTimeout(500);\n};\n\nexport const runDbsSql = async (\n  page: PageWIds,\n  query: string,\n  args?: any,\n  opts?: any,\n) => {\n  return runDbSql(page, query, args, opts, \"dbs\");\n};\n\nexport const runDbSql = async (\n  page: PageWIds,\n  query: string,\n  args?: any,\n  opts?: any,\n  dbType: \"db\" | \"dbs\" = \"db\",\n) => {\n  const [error, sqlResult] = (await page.evaluate(\n    async ([query, args, opts, dbType]) => {\n      try {\n        const db = (window as any)[dbType];\n        if (!db) throw dbType + \" is missing\";\n        const data = await db.sql(query, args, opts);\n        return [undefined, data];\n      } catch (error) {\n        return [error];\n      }\n    },\n    [query, args, opts, dbType],\n  )) as any;\n  if (error) {\n    console.error(`Error running sql:`, error);\n    throw error;\n  }\n  return sqlResult;\n};\n\nexport const openTable = async (page: PageWIds, namePartStart: string) => {\n  await page.getByTestId(\"dashboard.menu\").waitFor({ state: \"visible\" });\n  await page.keyboard.press(\"Control+KeyP\");\n  await page.waitForTimeout(200);\n  const searchAlLInput = await page.getByTestId(\"SearchAll\");\n  await searchAlLInput.waitFor({ state: \"visible\" });\n  await searchAlLInput.fill(namePartStart);\n  await page.waitForTimeout(200);\n  await page.keyboard.press(\"Enter\");\n  await page.waitForTimeout(500);\n  /** Ensure table_name strats with */\n  const table = page.locator(\n    `[data-table-name^=${JSON.stringify(namePartStart)}]`,\n  );\n\n  /** Used for debugging */\n  if (!table.isVisible()) {\n    const v_triggers = await runDbsSql(\n      page,\n      `SELECT * FROM prostgles.v_triggers;`,\n      {},\n      { returnType: \"rows\" },\n    );\n    console.log(JSON.stringify({ v_triggers, namePartStart }));\n    await page.waitForTimeout(500);\n  }\n  await expect(table).toBeVisible();\n  await page.waitForTimeout(1000);\n};\nexport const MINUTE = 60e3;\nexport const createDatabase = async (\n  dbName: string,\n  page: PageWIds,\n  fromTemplates = false,\n  owner?: { name: string; pass: string },\n) => {\n  await goTo(page, \"localhost:3004/connections\");\n  await page\n    .locator(`[data-command=\"ConnectionServer.add\"][data-key^=\"usr@localhost\"]`)\n    .first()\n    .click();\n  // await page.waitForTimeout(3000);\n  if (fromTemplates) {\n    await page.getByTestId(\"ConnectionServer.add.newDatabase\").click();\n    await page.getByTestId(\"ConnectionServer.SampleSchemas\").click();\n    await page\n      .getByTestId(\"ConnectionServer.SampleSchemas\")\n      .locator(`[data-key=${JSON.stringify(dbName)}]`)\n      .click();\n  } else {\n    await page.getByTestId(\"ConnectionServer.add.newDatabase\").click();\n    await page\n      .getByTestId(\"ConnectionServer.NewDbName\")\n      .locator(\"input\")\n      .fill(dbName);\n  }\n  if (owner) {\n    await page.getByTestId(\"ConnectionServer.withNewOwnerToggle\").click();\n    await page.waitForTimeout(500);\n    await page\n      .getByTestId(\"ConnectionServer.NewUserName\")\n      .locator(\"input\")\n      .fill(owner.name);\n    await page\n      .getByTestId(\"ConnectionServer.NewUserPassword\")\n      .locator(\"input\")\n      .fill(owner.pass);\n    await page.waitForTimeout(500);\n  }\n  await page.getByTestId(\"ConnectionServer.add.confirm\").click();\n  /* Wait until db is created */\n  const databaseCreationTime = (fromTemplates ? 4 : 1) * MINUTE;\n  const workspaceCreationAndLoatTime = 3 * MINUTE;\n  await page\n    .getByTestId(\"ConnectionServer.add.confirm\")\n    .waitFor({ state: \"detached\", timeout: databaseCreationTime });\n  await page\n    .getByTestId(\"dashboard.menu\")\n    .waitFor({ state: \"visible\", timeout: workspaceCreationAndLoatTime });\n};\n\nexport const dropConnectionAndDatabase = async (\n  dbName: string,\n  page: PageWIds,\n) => {\n  await page.waitForTimeout(2000);\n  const connectionSelector = `[data-key=${JSON.stringify(dbName)}]`;\n  await page.locator(connectionSelector).getByTestId(\"Connection.edit\").click();\n\n  await page.getByTestId(\"Connection.edit.delete\").click();\n  await page.getByTestId(\"Connection.edit.delete.dropDatabase\").click();\n  await typeConfirmationCode(page);\n  await page.getByTestId(\"Connection.edit.delete.confirm\").click();\n  await page.waitForTimeout(5000);\n\n  return { connectionSelector };\n};\n\nexport const selectAndUpsertFile = async (\n  page: PageWIds,\n  onOpenFileDialog: (page: PageWIds) => Promise<void>,\n  onBeforePressInsert?: () => Promise<void>,\n  isUpdate = false,\n) => {\n  // Start waiting for file chooser before clicking. Note no await.\n  const fileChooserPromise = page.waitForEvent(\"filechooser\");\n  await onOpenFileDialog(page);\n  const fileChooser = await fileChooserPromise;\n  const resolvedPath = path.resolve(path.join(__dirname, \"../../\" + fileName));\n\n  await fileChooser.setFiles(resolvedPath);\n  await onBeforePressInsert?.();\n  await page\n    .getByRole(\"button\", { name: isUpdate ? \"Update\" : \"Insert\", exact: true })\n    .click();\n  if (isUpdate) {\n    await page\n      .getByRole(\"button\", { name: \"Update row!\", exact: true })\n      .click();\n  }\n};\n\nexport const fileName = \"icon512.png\";\nexport const uploadFile = async (page: PageWIds) => {\n  await clickInsertRow(page, \"files\");\n  await page.waitForTimeout(200);\n  await selectAndUpsertFile(page, (page) =>\n    page.getByTestId(\"FileBtn\").click(),\n  );\n};\n\nexport const isEmpty = (obj?: any) => {\n  return !obj || Object.keys(obj).length === 0;\n};\n\nexport const getSearchListItem = (\n  page: PageWIds | LocatorWIds,\n  { dataKey }: { dataKey: string },\n) => {\n  return page\n    .getByTestId(\"SearchList.List\")\n    .locator(`[data-key=${JSON.stringify(dataKey)}]`);\n};\nexport const getTableWindow = (page: PageWIds, tableName: string) => {\n  return page.locator(`[data-table-name=${JSON.stringify(tableName)}]`);\n};\n\nexport const setWspColLayout = async (page: PageWIds) => {\n  await page.getByTestId(\"dashboard.menu\").waitFor({ state: \"visible\" });\n  await page.getByTestId(\"dashboard.menu.settingsToggle\").click();\n  await page.getByTestId(\"dashboard.menu.settings.defaultLayoutType\").click();\n  await page\n    .getByTestId(\"dashboard.menu.settings.defaultLayoutType\")\n    .locator(`[data-key=\"col\"]`)\n    .click();\n  await page.getByTestId(\"Popup.close\").click();\n};\nexport const disablePwdlessAdminAndCreateUser = async (page: PageWIds) => {\n  await goTo(page);\n  await page\n    .getByRole(\"link\", { name: \"Users\" })\n    .waitFor({ state: \"visible\", timeout: 60e3 });\n  await page.getByRole(\"link\", { name: \"Users\" }).click();\n  await expect(page as PG).toHaveURL(/.*users/);\n  await page.goto(\"localhost:3004/users\", {\n    waitUntil: \"networkidle\",\n    timeout: 10e3,\n  });\n  await page.getByRole(\"button\", { name: \"Create admin user\" }).click();\n  await page.locator(\"#username\").fill(USERS.test_user);\n  await page.locator(\"#new-password\").fill(USERS.test_user);\n  await page.locator(\"#confirm_password\").fill(USERS.test_user);\n  await page.getByRole(\"button\", { name: \"Create\", exact: true }).click();\n  await page.waitForTimeout(5e3);\n};\n\nexport const createAccessRule = async (\n  page: PageWIds,\n  userType: \"default\" | \"public\",\n) => {\n  /** Set permissions */\n  await page.getByTestId(\"config.ac\").click();\n  await page.waitForTimeout(1e3);\n  await page.getByTestId(\"config.ac.create\").click();\n\n  /** Setting user type to default */\n  await page.getByTestId(\"config.ac.edit.user\").click();\n  await page.getByRole(\"option\").getByText(userType).click();\n  await page.getByRole(\"button\", { name: \"Done\", exact: true }).click();\n\n  /** Setting AC Type to custom */\n  await page\n    .getByTestId(\"config.ac.edit.type\")\n    .locator(`button[value=\"Custom\"]`)\n    .click();\n};\nexport const createAccessRuleForTestDB = async (\n  page: PageWIds,\n  userType: \"default\" | \"public\",\n) => {\n  await login(page);\n  await page.getByRole(\"link\", { name: \"Connections\" }).click();\n  await page.getByRole(\"link\", { name: TEST_DB_NAME }).click();\n  await page\n    .getByTestId(\"dashboard.goToConnConfig\")\n    .waitFor({ state: \"visible\", timeout: 10e3 });\n  await page.getByTestId(\"dashboard.goToConnConfig\").click();\n\n  await createAccessRule(page, userType);\n};\n\nexport const enableAskLLM = async (\n  page: PageWIds,\n  maxRequestsPerDay: number,\n  credsProvided = false,\n) => {\n  await page.getByTestId(\"AskLLMAccessControl\").click();\n  if (!credsProvided) {\n    await page.getByTestId(\"SetupLLMCredentials.api\").click();\n    // await page.getByTestId(\"AddLLMCredentialForm\").click();\n    await page.getByTestId(\"dashboard.window.rowInsertTop\").click();\n    // await page.getByTestId(\"AddLLMCredentialForm.Provider\").click();\n\n    // await page\n    //   .getByTestId(\"AddLLMCredentialForm.Provider\")\n    //   .locator(`[data-key=\"Custom\"]`)\n    //   .click();\n    await runDbsSql(page, \"DELETE FROM llm_providers WHERE id = 'Custom' \");\n    await fillSmartFormAndInsert(page, \"llm_credentials\", {\n      name: \"my credential\",\n      api_key: \"nothing\",\n      provider_id: {\n        id: \"Custom\",\n        api_url: \"http://localhost:3004/mocked-llm\",\n        logo_url: \"/icons/CloudQuestionOutline.svg\",\n        llm_models: [\n          {\n            name: \"mymodel\",\n          },\n        ],\n      },\n    });\n    // await page.locator(`#endpoint`).fill(\"http://localhost:3004/mocked-llm\");\n    // await page.getByTestId(\"AddLLMCredentialForm.Save\").click();\n  }\n  await page.getByTestId(\"AskLLMAccessControl.AllowAll\").click();\n  await page.waitForTimeout(1e3);\n  if (maxRequestsPerDay) {\n    await page\n      .getByTestId(\"AskLLMAccessControl.llm_daily_limit\")\n      .locator(\"input\")\n      .fill(maxRequestsPerDay.toString());\n  }\n  await page.getByTestId(\"Popup.close\").click();\n};\nexport const getAskLLMLastMessage = async (page: PageWIds) => {\n  const lastIncomingMessage = await page\n    .getByTestId(\"AskLLM.popup\")\n    .locator(\".message.incoming\")\n    .last();\n\n  /** Await any in progress tool calls */\n  const toolCallBtns = await lastIncomingMessage\n    .getByTestId(\"ToolUseMessage.toggle\")\n    .all();\n\n  await Promise.all(\n    [...toolCallBtns, lastIncomingMessage].map((btn) =>\n      btn.locator(\".Loading\").waitFor({ state: \"detached\", timeout: 30_000 }),\n    ),\n  );\n  await page.waitForTimeout(1500);\n  const response = await lastIncomingMessage.textContent();\n  return response;\n};\n\nconst waitForAllMatchingLocatorsToDisappear = async (\n  locator: LocatorWIds,\n  timeout = 30_000,\n) => {\n  const start = Date.now();\n  let tries = 0;\n  while (true) {\n    const count = await locator.count();\n    if (!count) {\n      if (tries > 2) {\n        return;\n      } else {\n        tries++;\n      }\n    } else if (Date.now() - start > timeout) {\n      throw new Error(\n        `Timeout waiting for locators to disappear. Still ${count} remaining.`,\n      );\n    }\n    await new Promise((resolve) => setTimeout(resolve, 100));\n  }\n};\n\nexport const sendAskLLMMessage = async (\n  page: PageWIds,\n  msg: string,\n  waitForLoadingToStop:\n    | boolean\n    | {\n        onAfterSend: () => Promise<void>;\n      } = false,\n) => {\n  await page.getByTestId(\"AskLLM.popup\").getByTestId(\"Chat.textarea\").fill(msg);\n  await page.keyboard.press(\"Enter\");\n  await page.waitForTimeout(500);\n  if (waitForLoadingToStop) {\n    if (typeof waitForLoadingToStop === \"object\") {\n      await waitForLoadingToStop.onAfterSend();\n    }\n    await waitForAllMatchingLocatorsToDisappear(\n      page\n        .getByTestId(\"Chat.messageList\")\n        .locator(\".message.incoming .Loading\")\n        .last(),\n    );\n  }\n};\nexport const getLLMResponses = async (\n  page: PageWIds,\n  questions: string[],\n  openWindow = true,\n) => {\n  if (openWindow) {\n    await page.getByTestId(\"AskLLM\").click();\n  }\n  await page.getByTestId(\"AskLLM.popup\").waitFor({ state: \"visible\" });\n  await page.waitForTimeout(2e3);\n  const result: {\n    response: string | null;\n    isOk: boolean;\n  }[] = [];\n\n  for (const question of questions) {\n    await sendAskLLMMessage(page, question);\n    const response = await getAskLLMLastMessage(page);\n    result.push({\n      response,\n      isOk: !!response?.includes(\"Mocked response\"),\n    });\n  }\n  if (openWindow) {\n    await page.getByTestId(\"Popup.close\").click();\n  }\n  return result;\n};\n\nexport const openConnection = async (\n  page: PageWIds,\n  connectionName:\n    | \"sample_database\"\n    | \"cloud\"\n    | \"crypto\"\n    | \"food_delivery\"\n    | \"Prostgles UI state\"\n    | \"prostgles_video_demo\"\n    | \"Prostgles UI automated tests database\",\n) => {\n  await goTo(page, \"/connections\");\n  await page\n    .locator(getDataKeyElemSelector(connectionName))\n    .getByTestId(\"Connection.openConnection\")\n    .click();\n  await page.waitForTimeout(1000);\n};\n\nexport const loginWhenSignupIsEnabled = async (page: PageWIds) => {\n  await goTo(page, \"/login\");\n  await page.locator(\"#username\").fill(USERS.test_user);\n  await page.getByRole(\"button\", { name: \"Continue\" }).click();\n  await page.locator(\"#password\").waitFor({ state: \"visible\" });\n  await page.locator(\"#password\").fill(USERS.test_user);\n  await page.getByRole(\"button\", { name: \"Continue\" }).click();\n  await page.getByTestId(\"App.colorScheme\").waitFor({ state: \"visible\" });\n  await page.waitForTimeout(500);\n};\n\nconst backupNames = \"Demo\";\nexport const restoreFromBackup = async (\n  page: PageWIds,\n  backupFileName?: typeof backupNames,\n) => {\n  const backupListItems = await page\n    .getByTestId(\"BackupsControls.Completed\")\n    .locator(\".SmartCard\");\n  await backupListItems.first().waitFor({ state: \"visible\" });\n  const backupItem =\n    backupFileName ?\n      backupListItems.filter({ hasText: `Backup name${backupFileName}` })\n    : backupListItems;\n  await backupItem\n    .getByRole(\"button\", { name: \"Restore...\", exact: true })\n    .click();\n  await typeConfirmationCode(page);\n  await page.getByRole(\"button\", { name: \"Restore\", exact: true }).click();\n};\n\nexport const getDashboardUtils = (page: PageWIds) => {\n  const _open = openConnection.bind(null, page);\n  const openMenuIfClosed = async (closeMenu = false) => {\n    await page.waitForTimeout(1500);\n    const menuCloseButton = await page\n      .getByTestId(\"DashboardMenu\")\n      .getByTestId(\"Popup.close\");\n    const menuPinnedButton = await page.locator(\n      getCommandElemSelector(\"DashboardMenuHeader.togglePinned\") +\n        getDataKey(\"pinned\"),\n    );\n    const menuIsOpen =\n      (await menuCloseButton.count()) ? \"close\"\n      : (await menuPinnedButton.count()) ? \"unpinn\"\n      : undefined;\n    if (menuIsOpen) {\n      if (closeMenu) {\n        if (menuIsOpen === \"close\") {\n          await menuCloseButton.click();\n        } else {\n          await menuPinnedButton.click();\n        }\n      }\n      return;\n    }\n    const menuBtn = await page.getByTestId(\"dashboard.menu\");\n    if ((await menuBtn.count()) && (await menuBtn.isEnabled())) {\n      await menuBtn.click();\n    }\n  };\n  const toggleMenuPinned = async (shouldBePinned?: boolean) => {\n    await page.waitForTimeout(1500);\n    const toggleBtn = await page.getByTestId(\n      \"DashboardMenuHeader.togglePinned\",\n    );\n    const menuBtn = await page.getByTestId(\"dashboard.menu\");\n    const menuIsPinned = await menuBtn.isDisabled();\n    if ((await toggleBtn.count()) && (await toggleBtn.isEnabled())) {\n      if (menuIsPinned && shouldBePinned) {\n        return;\n      }\n      await toggleBtn.click();\n    } else if (shouldBePinned) {\n      await menuBtn.click();\n      await page.getByTestId(\"DashboardMenuHeader.togglePinned\").click();\n    }\n  };\n\n  return {\n    openConnection: _open,\n    openMenuIfClosed,\n    toggleMenuPinned,\n  };\n};\n\nexport const setPromptByText = async (page: PageWIds, text: string) => {\n  await page.getByTestId(\"LLMChatOptions.Prompt\").click();\n  await page.locator(\".SmartCard\").getByText(text).first().click();\n  await page\n    .getByTestId(\"LLMChatOptions.Prompt\")\n    .getByTestId(\"Popup.close\")\n    .click();\n};\n\nexport const setModelByText = async (page: PageWIds, text: string) => {\n  await page.getByTestId(\"LLMChatOptions.Model\").click();\n  await page.waitForTimeout(500);\n  await page.keyboard.type(text);\n  await page.keyboard.press(\"Enter\");\n  await page.waitForTimeout(1e3); // wait for model to be set\n};\n\nexport const setupProstglesLLMProvider = async (page: PageWIds) => {\n  const aiDemoDBName = \"prostgles_video_demo\" as const;\n  await openConnection(page, \"cloud\");\n  await openConnection(page, aiDemoDBName);\n  await runDbsSql(page, \"TRUNCATE llm_chats CASCADE\");\n  await runDbsSql(\n    page,\n    `\n      DELETE FROM llm_credentials WHERE provider_id = 'Prostgles';\n      UPDATE llm_providers \n      SET api_url = 'http://localhost:3004/rest-api/cloud/methods/askLLM'\n      WHERE id = 'Prostgles';\n      /*\n        UPDATE published_methods\n        SET connection_id = (\n          SELECT id FROM connections WHERE \"name\" = '${aiDemoDBName}'\n        )\n        WHERE name = 'askLLM';\n      */\n     `,\n  );\n  const activeSession = await runDbsSql(\n    page,\n    `SELECT *\n        FROM sessions\n        WHERE user_id = (SELECT id FROM users WHERE username = 'test_user')\n        AND active = true\n        ORDER BY id_num DESC\n        LIMIT 1`,\n    {},\n    { returnType: \"rows\" },\n  );\n  if (!activeSession.length) {\n    throw new Error(\"No active session found for test_user\");\n  }\n  await runDbsSql(\n    page,\n    `INSERT INTO llm_credentials(provider_id, api_key, user_id)\n      VALUES('Prostgles', \\${api_key} , \\${user_id} )\n      `,\n    { api_key: btoa(activeSession[0].id), user_id: activeSession[0].user_id },\n  );\n};\n\n/** Delete existing chat during local testing */\nexport const deleteExistingLLMChat = async (page: PageWIds) => {\n  const optsToggle = page.getByTestId(\"LLMChatOptions.toggle\");\n  if ((await optsToggle.count()) === 0) {\n    await page.getByTestId(\"AskLLM\").click();\n  }\n  await page.getByTestId(\"LLMChatOptions.toggle\").click();\n  await page.getByTestId(\"SmartForm.delete\").click();\n  await page.getByTestId(\"SmartForm.delete.confirm\").click();\n  await page.waitForTimeout(1e3);\n};\n\nexport const deleteAllWorkspaces = async (page: PageWIds): Promise<void> => {\n  const list = await page.getByTestId(\"WorkspaceMenu.list\");\n  await list.waitFor({ state: \"visible\", timeout: 10e3 });\n  await page.waitForTimeout(1500);\n  const listTextContent = await list.textContent();\n  if (listTextContent?.trim().toLowerCase() === \"default\") return; // only default workspace exists\n  await page.getByTestId(\"WorkspaceMenuDropDown\").click();\n  await page.getByTestId(\"WorkspaceDeleteBtn\").last().click();\n  await page.getByTestId(\"WorkspaceDeleteBtn.Confirm\").click();\n  await page.waitForTimeout(555);\n  await page.reload();\n  return deleteAllWorkspaces(page);\n};\n\nexport const setOrAddWorkspace = async (\n  page: PageWIds,\n  workspaceName: string,\n) => {\n  const workspaceList = await page.getByTestId(\"WorkspaceMenu.list\");\n  await workspaceList.waitFor({ state: \"visible\", timeout: 10e3 });\n  await page.waitForTimeout(1500);\n  const toggleBtn = await page\n    .getByTestId(\"WorkspaceMenu.list\")\n    .getByText(workspaceName, { exact: true });\n\n  if (await toggleBtn.count()) {\n    await toggleBtn.click();\n  } else {\n    await page.getByTestId(\"WorkspaceMenuDropDown\").click();\n    await page.getByTestId(\"WorkspaceMenuDropDown.WorkspaceAddBtn\").click();\n    await page.getByLabel(\"New workspace name\").fill(workspaceName);\n    await page.getByTestId(\"WorkspaceAddBtn.Create\").click();\n  }\n  await page.waitForTimeout(1e3);\n};\n"
  },
  {
    "path": "e2e/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\" // This must be specified if \"paths\" is.\n  }\n}\n"
  },
  {
    "path": "electron/.gitignore",
    "content": "dist\nui\nbuild\nnode_modules\nplaywright-report\nelectron-report\ntest-results"
  },
  {
    "path": "electron/README.md",
    "content": "# Prostgles Desktop\n\nElectron Wrapper for Prostgles UI\n\n## Overview\n\nProstgles Desktop is a cross platform PostgreSQL database tool. The focus is on providing one of the best sql writing and data exploration experience for PostgreSQL for free\n\n### Demo\n\nVisit https://playground.prostgles.com\n\n### Features\n\n- SQL Editor with context-aware schema auto-completion and documentation extracts and hints\n- Realtime data exploration dashboard with versatile layout system (tab and side-by-side view)\n- Table view with controls to view related data, sort, filter and cross-filter\n- Map and Time charts with aggregations\n- Data insert/update forms with autocomplete\n- Search all tables from public schema\n- Media file display (audio/video/image/html/svg)\n- Data import (CSV, JSON and GeoJSON)\n- Backup/Restore (locally or to cloud)\n- TypeScript server-side functions (experimental)\n- LISTEN NOTIFY support\n\n### Installation\n\nVisit our [releases page](https://github.com/prostgles/ui/releases) to download the latest installation binaries.\n\n### Building\n\nDownload the source code:\n\n```\ngit clone https://github.com/prostgles/ui.git\ncd ui/electron\n```\n\nThe build commands are specific to your operating system: build-linux, build-macos or build-win.\n\nConsult [our workflow file](../.github/workflows/on_release.yml) to get up to date build commands for your platform\n\nMacOS build\n\n```\nnpm run build-macos\n```\n\nGo to build files\n\n```\ncd dist\n```\n"
  },
  {
    "path": "electron/build.sh",
    "content": "\n#!/bin/bash\n\nnpm version \"$(../scripts/get_version.sh ../package.json)\" --force --no-git-tag-version --silent\n\nrm -rf ./ui\nrm -rf ./dist\nmkdir -p ./dist\n\ncd ..\n\nrm -rf ./ui/*\n\nmkdir -p ./ui/server/src\nmkdir -p ./ui/server/dist\nmkdir -p ./ui/server/connection_dbo\nmkdir -p ./ui/common\nmkdir -p ./ui/client\nmkdir -p ./ui/electron\n\ncp -R ./.github ./ui/\n\ncp -R ./electron/*.json ./ui/electron/\n\ncp -R ./server/src ./ui/server/\ncp -R ./server/sample_schemas ./ui/server/ \ncp -R ./common ./ui/\ncp ./server/tsconfig.json ./ui/server/\ncp ./server/.gitignore ./ui/server/\ncp ./server/tslint.json ./ui/server/ \ncp ./server/licenses.json ./ui/server/\ncp ./server/package.json ./ui/server/\ncp ./server/package-lock.json ./ui/server/\n\ncd ./ui/server/\nnpm run build\ncd ../../\n\ncd ./client\nnpm run build\n\ncd ..\n\nmkdir -p ./ui/client/build\nmkdir -p ./ui/client/public\nmkdir -p ./ui/client/static\n\ncp -R ./client/* ./ui/client/\nrm -rf ./ui/client/node_modules\n\ncp ./docker-compose.yml ./ui/\ncp ./LICENSE ./ui/\ncp ./README.md ./ui/\ncp ./PRIVACY ./ui/\ncp ./server/.gitignore ./ui/\ncp ./Dockerfile ./ui/\ncp ./.env ./ui/ \n\nmv ./ui ./electron\n\ncd ./electron/\n\nnpm ci \nnpm run build-tsc "
  },
  {
    "path": "electron/e2e-electron/electron.spec.tsx",
    "content": "import {\n  expect,\n  test,\n  ElectronApplication,\n  _electron as electron,\n  Page,\n} from \"@playwright/test\";\nimport { rmSync } from \"original-fs\";\nimport * as path from \"path\";\nlet electronApp: ElectronApplication | undefined;\n\nconst start = Date.now();\nconst urlsOpened: string[] = [];\ntest.beforeAll(async () => {\n  process.env.CI = \"e2e\";\n  electronApp = await electron.launch({\n    args: [__dirname + \"/../build/main.js\", \"--no-sandbox\"],\n    env: {\n      ...process.env,\n      NODE_ENV: \"development\",\n      //'development'\n    },\n    // tracesDir: './dist',\n    // recordVideo: { dir: './dist' }\n  });\n  electronApp.on(\"window\", async (page) => {\n    urlsOpened.push(page.url());\n    console.log(`Windows opened: `, urlsOpened);\n\n    page.on(\"pageerror\", (error) => {\n      console.error(error);\n    });\n    page.on(\"console\", (msg) => {\n      console.log(msg.text());\n    });\n  });\n  electronApp\n    .process()\n    .stdout?.on(\"data\", (data) => console.log(`stdout: ${data}`));\n  electronApp\n    .process()\n    .stderr?.on(\"data\", (error) => console.log(`stderr: ${error}`));\n});\n\ntest.afterAll(async () => {\n  let waitTimeSeconds = 20;\n  console.trace(\"closing app\");\n  setInterval(() => {\n    console.log(\n      (Date.now() - start) / 1e3,\n      \" seconds since started. trying to close... \",\n    );\n    waitTimeSeconds--;\n    if (waitTimeSeconds <= 0) {\n      console.trace(\"Force closing app\");\n      // process.exit(0);\n    }\n  }, 1e3);\n  await electronApp?.close();\n  waitTimeSeconds = 0;\n  console.log(\"afterAll electronApp\", !!electronApp);\n});\n\ntest.setTimeout(2 * 60e3);\n\ntest(\"renders the first page\", async () => {\n  if (!electronApp) {\n    console.error(\"No electronApp\");\n    return;\n  }\n\n  const page = await electronApp.firstWindow();\n\n  const screenshot = async (name?: string) => {\n    if (!page) return;\n    await page.screenshot({\n      path: `../e2e/electron-report/s-${name ?? new Date().toISOString().replaceAll(\":\", \"\")}.png`,\n    });\n  };\n\n  /** Privacy */\n  await page\n    .getByTestId(\"ElectronSetup.Next\")\n    .waitFor({ state: \"visible\", timeout: 120e3 });\n  await screenshot();\n  await page.getByTestId(\"ElectronSetup.Next\").click();\n\n  /** PG Installation info */\n  await page\n    .getByTestId(\"PostgresInstallationInstructions\")\n    .waitFor({ state: \"visible\", timeout: 120e3 });\n  await screenshot();\n  await page.getByTestId(\"PostgresInstallationInstructions\").click();\n  await page.waitForTimeout(1000);\n  await screenshot();\n  // await page.getByTestId(\"PostgresInstallationInstructions.Close\").click();\n  await page.getByTestId(\"Popup.close\").click();\n  // await page.getByTestId(\"ElectronSetup.Next\").click();\n\n  /** State db connection details */\n  await page.waitForTimeout(1000);\n  await screenshot();\n  await page.getByText(\"Manual setup\").click();\n  await page.waitForTimeout(200);\n  await screenshot();\n  await page.getByLabel(\"user\").fill(\"usr\");\n  await page.getByLabel(\"password\").fill(\"psw\");\n  await page.getByLabel(\"database\").fill(\"prostgles_desktop_db\");\n\n  /** Ensure overflow does not obscure done button */\n  await page.getByTestId(\"NewConnectionForm.MoreOptionsToggle\").click();\n  const doneBtn = await page.getByTestId(\"ElectronSetup.Done\");\n  await expect(doneBtn).toBeVisible();\n  await screenshot();\n  await doneBtn.click();\n  await page.waitForTimeout(1000);\n\n  await screenshot();\n\n  await page\n    .getByTestId(\"ConnectionServer.add\")\n    .waitFor({ state: \"visible\", timeout: 60e3 });\n\n  /** TODO: Delete creds and re-try with smart setup */\n  // const localDataDir =\n  //   (await electronApp?.evaluate(({ app }) => app.getPath(\"userData\"))) || \"\";\n  // rmSync(path.resolve(`${localDataDir}/.prostgles-desktop-config.json`));\n  // await page.reload();\n\n  await screenshot();\n  await page.getByTestId(\"Connection.openConnection\").click();\n  await screenshot();\n  await page\n    .getByTestId(\"dashboard.goToConnConfig\")\n    .waitFor({ state: \"visible\", timeout: 5e3 });\n  await screenshot();\n\n  await page.getByTestId(\"dashboard.goToConnections\").click();\n  await createDatabase(\"sample_db\", page);\n  await screenshot();\n  await page.waitForTimeout(1000);\n  await page\n    .getByTestId(\"dashboard.goToConnConfig\")\n    .waitFor({ state: \"visible\", timeout: 10e3 });\n  await screenshot();\n  await page.waitForTimeout(1000);\n  await page.getByTestId(\"dashboard.goToConnections\").click();\n  await createDatabase(\"crypto\", page, true);\n  await screenshot();\n  await page.waitForTimeout(5e3);\n  await screenshot();\n  await page.waitForTimeout(5e3);\n  await screenshot();\n  await page.getByTestId(\"dashboard.goToConnConfig\").click();\n  await screenshot();\n  await page.getByTestId(\"config.bkp\").click();\n  await screenshot();\n  await page.getByTestId(\"config.bkp.create\").click();\n  await screenshot();\n  await page.getByTestId(\"config.bkp.create.start\").click();\n  await page.waitForTimeout(3e3);\n  await screenshot();\n  await page\n    .getByTestId(\"BackupControls.Restore\")\n    .waitFor({ state: \"visible\", timeout: 2e3 });\n  console.log(\"electronApp\", !!electronApp);\n  // await browser.close();\n  // await page.close();\n  // passed = true;\n});\n\nconst MINUTE = 60e3;\nexport const createDatabase = async (\n  dbName: string,\n  page: Page,\n  fromTemplates = false,\n  owner?: { name: string; pass: string },\n) => {\n  await page.locator(`[data-command=\"ConnectionServer.add\"]`).first().click();\n  // await page.waitForTimeout(3000);\n  if (fromTemplates) {\n    await page.getByTestId(\"ConnectionServer.add.newDatabase\").click();\n    await page.getByTestId(\"ConnectionServer.SampleSchemas\").click();\n    await page\n      .getByTestId(\"ConnectionServer.SampleSchemas\")\n      .locator(`[data-key=${JSON.stringify(dbName)}]`)\n      .click();\n  } else {\n    await page.getByTestId(\"ConnectionServer.add.newDatabase\").click();\n    await page\n      .getByTestId(\"ConnectionServer.NewDbName\")\n      .locator(\"input\")\n      .fill(dbName);\n  }\n  if (owner) {\n    await page.getByTestId(\"ConnectionServer.withNewOwnerToggle\").click();\n    await page.waitForTimeout(500);\n    await page\n      .getByTestId(\"ConnectionServer.NewUserName\")\n      .locator(\"input\")\n      .fill(owner.name);\n    await page\n      .getByTestId(\"ConnectionServer.NewUserPassword\")\n      .locator(\"input\")\n      .fill(owner.pass);\n    await page.waitForTimeout(500);\n  }\n  await page.getByTestId(\"ConnectionServer.add.confirm\").click();\n  /* Wait until db is created */\n  const databaseCreationTime = (fromTemplates ? 4 : 1) * MINUTE;\n  await page\n    .getByTestId(\"ConnectionServer.add.confirm\")\n    .waitFor({ state: \"detached\", timeout: databaseCreationTime });\n};\n"
  },
  {
    "path": "electron/e2e-electron/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"Node\",\n    \"sourceMap\": true,\n    \"outDir\": \"../tests-out\",\n    \"strict\": true\n  }\n}\n"
  },
  {
    "path": "electron/forge.config.js",
    "content": "module.exports = {\n  packagerConfig: {\n    icon: \"icon512.png\",\n  },\n  electronPackagerConfig: {\n    icon: \"icon512.png\",\n  },\n  rebuildConfig: {},\n  makers: [\n    {\n      name: \"@electron-forge/maker-dmg\",\n      config: {\n        icon: \"image.ico\",\n      },\n    },\n    {\n      name: \"@electron-forge/maker-deb\",\n      config: {\n        options: {\n          icon: \"icon512.png\",\n          homepage: \"https://prostgles.com/desktop\",\n        },\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "electron/getProtocolHandler.ts",
    "content": "import type { App } from \"electron\";\n\n/**\n * Disabled due to issue with text/html file association\n * https://github.com/electron/electron/issues/20382\n */\nexport const getProtocolHandler = ({\n  app,\n  expressApp,\n}: {\n  app: App;\n  expressApp: any;\n}) => {\n  const protocolName = \"prostgles-desktop\";\n  app.setAsDefaultProtocolClient(protocolName);\n\n  const onOpenedProtocol = (url: string) => {\n    console.log(\"onOpenedProtocol\", { url });\n    expressApp.setProstglesToken(url);\n  };\n\n  const onSecondWindow = (commandLine: string[]) => {\n    /** Gets triggered if app is running and a protocol url was clicked */\n    const protocolUrl = commandLine.find((v) =>\n      v.startsWith(protocolName + \"://\"),\n    );\n    if (protocolUrl) {\n      onOpenedProtocol(protocolUrl);\n    }\n  };\n\n  const setOpenUrlListener = () => {\n    /** Gets triggered if app is NOT running and a protocol url was clicked */\n    app.on(\"open-url\", function (event, data) {\n      event.preventDefault();\n      onOpenedProtocol(data);\n    });\n  };\n\n  return { onOpenedProtocol, onSecondWindow, setOpenUrlListener };\n};\n"
  },
  {
    "path": "electron/images/generate_from_svg.sh",
    "content": "# apt install inkscape\n# apt install imagemagick\n\n# -b FFFFFF used for white bg\ninkscape -w 512 -h 512 prostgles-logo-rounded.svg -o ./rounded/icon512.png\ninkscape -w 512 -h 512 prostgles-logo-rounded.svg -o ./rounded/512x512.png\ninkscape -w 256 -h 256 prostgles-logo-rounded.svg -o ./rounded/256x256.png\ninkscape -w 64 -h 64 prostgles-logo-rounded.svg   -o ./rounded/64x64.png\n# convert prostgles-logo-rounded.svg -define icon:auto-resize=256 ./rounded/icon256.ico\n# convert icon512rounded.png -define icon:auto-resize=32 ./rounded/favicon.ico\nconvert -define 'icon:auto-resize=16,24,32,64,128,256' prostgles-logo-rounded.svg ./rounded/icon.ico\n\n\n"
  },
  {
    "path": "electron/images/loading-effect.css",
    "content": "#prostgles-logo #eye {\n  -webkit-animation-name: prostgles-logo-animation;\n  -webkit-animation-duration: 5s;\n  -webkit-animation-timing-function: ease-in-out;\n  -webkit-animation-iteration-count: infinite;\n  -webkit-animation-play-state: running;\n\n  animation-name: prostgles-logo-animation;\n  animation-duration: 5s;\n  animation-timing-function: ease-in-out;\n  animation-iteration-count: infinite;\n  animation-play-state: running;\n}\n\n@-webkit-keyframes prostgles-logo-animation {\n  0% {\n    fill: #57a9e0;\n  }\n  25.0% {\n    fill: #a3a3a3;\n  }\n  50.0% {\n    fill: #f52424;\n  }\n  100.0% {\n    fill: initial;\n  }\n}\n\n@keyframes prostgles-logo-animation {\n  0% {\n    fill: #57a9e0;\n  }\n  25.0% {\n    fill: #a3a3a3;\n  }\n  50.0% {\n    fill: #f52424;\n  }\n  100.0% {\n    fill: initial;\n  }\n}\n"
  },
  {
    "path": "electron/loadingHTML.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\n\nconst svgLogo = fs.readFileSync(\n  path.join(__dirname, \"/../images/prostgles-logo-rounded.svg\"),\n  { encoding: \"utf-8\" },\n);\nconst logoWithoutFirstLine = svgLogo\n  .split(\"\\n\")\n  .slice(1)\n  .join(\"\\n\")\n  .replace(`height=\"512\"`, \"\")\n  .replace(`width=\"512\"`, \"\");\n\nconst cssAnimation = fs.readFileSync(\n  path.join(__dirname, \"/../images/loading-effect.css\"),\n  { encoding: \"utf-8\" },\n);\n\nconst htmlToDataUrl = (html: string) =>\n  \"data:text/html;charset=UTF-8,\" + encodeURIComponent(html);\n\nexport const loadingHTML = htmlToDataUrl(`\n<!DOCTYPE html>\n<html lang=\"en\" class=\"o-hidden\">\n  <head>\n\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1\" />\n    <meta\n      name=\"description\"\n      content=\" \"\n    />\n    \n    <title>Prostgles Desktop</title>\n    <style>\n      html, body {\n        width: 100%;\n        height: 100%;\n        text-align: center;\n        display: flex;\n        flex: 1;\n        display: flex;\n        flex: 1;\n        height: 100%;\n        overflow: hidden;\n        align-items: center;\n        justify-content: center;\n      }\n\n      ${cssAnimation}\n    </style>\n  </head>\n  <body>\n    <main id=\"root\" style=\"width: 100px; height: 100px;\">\n      ${logoWithoutFirstLine}\n    </main>\n    \n  </body>\n</html>\n\n`);\n"
  },
  {
    "path": "electron/main.ts",
    "content": "const unhandled = require(\"electron-unhandled\");\nunhandled();\nimport { app, shell, safeStorage as ss, type SafeStorage } from \"electron\";\nimport * as path from \"path\";\nimport {\n  electronSid,\n  focusIfOpen,\n  openLoadingScreen,\n  openProstglesApp,\n} from \"./mainWindow\";\n// import { getProtocolHandler } from \"./getProtocolHandler\";\n\nlet localCreds: any;\n\n/**\n * Safe storage encryption works only with a launched browser (electron.launch without \"--no-sandbox\") and launch without xvfb-run\n * but this does not work within containers\n */\nconst safeStorage: SafeStorageHandles =\n  process.env.TEST_MODE === \"true\" ?\n    ({\n      encryptString: (str: string) => {\n        localCreds = str;\n        console.log(\"encryptString\", { str });\n        return str;\n      },\n      decryptString: (str: string) => {\n        console.log(\"decryptString\", { str, localCreds });\n        return localCreds;\n      },\n    } as unknown as SafeStorageHandles)\n  : ss;\n\ntype SafeStorageHandles = Pick<SafeStorage, \"encryptString\" | \"decryptString\">;\n\ntype StartParams = {\n  safeStorage: SafeStorageHandles;\n  rootDir: string;\n  electronSid: string;\n  openPath: (path: string, isFile?: boolean) => void;\n  onReady: (port: number) => void;\n};\nprocess.env.NODE_ENV = \"production\";\nconst expressApp = require(\"../ui/server/dist/server/src/electronConfig\") as {\n  start: (params: StartParams) => Promise<{ destroy: () => Promise<void> }>;\n};\n\n// const protocolHandler = getProtocolHandler({\n//   app,\n//   expressApp,\n// });\n\nconst gotTheLock = app.requestSingleInstanceLock();\nif (!gotTheLock) {\n  console.log(\"Electron multi instance not allowed. Exiting...\");\n  app.quit();\n} else {\n  app.on(\"second-instance\", (event, commandLine, workingDirectory) => {\n    focusIfOpen();\n  });\n\n  initApp();\n}\n\nlet onDestroy: (() => Promise<void>) | undefined;\nfunction initApp() {\n  // This method will be called when Electron has finished\n  // initialization and is ready to create browser windows.\n  // Some APIs can only be used after this event occurs.\n  console.log(\"Initialising electron app...\");\n\n  app.whenReady().then(async () => {\n    await openLoadingScreen();\n\n    console.log(\"Electron app ready, starting express server...\");\n    console.log(\n      \"State db auth file: \" +\n        path.resolve(\n          `${app.getPath(\"userData\")}/.prostgles-desktop-config.json`,\n        ),\n    );\n\n    const hooks = await expressApp\n      .start({\n        safeStorage,\n        rootDir: app.getPath(\"userData\"),\n        electronSid,\n        openPath: (path: string, isFile?: boolean) => {\n          // Show the given file in a file manager. If possible, select the file.\n          if (isFile) {\n            shell.showItemInFolder(path);\n          } else {\n            shell.openPath(path);\n          }\n        },\n        onReady: (actualPort: number) => {\n          console.log(\"Express server started on port \" + actualPort);\n          openProstglesApp(actualPort);\n        },\n      })\n      .catch((err: any) => {\n        console.error(\"Failed to start expressApp.start\", err);\n      });\n    onDestroy = hooks?.destroy;\n\n    app.on(\"before-quit\", async (e) => {\n      onDestroy?.();\n    });\n  });\n\n  // Quit when all windows are closed, except on macOS. There, it's common\n  // for applications and their menu bar to stay active until the user quits\n  // explicitly with Cmd + Q.\n  app.on(\"window-all-closed\", function () {\n    if (process.platform !== \"darwin\") {\n      app.quit();\n    }\n  });\n\n  /* protocolHandler.setOpenUrlListener(); */\n}\n"
  },
  {
    "path": "electron/mainWindow.ts",
    "content": "import * as crypto from \"crypto\";\nimport * as path from \"path\";\nimport { BrowserWindow, screen, shell, Tray, nativeImage, app } from \"electron\";\nimport { loadingHTML } from \"./loadingHTML\";\nimport { setContextMenu } from \"./setContextMenu\";\n\nconst iconPath = path.join(__dirname, \"/../images/icon.ico\");\n\n/* createSessionSecret */\nconst electronSid = crypto.randomBytes(48).toString(\"hex\");\n\n/** Limit to single instance */\nlet mainWindow: BrowserWindow;\n\nconst focusIfOpen = () => {\n  // Tried to run a second instance - focus main window.\n  if (mainWindow) {\n    /* protocolHandler.onSecondWindow(commandLine); */\n    if (mainWindow.isMinimized()) mainWindow.restore();\n    mainWindow.focus();\n  }\n};\n\nconst createWindow = () => {\n  if (mainWindow) return mainWindow;\n\n  /** Make sure we open it on primary display */\n  const primaryDisplay = screen.getPrimaryDisplay();\n  const { width, height } = primaryDisplay.workAreaSize;\n  const desiredHeight = 1000;\n  const desiredWidth = 1400;\n  const startupWidth = Math.min(desiredWidth, width);\n  const startupHeight = Math.min(desiredHeight, height);\n  const x = Math.round(primaryDisplay.bounds.x + (width - startupWidth) / 2);\n  const y = Math.round(primaryDisplay.bounds.y + (height - startupHeight) / 2);\n\n  mainWindow = new BrowserWindow({\n    x,\n    y,\n    width: startupWidth,\n    height: startupHeight,\n    icon: iconPath,\n  });\n  setContextMenu(mainWindow);\n  mainWindow.setMenuBarVisibility(false);\n  mainWindow.webContents.setWindowOpenHandler(({ url }) => {\n    // open url in a browser and prevent default\n    shell.openExternal(url);\n    return { action: \"deny\" };\n  });\n\n  return mainWindow;\n};\n\nconst openLoadingScreen = async () => {\n  // let loading = new BrowserWindow({ show: false, frame: false });\n  createWindow();\n\n  /** Show loading screen */\n  try {\n    await mainWindow.loadURL(loadingHTML);\n  } catch (error) {\n    console.error(\"Failed to load loading screen\", error);\n  }\n};\n\nlet mainWindowLoaded: { port: number };\nlet didSetActivate = false;\nconst openProstglesApp = (port: number, delay = 1100) => {\n  if (!port) return;\n  const url = `http://localhost:${port}`;\n\n  createWindow();\n  setTimeout(async () => {\n    if (port !== mainWindowLoaded?.port) {\n      await loadWithRetries(url, port);\n    }\n    mainWindowLoaded = { port };\n  }, delay);\n};\n\nconst setSIDCookies = async (url: string) => {\n  try {\n    const ses = mainWindow.webContents.session;\n    const cookie = { url, name: \"sid_token\", value: electronSid };\n    await ses.clearStorageData({ storages: [\"cookies\"] });\n    await ses.cookies.set(cookie);\n    console.log(\"Setting cookie mainWindow.reload\");\n    mainWindow.reload();\n  } catch (error) {\n    console.error(\"Failed to set cookie: \", error);\n  }\n};\n\n/** \n   * https://github.com/electron/electron/blob/c41b8d536b2d886abbe739374c0a46f99242a894/lib/browser/navigation-controller.ts#L53 \n      In some cases the app crashes for (errno: -3):\n    {\n      errno: -3,\n      code: 'ERR_ABORTED',\n      url: 'http://localhost:43909/'\n    }\n    Trace/breakpoint trap (core dumped)\n  */\nlet tries = 5;\nconst loadWithRetries = async (url: string, port: number): Promise<void> => {\n  try {\n    await setSIDCookies(url);\n    try {\n      await mainWindow.webContents.stop();\n    } catch (error) {\n      console.error(\"Failed to mainWindow.webContents.stop: \", error);\n    }\n    await mainWindow.loadURL(url);\n  } catch (error) {\n    tries--;\n    if (tries > 0) {\n      console.error(\n        `Failed to mainWindow.loadURL: (${tries} tries left)`,\n        error,\n      );\n      return loadWithRetries(url, port);\n    } else {\n      console.error(\"Failed to mainWindow.loadURL: \", error);\n      return;\n    }\n  }\n\n  try {\n    new Tray(nativeImage.createFromPath(iconPath));\n  } catch (error) {\n    console.error(\"Failed to create tray\", error);\n  }\n  if (didSetActivate) return;\n  didSetActivate = true;\n\n  app.on(\"activate\", function () {\n    // macOS keeps apps running after all windows close.\n    // Must recreate window when user clicks dock icon to reactivate the app\n    if (BrowserWindow.getAllWindows().length === 0) {\n      openProstglesApp(port);\n    }\n  });\n};\n\nexport { electronSid, focusIfOpen, openLoadingScreen, openProstglesApp };\n"
  },
  {
    "path": "electron/package.json",
    "content": "{\n  \"name\": \"prostgles-desktop\",\n  \"version\": \"2.2.4\",\n  \"homepage\": \"https://prostgles.com/desktop\",\n  \"description\": \"Realtime Dashboard and SQL editor for PostgreSQL\",\n  \"author\": \"Prostgles LTD  <office@prostgles.com>\",\n  \"projectUrl\": \"https://github.com/prostgles/ui\",\n  \"keywords\": [\n    \"postgres\",\n    \"postgis\",\n    \"sql\",\n    \"PostgreSQL\",\n    \"electron\",\n    \"react\",\n    \"typescript\",\n    \"dashboard\"\n  ],\n  \"main\": \"build/main.js\",\n  \"icon\": \"images/icon.ico\",\n  \"scripts\": {\n    \"dev\": \"DEBUG=* && NODE_OPTIONS='--max-old-space-size=2048' npm i && tsc-watch  --onSuccess \\\"electron --inspect-brk=9228 ./build/main.js \\\"\",\n    \"start\": \"tsc && electron ./build/main.js .\",\n    \"package\": \"electron-forge package\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"make\": \"rm -rf ./out && rm -rf ./packed && npx tsc && rm -rf ./out && electron-forge make\",\n    \"build\": \"DEBUG=* electron-builder -l\",\n    \"build-tsc\": \"tsc\",\n    \"dist\": \"electron-builder\",\n    \"pack\": \"electron-packager . --out ./packed --overwrite true --icon ./image.ico\",\n    \"makew\": \"npx tsc && npm run pack && node win.js\",\n    \"publish\": \"electron-forge publish\",\n    \"build-linux\": \"./build.sh && electron-builder --linux && rm -rf ./dist/linux-unpacked\",\n    \"build-macos\": \"./build.sh && electron-builder --mac && rm -rf ./dist/mac && rm -rf ./dist/.icon-icns\",\n    \"build-win\": \"bash ./build.sh && electron-builder --win && rm -rf ./dist/win-unpacked\",\n    \"test-start\": \"playwright test\",\n    \"test\": \"bash ./test.sh\",\n    \"test-linux\": \"bash ./build.sh && bash ./test-linux.sh\",\n    \"test-macos\": \"bash ./build.sh && bash ./test-linux-macos.sh\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/prostgles/ui/issues/new/choose\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.57.0\",\n    \"electron\": \"^38.3.0\",\n    \"electron-builder\": \"^26.0.12\",\n    \"electron-squirrel-startup\": \"^1.0.1\",\n    \"tsc-watch\": \"^7.1.1\",\n    \"typescript\": \"^5.0.3\"\n  },\n  \"dependencies\": {\n    \"electron-unhandled\": \"^4.0.1\"\n  },\n  \"build\": {\n    \"asar\": false,\n    \"extraMetadata\": {\n      \"main\": \"build/main.js\"\n    },\n    \"productName\": \"Prostgles-Desktop\",\n    \"appId\": \"com.electron.prostgles-desktop\",\n    \"files\": [\n      \"build/**/*\",\n      \"ui/**/*\",\n      \"images/**/*\"\n    ],\n    \"mac\": {\n      \"icon\": \"./images/rounded/icon512.png\",\n      \"artifactName\": \"${productName}-${version}.${ext}\",\n      \"category\": \"public.app-category.productivity\",\n      \"target\": [\n        {\n          \"target\": \"dmg\",\n          \"arch\": [\n            \"universal\"\n          ]\n        }\n      ]\n    },\n    \"linux\": {\n      \"artifactName\": \"${productName}-${version}-${arch}.${ext}\",\n      \"executableName\": \"prostgles-desktop\",\n      \"icon\": \"./images/rounded/\",\n      \"maintainer\": \"prostgles\",\n      \"category\": \"Development,IDE\",\n      \"target\": [\n        \"deb\",\n        \"AppImage\",\n        \"rpm\"\n      ],\n      \"desktop\": {\n        \"entry\": {\n          \"Terminal\": \"false\"\n        }\n      }\n    },\n    \"win\": {\n      \"artifactName\": \"${productName}-${version}.${ext}\",\n      \"icon\": \"./images/icon.ico\",\n      \"target\": [\n        \"nsis\"\n      ]\n    },\n    \"nsis\": {\n      \"oneClick\": false,\n      \"perMachine\": true,\n      \"allowToChangeInstallationDirectory\": true,\n      \"uninstallDisplayName\": \"${productName}\"\n    }\n  }\n}\n"
  },
  {
    "path": "electron/playwright.config.ts",
    "content": "import { defineConfig } from \"@playwright/test\";\n\nconst timeoutMinutes = 2;\nexport default defineConfig({\n  timeout: timeoutMinutes * 60e3,\n  testDir: \"./e2e-electron\",\n  maxFailures: 0,\n  workers: 1,\n  reporter: \"list\",\n  retries: 0,\n  use: {\n    /** https://github.com/microsoft/playwright/issues/27048 */\n    trace: \"off\",\n    video: \"off\",\n    testIdAttribute: \"data-command\",\n  },\n});\n"
  },
  {
    "path": "electron/screenCapture.bat",
    "content": "// 2>nul||@goto :batch\n/*\n:batch\n@echo off\nsetlocal\n:: Source https://raw.githubusercontent.com/npocmaka/batch.scripts/master/hybrids/.net/c/screenCapture.bat\n:: find csc.exe\nset \"csc=\"\nfor /r \"%SystemRoot%\\Microsoft.NET\\Framework\\\" %%# in (\"*csc.exe\") do  set \"csc=%%#\"\n\nif not exist \"%csc%\" (\n   echo no .net framework installed\n   exit /b 10\n)\n\nif not exist \"%~n0.exe\" (\n   call %csc% /nologo /r:\"Microsoft.VisualBasic.dll\" /out:\"%~n0.exe\" \"%~dpsfnx0\" || (\n      exit /b %errorlevel% \n   )\n)\n%~n0.exe %*\nendlocal & exit /b %errorlevel%\n\n*/\n\n// reference  \n// https://gallery.technet.microsoft.com/scriptcenter/eeff544a-f690-4f6b-a586-11eea6fc5eb8\n\nusing System;\nusing System.Runtime.InteropServices;\nusing System.Drawing;\nusing System.Drawing.Imaging;\nusing System.Collections.Generic;\nusing Microsoft.VisualBasic;\n\n\n/// Provides functions to capture the entire screen, or a particular window, and save it to a file. \n\npublic class ScreenCapture\n{\n\n    /// Creates an Image object containing a screen shot the active window \n\n    public Image CaptureActiveWindow()\n    {\n        return CaptureWindow(User32.GetForegroundWindow());\n    }\n\n    /// Creates an Image object containing a screen shot of the entire desktop \n\n    public Image CaptureScreen()\n    {\n        return CaptureWindow(User32.GetDesktopWindow());\n    }\n\n    /// Creates an Image object containing a screen shot of a specific window \n\n    private Image CaptureWindow(IntPtr handle)\n    {\n        // get te hDC of the target window \n        IntPtr hdcSrc = User32.GetWindowDC(handle);\n        // get the size \n        User32.RECT windowRect = new User32.RECT();\n        User32.GetWindowRect(handle, ref windowRect);\n        int width = windowRect.right - windowRect.left;\n        int height = windowRect.bottom - windowRect.top;\n        // create a device context we can copy to \n        IntPtr hdcDest = GDI32.CreateCompatibleDC(hdcSrc);\n        // create a bitmap we can copy it to, \n        // using GetDeviceCaps to get the width/height \n        IntPtr hBitmap = GDI32.CreateCompatibleBitmap(hdcSrc, width, height);\n        // select the bitmap object \n        IntPtr hOld = GDI32.SelectObject(hdcDest, hBitmap);\n        // bitblt over \n        GDI32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, GDI32.SRCCOPY);\n        // restore selection \n        GDI32.SelectObject(hdcDest, hOld);\n        // clean up \n        GDI32.DeleteDC(hdcDest);\n        User32.ReleaseDC(handle, hdcSrc);\n        // get a .NET image object for it \n        Image img = Image.FromHbitmap(hBitmap);\n        // free up the Bitmap object \n        GDI32.DeleteObject(hBitmap);\n        return img;\n    }\n\n    public void CaptureActiveWindowToFile(string filename, ImageFormat format)\n    {\n        Image img = CaptureActiveWindow();\n        img.Save(filename, format);\n    }\n\n    public void CaptureScreenToFile(string filename, ImageFormat format)\n    {\n        Image img = CaptureScreen();\n        img.Save(filename, format);\n    }\n\n    static bool fullscreen = true;\n    static String file = \"screenshot.bmp\";\n    static System.Drawing.Imaging.ImageFormat format = System.Drawing.Imaging.ImageFormat.Bmp;\n    static String windowTitle = \"\";\n\n    static void parseArguments()\n    {\n        String[] arguments = Environment.GetCommandLineArgs();\n        if (arguments.Length == 1)\n        {\n            printHelp();\n            Environment.Exit(0);\n        }\n        if (arguments[1].ToLower().Equals(\"/h\") || arguments[1].ToLower().Equals(\"/help\"))\n        {\n            printHelp();\n            Environment.Exit(0);\n        }\n\n        file = arguments[1];\n        Dictionary<String, System.Drawing.Imaging.ImageFormat> formats =\n        new Dictionary<String, System.Drawing.Imaging.ImageFormat>();\n\n        formats.Add(\"bmp\", System.Drawing.Imaging.ImageFormat.Bmp);\n        formats.Add(\"emf\", System.Drawing.Imaging.ImageFormat.Emf);\n        formats.Add(\"exif\", System.Drawing.Imaging.ImageFormat.Exif);\n        formats.Add(\"jpg\", System.Drawing.Imaging.ImageFormat.Jpeg);\n        formats.Add(\"jpeg\", System.Drawing.Imaging.ImageFormat.Jpeg);\n        formats.Add(\"gif\", System.Drawing.Imaging.ImageFormat.Gif);\n        formats.Add(\"png\", System.Drawing.Imaging.ImageFormat.Png);\n        formats.Add(\"tiff\", System.Drawing.Imaging.ImageFormat.Tiff);\n        formats.Add(\"wmf\", System.Drawing.Imaging.ImageFormat.Wmf);\n\n\n        String ext = \"\";\n        if (file.LastIndexOf('.') > -1)\n        {\n            ext = file.ToLower().Substring(file.LastIndexOf('.') + 1, file.Length - file.LastIndexOf('.') - 1);\n        }\n        else\n        {\n            Console.WriteLine(\"Invalid file name - no extension\");\n            Environment.Exit(7);\n        }\n\n        try\n        {\n            format = formats[ext];\n        }\n        catch (Exception e)\n        {\n            Console.WriteLine(\"Probably wrong file format:\" + ext);\n            Console.WriteLine(e.ToString());\n            Environment.Exit(8);\n        }\n\n\n        if (arguments.Length > 2)\n        {\n            windowTitle = arguments[2];\n            fullscreen = false;\n        }\n\n    }\n\n    static void printHelp()\n    {\n        //clears the extension from the script name\n        String scriptName = Environment.GetCommandLineArgs()[0];\n        scriptName = scriptName.Substring(0, scriptName.Length);\n        Console.WriteLine(scriptName + \" captures the screen or the active window and saves it to a file.\");\n        Console.WriteLine(\"\");\n        Console.WriteLine(\"Usage:\");\n        Console.WriteLine(\" \" + scriptName + \" filename  [WindowTitle]\");\n        Console.WriteLine(\"\");\n        Console.WriteLine(\"filename - the file where the screen capture will be saved\");\n        Console.WriteLine(\"     allowed file extensions are - Bmp,Emf,Exif,Gif,Icon,Jpeg,Png,Tiff,Wmf.\");\n        Console.WriteLine(\"WindowTitle - instead of capture whole screen you can point to a window \");\n        Console.WriteLine(\"     with a title which will put on focus and captuted.\");\n        Console.WriteLine(\"     For WindowTitle you can pass only the first few characters.\");\n        Console.WriteLine(\"     If don't want to change the current active window pass only \\\"\\\"\");\n    }\n\n    public static void Main()\n    {\n        User32.SetProcessDPIAware();\n        \n        parseArguments();\n        ScreenCapture sc = new ScreenCapture();\n        if (!fullscreen && !windowTitle.Equals(\"\"))\n        {\n            try\n            {\n\n                Interaction.AppActivate(windowTitle);\n                Console.WriteLine(\"setting \" + windowTitle + \" on focus\");\n            }\n            catch (Exception e)\n            {\n                Console.WriteLine(\"Probably there's no window like \" + windowTitle);\n                Console.WriteLine(e.ToString());\n                Environment.Exit(9);\n            }\n\n\n        }\n        try\n        {\n            if (fullscreen)\n            {\n                Console.WriteLine(\"Taking a capture of the whole screen to \" + file);\n                sc.CaptureScreenToFile(file, format);\n            }\n            else\n            {\n                Console.WriteLine(\"Taking a capture of the active window to \" + file);\n                sc.CaptureActiveWindowToFile(file, format);\n            }\n        }\n        catch (Exception e)\n        {\n            Console.WriteLine(\"Check if file path is valid \" + file);\n            Console.WriteLine(e.ToString());\n        }\n    }\n\n    /// Helper class containing Gdi32 API functions \n\n    private class GDI32\n    {\n\n        public const int SRCCOPY = 0x00CC0020; // BitBlt dwRop parameter \n        [DllImport(\"gdi32.dll\")]\n        public static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest,\n          int nWidth, int nHeight, IntPtr hObjectSource,\n          int nXSrc, int nYSrc, int dwRop);\n        [DllImport(\"gdi32.dll\")]\n        public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int nWidth,\n          int nHeight);\n        [DllImport(\"gdi32.dll\")]\n        public static extern IntPtr CreateCompatibleDC(IntPtr hDC);\n        [DllImport(\"gdi32.dll\")]\n        public static extern bool DeleteDC(IntPtr hDC);\n        [DllImport(\"gdi32.dll\")]\n        public static extern bool DeleteObject(IntPtr hObject);\n        [DllImport(\"gdi32.dll\")]\n        public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);\n    }\n\n\n    /// Helper class containing User32 API functions \n\n    private class User32\n    {\n        [StructLayout(LayoutKind.Sequential)]\n        public struct RECT\n        {\n            public int left;\n            public int top;\n            public int right;\n            public int bottom;\n        }\n        [DllImport(\"user32.dll\")]\n        public static extern IntPtr GetDesktopWindow();\n        [DllImport(\"user32.dll\")]\n        public static extern IntPtr GetWindowDC(IntPtr hWnd);\n        [DllImport(\"user32.dll\")]\n        public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);\n        [DllImport(\"user32.dll\")]\n        public static extern IntPtr GetWindowRect(IntPtr hWnd, ref RECT rect);\n        [DllImport(\"user32.dll\")]\n        public static extern IntPtr GetForegroundWindow();\n        [DllImport(\"user32.dll\")]\n        public static extern int SetProcessDPIAware();\n    }\n}\n"
  },
  {
    "path": "electron/setContextMenu.ts",
    "content": "import { Menu, type BrowserWindow } from \"electron\";\nexport const setContextMenu = (mainWindow: BrowserWindow) => {\n  mainWindow.webContents.on(\n    \"context-menu\",\n    (\n      _event,\n      { misspelledWord, dictionarySuggestions, isEditable, selectionText },\n    ) => {\n      const spellCheckMenuItems: Electron.MenuItemConstructorOptions[] = [];\n\n      // Add each spelling suggestion\n      for (const suggestion of dictionarySuggestions) {\n        spellCheckMenuItems.push({\n          label: suggestion,\n          click: () => mainWindow.webContents.replaceMisspelling(suggestion),\n        });\n      }\n\n      // Allow users to add the misspelled word to the dictionary\n      if (misspelledWord) {\n        spellCheckMenuItems.push({\n          label: \"Add to dictionary\",\n          click: () =>\n            mainWindow.webContents.session.addWordToSpellCheckerDictionary(\n              misspelledWord,\n            ),\n        });\n      }\n\n      const menu = Menu.buildFromTemplate([\n        { role: \"copy\", enabled: Boolean(selectionText) },\n        { role: \"cut\", enabled: Boolean(selectionText) && isEditable },\n        { role: \"paste\", enabled: isEditable },\n        { role: \"selectAll\", enabled: isEditable },\n        ...(isEditable ? [{ role: \"toggleSpellChecker\" } as const] : []),\n        { role: \"toggleDevTools\" },\n        ...spellCheckMenuItems,\n      ]);\n      menu.popup();\n    },\n  );\n};\n"
  },
  {
    "path": "electron/start.sh",
    "content": "#!/bin/bash\n\ncd ui/server\n\n# git stash && git pull && npm i && npm run build\nnpm i && npm run build\n\ncd -\n\nnpm start"
  },
  {
    "path": "electron/test-linux-macos.sh",
    "content": "TEST_MODE=true npm run test-start"
  },
  {
    "path": "electron/test-linux.sh",
    "content": "# LIBGL_ALWAYS_INDIRECT=1 DISPLAY=:0 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \nTEST_MODE=true DEBUG=pw:browser* xvfb-run --auto-servernum --server-args=\"-screen 0 1920x1080x24\" -- npx playwright test"
  },
  {
    "path": "electron/test-setup.js",
    "content": "const start = require(\"./ui/server/dist/server/src/electronConfig\");\n\nlet safeStorage = {\n  encryptString: (v) => Buffer.from(v),\n  decryptString: (v) => v.toString(),\n};\n\nstart.start(safeStorage, 3099);\n"
  },
  {
    "path": "electron/test-w-debug.sh",
    "content": "DEBUG=pw:browser* npm run test-start"
  },
  {
    "path": "electron/test.sh",
    "content": "TEST_MODE=true LIBGL_ALWAYS_INDIRECT=1 DISPLAY=:0 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 DEBUG=pw:browser xvfb-run --auto-servernum --server-args=\"-screen 0 1280x960x24\" -- npm run test-start"
  },
  {
    "path": "electron/tsconfig.json",
    "content": "{\n  \"files\": [\"./main.ts\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2017\", \"es2019\", \"ES2021.String\", \"dom\"],\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"allowJs\": true,\n    \"module\": \"commonjs\",\n    \"sourceMap\": true,\n    // \"rootDir\": \".\",\n    \"moduleResolution\": \"node\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"ignoreDeprecations\": \"5.0\",\n    \"strict\": true,\n    \"outDir\": \"build\",\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true //excludes node_modules\n  },\n  \"exclude\": [\"ui\", \"*.conf\", \"node_modules\"]\n}\n"
  },
  {
    "path": "electron/win-inno-setup.ts",
    "content": "import * as fs from \"fs\";\nimport pkg from \"./package.json\";\n\nexport const make = () => {\n  const packed = `${__dirname}\\\\..\\\\packed`;\n  const folders = fs.readdirSync(packed);\n  const packedFolder = folders.find((d) => d.includes(\"prostgles-desktop\"));\n  if (packedFolder) {\n    const buildDir = `${packed}\\\\${packedFolder}`;\n\n    /** Move all packed root folders to Data */\n    const dataDir = `${buildDir}/Data`;\n    fs.mkdirSync(dataDir);\n\n    const otherFolders = fs.readdirSync(buildDir, { withFileTypes: true });\n    otherFolders.forEach((f) => {\n      if (f.isDirectory() && f.name !== \"Data\") {\n        fs.renameSync(`${buildDir}/${f.name}/`, `${dataDir}/${f.name}/`);\n      }\n    });\n\n    const buildDirFiles = fs.readdirSync(buildDir, { withFileTypes: true });\n    fs.writeFileSync(\n      `${packed}\\\\inno.iss`,\n      getInnoConfig(buildDirFiles, buildDir),\n      { encoding: \"utf8\" },\n    );\n  }\n};\n\nfunction getInnoConfig(rootFiles: fs.Dirent[], rootLocationWin: string) {\n  return `\n\n#define MyAppName \"Prostgles Desktop\"\n#define MyAppVersion ${JSON.stringify(pkg.version)}\n#define MyAppPublisher ${JSON.stringify(pkg.author)}\n#define MyAppURL ${JSON.stringify(pkg.homepage)}\n#define MyAppExeName ${JSON.stringify(pkg.name + \".exe\")}\n#define MyAppSetupName ${JSON.stringify([pkg.name, pkg.version, \"amd64\", \"setup\"].join(\"_\"))}\n\n#define MyAppAssocName MyAppName + \" File\"\n#define MyAppAssocExt \".sql\"\n#define MyAppAssocKey StringChange(MyAppAssocName, \" \", \"\") + MyAppAssocExt\n\n[Setup]\n; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.\n; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)\nAppId={{AF473B76-6E5B-4A5C-9BB9-87B808EFA451}\nAppName={#MyAppName}\nAppVersion={#MyAppVersion}\n;AppVerName={#MyAppName} {#MyAppVersion}\nAppPublisher={#MyAppPublisher}\nAppPublisherURL={#MyAppURL}\nAppSupportURL={#MyAppURL}\nAppUpdatesURL={#MyAppURL}\nDefaultDirName={autopf}\\\\{#MyAppName}\nChangesAssociations=yes\nDisableProgramGroupPage=yes\n; Uncomment the following line to run in non administrative install mode (install for current user only.)\n;PrivilegesRequired=lowest\n\nOutputBaseFilename={#MyAppSetupName}\nCompression=lzma\nSolidCompression=yes\nWizardStyle=modern\n\n[Languages]\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked\n\n[Files]\nSource: \"${rootLocationWin}\\\\{#MyAppExeName}\"; DestDir: \"{app}\"; Flags: ignoreversion\n${rootFiles\n  .filter((f) => !f.name.endsWith(\".exe\") && f.name !== \"Data\")\n  .map((f) => {\n    return `Source: \"${rootLocationWin}\\\\${f.name}\"; DestDir: \"{app}\"; Flags: ignoreversion`;\n  })\n  .join(\" \\n\")}\nSource: \"${rootLocationWin}\\\\Data\\\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs ; Permissions: everyone-full\n; NOTE: Don't use \"Flags: ignoreversion\" on any shared system files\n\n\n[Registry]\nRoot: HKA; Subkey: \"Software\\\\Classes\\\\{#MyAppAssocExt}\\\\OpenWithProgids\"; ValueType: string; ValueName: \"{#MyAppAssocKey}\"; ValueData: \"\"; Flags: uninsdeletevalue\nRoot: HKA; Subkey: \"Software\\\\Classes\\\\{#MyAppAssocKey}\"; ValueType: string; ValueName: \"\"; ValueData: \"{#MyAppAssocName}\"; Flags: uninsdeletekey\nRoot: HKA; Subkey: \"Software\\\\Classes\\\\{#MyAppAssocKey}\\\\DefaultIcon\"; ValueType: string; ValueName: \"\"; ValueData: \"{app}\\\\{#MyAppExeName},0\"\nRoot: HKA; Subkey: \"Software\\\\Classes\\\\{#MyAppAssocKey}\\\\shell\\\\open\\\\command\"; ValueType: string; ValueName: \"\"; ValueData: \"\"\"{app}\\\\{#MyAppExeName}\"\" \"\"%1\"\"\"\nRoot: HKA; Subkey: \"Software\\\\Classes\\\\Applications\\\\{#MyAppExeName}\\\\SupportedTypes\"; ValueType: string; ValueName: \".sql\"; ValueData: \"\"\n\n[Icons]\nName: \"{autoprograms}\\\\{#MyAppName}\"; Filename: \"{app}\\\\{#MyAppExeName}\"\nName: \"{autodesktop}\\\\{#MyAppName}\"; Filename: \"{app}\\\\{#MyAppExeName}\"; Tasks: desktopicon\n\n[Run]\nFilename: \"{app}\\\\{#MyAppExeName}\"; Description: \"{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}\"; Flags: nowait postinstall skipifsilent\n\n`;\n}\n"
  },
  {
    "path": "electron/win.js",
    "content": "const { make } = require(\"./dist/win-inno-setup\");\n\nmake();\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"prostgles-ui\",\n  \"private\": true,\n  \"version\": \"2.2.4\",\n  \"homepage\": \"https://prostgles.com/ui\",\n  \"description\": \"Realtime Dashboard and SQL editor for PostgreSQL\",\n  \"author\": \"Prostgles LTD <office@prostgles.com>\",\n  \"projectUrl\": \"https://github.com/prostgles/ui\",\n  \"keywords\": [\n    \"postgres\",\n    \"postgis\",\n    \"sql\",\n    \"PostgreSQL\",\n    \"electron\",\n    \"react\",\n    \"typescript\",\n    \"dashboard\"\n  ],\n  \"scripts\": {\n    \"dev\": \"bash ./scripts/start-dev.sh\",\n    \"dev:electron\": \"bash ./scripts/start-dev-electron.sh\",\n    \"start\": \"bash ./scripts/start-prod.sh\",\n    \"test\": \"bash ./scripts/test.sh\",\n    \"demo:record\": \"bash ./scripts/demo/record-local.sh\"\n  },\n  \"devDependencies\": {\n    \"prettier\": \"^3.4.2\",\n    \"typescript\": \"^5.8.3\"\n  }\n}\n"
  },
  {
    "path": "releases/v1.0.0.md",
    "content": "Prostgles UI v1.0.0\n\nWorkspaces\nEach database connection now allows multiple workspaces. You can create and switch between different dashboards\n\n"
  },
  {
    "path": "releases/v2.0.0.md",
    "content": "Prostgles UI v2\n\nImproved UI\n- Referenced tables tab\n- SQL code blocks\n- SQL snippets\n\nAccess control\n- Role-based access rules\n- API\n\nFile storage\n- Saved locally or to S3\n- File type and size rules\n\nBackup and Restore\n- Automatic or manual\n- Saved locally or to S3\n\nSecurity\n- App based 2 factor-authentication\n- IP subnet rules\n- CORS\n\n\n"
  },
  {
    "path": "releases/v2.2.2.md",
    "content": "- Schema Diagram\n  - Link colouring modes to better understand related tables and foreign key properties\n  - Filtering by relationship type, schema\n- Command and settings quick search (Ctrl+K). Access any command, setting, or action through our new command palette\n- AI Assistant:\n  - Data access controls. Allow the assistant to execute sql with rollback, commit or specify allowed tables and commands for each chat\n  - Dashboard Generation: Create dashboards from your database schema and requirements\n  - Create task mode: Get suggested tools and data access permissions for the chat\n  - MCP Server Tools support: Install and allow the assistant to access Model Context Protocol server tools.\n  - File upload support\n  - Execute sql snippets directly in chat\n  - Real-time cost tracking per chat and configurable spending limits\n- Improved setup for Prostgles Desktop: \"Quick setup\" mode handles the creation of a state database and user.\n"
  },
  {
    "path": "releases/v2.2.3.md",
    "content": "- Docker MCP bug fixes\n"
  },
  {
    "path": "releases/v2.2.4.md",
    "content": "- Parallel tool call bug fix\n"
  },
  {
    "path": "scripts/demo/SCREEN RECORD.txt",
    "content": "\n# For SQL: set viewport width to 630px, disable minimap, remove top navbar\n# Must check to ensure x11 is enabled:\necho $XDG_SESSION_TYPE\n\n# SLQ Demo\nffmpeg -y -f x11grab  -s 630x700  -i :0.0+0,190 -vcodec libx264 -vf format=yuv420p  demo_sql.mp4\n\n# Backups (delete both headers, click on create backup then start)\nffmpeg -y -f x11grab  -s 630x860  -i :0.0+0,160 -vcodec libx264 -vf format=yuv420p  demo_backups.mp4\n\n# Access control (delete both headers, click on create backup then start)\nffmpeg -y -f x11grab  -s 630x860  -i :0.0+0,160 -vcodec libx264 -vf format=yuv420p  demo_access_control.mp4\n\nffmpeg -y -f x11grab  -s 630x860  -i :0.0+0,160 -vcodec libx264 -vf format=yuv420p  demo_query_stats.mp4\nffmpeg -y -f x11grab  -s 630x860  -i :0.0+0,160 -vcodec libx264 -vf format=yuv420p  demo_api.mp4\nffmpeg -y -f x11grab  -s 630x860  -i :0.0+0,160 -vcodec libx264 -vf format=yuv420p  demo_api.mp4\n\n\n\n# For All else\nffmpeg -y -f x11grab  -s 570x460  -i :0.0+0,240 -vcodec libx264 -vf format=yuv420p  create.mp4\n\n"
  },
  {
    "path": "scripts/demo/record-local.sh",
    "content": "cd e2e\nPW_TEST_HTML_REPORT_OPEN='never' npx playwright test demo_video.spec.ts --headed &\nffmpeg \\\n  -video_size 1280x1080 \\\n  -framerate 24 \\\n  -f x11grab -i :0.0+3275,+118 \\\n  -pix_fmt yuv444p \\\n  -c:v libx264rgb \\\n  -crf 0 \\\n  -preset ultrafast \\\n  ./demo/video.mp4 \\\n  -y\n\n"
  },
  {
    "path": "scripts/demo/split-video.sh",
    "content": "ffmpeg -ss 00:00:30 -i *.webm -t 00:00:09 ./clips/1_context_suggestions.webm -y\nffmpeg -ss 00:00:40 -i *.webm -t 00:00:06 ./clips/2_execution_options.webm -y\nffmpeg -ss 00:01:10 -i *.webm -t 00:00:10 ./clips/3_doc_and_schema_extracts.webm -y\nffmpeg -ss 00:01:46 -i *.webm -t 00:00:05 ./clips/4_user_access_details.webm -y\n\n\n"
  },
  {
    "path": "scripts/get_version.sh",
    "content": "#!/bin/bash\n\nfile_path=\"${1:-./package.json}\"\nversion=$(grep -m1 '\"version\":' \"$file_path\" | cut -d '\"' -f4)\n\nif [ -z \"$version\" ]; then\n  echo \"Version not found in $file_path\"\n  exit 1\nfi\n\necho \"$version\""
  },
  {
    "path": "scripts/release.sh",
    "content": "#!/bin/bash\nset -e\n\nversion=$(./scripts/get_version.sh)\ntag=\"v$version\"\n\necho \"Releasing version $tag\"\ngit tag -a \"$tag\" -m \"Prostgles UI release $tag\"\ngit push origin \"$tag\""
  },
  {
    "path": "scripts/start-dev-electron.sh",
    "content": "#!/bin/bash\n\nset -e\n\n# Create a process group\n# Use PGID to kill all processes in this group when script exits\ntrap 'kill 0' EXIT\n\ncd server \nnpm run dev:electron &\n\ncd ../client \nrm -rf ./build\nnpm run dev"
  },
  {
    "path": "scripts/start-dev.sh",
    "content": "#!/bin/bash\n\nset -e\n\n# Create a process group\n# Use PGID to kill all processes in this group when script exits\ntrap 'kill 0' EXIT\n\n# Install root dev extension tools\nnpm i --no-audit\n\ncd server \nnpm run dev &\n\ncd ../client \nrm -rf ./build\nnpm run dev"
  },
  {
    "path": "scripts/start-prod.sh",
    "content": "cd ./client\nrm -rf ./build\nnpm run build\ncd ../server \nnpm start"
  },
  {
    "path": "scripts/test.sh",
    "content": "set -e\n\n# Compile TS to Ensure any errors are caught\ncd client\nnpm i\nnpx tsc\n\ncd ../server\nnpm i\nnpx tsc\nnpm test\n\ncd ..\n\necho \">>> Running e2e tests\"\n\nrm -f ./client/configs/last_compiled.txt\nPRGL_TEST=true npm run dev &\nSTART_SCRIPT_PID=$!\n\n# Ensure the process is killed even if the script exits early (e.g., due to set -e or Ctrl+C)\ntrap 'echo \">>> Cleaning up processes\"; kill -9 $START_SCRIPT_PID 2>/dev/null' EXIT\n\nuntil [ -f ./client/configs/last_compiled.txt ]\ndo\n  sleep 1\ndone\necho \"UI Compiled\"\nsleep 3\ncd e2e && npm test \n"
  },
  {
    "path": "server/.gitignore",
    "content": "node_modules\nprostgles_media\nprostgles_storage\nprostgles_backups\nprostgles_certificates\n.electron-auth.json\n**/.electron-auth.json\n.prostgles-desktop-config.json\n**/.prostgles-desktop-config.json"
  },
  {
    "path": "server/.vscode/settings.json",
    "content": "{\n  \"typescript.tsdk\": \"node_modules/typescript/lib\", \n}\n"
  },
  {
    "path": "server/eslint.config.mjs",
    "content": "import pluginSecurity from \"eslint-plugin-security\";\nimport eslint from \"@eslint/js\";\nimport tseslint from \"typescript-eslint\";\nimport { defineConfig } from \"eslint/config\";\n\nexport default defineConfig(\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n  pluginSecurity.configs.recommended,\n  eslint.configs.recommended,\n  tseslint.configs.recommendedTypeChecked,\n  {\n    ignores: [\n      \"node_modules\",\n      \"dist\",\n      \"examples\",\n      \"**/*.d.ts\",\n      \"tests\",\n      \"docs\",\n      \"*.mjs\",\n      \"sample_schemas\",\n      \"**/*.d.ts\",\n      \"**/*.js\",\n    ],\n  },\n  {\n    languageOptions: {\n      parserOptions: {\n        projectService: {\n          allowDefaultProject: [\"*.js\", \"*.mjs\"],\n        },\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n  },\n  {\n    files: [\"**/*.js\", \"**/*.ts\"],\n    rules: {\n      \"no-cond-assign\": \"error\",\n      \"@typescript-eslint/no-namespace\": \"off\",\n      \"@typescript-eslint/no-explicit-any\": \"off\",\n      \"@typescript-eslint/no-non-null-assertion\": \"off\",\n      \"@typescript-eslint/ban-types\": \"off\",\n      \"@typescript-eslint/ban-ts-comment\": \"off\",\n      \"@typescript-eslint/no-unused-expressions\": \"off\",\n      \"@typescript-eslint/no-require-imports\": \"off\",\n      \"@typescript-eslint/no-empty-object-type\": \"off\",\n      \"no-async-promise-executor\": \"off\",\n      \"@typescript-eslint/no-var-requires\": \"off\",\n      \"@typescript-eslint/no-unnecessary-condition\": \"error\",\n      \"@typescript-eslint/no-floating-promises\": \"error\",\n      \"no-unused-vars\": \"off\",\n      \"no-empty\": \"off\",\n      \"security/detect-object-injection\": \"off\",\n      \"security/detect-non-literal-fs-filename\": \"off\",\n      \"@typescript-eslint/only-throw-error\": \"off\",\n      \"@typescript-eslint/prefer-promise-reject-errors\": \"off\",\n      \"@typescript-eslint/restrict-template-expressions\": \"warn\",\n      // \"@typescript-eslint/no-misused-promises\": [\n      //   \"warn\",\n      //   { checksVoidReturn: false },\n      // ],\n      \"@typescript-eslint/no-unsafe-assignment\": \"warn\",\n      \"@typescript-eslint/no-unsafe-argument\": \"warn\",\n      \"@typescript-eslint/no-unsafe-return\": \"warn\",\n      \"@typescript-eslint/await-thenable\": \"warn\",\n      \"@typescript-eslint/no-unsafe-member-access\": \"warn\",\n      \"@typescript-eslint/no-unsafe-call\": \"warn\",\n      \"@typescript-eslint/no-unused-vars\": [\n        \"warn\",\n        {\n          argsIgnorePattern: \"^_\",\n          varsIgnorePattern: \"^_\",\n          caughtErrorsIgnorePattern: \"^_\",\n        },\n      ],\n    },\n  },\n);\n"
  },
  {
    "path": "server/licenses.json",
    "content": "{\n  \"@aws-crypto/crc32@2.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-crypto/crc32\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-crypto/crc32/LICENSE\"\n  },\n  \"@aws-crypto/ie11-detection@2.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-crypto/ie11-detection\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-crypto/ie11-detection/LICENSE\"\n  },\n  \"@aws-crypto/sha256-browser@2.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-crypto/sha256-browser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-crypto/sha256-browser/LICENSE\"\n  },\n  \"@aws-crypto/sha256-js@2.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-crypto/sha256-js\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-crypto/sha256-js/LICENSE\"\n  },\n  \"@aws-crypto/supports-web-crypto@2.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-crypto/supports-web-crypto\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-crypto/supports-web-crypto/LICENSE\"\n  },\n  \"@aws-crypto/util@2.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-crypto/util\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-crypto/util/LICENSE\"\n  },\n  \"@aws-sdk/abort-controller@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/abort-controller\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/abort-controller/LICENSE\"\n  },\n  \"@aws-sdk/chunked-blob-reader-native@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/chunked-blob-reader-native\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/chunked-blob-reader-native/LICENSE\"\n  },\n  \"@aws-sdk/chunked-blob-reader@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/chunked-blob-reader\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/chunked-blob-reader/LICENSE\"\n  },\n  \"@aws-sdk/client-s3@3.42.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/client-s3\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/client-s3/LICENSE\"\n  },\n  \"@aws-sdk/client-sso@3.41.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/client-sso\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/client-sso/LICENSE\"\n  },\n  \"@aws-sdk/client-sts@3.42.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/client-sts\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/client-sts/LICENSE\"\n  },\n  \"@aws-sdk/config-resolver@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/config-resolver\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/config-resolver/LICENSE\"\n  },\n  \"@aws-sdk/credential-provider-env@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-env\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-env/LICENSE\"\n  },\n  \"@aws-sdk/credential-provider-imds@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-imds\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-imds/LICENSE\"\n  },\n  \"@aws-sdk/credential-provider-ini@3.41.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-ini\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-ini/LICENSE\"\n  },\n  \"@aws-sdk/credential-provider-node@3.41.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-node/LICENSE\"\n  },\n  \"@aws-sdk/credential-provider-process@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-process\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-process/LICENSE\"\n  },\n  \"@aws-sdk/credential-provider-sso@3.41.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-sso\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-sso/LICENSE\"\n  },\n  \"@aws-sdk/credential-provider-web-identity@3.41.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-web-identity\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/credential-provider-web-identity/LICENSE\"\n  },\n  \"@aws-sdk/eventstream-marshaller@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-marshaller\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-marshaller/LICENSE\"\n  },\n  \"@aws-sdk/eventstream-serde-browser@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-serde-browser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-serde-browser/LICENSE\"\n  },\n  \"@aws-sdk/eventstream-serde-config-resolver@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-serde-config-resolver\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-serde-config-resolver/LICENSE\"\n  },\n  \"@aws-sdk/eventstream-serde-node@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-serde-node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-serde-node/LICENSE\"\n  },\n  \"@aws-sdk/eventstream-serde-universal@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-serde-universal\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/eventstream-serde-universal/LICENSE\"\n  },\n  \"@aws-sdk/fetch-http-handler@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/fetch-http-handler\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/fetch-http-handler/LICENSE\"\n  },\n  \"@aws-sdk/hash-blob-browser@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/hash-blob-browser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/hash-blob-browser/LICENSE\"\n  },\n  \"@aws-sdk/hash-node@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/hash-node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/hash-node/LICENSE\"\n  },\n  \"@aws-sdk/hash-stream-node@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/hash-stream-node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/hash-stream-node/LICENSE\"\n  },\n  \"@aws-sdk/invalid-dependency@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/invalid-dependency\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/invalid-dependency/LICENSE\"\n  },\n  \"@aws-sdk/is-array-buffer@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/is-array-buffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/is-array-buffer/LICENSE\"\n  },\n  \"@aws-sdk/md5-js@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/md5-js\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/md5-js/LICENSE\"\n  },\n  \"@aws-sdk/middleware-apply-body-checksum@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-apply-body-checksum\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-apply-body-checksum/LICENSE\"\n  },\n  \"@aws-sdk/middleware-bucket-endpoint@3.41.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-bucket-endpoint\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-bucket-endpoint/LICENSE\"\n  },\n  \"@aws-sdk/middleware-content-length@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-content-length\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-content-length/LICENSE\"\n  },\n  \"@aws-sdk/middleware-expect-continue@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-expect-continue\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-expect-continue/LICENSE\"\n  },\n  \"@aws-sdk/middleware-header-default@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-header-default\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-header-default/LICENSE\"\n  },\n  \"@aws-sdk/middleware-host-header@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-host-header\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-host-header/LICENSE\"\n  },\n  \"@aws-sdk/middleware-location-constraint@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-location-constraint\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-location-constraint/LICENSE\"\n  },\n  \"@aws-sdk/middleware-logger@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-logger\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-logger/LICENSE\"\n  },\n  \"@aws-sdk/middleware-retry@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-retry\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-retry/LICENSE\"\n  },\n  \"@aws-sdk/middleware-sdk-s3@3.41.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-sdk-s3\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-sdk-s3/LICENSE\"\n  },\n  \"@aws-sdk/middleware-sdk-sts@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-sdk-sts\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-sdk-sts/LICENSE\"\n  },\n  \"@aws-sdk/middleware-serde@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-serde\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-serde/LICENSE\"\n  },\n  \"@aws-sdk/middleware-signing@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-signing\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-signing/LICENSE\"\n  },\n  \"@aws-sdk/middleware-ssec@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-ssec\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-ssec/LICENSE\"\n  },\n  \"@aws-sdk/middleware-stack@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-stack\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-stack/LICENSE\"\n  },\n  \"@aws-sdk/middleware-user-agent@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-user-agent\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/middleware-user-agent/LICENSE\"\n  },\n  \"@aws-sdk/node-config-provider@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/node-config-provider\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/node-config-provider/LICENSE\"\n  },\n  \"@aws-sdk/node-http-handler@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/node-http-handler\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/node-http-handler/LICENSE\"\n  },\n  \"@aws-sdk/property-provider@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/property-provider\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/property-provider/LICENSE\"\n  },\n  \"@aws-sdk/protocol-http@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/protocol-http\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/protocol-http/LICENSE\"\n  },\n  \"@aws-sdk/querystring-builder@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/querystring-builder\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/querystring-builder/LICENSE\"\n  },\n  \"@aws-sdk/querystring-parser@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/querystring-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/querystring-parser/LICENSE\"\n  },\n  \"@aws-sdk/service-error-classification@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/service-error-classification\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/service-error-classification/LICENSE\"\n  },\n  \"@aws-sdk/shared-ini-file-loader@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/shared-ini-file-loader\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/shared-ini-file-loader/LICENSE\"\n  },\n  \"@aws-sdk/signature-v4@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/signature-v4\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/signature-v4/LICENSE\"\n  },\n  \"@aws-sdk/smithy-client@3.41.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/smithy-client\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/smithy-client/LICENSE\"\n  },\n  \"@aws-sdk/types@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/types\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/types/LICENSE\"\n  },\n  \"@aws-sdk/url-parser@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/url-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/url-parser/LICENSE\"\n  },\n  \"@aws-sdk/util-arn-parser@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-arn-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-arn-parser/LICENSE\"\n  },\n  \"@aws-sdk/util-base64-browser@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-base64-browser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-base64-browser/LICENSE\"\n  },\n  \"@aws-sdk/util-base64-node@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-base64-node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-base64-node/LICENSE\"\n  },\n  \"@aws-sdk/util-body-length-browser@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-body-length-browser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-body-length-browser/LICENSE\"\n  },\n  \"@aws-sdk/util-body-length-node@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-body-length-node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-body-length-node/LICENSE\"\n  },\n  \"@aws-sdk/util-buffer-from@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-buffer-from\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-buffer-from/LICENSE\"\n  },\n  \"@aws-sdk/util-config-provider@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-config-provider\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-config-provider/LICENSE\"\n  },\n  \"@aws-sdk/util-credentials@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-credentials\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-credentials/LICENSE\"\n  },\n  \"@aws-sdk/util-hex-encoding@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-hex-encoding\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-hex-encoding/LICENSE\"\n  },\n  \"@aws-sdk/util-locate-window@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-locate-window\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-locate-window/LICENSE\"\n  },\n  \"@aws-sdk/util-uri-escape@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-uri-escape\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-uri-escape/LICENSE\"\n  },\n  \"@aws-sdk/util-user-agent-browser@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-user-agent-browser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-user-agent-browser/LICENSE\"\n  },\n  \"@aws-sdk/util-user-agent-node@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-user-agent-node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-user-agent-node/LICENSE\"\n  },\n  \"@aws-sdk/util-utf8-browser@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-utf8-browser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-utf8-browser/LICENSE\"\n  },\n  \"@aws-sdk/util-utf8-node@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-utf8-node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-utf8-node/LICENSE\"\n  },\n  \"@aws-sdk/util-waiter@3.40.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-waiter\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/util-waiter/LICENSE\"\n  },\n  \"@aws-sdk/xml-builder@3.37.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-sdk/xml-builder\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-sdk/xml-builder/LICENSE\"\n  },\n  \"@gar/promisify@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/wraithgar/gar-promisify\",\n    \"publisher\": \"Gar\",\n    \"email\": \"gar+npm@danger.computer\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@gar/promisify\"\n  },\n  \"@isaacs/string-locale-compare@1.1.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/string-locale-compare\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@isaacs/string-locale-compare\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@isaacs/string-locale-compare/LICENSE\"\n  },\n  \"@npmcli/arborist@4.0.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/arborist\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/arborist\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/arborist/LICENSE.md\"\n  },\n  \"@npmcli/ci-detect@1.4.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/ci-detect\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/ci-detect\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/ci-detect/LICENSE\"\n  },\n  \"@npmcli/config@2.3.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/config\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/config\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/config/LICENSE\"\n  },\n  \"@npmcli/disparity-colors@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/disparity-colors\",\n    \"publisher\": \"npm Inc.\",\n    \"email\": \"support@npmjs.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/disparity-colors\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/disparity-colors/LICENSE\"\n  },\n  \"@npmcli/fs@1.0.0\": {\n    \"licenses\": \"ISC\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/fs\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/fs/LICENSE.md\"\n  },\n  \"@npmcli/git@2.1.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/git\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/git\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/git/LICENSE\"\n  },\n  \"@npmcli/installed-package-contents@1.0.7\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/installed-package-contents\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/installed-package-contents\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/installed-package-contents/LICENSE\"\n  },\n  \"@npmcli/map-workspaces@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/map-workspaces\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/map-workspaces\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/map-workspaces/LICENSE.md\"\n  },\n  \"@npmcli/metavuln-calculator@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/metavuln-calculator\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/metavuln-calculator\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/metavuln-calculator/LICENSE\"\n  },\n  \"@npmcli/move-file@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/npm/move-file\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/move-file\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/move-file/LICENSE.md\"\n  },\n  \"@npmcli/name-from-folder@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/name-from-folder\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/name-from-folder\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/name-from-folder/LICENSE\"\n  },\n  \"@npmcli/node-gyp@1.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/node-gyp\",\n    \"publisher\": \"Brian Jenkins\",\n    \"email\": \"bonkydog@bonkydog.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/node-gyp\"\n  },\n  \"@npmcli/package-json@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/package-json\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/package-json/LICENSE\"\n  },\n  \"@npmcli/promise-spawn@1.3.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/promise-spawn\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/promise-spawn\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/promise-spawn/LICENSE\"\n  },\n  \"@npmcli/run-script@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/run-script\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/run-script\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/@npmcli/run-script/LICENSE\"\n  },\n  \"@tokenizer/token@0.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/tokenizer-token\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/home/uiprgl/server/node_modules/@tokenizer/token\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@tokenizer/token/README.md\"\n  },\n  \"@tootallnate/once@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/once\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/@tootallnate/once\"\n  },\n  \"@types/body-parser@1.19.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/body-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/body-parser/LICENSE\"\n  },\n  \"@types/component-emitter@1.2.10\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/component-emitter\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/component-emitter/LICENSE\"\n  },\n  \"@types/connect@3.4.34\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/connect\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/connect/LICENSE\"\n  },\n  \"@types/cookie-parser@1.4.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/cookie-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/cookie-parser/LICENSE\"\n  },\n  \"@types/cookie@0.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/cookie\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/cookie/LICENSE\"\n  },\n  \"@types/cors@2.8.10\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/cors\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/cors/LICENSE\"\n  },\n  \"@types/express-serve-static-core@4.17.19\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/express-serve-static-core\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/express-serve-static-core/LICENSE\"\n  },\n  \"@types/express@4.17.11\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/express\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/express/LICENSE\"\n  },\n  \"@types/mime@1.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/mime\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/mime/LICENSE\"\n  },\n  \"@types/node@14.14.41\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/node\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/node/LICENSE\"\n  },\n  \"@types/qs@6.9.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/qs\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/qs/LICENSE\"\n  },\n  \"@types/range-parser@1.2.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/range-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/range-parser/LICENSE\"\n  },\n  \"@types/serve-static@1.13.9\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/serve-static\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/serve-static/LICENSE\"\n  },\n  \"@types/socket.io@3.0.2\": {\n    \"licenses\": \"MIT\",\n    \"path\": \"/home/uiprgl/server/node_modules/@types/socket.io\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@types/socket.io/LICENSE\"\n  },\n  \"abbrev@1.1.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/abbrev-js\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/abbrev\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/abbrev/LICENSE\"\n  },\n  \"accepts@1.3.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/accepts\",\n    \"path\": \"/home/uiprgl/server/node_modules/accepts\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/accepts/LICENSE\"\n  },\n  \"agent-base@6.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/node-agent-base\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/agent-base\"\n  },\n  \"agentkeepalive@4.1.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/node-modules/agentkeepalive\",\n    \"publisher\": \"fengmk2\",\n    \"email\": \"fengmk2@gmail.com\",\n    \"url\": \"https://fengmk2.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/agentkeepalive\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/agentkeepalive/LICENSE\"\n  },\n  \"aggregate-error@3.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/aggregate-error\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/aggregate-error\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/aggregate-error/license\"\n  },\n  \"ansi-regex@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ansi-regex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ansi-regex/license\"\n  },\n  \"ansi-regex@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/string-width/node_modules/ansi-regex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/string-width/node_modules/ansi-regex/license\"\n  },\n  \"ansi-regex@5.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-table3/node_modules/ansi-regex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-table3/node_modules/ansi-regex/license\"\n  },\n  \"ansi-regex@5.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/node_modules/ansi-regex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/node_modules/ansi-regex/license\"\n  },\n  \"ansi-styles@4.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-styles\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ansi-styles\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ansi-styles/license\"\n  },\n  \"ansicolors@0.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/thlorenz/ansicolors\",\n    \"publisher\": \"Thorsten Lorenz\",\n    \"email\": \"thlorenz@gmx.de\",\n    \"url\": \"thlorenz.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ansicolors\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ansicolors/LICENSE\"\n  },\n  \"ansistyles@0.1.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/thlorenz/ansistyles\",\n    \"publisher\": \"Thorsten Lorenz\",\n    \"email\": \"thlorenz@gmx.de\",\n    \"url\": \"thlorenz.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ansistyles\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ansistyles/LICENSE\"\n  },\n  \"aproba@1.2.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/aproba\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/aproba\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/aproba/LICENSE\"\n  },\n  \"aproba@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/aproba\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/aproba\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/aproba/LICENSE\"\n  },\n  \"archy@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/node-archy\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/archy\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/archy/LICENSE\"\n  },\n  \"are-we-there-yet@1.1.6\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/are-we-there-yet\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/are-we-there-yet\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/are-we-there-yet/LICENSE.md\"\n  },\n  \"are-we-there-yet@1.1.7\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/are-we-there-yet\",\n    \"publisher\": \"Rebecca Turner\",\n    \"url\": \"http://re-becca.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/are-we-there-yet\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/are-we-there-yet/LICENSE\"\n  },\n  \"are-we-there-yet@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/are-we-there-yet\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npmlog/node_modules/are-we-there-yet\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npmlog/node_modules/are-we-there-yet/LICENSE.md\"\n  },\n  \"array-flatten@1.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/blakeembrey/array-flatten\",\n    \"publisher\": \"Blake Embrey\",\n    \"email\": \"hello@blakeembrey.com\",\n    \"url\": \"http://blakeembrey.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/array-flatten\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/array-flatten/LICENSE\"\n  },\n  \"asap@2.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kriskowal/asap\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/asap\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/asap/LICENSE.md\"\n  },\n  \"assert-options@0.7.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vitaly-t/assert-options\",\n    \"publisher\": \"Vitaly Tomilov\",\n    \"email\": \"vitaly.tomilov@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/assert-options\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/assert-options/README.md\"\n  },\n  \"aws-sdk@2.1035.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js\",\n    \"publisher\": \"Amazon Web Services\",\n    \"url\": \"https://aws.amazon.com/\",\n    \"path\": \"/home/uiprgl/server/node_modules/aws-sdk\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/aws-sdk/LICENSE.txt\",\n    \"noticeFile\": \"/home/uiprgl/server/node_modules/aws-sdk/NOTICE.txt\"\n  },\n  \"balanced-match@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/balanced-match\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/balanced-match\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/balanced-match/LICENSE.md\"\n  },\n  \"base64-arraybuffer@0.1.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/niklasvh/base64-arraybuffer\",\n    \"publisher\": \"Niklas von Hertzen\",\n    \"email\": \"niklasvh@gmail.com\",\n    \"url\": \"http://hertzen.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/base64-arraybuffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/base64-arraybuffer/LICENSE-MIT\"\n  },\n  \"base64-js@1.5.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/beatgammit/base64-js\",\n    \"publisher\": \"T. Jameson Little\",\n    \"email\": \"t.jameson.little@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/base64-js\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/base64-js/LICENSE\"\n  },\n  \"base64id@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/faeldt/base64id\",\n    \"publisher\": \"Kristian Faeldt\",\n    \"email\": \"faeldt_kristian@cyberagent.co.jp\",\n    \"path\": \"/home/uiprgl/server/node_modules/base64id\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/base64id/LICENSE\"\n  },\n  \"bin-links@2.3.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/bin-links\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/bin-links\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/bin-links/LICENSE\"\n  },\n  \"binary-extensions@2.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/binary-extensions\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/binary-extensions\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/binary-extensions/license\"\n  },\n  \"bl@4.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/rvagg/bl\",\n    \"path\": \"/home/uiprgl/server/node_modules/bl\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/bl/LICENSE.md\"\n  },\n  \"bluebird@3.7.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/petkaantonov/bluebird\",\n    \"publisher\": \"Petka Antonov\",\n    \"email\": \"petka_antonov@hotmail.com\",\n    \"url\": \"http://github.com/petkaantonov/\",\n    \"path\": \"/home/uiprgl/server/node_modules/bluebird\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/bluebird/LICENSE\"\n  },\n  \"body-parser@1.19.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/expressjs/body-parser\",\n    \"path\": \"/home/uiprgl/server/node_modules/body-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/body-parser/LICENSE\"\n  },\n  \"bowser@2.11.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lancedikson/bowser\",\n    \"publisher\": \"Dustin Diaz\",\n    \"email\": \"dustin@dustindiaz.com\",\n    \"url\": \"http://dustindiaz.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/bowser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/bowser/LICENSE\"\n  },\n  \"brace-expansion@1.1.11\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/brace-expansion\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/brace-expansion\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/brace-expansion/LICENSE\"\n  },\n  \"buffer-writer@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianc/node-buffer-writer\",\n    \"publisher\": \"Brian M. Carlson\",\n    \"path\": \"/home/uiprgl/server/node_modules/buffer-writer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/buffer-writer/LICENSE\"\n  },\n  \"buffer@4.9.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"http://feross.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/buffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/buffer/LICENSE\"\n  },\n  \"buffer@5.7.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/bl/node_modules/buffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/bl/node_modules/buffer/LICENSE\"\n  },\n  \"builtins@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/builtins\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/builtins\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/builtins/License\"\n  },\n  \"bytes@3.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/visionmedia/bytes.js\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"url\": \"http://tjholowaychuk.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/bytes\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/bytes/LICENSE\"\n  },\n  \"cacache@15.3.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/cacache\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cacache\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cacache/LICENSE.md\"\n  },\n  \"chalk@4.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/chalk\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/chalk\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/chalk/license\"\n  },\n  \"chownr@1.1.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/chownr\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/chownr\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/chownr/LICENSE\"\n  },\n  \"chownr@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/chownr\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/chownr\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/chownr/LICENSE\"\n  },\n  \"cidr-regex@3.1.1\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/silverwind/cidr-regex\",\n    \"publisher\": \"silverwind\",\n    \"email\": \"me@silverwind.io\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cidr-regex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cidr-regex/LICENSE\"\n  },\n  \"clean-stack@2.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/clean-stack\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/clean-stack\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/clean-stack/license\"\n  },\n  \"cli-columns@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/shannonmoeller/cli-columns\",\n    \"publisher\": \"Shannon Moeller\",\n    \"email\": \"me@shannonmoeller\",\n    \"url\": \"http://shannonmoeller.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/LICENSE\"\n  },\n  \"cli-table3@0.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/cli-table/cli-table3\",\n    \"publisher\": \"James Talmage\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-table3\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-table3/LICENSE\"\n  },\n  \"clone@1.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/pvorb/node-clone\",\n    \"publisher\": \"Paul Vorbach\",\n    \"email\": \"paul@vorba.ch\",\n    \"url\": \"http://paul.vorba.ch/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/clone\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/clone/LICENSE\"\n  },\n  \"cmd-shim@4.1.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/cmd-shim\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cmd-shim\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cmd-shim/LICENSE\"\n  },\n  \"code-point-at@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/code-point-at\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/code-point-at\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/code-point-at/license\"\n  },\n  \"color-convert@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Qix-/color-convert\",\n    \"publisher\": \"Heather Arthur\",\n    \"email\": \"fayearthur@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/color-convert\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/color-convert/LICENSE\"\n  },\n  \"color-name@1.1.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/colorjs/color-name\",\n    \"publisher\": \"DY\",\n    \"email\": \"dfcreative@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/color-name\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/color-name/LICENSE\"\n  },\n  \"color-string@1.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Qix-/color-string\",\n    \"publisher\": \"Heather Arthur\",\n    \"email\": \"fayearthur@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/color-string\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/color-string/LICENSE\"\n  },\n  \"color-support@1.1.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/color-support\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/color-support\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/color-support/LICENSE\"\n  },\n  \"color@4.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Qix-/color\",\n    \"path\": \"/home/uiprgl/server/node_modules/color\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/color/LICENSE\"\n  },\n  \"colors@1.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Marak/colors.js\",\n    \"publisher\": \"Marak Squires\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/colors\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/colors/LICENSE\"\n  },\n  \"columnify@1.5.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/timoxley/columnify\",\n    \"publisher\": \"Tim Oxley\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/columnify\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/columnify/LICENSE\"\n  },\n  \"common-ancestor-path@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/common-ancestor-path\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/common-ancestor-path\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/common-ancestor-path/LICENSE\"\n  },\n  \"component-emitter@1.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/component/emitter\",\n    \"path\": \"/home/uiprgl/server/node_modules/component-emitter\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/component-emitter/LICENSE\"\n  },\n  \"concat-map@0.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/node-concat-map\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/concat-map\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/concat-map/LICENSE\"\n  },\n  \"console-control-strings@1.1.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/console-control-strings\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/console-control-strings\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/console-control-strings/LICENSE\"\n  },\n  \"content-disposition@0.5.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/content-disposition\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/content-disposition\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/content-disposition/LICENSE\"\n  },\n  \"content-type@1.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/content-type\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/content-type\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/content-type/LICENSE\"\n  },\n  \"cookie-parser@1.4.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/expressjs/cookie-parser\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"url\": \"http://tjholowaychuk.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/cookie-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/cookie-parser/LICENSE\"\n  },\n  \"cookie-signature@1.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/visionmedia/node-cookie-signature\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@learnboost.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/cookie-signature\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/cookie-signature/Readme.md\"\n  },\n  \"cookie@0.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/cookie\",\n    \"publisher\": \"Roman Shtylman\",\n    \"email\": \"shtylman@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/cookie\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/cookie/LICENSE\"\n  },\n  \"cookie@0.4.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/cookie\",\n    \"publisher\": \"Roman Shtylman\",\n    \"email\": \"shtylman@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/engine.io/node_modules/cookie\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/engine.io/node_modules/cookie/LICENSE\"\n  },\n  \"core-util-is@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/isaacs/core-util-is\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/core-util-is\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/core-util-is/LICENSE\"\n  },\n  \"cors@2.8.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/expressjs/cors\",\n    \"publisher\": \"Troy Goode\",\n    \"email\": \"troygoode@gmail.com\",\n    \"url\": \"https://github.com/troygoode/\",\n    \"path\": \"/home/uiprgl/server/node_modules/cors\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/cors/LICENSE\"\n  },\n  \"cross-spawn@7.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/moxystudio/node-cross-spawn\",\n    \"publisher\": \"André Cruz\",\n    \"email\": \"andre@moxy.studio\",\n    \"path\": \"/home/uiprgl/server/node_modules/cross-spawn\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/cross-spawn/LICENSE\"\n  },\n  \"csvtojson@2.0.10\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Keyang/node-csvtojson\",\n    \"publisher\": \"Keyang Xiang\",\n    \"email\": \"keyang.xiang@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/csvtojson\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/csvtojson/LICENSE\"\n  },\n  \"debug@2.6.9\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/visionmedia/debug\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"path\": \"/home/uiprgl/server/node_modules/debug\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/debug/LICENSE\"\n  },\n  \"debug@4.3.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/visionmedia/debug\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"path\": \"/home/uiprgl/server/node_modules/socket.io/node_modules/debug\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/socket.io/node_modules/debug/LICENSE\"\n  },\n  \"debug@4.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/visionmedia/debug\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/debug\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/debug/LICENSE\"\n  },\n  \"debuglog@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sam-github/node-debuglog\",\n    \"publisher\": \"Sam Roberts\",\n    \"email\": \"sam@strongloop.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/debuglog\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/debuglog/LICENSE\"\n  },\n  \"decompress-response@6.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/decompress-response\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/decompress-response\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/decompress-response/license\"\n  },\n  \"deep-extend@0.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/unclechu/node-deep-extend\",\n    \"publisher\": \"Viacheslav Lotsmanov\",\n    \"email\": \"lotsmanov89@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/deep-extend\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/deep-extend/LICENSE\"\n  },\n  \"defaults@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/tmpvar/defaults\",\n    \"publisher\": \"Elijah Insua\",\n    \"email\": \"tmpvar@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/defaults\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/defaults/LICENSE\"\n  },\n  \"delegates@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/visionmedia/node-delegates\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/delegates\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/delegates/License\"\n  },\n  \"depd@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dougwilson/nodejs-depd\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/depd\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/depd/LICENSE\"\n  },\n  \"destroy@1.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/stream-utils/destroy\",\n    \"publisher\": \"Jonathan Ong\",\n    \"email\": \"me@jongleberry.com\",\n    \"url\": \"http://jongleberry.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/destroy\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/destroy/LICENSE\"\n  },\n  \"detect-libc@1.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/lovell/detect-libc\",\n    \"publisher\": \"Lovell Fuller\",\n    \"email\": \"npm@lovell.info\",\n    \"path\": \"/home/uiprgl/server/node_modules/detect-libc\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/detect-libc/LICENSE\"\n  },\n  \"dezalgo@1.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/dezalgo\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/dezalgo\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/dezalgo/LICENSE\"\n  },\n  \"diff@5.0.0\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/kpdecker/jsdiff\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/diff\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/diff/LICENSE\"\n  },\n  \"dotenv@10.0.0\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/motdotla/dotenv\",\n    \"path\": \"/home/uiprgl/server/node_modules/dotenv\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/dotenv/LICENSE\"\n  },\n  \"duplexer@0.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/duplexer\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/duplexer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/duplexer/LICENCE\"\n  },\n  \"ee-first@1.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jonathanong/ee-first\",\n    \"publisher\": \"Jonathan Ong\",\n    \"email\": \"me@jongleberry.com\",\n    \"url\": \"http://jongleberry.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/ee-first\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/ee-first/LICENSE\"\n  },\n  \"emoji-regex@8.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mathiasbynens/emoji-regex\",\n    \"publisher\": \"Mathias Bynens\",\n    \"url\": \"https://mathiasbynens.be/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/emoji-regex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/emoji-regex/LICENSE-MIT.txt\"\n  },\n  \"encodeurl@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/pillarjs/encodeurl\",\n    \"path\": \"/home/uiprgl/server/node_modules/encodeurl\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/encodeurl/LICENSE\"\n  },\n  \"encoding@0.1.13\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/andris9/encoding\",\n    \"publisher\": \"Andris Reinman\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/encoding\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/encoding/LICENSE\"\n  },\n  \"end-of-stream@1.4.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/end-of-stream\",\n    \"publisher\": \"Mathias Buus\",\n    \"email\": \"mathiasbuus@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/end-of-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/end-of-stream/LICENSE\"\n  },\n  \"engine.io-parser@4.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/socketio/engine.io-parser\",\n    \"path\": \"/home/uiprgl/server/node_modules/engine.io-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/engine.io-parser/LICENSE\"\n  },\n  \"engine.io@4.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/socketio/engine.io\",\n    \"publisher\": \"Guillermo Rauch\",\n    \"email\": \"guillermo@learnboost.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/engine.io\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/engine.io/LICENSE\"\n  },\n  \"entities@2.2.0\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/fb55/entities\",\n    \"publisher\": \"Felix Boehm\",\n    \"email\": \"me@feedic.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/entities\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/entities/LICENSE\"\n  },\n  \"env-paths@2.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/env-paths\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/env-paths\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/env-paths/license\"\n  },\n  \"err-code@2.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/IndigoUnited/js-err-code\",\n    \"publisher\": \"IndigoUnited\",\n    \"email\": \"hello@indigounited.com\",\n    \"url\": \"http://indigounited.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/err-code\"\n  },\n  \"escape-html@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/component/escape-html\",\n    \"path\": \"/home/uiprgl/server/node_modules/escape-html\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/escape-html/LICENSE\"\n  },\n  \"etag@1.8.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/etag\",\n    \"path\": \"/home/uiprgl/server/node_modules/etag\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/etag/LICENSE\"\n  },\n  \"event-stream@3.3.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dominictarr/event-stream\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"http://bit.ly/dominictarr\",\n    \"path\": \"/home/uiprgl/server/node_modules/event-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/event-stream/LICENCE\"\n  },\n  \"events@1.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Gozala/events\",\n    \"publisher\": \"Irakli Gozalishvili\",\n    \"email\": \"rfobic@gmail.com\",\n    \"url\": \"http://jeditoolkit.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/events\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/events/LICENSE\"\n  },\n  \"expand-template@2.0.3\": {\n    \"licenses\": \"(MIT OR WTFPL)\",\n    \"repository\": \"https://github.com/ralphtheninja/expand-template\",\n    \"publisher\": \"LM\",\n    \"email\": \"ralphtheninja@riseup.net\",\n    \"path\": \"/home/uiprgl/server/node_modules/expand-template\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/expand-template/LICENSE\"\n  },\n  \"express@4.17.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/expressjs/express\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"path\": \"/home/uiprgl/server/node_modules/express\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/express/LICENSE\"\n  },\n  \"fast-xml-parser@3.19.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/NaturalIntelligence/fast-xml-parser\",\n    \"publisher\": \"Amit Gupta\",\n    \"url\": \"https://amitkumargupta.work/\",\n    \"path\": \"/home/uiprgl/server/node_modules/fast-xml-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/fast-xml-parser/LICENSE\"\n  },\n  \"fastest-levenshtein@1.0.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ka-weihe/fastest-levenshtein\",\n    \"publisher\": \"Kasper U. Weihe\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/fastest-levenshtein\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/fastest-levenshtein/LICENSE.md\"\n  },\n  \"file-type@16.5.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/file-type\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/file-type\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/file-type/license\"\n  },\n  \"finalhandler@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/pillarjs/finalhandler\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/finalhandler\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/finalhandler/LICENSE\"\n  },\n  \"forwarded@0.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/forwarded\",\n    \"path\": \"/home/uiprgl/server/node_modules/forwarded\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/forwarded/LICENSE\"\n  },\n  \"fresh@0.5.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/fresh\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"url\": \"http://tjholowaychuk.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/fresh\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/fresh/LICENSE\"\n  },\n  \"from@0.1.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dominictarr/from\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"dominictarr.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/from\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/from/LICENSE.APACHE2\"\n  },\n  \"fs-constants@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/fs-constants\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/home/uiprgl/server/node_modules/fs-constants\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/fs-constants/LICENSE\"\n  },\n  \"fs-minipass@2.1.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/fs-minipass\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/fs-minipass\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/fs-minipass/LICENSE\"\n  },\n  \"fs.realpath@1.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/fs.realpath\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/fs.realpath\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/fs.realpath/LICENSE\"\n  },\n  \"function-bind@1.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/function-bind\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/function-bind\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/function-bind/LICENSE\"\n  },\n  \"gauge@2.7.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/gauge\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/gauge\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/gauge/LICENSE\"\n  },\n  \"gauge@4.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/gauge\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/gauge\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/gauge/LICENSE.md\"\n  },\n  \"github-from-package@0.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/github-from-package\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/home/uiprgl/server/node_modules/github-from-package\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/github-from-package/LICENSE\"\n  },\n  \"glob@7.2.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-glob\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/glob\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/glob/LICENSE\"\n  },\n  \"graceful-fs@4.2.8\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-graceful-fs\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/graceful-fs\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/graceful-fs/LICENSE\"\n  },\n  \"has-flag@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/has-flag\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/has-flag\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/has-flag/license\"\n  },\n  \"has-unicode@2.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/has-unicode\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/has-unicode\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/has-unicode/LICENSE\"\n  },\n  \"has@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/tarruda/has\",\n    \"publisher\": \"Thiago de Arruda\",\n    \"email\": \"tpadilha84@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/has\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/has/LICENSE-MIT\"\n  },\n  \"hosted-git-info@4.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/hosted-git-info\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/hosted-git-info\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/hosted-git-info/LICENSE\"\n  },\n  \"http-cache-semantics@4.1.0\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/kornelski/http-cache-semantics\",\n    \"publisher\": \"Kornel Lesiński\",\n    \"email\": \"kornel@geekhood.net\",\n    \"url\": \"https://kornel.ski/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/http-cache-semantics\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/http-cache-semantics/LICENSE\"\n  },\n  \"http-errors@1.7.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/http-errors\",\n    \"publisher\": \"Jonathan Ong\",\n    \"email\": \"me@jongleberry.com\",\n    \"url\": \"http://jongleberry.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/http-errors\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/http-errors/LICENSE\"\n  },\n  \"http-proxy-agent@4.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/node-http-proxy-agent\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/http-proxy-agent\"\n  },\n  \"https-proxy-agent@5.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/node-https-proxy-agent\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/https-proxy-agent\"\n  },\n  \"humanize-ms@1.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/node-modules/humanize-ms\",\n    \"publisher\": \"dead-horse\",\n    \"email\": \"dead_horse@qq.com\",\n    \"url\": \"http://deadhorse.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/humanize-ms\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/humanize-ms/LICENSE\"\n  },\n  \"i@0.3.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/pksunkara/inflect\",\n    \"publisher\": \"Pavan Kumar Sunkara\",\n    \"email\": \"pavan.sss1991@gmail.com\",\n    \"url\": \"pksunkara.github.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/i\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/i/LICENSE\"\n  },\n  \"iconv-lite@0.4.24\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ashtuchkin/iconv-lite\",\n    \"publisher\": \"Alexander Shtuchkin\",\n    \"email\": \"ashtuchkin@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/iconv-lite\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/iconv-lite/LICENSE\"\n  },\n  \"iconv-lite@0.6.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ashtuchkin/iconv-lite\",\n    \"publisher\": \"Alexander Shtuchkin\",\n    \"email\": \"ashtuchkin@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/iconv-lite\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/iconv-lite/LICENSE\"\n  },\n  \"ieee754@1.1.13\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/feross/ieee754\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"http://feross.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/ieee754\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/ieee754/LICENSE\"\n  },\n  \"ieee754@1.2.1\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/feross/ieee754\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/token-types/node_modules/ieee754\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/token-types/node_modules/ieee754/LICENSE\"\n  },\n  \"ignore-walk@4.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/ignore-walk\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ignore-walk\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ignore-walk/LICENSE\"\n  },\n  \"imurmurhash@0.1.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jensyt/imurmurhash-js\",\n    \"publisher\": \"Jens Taylor\",\n    \"email\": \"jensyt@gmail.com\",\n    \"url\": \"https://github.com/homebrewing\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/imurmurhash\"\n  },\n  \"indent-string@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/indent-string\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/indent-string\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/indent-string/license\"\n  },\n  \"infer-owner@1.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/infer-owner\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/infer-owner\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/infer-owner/LICENSE\"\n  },\n  \"inflight@1.0.6\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/inflight\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/inflight\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/inflight/LICENSE\"\n  },\n  \"inherits@2.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/inherits\",\n    \"path\": \"/home/uiprgl/server/node_modules/inherits\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/inherits/LICENSE\"\n  },\n  \"inherits@2.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/inherits\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/inherits\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/inherits/LICENSE\"\n  },\n  \"ini@1.3.8\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/ini\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/ini\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/ini/LICENSE\"\n  },\n  \"ini@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/ini\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ini\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ini/LICENSE\"\n  },\n  \"init-package-json@2.0.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/init-package-json\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/init-package-json\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/init-package-json/LICENSE.md\"\n  },\n  \"ip-regex@4.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/ip-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ip-regex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ip-regex/license\"\n  },\n  \"ip@1.1.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/indutny/node-ip\",\n    \"publisher\": \"Fedor Indutny\",\n    \"email\": \"fedor@indutny.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ip\"\n  },\n  \"ipaddr.js@1.9.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/whitequark/ipaddr.js\",\n    \"publisher\": \"whitequark\",\n    \"email\": \"whitequark@whitequark.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/ipaddr.js\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/ipaddr.js/LICENSE\"\n  },\n  \"is-arrayish@0.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/qix-/node-is-arrayish\",\n    \"publisher\": \"Qix\",\n    \"url\": \"http://github.com/qix-\",\n    \"path\": \"/home/uiprgl/server/node_modules/is-arrayish\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/is-arrayish/LICENSE\"\n  },\n  \"is-cidr@4.0.2\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/silverwind/is-cidr\",\n    \"publisher\": \"silverwind\",\n    \"email\": \"me@silverwind.io\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-cidr\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-cidr/LICENSE\"\n  },\n  \"is-core-module@2.7.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/is-core-module\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-core-module\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-core-module/LICENSE\"\n  },\n  \"is-fullwidth-code-point@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/is-fullwidth-code-point\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/is-fullwidth-code-point\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/is-fullwidth-code-point/license\"\n  },\n  \"is-fullwidth-code-point@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/is-fullwidth-code-point\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-fullwidth-code-point\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-fullwidth-code-point/license\"\n  },\n  \"is-fullwidth-code-point@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/is-fullwidth-code-point\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/node_modules/is-fullwidth-code-point\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/node_modules/is-fullwidth-code-point/license\"\n  },\n  \"is-lambda@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/watson/is-lambda\",\n    \"publisher\": \"Thomas Watson Steen\",\n    \"email\": \"w@tson.dk\",\n    \"url\": \"https://twitter.com/wa7son\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-lambda\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-lambda/LICENSE\"\n  },\n  \"is-typedarray@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/hughsk/is-typedarray\",\n    \"publisher\": \"Hugh Kennedy\",\n    \"email\": \"hughskennedy@gmail.com\",\n    \"url\": \"http://hughsk.io/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-typedarray\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/is-typedarray/LICENSE.md\"\n  },\n  \"is-utf8@0.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/wayfind/is-utf8\",\n    \"publisher\": \"wayfind\",\n    \"path\": \"/home/uiprgl/server/node_modules/is-utf8\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/is-utf8/LICENSE\"\n  },\n  \"isarray@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/isarray\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/isarray\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/isarray/README.md\"\n  },\n  \"isexe@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/isexe\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/isexe\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/isexe/LICENSE\"\n  },\n  \"jmespath@0.15.0\": {\n    \"licenses\": \"Apache 2.0\",\n    \"repository\": \"https://github.com/jmespath/jmespath.js\",\n    \"publisher\": \"James Saryerwinnie\",\n    \"email\": \"js@jamesls.com\",\n    \"url\": \"http://jamesls.com/\",\n    \"path\": \"/home/uiprgl/server/node_modules/jmespath\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/jmespath/LICENSE\"\n  },\n  \"json-parse-even-better-errors@2.3.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/npm/json-parse-even-better-errors\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@zkat.tech\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/json-parse-even-better-errors\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/json-parse-even-better-errors/LICENSE.md\"\n  },\n  \"json-stringify-nice@1.1.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/json-stringify-nice\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/json-stringify-nice\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/json-stringify-nice/LICENSE\"\n  },\n  \"jsonparse@1.3.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/creationix/jsonparse\",\n    \"publisher\": \"Tim Caswell\",\n    \"email\": \"tim@creationix.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/jsonparse\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/jsonparse/LICENSE\"\n  },\n  \"just-diff-apply@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/angus-c/just\",\n    \"publisher\": \"Angus Croll\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/just-diff-apply\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/just-diff-apply/LICENSE\"\n  },\n  \"just-diff@3.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/angus-c/just\",\n    \"publisher\": \"Angus Croll\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/just-diff\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/just-diff/LICENSE\"\n  },\n  \"libnpmaccess@4.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmaccess\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@sykosomatic.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmaccess\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmaccess/LICENSE\"\n  },\n  \"libnpmdiff@2.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmdiff\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmdiff\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmdiff/LICENSE\"\n  },\n  \"libnpmexec@3.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmexec\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmexec\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmexec/LICENSE\"\n  },\n  \"libnpmfund@2.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmfund\",\n    \"publisher\": \"npm Inc.\",\n    \"email\": \"support@npmjs.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmfund\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmfund/LICENSE\"\n  },\n  \"libnpmhook@6.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmhook\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@sykosomatic.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmhook\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmhook/LICENSE.md\"\n  },\n  \"libnpmorg@2.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmorg\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@sykosomatic.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmorg\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmorg/LICENSE\"\n  },\n  \"libnpmpack@3.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmpack\",\n    \"publisher\": \"npm Inc.\",\n    \"email\": \"support@npmjs.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmpack\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmpack/LICENSE\"\n  },\n  \"libnpmpublish@4.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmpublish\",\n    \"publisher\": \"npm Inc.\",\n    \"email\": \"support@npmjs.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmpublish\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmpublish/LICENSE\"\n  },\n  \"libnpmsearch@3.1.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmsearch\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@sykosomatic.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmsearch\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmsearch/LICENSE\"\n  },\n  \"libnpmteam@2.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmteam\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@zkat.tech\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmteam\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmteam/LICENSE\"\n  },\n  \"libnpmversion@2.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/libnpmversion\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmversion\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/libnpmversion/LICENSE\"\n  },\n  \"lodash@4.17.21\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/lodash\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/lodash/LICENSE\"\n  },\n  \"lru-cache@6.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-lru-cache\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/lru-cache\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/lru-cache/LICENSE\"\n  },\n  \"make-fetch-happen@9.1.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/make-fetch-happen\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@zkat.tech\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/make-fetch-happen\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/make-fetch-happen/LICENSE\"\n  },\n  \"map-stream@0.1.0\": {\n    \"licenses\": \"Custom: https://github.com/dominictarr/event-stream\",\n    \"repository\": \"https://github.com/dominictarr/map-stream\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"http://dominictarr.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/map-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/map-stream/LICENCE\"\n  },\n  \"media-typer@0.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/media-typer\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/media-typer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/media-typer/LICENSE\"\n  },\n  \"merge-descriptors@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/component/merge-descriptors\",\n    \"publisher\": \"Jonathan Ong\",\n    \"email\": \"me@jongleberry.com\",\n    \"url\": \"http://jongleberry.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/merge-descriptors\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/merge-descriptors/LICENSE\"\n  },\n  \"methods@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/methods\",\n    \"path\": \"/home/uiprgl/server/node_modules/methods\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/methods/LICENSE\"\n  },\n  \"mime-db@1.47.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/mime-db\",\n    \"path\": \"/home/uiprgl/server/node_modules/mime-db\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/mime-db/LICENSE\"\n  },\n  \"mime-types@2.1.30\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/mime-types\",\n    \"path\": \"/home/uiprgl/server/node_modules/mime-types\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/mime-types/LICENSE\"\n  },\n  \"mime@1.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/broofa/node-mime\",\n    \"publisher\": \"Robert Kieffer\",\n    \"email\": \"robert@broofa.com\",\n    \"url\": \"http://github.com/broofa\",\n    \"path\": \"/home/uiprgl/server/node_modules/mime\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/mime/LICENSE\"\n  },\n  \"mimic-response@3.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/mimic-response\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/mimic-response\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/mimic-response/license\"\n  },\n  \"minimatch@3.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minimatch\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minimatch\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minimatch/LICENSE\"\n  },\n  \"minimist@1.2.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/minimist\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/home/uiprgl/server/node_modules/minimist\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/minimist/LICENSE\"\n  },\n  \"minipass-collect@1.0.2\": {\n    \"licenses\": \"ISC\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-collect\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-collect/LICENSE\"\n  },\n  \"minipass-fetch@1.4.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/npm/minipass-fetch\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-fetch\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-fetch/LICENSE\"\n  },\n  \"minipass-flush@1.0.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minipass-flush\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-flush\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-flush/LICENSE\"\n  },\n  \"minipass-json-stream@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/npm/minipass-json-stream\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-json-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-json-stream/LICENSE\"\n  },\n  \"minipass-pipeline@1.2.4\": {\n    \"licenses\": \"ISC\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-pipeline\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-pipeline/LICENSE\"\n  },\n  \"minipass-sized@1.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minipass-sized\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-sized\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass-sized/LICENSE\"\n  },\n  \"minipass@3.1.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minipass\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minipass/LICENSE\"\n  },\n  \"minizlib@2.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/isaacs/minizlib\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/minizlib\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/minizlib/LICENSE\"\n  },\n  \"mkdirp-classic@0.5.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/mkdirp-classic\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/home/uiprgl/server/node_modules/mkdirp-classic\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/mkdirp-classic/LICENSE\"\n  },\n  \"mkdirp-infer-owner@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/mkdirp-infer-owner\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/mkdirp-infer-owner\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/mkdirp-infer-owner/LICENSE\"\n  },\n  \"mkdirp@1.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/isaacs/node-mkdirp\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/mkdirp\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/mkdirp/LICENSE\"\n  },\n  \"monaco-editor@0.29.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/microsoft/monaco-editor\",\n    \"publisher\": \"Microsoft Corporation\",\n    \"path\": \"/home/uiprgl/server/node_modules/monaco-editor\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/monaco-editor/LICENSE\"\n  },\n  \"ms@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/zeit/ms\",\n    \"path\": \"/home/uiprgl/server/node_modules/ms\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/ms/license.md\"\n  },\n  \"ms@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/zeit/ms\",\n    \"path\": \"/home/uiprgl/server/node_modules/send/node_modules/ms\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/send/node_modules/ms/license.md\"\n  },\n  \"ms@2.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/zeit/ms\",\n    \"path\": \"/home/uiprgl/server/node_modules/socket.io/node_modules/ms\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/socket.io/node_modules/ms/license.md\"\n  },\n  \"ms@2.1.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vercel/ms\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ms\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ms/license.md\"\n  },\n  \"mute-stream@0.0.8\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/mute-stream\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/mute-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/mute-stream/LICENSE\"\n  },\n  \"napi-build-utils@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspiredware/napi-build-utils\",\n    \"publisher\": \"Jim Schlight\",\n    \"path\": \"/home/uiprgl/server/node_modules/napi-build-utils\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/napi-build-utils/LICENSE\"\n  },\n  \"negotiator@0.6.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/negotiator\",\n    \"path\": \"/home/uiprgl/server/node_modules/negotiator\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/negotiator/LICENSE\"\n  },\n  \"node-abi@3.5.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lgeiger/node-abi\",\n    \"publisher\": \"Lukas Geiger\",\n    \"path\": \"/home/uiprgl/server/node_modules/node-abi\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/node-abi/LICENSE\"\n  },\n  \"node-addon-api@4.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/node-addon-api\",\n    \"path\": \"/home/uiprgl/server/node_modules/node-addon-api\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/node-addon-api/LICENSE.md\"\n  },\n  \"node-cleanup@2.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jtlapp/node-cleanup\",\n    \"path\": \"/home/uiprgl/server/node_modules/node-cleanup\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/node-cleanup/LICENSE\"\n  },\n  \"node-gyp@8.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/node-gyp\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://tootallnate.net\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/LICENSE\"\n  },\n  \"nopt@5.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/nopt\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/nopt\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/nopt/LICENSE\"\n  },\n  \"normalize-package-data@3.0.3\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/npm/normalize-package-data\",\n    \"publisher\": \"Meryn Stol\",\n    \"email\": \"merynstol@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/normalize-package-data\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/normalize-package-data/LICENSE\"\n  },\n  \"npm-audit-report@2.1.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npm-audit-report\",\n    \"publisher\": \"Adam Baldwin\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-audit-report\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-audit-report/LICENSE\"\n  },\n  \"npm-bundled@1.1.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npm-bundled\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-bundled\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-bundled/LICENSE\"\n  },\n  \"npm-install-checks@4.0.0\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/npm/npm-install-checks\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-install-checks\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-install-checks/LICENSE\"\n  },\n  \"npm-normalize-package-bin@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npm-normalize-package-bin\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-normalize-package-bin\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-normalize-package-bin/LICENSE\"\n  },\n  \"npm-package-arg@8.1.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npm-package-arg\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-package-arg\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-package-arg/LICENSE\"\n  },\n  \"npm-packlist@3.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npm-packlist\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-packlist\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-packlist/LICENSE\"\n  },\n  \"npm-pick-manifest@6.1.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npm-pick-manifest\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@sykosomatic.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-pick-manifest\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-pick-manifest/LICENSE.md\"\n  },\n  \"npm-profile@5.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npm-profile\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-profile\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-profile/LICENSE\"\n  },\n  \"npm-registry-fetch@11.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npm-registry-fetch\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@sykosomatic.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-registry-fetch\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-registry-fetch/LICENSE.md\"\n  },\n  \"npm-user-validate@1.0.1\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/npm/npm-user-validate\",\n    \"publisher\": \"Robert Kowalski\",\n    \"email\": \"rok@kowalski.gd\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-user-validate\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npm-user-validate/LICENSE\"\n  },\n  \"npm@8.1.4\": {\n    \"licenses\": \"Artistic-2.0\",\n    \"repository\": \"https://github.com/npm/cli\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/LICENSE\"\n  },\n  \"npmlog@4.1.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npmlog\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/npmlog\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/npmlog/LICENSE\"\n  },\n  \"npmlog@6.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/npmlog\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/npmlog\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/npmlog/LICENSE.md\"\n  },\n  \"number-is-nan@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/number-is-nan\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/number-is-nan\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/number-is-nan/license\"\n  },\n  \"object-assign@4.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/object-assign\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/object-assign\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/object-assign/license\"\n  },\n  \"on-finished@2.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/on-finished\",\n    \"path\": \"/home/uiprgl/server/node_modules/on-finished\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/on-finished/LICENSE\"\n  },\n  \"once@1.4.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/once\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/once\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/once/LICENSE\"\n  },\n  \"opener@1.5.2\": {\n    \"licenses\": \"(WTFPL OR MIT)\",\n    \"repository\": \"https://github.com/domenic/opener\",\n    \"publisher\": \"Domenic Denicola\",\n    \"email\": \"d@domenic.me\",\n    \"url\": \"https://domenic.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/opener\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/opener/LICENSE.txt\"\n  },\n  \"p-map@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/p-map\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/p-map\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/p-map/license\"\n  },\n  \"packet-reader@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianc/node-packet-reader\",\n    \"publisher\": \"Brian M. Carlson\",\n    \"path\": \"/home/uiprgl/server/node_modules/packet-reader\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/packet-reader/README.md\"\n  },\n  \"pacote@12.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/pacote\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/pacote\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/pacote/LICENSE\"\n  },\n  \"parse-conflict-json@1.1.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/parse-conflict-json\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/parse-conflict-json\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/parse-conflict-json/LICENSE\"\n  },\n  \"parseurl@1.3.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/pillarjs/parseurl\",\n    \"path\": \"/home/uiprgl/server/node_modules/parseurl\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/parseurl/LICENSE\"\n  },\n  \"path-is-absolute@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/path-is-absolute\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/path-is-absolute\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/path-is-absolute/license\"\n  },\n  \"path-key@3.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/path-key\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/path-key\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/path-key/license\"\n  },\n  \"path-to-regexp@0.1.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/component/path-to-regexp\",\n    \"path\": \"/home/uiprgl/server/node_modules/path-to-regexp\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/path-to-regexp/LICENSE\"\n  },\n  \"pause-stream@0.0.11\": {\n    \"licenses\": [\"MIT\", \"Apache2\"],\n    \"repository\": \"https://github.com/dominictarr/pause-stream\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"dominictarr.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/pause-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pause-stream/LICENSE\"\n  },\n  \"peek-readable@4.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/peek-readable\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/home/uiprgl/server/node_modules/peek-readable\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/peek-readable/LICENSE\"\n  },\n  \"pg-connection-string@2.5.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianc/node-postgres\",\n    \"publisher\": \"Blaine Bublitz\",\n    \"email\": \"blaine@iceddev.com\",\n    \"url\": \"http://iceddev.com/\",\n    \"path\": \"/home/uiprgl/server/node_modules/pg-connection-string\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pg-connection-string/LICENSE\"\n  },\n  \"pg-int8@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/charmander/pg-int8\",\n    \"path\": \"/home/uiprgl/server/node_modules/pg-int8\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pg-int8/LICENSE\"\n  },\n  \"pg-minify@1.6.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vitaly-t/pg-minify\",\n    \"publisher\": \"Vitaly Tomilov\",\n    \"email\": \"vitaly.tomilov@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/pg-minify\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pg-minify/README.md\"\n  },\n  \"pg-pool@3.4.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianc/node-postgres\",\n    \"publisher\": \"Brian M. Carlson\",\n    \"path\": \"/home/uiprgl/server/node_modules/pg-pool\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pg-pool/LICENSE\"\n  },\n  \"pg-promise@10.11.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vitaly-t/pg-promise\",\n    \"publisher\": \"Vitaly Tomilov\",\n    \"email\": \"vitaly.tomilov@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/pg-promise\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pg-promise/LICENSE\"\n  },\n  \"pg-protocol@1.5.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianc/node-postgres\",\n    \"path\": \"/home/uiprgl/server/node_modules/pg-protocol\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pg-protocol/LICENSE\"\n  },\n  \"pg-types@2.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianc/node-pg-types\",\n    \"publisher\": \"Brian M. Carlson\",\n    \"path\": \"/home/uiprgl/server/node_modules/pg-types\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pg-types/README.md\"\n  },\n  \"pg@8.7.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianc/node-postgres\",\n    \"publisher\": \"Brian Carlson\",\n    \"email\": \"brian.m.carlson@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/pg\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pg/LICENSE\"\n  },\n  \"pgpass@1.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/hoegaarden/pgpass\",\n    \"publisher\": \"Hannes Hörl\",\n    \"email\": \"hannes.hoerl+pgpass@snowreporter.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/pgpass\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pgpass/README.md\"\n  },\n  \"postgres-array@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/bendrucker/postgres-array\",\n    \"publisher\": \"Ben Drucker\",\n    \"email\": \"bvdrucker@gmail.com\",\n    \"url\": \"bendrucker.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/postgres-array\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/postgres-array/license\"\n  },\n  \"postgres-bytea@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/bendrucker/postgres-bytea\",\n    \"publisher\": \"Ben Drucker\",\n    \"email\": \"bvdrucker@gmail.com\",\n    \"url\": \"bendrucker.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/postgres-bytea\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/postgres-bytea/license\"\n  },\n  \"postgres-date@1.0.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/bendrucker/postgres-date\",\n    \"publisher\": \"Ben Drucker\",\n    \"email\": \"bvdrucker@gmail.com\",\n    \"url\": \"bendrucker.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/postgres-date\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/postgres-date/license\"\n  },\n  \"postgres-interval@1.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/bendrucker/postgres-interval\",\n    \"publisher\": \"Ben Drucker\",\n    \"email\": \"bvdrucker@gmail.com\",\n    \"url\": \"bendrucker.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/postgres-interval\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/postgres-interval/license\"\n  },\n  \"prebuild-install@7.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/prebuild/prebuild-install\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/home/uiprgl/server/node_modules/prebuild-install\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/prebuild-install/LICENSE\"\n  },\n  \"prgl_docker@1.0.0\": {\n    \"licenses\": \"ISC\",\n    \"path\": \"/home/uiprgl/server\"\n  },\n  \"proc-log@1.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/proc-log\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/proc-log\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/proc-log/LICENSE\"\n  },\n  \"process-nextick-args@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/calvinmetcalf/process-nextick-args\",\n    \"path\": \"/home/uiprgl/server/node_modules/process-nextick-args\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/process-nextick-args/license.md\"\n  },\n  \"promise-all-reject-late@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/promise-all-reject-late\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/promise-all-reject-late/LICENSE\"\n  },\n  \"promise-call-limit@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/promise-call-limit\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/promise-call-limit\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/promise-call-limit/LICENSE\"\n  },\n  \"promise-inflight@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/promise-inflight\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/promise-inflight\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/promise-inflight/LICENSE\"\n  },\n  \"promise-retry@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/IndigoUnited/node-promise-retry\",\n    \"publisher\": \"IndigoUnited\",\n    \"email\": \"hello@indigounited.com\",\n    \"url\": \"http://indigounited.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/promise-retry\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/promise-retry/LICENSE\"\n  },\n  \"promzard@0.3.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/promzard\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/promzard\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/promzard/LICENSE\"\n  },\n  \"prostgles-server@2.0.106\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/prostgles/prostgles-server-js\",\n    \"publisher\": \"Stefan L\",\n    \"path\": \"/home/uiprgl/server/node_modules/prostgles-server\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/prostgles-server/LICENSE\"\n  },\n  \"prostgles-types@1.5.120\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/prostgles/prostgles-types\",\n    \"publisher\": \"Stefan L\",\n    \"path\": \"/home/uiprgl/server/node_modules/prostgles-types\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/prostgles-types/LICENSE\"\n  },\n  \"proxy-addr@2.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/proxy-addr\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/proxy-addr\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/proxy-addr/LICENSE\"\n  },\n  \"ps-tree@1.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/indexzero/ps-tree\",\n    \"publisher\": \"Charlie Robbins\",\n    \"email\": \"charlie.robbins@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/ps-tree\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/ps-tree/LICENSE\"\n  },\n  \"pump@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/pump\",\n    \"publisher\": \"Mathias Buus Madsen\",\n    \"email\": \"mathiasbuus@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/pump\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/pump/LICENSE\"\n  },\n  \"punycode@1.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/bestiejs/punycode.js\",\n    \"publisher\": \"Mathias Bynens\",\n    \"url\": \"https://mathiasbynens.be/\",\n    \"path\": \"/home/uiprgl/server/node_modules/punycode\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/punycode/LICENSE-MIT.txt\"\n  },\n  \"qrcode-terminal@0.12.0\": {\n    \"licenses\": \"Apache 2.0\",\n    \"repository\": \"https://github.com/gtanner/qrcode-terminal\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/qrcode-terminal\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/qrcode-terminal/LICENSE\"\n  },\n  \"qs@6.7.0\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/ljharb/qs\",\n    \"path\": \"/home/uiprgl/server/node_modules/qs\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/qs/LICENSE\"\n  },\n  \"querystring@0.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Gozala/querystring\",\n    \"publisher\": \"Irakli Gozalishvili\",\n    \"email\": \"rfobic@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/querystring\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/querystring/License.md\"\n  },\n  \"range-parser@1.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/range-parser\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"url\": \"http://tjholowaychuk.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/range-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/range-parser/LICENSE\"\n  },\n  \"raw-body@2.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/stream-utils/raw-body\",\n    \"publisher\": \"Jonathan Ong\",\n    \"email\": \"me@jongleberry.com\",\n    \"url\": \"http://jongleberry.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/raw-body\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/raw-body/LICENSE\"\n  },\n  \"rc@1.2.8\": {\n    \"licenses\": \"(BSD-2-Clause OR MIT OR Apache-2.0)\",\n    \"repository\": \"https://github.com/dominictarr/rc\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"dominictarr.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/rc\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/rc/LICENSE.APACHE2\"\n  },\n  \"read-cmd-shim@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/read-cmd-shim\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/read-cmd-shim\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/read-cmd-shim/LICENSE\"\n  },\n  \"read-package-json-fast@2.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/read-package-json-fast\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/read-package-json-fast\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/read-package-json-fast/LICENSE\"\n  },\n  \"read-package-json@4.1.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/read-package-json\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/read-package-json\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/read-package-json/LICENSE\"\n  },\n  \"read@1.0.7\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/read\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/read\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/read/LICENSE\"\n  },\n  \"readable-stream@2.3.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/readable-stream\",\n    \"path\": \"/home/uiprgl/server/node_modules/are-we-there-yet/node_modules/readable-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/are-we-there-yet/node_modules/readable-stream/LICENSE\"\n  },\n  \"readable-stream@3.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/readable-stream\",\n    \"path\": \"/home/uiprgl/server/node_modules/readable-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/readable-stream/LICENSE\"\n  },\n  \"readable-web-to-node-stream@3.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/readable-web-to-node-stream\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/home/uiprgl/server/node_modules/readable-web-to-node-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/readable-web-to-node-stream/README.md\"\n  },\n  \"readdir-scoped-modules@1.1.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/readdir-scoped-modules\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/readdir-scoped-modules\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/readdir-scoped-modules/LICENSE\"\n  },\n  \"retry@0.12.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/tim-kos/node-retry\",\n    \"publisher\": \"Tim Koschützki\",\n    \"email\": \"tim@debuggable.com\",\n    \"url\": \"http://debuggable.com/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/retry\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/retry/License\"\n  },\n  \"rimraf@3.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/rimraf\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/rimraf\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/rimraf/LICENSE\"\n  },\n  \"safe-buffer@5.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/safe-buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"http://feross.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/safe-buffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/safe-buffer/LICENSE\"\n  },\n  \"safe-buffer@5.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/safe-buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/string_decoder/node_modules/safe-buffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/string_decoder/node_modules/safe-buffer/LICENSE\"\n  },\n  \"safer-buffer@2.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ChALkeR/safer-buffer\",\n    \"publisher\": \"Nikita Skovoroda\",\n    \"email\": \"chalkerx@gmail.com\",\n    \"url\": \"https://github.com/ChALkeR\",\n    \"path\": \"/home/uiprgl/server/node_modules/safer-buffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/safer-buffer/LICENSE\"\n  },\n  \"sax@1.2.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/sax-js\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/sax\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/sax/LICENSE\"\n  },\n  \"semver@7.3.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/node-semver\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/semver\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/semver/LICENSE\"\n  },\n  \"send@0.17.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/pillarjs/send\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"path\": \"/home/uiprgl/server/node_modules/send\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/send/LICENSE\"\n  },\n  \"serve-static@1.14.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/expressjs/serve-static\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/serve-static\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/serve-static/LICENSE\"\n  },\n  \"set-blocking@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/yargs/set-blocking\",\n    \"publisher\": \"Ben Coe\",\n    \"email\": \"ben@npmjs.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/set-blocking\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/set-blocking/LICENSE.txt\"\n  },\n  \"setprototypeof@1.1.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/wesleytodd/setprototypeof\",\n    \"publisher\": \"Wes Todd\",\n    \"path\": \"/home/uiprgl/server/node_modules/setprototypeof\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/setprototypeof/LICENSE\"\n  },\n  \"sharp@0.29.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/lovell/sharp\",\n    \"publisher\": \"Lovell Fuller\",\n    \"email\": \"npm@lovell.info\",\n    \"path\": \"/home/uiprgl/server/node_modules/sharp\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/sharp/LICENSE\"\n  },\n  \"shebang-command@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kevva/shebang-command\",\n    \"publisher\": \"Kevin Mårtensson\",\n    \"email\": \"kevinmartensson@gmail.com\",\n    \"url\": \"github.com/kevva\",\n    \"path\": \"/home/uiprgl/server/node_modules/shebang-command\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/shebang-command/license\"\n  },\n  \"shebang-regex@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/shebang-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/shebang-regex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/shebang-regex/license\"\n  },\n  \"signal-exit@3.0.6\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/tapjs/signal-exit\",\n    \"publisher\": \"Ben Coe\",\n    \"email\": \"ben@npmjs.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/signal-exit\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/signal-exit/LICENSE.txt\"\n  },\n  \"simple-concat@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/simple-concat\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/simple-concat\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/simple-concat/LICENSE\"\n  },\n  \"simple-get@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/simple-get\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/simple-get\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/simple-get/LICENSE\"\n  },\n  \"simple-swizzle@0.2.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/qix-/node-simple-swizzle\",\n    \"publisher\": \"Qix\",\n    \"url\": \"http://github.com/qix-\",\n    \"path\": \"/home/uiprgl/server/node_modules/simple-swizzle\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/simple-swizzle/LICENSE\"\n  },\n  \"smart-buffer@4.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/JoshGlazebrook/smart-buffer\",\n    \"publisher\": \"Josh Glazebrook\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/smart-buffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/smart-buffer/LICENSE\"\n  },\n  \"socket.io-adapter@2.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/socketio/socket.io-adapter\",\n    \"path\": \"/home/uiprgl/server/node_modules/socket.io-adapter\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/socket.io-adapter/LICENSE\"\n  },\n  \"socket.io-parser@4.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/socketio/socket.io-parser\",\n    \"path\": \"/home/uiprgl/server/node_modules/socket.io-parser\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/socket.io-parser/LICENSE\"\n  },\n  \"socket.io@3.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/socketio/socket.io\",\n    \"path\": \"/home/uiprgl/server/node_modules/socket.io\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/socket.io/LICENSE\"\n  },\n  \"socks-proxy-agent@6.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/node-socks-proxy-agent\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/socks-proxy-agent\"\n  },\n  \"socks@2.6.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/JoshGlazebrook/socks\",\n    \"publisher\": \"Josh Glazebrook\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/socks\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/socks/LICENSE\"\n  },\n  \"spdx-correct@3.1.1\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/jslicense/spdx-correct.js\",\n    \"publisher\": \"Kyle E. Mitchell\",\n    \"email\": \"kyle@kemitchell.com\",\n    \"url\": \"https://kemitchell.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/spdx-correct\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/spdx-correct/LICENSE\"\n  },\n  \"spdx-exceptions@2.3.0\": {\n    \"licenses\": \"CC-BY-3.0\",\n    \"repository\": \"https://github.com/kemitchell/spdx-exceptions.json\",\n    \"publisher\": \"The Linux Foundation\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/spdx-exceptions\"\n  },\n  \"spdx-expression-parse@3.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jslicense/spdx-expression-parse.js\",\n    \"publisher\": \"Kyle E. Mitchell\",\n    \"email\": \"kyle@kemitchell.com\",\n    \"url\": \"https://kemitchell.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/spdx-expression-parse\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/spdx-expression-parse/LICENSE\"\n  },\n  \"spdx-license-ids@3.0.10\": {\n    \"licenses\": \"CC0-1.0\",\n    \"repository\": \"https://github.com/jslicense/spdx-license-ids\",\n    \"publisher\": \"Shinnosuke Watanabe\",\n    \"url\": \"https://github.com/shinnn\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/spdx-license-ids\"\n  },\n  \"spex@3.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vitaly-t/spex\",\n    \"publisher\": \"Vitaly Tomilov\",\n    \"email\": \"vitaly.tomilov@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/spex\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/spex/README.md\"\n  },\n  \"split2@3.2.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/mcollina/split2\",\n    \"publisher\": \"Matteo Collina\",\n    \"email\": \"hello@matteocollina.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/split2\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/split2/LICENSE\"\n  },\n  \"split@0.3.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dominictarr/split\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"http://bit.ly/dominictarr\",\n    \"path\": \"/home/uiprgl/server/node_modules/split\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/split/LICENCE\"\n  },\n  \"ssri@8.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/ssri\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@sykosomatic.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/ssri\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/ssri/LICENSE.md\"\n  },\n  \"statuses@1.5.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/statuses\",\n    \"path\": \"/home/uiprgl/server/node_modules/statuses\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/statuses/LICENSE\"\n  },\n  \"stream-combiner@0.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dominictarr/stream-combiner\",\n    \"publisher\": \"'Dominic Tarr'\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"http://dominictarr.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/stream-combiner\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/stream-combiner/LICENSE\"\n  },\n  \"string-argv@0.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mccormicka/string-argv\",\n    \"publisher\": \"Anthony McCormick\",\n    \"email\": \"anthony.mccormick@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/string-argv\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/string-argv/LICENSE\"\n  },\n  \"string-width@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/string-width\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/string-width\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/node-gyp/node_modules/string-width/license\"\n  },\n  \"string-width@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/string-width\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/string-width\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/string-width/license\"\n  },\n  \"string-width@4.2.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/string-width\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-table3/node_modules/string-width\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-table3/node_modules/string-width/license\"\n  },\n  \"string-width@4.2.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/string-width\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/node_modules/string-width\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/node_modules/string-width/license\"\n  },\n  \"string_decoder@1.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/string_decoder\",\n    \"path\": \"/home/uiprgl/server/node_modules/are-we-there-yet/node_modules/string_decoder\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/are-we-there-yet/node_modules/string_decoder/LICENSE\"\n  },\n  \"string_decoder@1.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/string_decoder\",\n    \"path\": \"/home/uiprgl/server/node_modules/string_decoder\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/string_decoder/LICENSE\"\n  },\n  \"stringify-package@1.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/stringify-package\",\n    \"publisher\": \"Kat Marchán\",\n    \"email\": \"kzm@zkat.tech\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/stringify-package\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/stringify-package/LICENSE\"\n  },\n  \"strip-ansi@3.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/strip-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/strip-ansi\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/strip-ansi/license\"\n  },\n  \"strip-ansi@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/strip-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/string-width/node_modules/strip-ansi\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/string-width/node_modules/strip-ansi/license\"\n  },\n  \"strip-ansi@6.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/strip-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-table3/node_modules/strip-ansi\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-table3/node_modules/strip-ansi/license\"\n  },\n  \"strip-ansi@6.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/strip-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/node_modules/strip-ansi\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/cli-columns/node_modules/strip-ansi/license\"\n  },\n  \"strip-bom@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/strip-bom\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/strip-bom\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/strip-bom/license\"\n  },\n  \"strip-json-comments@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/strip-json-comments\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/strip-json-comments\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/strip-json-comments/license\"\n  },\n  \"strtok3@6.2.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/strtok3\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/home/uiprgl/server/node_modules/strtok3\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/strtok3/LICENSE\"\n  },\n  \"supports-color@7.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/supports-color\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/supports-color\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/supports-color/license\"\n  },\n  \"tar-fs@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/tar-fs\",\n    \"publisher\": \"Mathias Buus\",\n    \"path\": \"/home/uiprgl/server/node_modules/tar-fs\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/tar-fs/LICENSE\"\n  },\n  \"tar-stream@2.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/tar-stream\",\n    \"publisher\": \"Mathias Buus\",\n    \"email\": \"mathiasbuus@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/tar-stream\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/tar-stream/LICENSE\"\n  },\n  \"tar@6.1.11\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/node-tar\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/tar\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/tar/LICENSE\"\n  },\n  \"text-table@0.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/text-table\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/text-table\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/text-table/LICENSE\"\n  },\n  \"through@2.3.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dominictarr/through\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"dominictarr.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/through\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/through/LICENSE.APACHE2\"\n  },\n  \"tiny-relative-date@1.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/wildlyinaccurate/relative-date\",\n    \"publisher\": \"Joseph Wynn\",\n    \"email\": \"joseph@wildlyinaccurate.com\",\n    \"url\": \"https://wildlyinaccurate.com/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/tiny-relative-date\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/tiny-relative-date/LICENSE.md\"\n  },\n  \"toidentifier@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/component/toidentifier\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/toidentifier\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/toidentifier/LICENSE\"\n  },\n  \"token-types@4.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/token-types\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/home/uiprgl/server/node_modules/token-types\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/token-types/LICENSE\"\n  },\n  \"treeverse@1.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/treeverse\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/treeverse\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/treeverse/LICENSE\"\n  },\n  \"tsc-watch@4.2.9\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/gilamran/tsc-watch\",\n    \"publisher\": \"Gil Amran\",\n    \"path\": \"/home/uiprgl/server/node_modules/tsc-watch\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/tsc-watch/LICENSE\"\n  },\n  \"tslib@1.14.1\": {\n    \"licenses\": \"0BSD\",\n    \"repository\": \"https://github.com/Microsoft/tslib\",\n    \"publisher\": \"Microsoft Corp.\",\n    \"path\": \"/home/uiprgl/server/node_modules/@aws-crypto/ie11-detection/node_modules/tslib\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/@aws-crypto/ie11-detection/node_modules/tslib/LICENSE.txt\"\n  },\n  \"tslib@2.3.1\": {\n    \"licenses\": \"0BSD\",\n    \"repository\": \"https://github.com/Microsoft/tslib\",\n    \"publisher\": \"Microsoft Corp.\",\n    \"path\": \"/home/uiprgl/server/node_modules/tslib\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/tslib/LICENSE.txt\"\n  },\n  \"tunnel-agent@0.6.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/mikeal/tunnel-agent\",\n    \"publisher\": \"Mikeal Rogers\",\n    \"email\": \"mikeal.rogers@gmail.com\",\n    \"url\": \"http://www.futurealoof.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/tunnel-agent\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/tunnel-agent/LICENSE\"\n  },\n  \"type-is@1.6.18\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/type-is\",\n    \"path\": \"/home/uiprgl/server/node_modules/type-is\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/type-is/LICENSE\"\n  },\n  \"typedarray-to-buffer@3.1.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/typedarray-to-buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"http://feross.org/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/typedarray-to-buffer\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/typedarray-to-buffer/LICENSE\"\n  },\n  \"typescript@4.2.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/Microsoft/TypeScript\",\n    \"publisher\": \"Microsoft Corp.\",\n    \"path\": \"/home/uiprgl/server/node_modules/typescript\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/typescript/LICENSE.txt\"\n  },\n  \"unique-filename@1.1.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/unique-filename\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/unique-filename\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/unique-filename/LICENSE\"\n  },\n  \"unique-slug@2.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/unique-slug\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/unique-slug\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/unique-slug/LICENSE\"\n  },\n  \"unpipe@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/stream-utils/unpipe\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/unpipe\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/unpipe/LICENSE\"\n  },\n  \"url@0.10.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/defunctzombie/node-url\",\n    \"path\": \"/home/uiprgl/server/node_modules/url\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/url/LICENSE\"\n  },\n  \"util-deprecate@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/util-deprecate\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/home/uiprgl/server/node_modules/util-deprecate\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/util-deprecate/LICENSE\"\n  },\n  \"utils-merge@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jaredhanson/utils-merge\",\n    \"publisher\": \"Jared Hanson\",\n    \"email\": \"jaredhanson@gmail.com\",\n    \"url\": \"http://www.jaredhanson.net/\",\n    \"path\": \"/home/uiprgl/server/node_modules/utils-merge\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/utils-merge/LICENSE\"\n  },\n  \"uuid@3.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kelektiv/node-uuid\",\n    \"path\": \"/home/uiprgl/server/node_modules/aws-sdk/node_modules/uuid\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/aws-sdk/node_modules/uuid/LICENSE.md\"\n  },\n  \"uuid@8.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/uuidjs/uuid\",\n    \"path\": \"/home/uiprgl/server/node_modules/uuid\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/uuid/LICENSE.md\"\n  },\n  \"validate-npm-package-license@3.0.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/kemitchell/validate-npm-package-license.js\",\n    \"publisher\": \"Kyle E. Mitchell\",\n    \"email\": \"kyle@kemitchell.com\",\n    \"url\": \"https://kemitchell.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/validate-npm-package-license\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/validate-npm-package-license/LICENSE\"\n  },\n  \"validate-npm-package-name@3.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/validate-npm-package-name\",\n    \"publisher\": \"zeke\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/validate-npm-package-name\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/validate-npm-package-name/LICENSE\"\n  },\n  \"vary@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/vary\",\n    \"publisher\": \"Douglas Christopher Wilson\",\n    \"email\": \"doug@somethingdoug.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/vary\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/vary/LICENSE\"\n  },\n  \"walk-up-path@1.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/walk-up-path\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/walk-up-path\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/walk-up-path/LICENSE\"\n  },\n  \"wcwidth@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/timoxley/wcwidth\",\n    \"publisher\": \"Tim Oxley\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/wcwidth\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/wcwidth/LICENSE\"\n  },\n  \"which@2.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-which\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/which\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/which/LICENSE\"\n  },\n  \"wide-align@1.1.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/wide-align\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/wide-align\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/wide-align/LICENSE\"\n  },\n  \"wide-align@1.1.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/iarna/wide-align\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org/\",\n    \"path\": \"/home/uiprgl/server/node_modules/wide-align\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/wide-align/LICENSE\"\n  },\n  \"wrappy@1.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/wrappy\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/wrappy\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/wrappy/LICENSE\"\n  },\n  \"write-file-atomic@3.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/write-file-atomic\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/write-file-atomic\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/write-file-atomic/LICENSE\"\n  },\n  \"ws@7.4.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/websockets/ws\",\n    \"publisher\": \"Einar Otto Stangvik\",\n    \"email\": \"einaros@gmail.com\",\n    \"url\": \"http://2x.io\",\n    \"path\": \"/home/uiprgl/server/node_modules/ws\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/ws/LICENSE\"\n  },\n  \"xml2js@0.4.19\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Leonidas-from-XIV/node-xml2js\",\n    \"publisher\": \"Marek Kubica\",\n    \"email\": \"marek@xivilization.net\",\n    \"url\": \"https://xivilization.net\",\n    \"path\": \"/home/uiprgl/server/node_modules/xml2js\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/xml2js/LICENSE\"\n  },\n  \"xmlbuilder@9.0.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/oozcitak/xmlbuilder-js\",\n    \"publisher\": \"Ozgur Ozcitak\",\n    \"email\": \"oozcitak@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/xmlbuilder\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/xmlbuilder/LICENSE\"\n  },\n  \"xtend@4.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/xtend\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/home/uiprgl/server/node_modules/xtend\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/xtend/LICENSE\"\n  },\n  \"yallist@4.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/yallist\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/home/uiprgl/server/node_modules/npm/node_modules/yallist\",\n    \"licenseFile\": \"/home/uiprgl/server/node_modules/npm/node_modules/yallist/LICENSE\"\n  }\n}\n"
  },
  {
    "path": "server/package.json",
    "content": "{\n  \"name\": \"prostgles-ui-server\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"npm i && cross-env NODE_ENV='production' NODE_OPTIONS='--max-old-space-size=2048' tsc && tsc-alias && npm prune --omit=dev\",\n    \"start\": \"npm run build && node dist/server/src/index.js\",\n    \"test\": \"npm run build && node --test dist/server/**/**/*.spec.js\",\n    \"lint\": \"eslint . --ext .ts --quiet --fix\",\n    \"dev\": \"npm i --no-audit && cross-env NODE_ENV='development' NODE_OPTIONS='--max-old-space-size=4048' concurrently --kill-others \\\"tsc -w\\\" \\\"tsc-alias -w\\\" \\\"nodemon  --delay 500ms --trace-warnings --inspect dist/server/src/index.js\\\"\",\n    \"dev:electron\": \"npm i && cross-env NODE_ENV='development' NODE_OPTIONS='--max-old-space-size=4048' concurrently --kill-others \\\"tsc -w\\\" \\\"tsc-alias -w\\\" \\\"nodemon  --delay 500ms --trace-warnings --inspect dist/server/src/testElectron.js\\\"\"\n  },\n  \"author\": \"\",\n  \"license\": \"\",\n  \"dependencies\": {\n    \"@aws-sdk/client-s3\": \"^3.940.0\",\n    \"@aws-sdk/lib-storage\": \"^3.940.0\",\n    \"@aws-sdk/s3-request-presigner\": \"^3.940.0\",\n    \"@modelcontextprotocol/sdk\": \"^1.24.1\",\n    \"@types/cookie-parser\": \"^1.4.2\",\n    \"@types/pidusage\": \"^2.0.5\",\n    \"check-disk-space\": \"^3.3.1\",\n    \"connection-string\": \"^4.4.0\",\n    \"cookie-parser\": \"^1.4.5\",\n    \"dotenv\": \"^10.0.0\",\n    \"express\": \"^5.2.1\",\n    \"glob\": \"^13.0.0\",\n    \"helmet\": \"^7.1.0\",\n    \"otplib\": \"^12.0.1\",\n    \"pidusage\": \"^4.0.1\",\n    \"prostgles-server\": \"^4.2.367\",\n    \"prostgles-types\": \"^4.0.89\",\n    \"qrcode\": \"^1.5.1\",\n    \"simple-git\": \"^3.27.0\",\n    \"smtp-server\": \"^3.16.1\",\n    \"socket.io\": \"^4.8.1\",\n    \"typescript\": \"^5.8.3\",\n    \"ws\": \"^8.17.1\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.39.1\",\n    \"@types/express\": \"^5.0.4\",\n    \"@types/node\": \"^22.15.2\",\n    \"@types/smtp-server\": \"^3.5.10\",\n    \"@types/ws\": \"^8.5.3\",\n    \"concurrently\": \"^9.2.0\",\n    \"cross-env\": \"^10.0.0\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-plugin-import\": \"^2.32.0\",\n    \"eslint-plugin-security\": \"^3.0.1\",\n    \"nodemon\": \"^3.1.10\",\n    \"tsc-alias\": \"^1.8.16\",\n    \"typescript-eslint\": \"^8.46.4\"\n  }\n}\n"
  },
  {
    "path": "server/proj-prgl.js",
    "content": "const prostgles = require(\"prostgles-server\");\nconst path = require(\"path\");\nconst express = require(\"express\");\nconst app = express();\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\n\n// Add headers before the routes are defined\napp.use(function (req, res, next) {\n  // Website you wish to allow to connect\n  res.setHeader(\"Access-Control-Allow-Origin\", \"http://localhost:3004\");\n\n  // Request methods you wish to allow\n  res.setHeader(\n    \"Access-Control-Allow-Methods\",\n    \"GET, POST, OPTIONS, PUT, PATCH, DELETE\",\n  );\n\n  // Request headers you wish to allow\n  res.setHeader(\n    \"Access-Control-Allow-Headers\",\n    \"X-Requested-With,content-type\",\n  );\n\n  // Set to true if you need the website to include cookies in the requests sent\n  // to the API (e.g. in case you use sessions)\n  res.setHeader(\"Access-Control-Allow-Credentials\", true);\n\n  // Pass to next layer of middleware\n  next();\n});\n\nconst _http = require(\"http\");\nconst http = _http.createServer(app);\n\nconst {\n  app_port,\n  db_conn,\n  db_host,\n  db_port,\n  db_name,\n  db_user,\n  db_pass,\n  socket_path = \"/iosckt\",\n} = process.env;\n\nconsole.log(\n  process.env,\n  JSON.stringify({\n    app_port,\n    db_conn,\n    db_host,\n    db_port,\n    db_name,\n    db_user,\n    db_pass,\n    socket_path,\n  }),\n);\n\nconst { Server } = require(\"socket.io\");\nconst io = new Server(http, { path: socket_path });\n\nhttp.listen(app_port);\n\nprostgles({\n  dbConnection:\n    db_conn ?\n      { connectionString: db_conn }\n    : {\n        host: db_host,\n        port: db_port,\n        database: db_name,\n        user: db_user,\n        password: db_pass,\n      },\n  io,\n  watchSchema: true,\n\n  // DEBUG_MODE: true,\n\n  joins: \"inferred\",\n  publish: \"*\",\n  publishRawSQL: () => \"*\",\n  onSocketConnect: () => {\n    console.log(\"onSocketConnect\");\n  },\n  onReady: (db, _db) => {\n    console.log(\"onReady\", Object.keys(db));\n  },\n});\n"
  },
  {
    "path": "server/sample_schemas/_crypto.sql",
    "content": "CREATE TABLE user_statuses (\n  id TEXT PRIMARY KEY, \n  description TEXT\n);\n\nINSERT INTO user_statuses(id) VALUES ('active'), ('inactive'), ('pending');\n\nCREATE TABLE users (\n  id serial PRIMARY KEY,\n  name VARCHAR(255) NOT NULL,\n  email VARCHAR(255) NOT NULL UNIQUE,\n  status TEXT NOT NULL DEFAULT 'active' REFERENCES user_statuses,\n  password VARCHAR(255) NOT NULL\n);\n\nCREATE TABLE registrations (\n  id serial PRIMARY KEY,\n  user_id INTEGER REFERENCES users(id),\n  first_name VARCHAR(255) NOT NULL,\n  last_name VARCHAR(255) NOT NULL,\n  date_of_birth DATE NOT NULL,\n  address VARCHAR(255) NOT NULL,\n  identification_type VARCHAR(255) NOT NULL,\n  identification_number VARCHAR(255) NOT NULL,\n  identification_document TEXT NOT NULL,\n  status VARCHAR(255) NOT NULL DEFAULT 'pending',\n  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\nCREATE INDEX idx_registrations_user_id ON registrations (user_id);\n\nCREATE TABLE registration_statuses (\n  id TEXT PRIMARY KEY,  \n  description TEXT\n);\n\nINSERT INTO registration_statuses (id, description) \nVALUES\n  ('pending',   'The registration is pending review by customer support'),\n  ('approved',  'The registration has been approved'),\n  ('rejected',  'The registration has been rejected'); \n\nCREATE TABLE cryptocurrencies (\n  id serial PRIMARY KEY,\n  name VARCHAR(255) NOT NULL,\n  symbol VARCHAR(10) NOT NULL\n);\n\nCREATE TABLE wallets (\n  id serial PRIMARY KEY,\n  user_id INTEGER REFERENCES users(id),\n  cryptocurrency_id INTEGER REFERENCES cryptocurrencies(id),\n  balance DECIMAL(30,10) NOT NULL\n);\nCREATE INDEX idx_wallets_user_id ON wallets (user_id);\n\n\nCREATE TABLE trades (\n  id serial PRIMARY KEY,\n  user_id INTEGER REFERENCES users(id),\n  cryptocurrency_id INTEGER REFERENCES cryptocurrencies(id),\n  price DECIMAL(30,10) NOT NULL,\n  quantity DECIMAL(30,10) NOT NULL,\n  timestamp TIMESTAMP NOT NULL\n);\nCREATE INDEX idx_trades_user_id ON trades (user_id);\nCREATE INDEX idx_trades_cryptocurrency_id ON trades (cryptocurrency_id);\n\nCREATE TABLE audit_logs (\n  id serial PRIMARY KEY,\n  user_id INTEGER REFERENCES users(id),\n  action VARCHAR(255) NOT NULL,\n  timestamp TIMESTAMP NOT NULL\n);\n\n\nCREATE TABLE support_tickets (\n  id serial PRIMARY KEY,\n  user_id INTEGER REFERENCES users(id),\n  subject VARCHAR(255) NOT NULL,\n  message TEXT NOT NULL,\n  status VARCHAR(255) NOT NULL DEFAULT 'open',\n  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\nCREATE INDEX idx_support_tickets_user_id ON support_tickets (user_id);\n\n\nCREATE TABLE support_replies (\n  id serial PRIMARY KEY,\n  support_ticket_id INTEGER REFERENCES support_tickets(id),\n  user_id INTEGER REFERENCES users(id),\n  message TEXT NOT NULL,\n  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\nCREATE INDEX idx_support_replies_support_ticket_id ON support_replies (support_ticket_id);\n\n"
  },
  {
    "path": "server/sample_schemas/cleaning.sql",
    "content": "CREATE TABLE users (\n  id serial PRIMARY KEY,\n  name VARCHAR(255) NOT NULL,\n  email VARCHAR(255) NOT NULL UNIQUE,\n  password VARCHAR(255) NOT NULL\n);\n\nCREATE TABLE cleaners (\n  id serial PRIMARY KEY,\n  user_id INTEGER REFERENCES users(id),\n  availability VARCHAR(255) NOT NULL,\n  rating DECIMAL(3,2) DEFAULT 0,\n  hourly_rate DECIMAL(6,2),\n  cleaning_methods VARCHAR(255),\n  certifications VARCHAR(255)\n);\n\nCREATE TABLE houses (\n  id serial PRIMARY KEY,\n  user_id INTEGER REFERENCES users(id),\n  address VARCHAR(255) NOT NULL\n);\n\nCREATE TABLE appointments (\n  id serial PRIMARY KEY,\n  house_id INTEGER REFERENCES houses(id),\n  cleaner_id INTEGER REFERENCES cleaners(id),\n  start_time TIMESTAMP,\n  cleaning_type VARCHAR(255),\n  special_instructions VARCHAR(255),\n  duration INTEGER NOT NULL\n);\nCREATE INDEX idx_appointments_date ON appointments (start_time);\n \nCREATE TABLE payments (\n  id serial PRIMARY KEY,\n  appointment_id INTEGER REFERENCES appointments(id),\n  payment_method VARCHAR(255),\n  amount DECIMAL(10,2)\n);\n \nCREATE TABLE reviews (\n  id serial PRIMARY KEY,\n  appointment_id INTEGER REFERENCES appointments(id),\n  rating INTEGER,\n  review VARCHAR(255)\n);\n \n \n"
  },
  {
    "path": "server/sample_schemas/cloud_computing.sql",
    "content": "\nCREATE TABLE users (\n  id SERIAL PRIMARY KEY,\n  email VARCHAR(100) NOT NULL UNIQUE,\n  password VARCHAR(100) NOT NULL,\n  first_name VARCHAR(50) NOT NULL,\n  last_name VARCHAR(50) NOT NULL,\n  phone_number VARCHAR(20) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n\nCREATE TABLE vm_status_values (\n  id TEXT PRIMARY KEY, \n  description VARCHAR(100) NOT NULL\n);\n \nINSERT INTO vm_status_values (id, description) \nVALUES\n  ('active', 'The vm is running and can be accessed.'),\n  ('off', 'The vm is powered off and cannot be accessed.'),\n  ('archive', 'The vm has been deleted and is only stored for a short time before it is permanently removed.'),\n  ('new', 'The vm is being created and has not yet been assigned a status.'),\n  ('active_locked', 'The vm is running but has been locked by DigitalOcean for security reasons.'),\n  ('off_locked', 'The vm is powered off and has been locked by DigitalOcean for security reasons.'),\n  ('archive_locked', 'The vm has been deleted and has been locked by DigitalOcean for security reasons.');\n\n \nCREATE TABLE vms (\n  id SERIAL PRIMARY KEY,\n  user_id INTEGER NOT NULL REFERENCES users(id),\n  name VARCHAR(100) NOT NULL,\n  region VARCHAR(20) NOT NULL,\n  size VARCHAR(20) NOT NULL,\n  image VARCHAR(100) NOT NULL,\n  ip_address VARCHAR(100) NOT NULL,\n  status VARCHAR(20) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE ssh_keys (\n  id SERIAL PRIMARY KEY,\n  user_id INTEGER NOT NULL REFERENCES users(id),\n  name VARCHAR(100) NOT NULL,\n  public_key TEXT NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE domains (\n  id SERIAL PRIMARY KEY,\n  user_id INTEGER NOT NULL REFERENCES users(id),\n  name VARCHAR(100) NOT NULL,\n  ip_address VARCHAR(100) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n); \n\nCREATE TABLE dns_records (\n  id SERIAL PRIMARY KEY,\n  domain_id INTEGER NOT NULL REFERENCES domains(id),\n  name VARCHAR(100) NOT NULL,\n  type VARCHAR(20) NOT NULL,\n  data VARCHAR(100) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);"
  },
  {
    "path": "server/sample_schemas/countries.sql",
    "content": "\nDROP TABLE IF EXISTS  countries;\nCREATE TABLE countries (\n  id BIGINT PRIMARY KEY,\n  type    TEXT,\n  name    TEXT,\n  name_en TEXT,\n  geom GEOGRAPHY\n);\n\nCREATE TABLE country_goejson (\n  id BIGINT PRIMARY KEY,\n  geojson JSON\n);\n\nCOPY countries (id, type, name, name_en) FROM PROGRAM $$\n  curl -d \"[out:csv(::id, ::type, name, 'name:en'; false)];relation[admin_level=2][boundary=administrative][type!=multilinestring]; out;\"  -X POST http://overpass-api.de/api/interpreter\n$$;\n\nCOPY country_goejson (geojson) FROM PROGRAM $$\n  curl -d \"[out:json];relation[admin_level=2][boundary=administrative][type=boundary]; out geom;\"  -X POST http://overpass-api.de/api/interpreter\n$$;\n\nCOPY country_goejson (geojson) FROM PROGRAM $$\n  curl -d \"[out:json];rel[boundary=administrative][admin_level=2](9407); out geom;\"  -X POST http://overpass-api.de/api/interpreter\n$$;\n\n\n\n"
  },
  {
    "path": "server/sample_schemas/crypto/onMount.ts",
    "content": "const SECOND = 1e3;\nimport { WebSocket } from \"ws\";\n\nconst FUNDING_SYMBOLS = [\n  \"BTCUSDT\",\n  \"ETHUSDT\",\n  \"BNBUSDT\",\n  \"SOLUSDT\",\n  \"XRPUSDT\",\n  \"TAOUSDT\",\n] as const;\n\nlet loadGasPrices = false;\n\nexport const onMount: ProstglesOnMount = async ({ dbo }) => {\n  const getMarketCaps = async () => {\n    const marketCaps = await fetch(\n      \"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250\",\n    ).then((d) => d.json());\n    const batchUpdate = marketCaps.map(({ id, ...otherData }) => [\n      { id },\n      otherData,\n    ]);\n    console.log(`marketCaps ${marketCaps.length} items`);\n    console.log(`batchUpdate ${batchUpdate.length} items`);\n    await dbo.market_caps.updateBatch(batchUpdate);\n    await dbo.market_caps.insert(marketCaps, { onConflict: \"DoUpdate\" });\n  };\n  setInterval(getMarketCaps, 30 * SECOND);\n  getMarketCaps();\n\n  const socket = new WebSocket(\n    \"wss://fstream.binance.com/ws/!markPrice@arr@1s\",\n  );\n\n  let futuresCount: number | undefined;\n  socket.onmessage = async (rawData) => {\n    const dataItems = JSON.parse(rawData.data as string) as any[];\n    const data = dataItems.map((data) => ({\n      symbol: data.s,\n      price: data.p,\n      timestamp: new Date(data.E),\n    }));\n    console.log(`dbo.symbols.insert ${data.length} items`);\n    await dbo.symbols.insert(\n      [\n        ...data.map(({ symbol }) => ({ pair: symbol })),\n        ...FUNDING_SYMBOLS.map((pair) => ({ pair })),\n      ],\n      { onConflict: \"DoNothing\" },\n    );\n\n    futuresCount ??= await dbo.futures.count();\n    if (!futuresCount) {\n      futuresCount = 1;\n      await loadHistorcalFutures(dbo);\n      await loadHistoricalFundingRates(dbo);\n    }\n    await dbo.futures.insert(data);\n  };\n\n  const frequency = 20 * SECOND;\n\n  const markets = Object.entries(MARKETS)\n    .map(([id, data]) => ({\n      id,\n      rpcNode: (data as any).rpcNode,\n      logoLink: (data as any).logoLink,\n      data,\n    }))\n    .filter((d) => d.rpcNode);\n\n  const marketsCount = await dbo.markets.count();\n  if (!marketsCount) {\n    // const marketAthCharts = Array.from({ length: 250 }, (_, i) => i + 1).map(i => ({\n    await dbo.markets.insert(markets);\n  }\n\n  if (loadGasPrices) {\n    setInterval(async () => {\n      const markets = await dbo.markets.find();\n      markets.forEach(async (market) => {\n        const { id, rpcNode } = market;\n        const setPrice = async (price_gwei: any) => {\n          if (Number.isFinite(price_gwei)) {\n            await dbo.gas_prices.insert({ market: id, price_gwei });\n            await dbo.markets.update({ id }, { current_price: price_gwei });\n          }\n        };\n        if (id === \"btc\") {\n          const resp = await fetch(\n            \"https://mempool.space/api/v1/fees/recommended\",\n          );\n          let price_gwei = NaN;\n          try {\n            price_gwei = await resp.json().then((d) => Number(d.hourFee));\n          } catch (e) {\n            console.log(resp);\n            throw e;\n          }\n          setPrice(price_gwei);\n          return;\n        }\n        let resp;\n        try {\n          resp = await fetch(rpcNode, {\n            method: \"POST\",\n            headers: {\n              \"Content-type\": \"application/json\",\n            },\n            body: JSON.stringify({\n              jsonrpc: \"2.0\",\n              id: 1,\n              method: \"eth_gasPrice\",\n              params: [],\n            }),\n          });\n          let price_gwei = NaN;\n          try {\n            price_gwei = await resp.json().then((d) => Number(d.result) / 1e9);\n          } catch (e) {\n            console.log(resp);\n            throw e;\n          }\n          setPrice(price_gwei);\n        } catch (error) {\n          console.log(id, error, resp);\n          await dbo.markets.update(\n            { id },\n            { fail_info: { error: error.message } },\n          );\n        }\n      });\n    }, frequency);\n  }\n\n  setInterval(\n    async () => {\n      await dbo.sql(\n        `\n      DELETE FROM gas_prices\n      WHERE id IN (\n        SELECT id\n        FROM (\n          SELECT *, row_number() over( PARTITION BY market, price_gwei ORDER BY \"timestamp\" ) as dupeno\n          FROM gas_prices\n        ) t\n        WHERE dupeno > 3\n      )\n      `,\n      );\n\n      const { futures_id, gas_id } = await dbo.sql(\n        \"SELECT (SELECT MAX(id) FROM futures) as futures_id, (SELECT MAX(id) FROM gas_prices) as gas_id\",\n        {},\n        { returnType: \"row\" },\n      );\n      let truncateQuery = \"\";\n      if (futures_id > 1e6) {\n        truncateQuery += `TRUNCATE futures RESTART IDENTITY;\\n`;\n      }\n      if (gas_id > 1e5) {\n        truncateQuery += `TRUNCATE gas_prices RESTART IDENTITY;\\n`;\n      }\n      if (truncateQuery) {\n        await dbo.sql(truncateQuery);\n      }\n    },\n    60 * 60 * SECOND,\n  );\n};\nconst HISTORICAL_DATA_START = Date.now() - 7 * 24 * 60 * 60 * 1000; // 7 days ago\nconst loadHistorcalFutures = async (dbo) => {\n  for (const pair of FUNDING_SYMBOLS) {\n    const data = await fetchHistoricalFutures(\n      pair,\n      \"1m\",\n      HISTORICAL_DATA_START,\n      Date.now(),\n    );\n    console.log(`Loaded ${data.length} historical futures for ${pair}`);\n    if (!data.length) continue;\n    await dbo.futures.insert(data);\n  }\n};\n\nconst fetchHistoricalFutures = async (\n  symbol: string,\n  interval: string,\n  startTime: number,\n  endTime: number,\n) => {\n  const allData: any[][] = [];\n  let currentStart = startTime;\n\n  while (currentStart < endTime) {\n    const url = `https://fapi.binance.com/fapi/v1/klines?symbol=${symbol}&interval=${interval}&startTime=${currentStart}&endTime=${endTime}&limit=1500`;\n    const res = await fetch(url, {\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n    });\n    const data = (await res.json()) as any[];\n    if (!res.ok) {\n      console.error(\"Error fetching historical futures:\", res.statusText, data);\n      break;\n    }\n    if (!data.length) break;\n\n    allData.push(...data);\n\n    // Move start to after the last candle\n    const lastCandleOpenTime = data[data.length - 1][0];\n    currentStart = lastCandleOpenTime + 1;\n\n    // Avoid rate limits\n    await new Promise((r) => setTimeout(r, 50));\n  }\n\n  return allData.map(([openTime, open, high, low, close, volume]) => ({\n    timestamp: new Date(openTime),\n    // open: parseFloat(open),\n    // high: parseFloat(high),\n    // low: parseFloat(low),\n    symbol,\n    // close: parseFloat(close),\n    price: parseFloat(close),\n    // volume: parseFloat(volume),\n  }));\n};\n\nconst loadHistoricalFundingRates = async (dbo) => {\n  for (const symbol of FUNDING_SYMBOLS) {\n    const data = await fetchHistoricalFundingRates(\n      symbol,\n      HISTORICAL_DATA_START,\n      Date.now(),\n    );\n    console.log(`Fetched ${data.length} funding rates for ${symbol}`);\n    await dbo.futures_funding_rates.insert(data, { onConflict: \"DoNothing\" });\n    console.log(`Loaded ${data.length} funding rates for ${symbol}`);\n  }\n};\n\nconst fetchHistoricalFundingRates = async (\n  symbol: string,\n  startTime: number,\n  endTime: number,\n) => {\n  const allData: any[] = [];\n  let currentStart = startTime;\n\n  while (currentStart < endTime) {\n    const url = `https://fapi.binance.com/fapi/v1/fundingRate?symbol=${symbol}&startTime=${currentStart}&endTime=${endTime}&limit=1000`;\n    const res = await fetch(url);\n    const data = (await res.json()) as any[];\n\n    if (!data.length) break;\n\n    allData.push(...data);\n\n    const lastFundingTime = data[data.length - 1].fundingTime;\n    currentStart = lastFundingTime + 1;\n\n    await new Promise((r) => setTimeout(r, 100));\n  }\n\n  return allData.map(({ symbol, fundingTime, fundingRate, markPrice }) => ({\n    symbol,\n    funding_rate: parseFloat(fundingRate),\n    mark_price: parseFloat(markPrice),\n    funding_time: new Date(fundingTime),\n  }));\n};\n\nconst MARKETS = {\n  btc: {\n    symbol: \"BTC\",\n    chainName: \"BTC\",\n    networkName: \"Bitcoin\",\n    icon: \"coin-btc2\",\n    blockBrowser: \"https://explorer.btc.com/\",\n    rpcNode: \"https://graphql.bitquery.io/\",\n  },\n  doge: {\n    symbol: \"DOGE\",\n    chainName: \"DOGE\",\n    networkName: \"DogeCoin\",\n    icon: \"coin-doge\",\n    blockBrowser: \"https://dogechain.info\",\n    rpcNode: \"https://rpc.dogechain.dog/\",\n  },\n  kovan: {\n    symbol: \"ETH\",\n    chainName: \"ETH\",\n    networkName: \"ERC20\",\n    icon: \"coin-eth2\",\n    blockBrowser: \"https://kovan.etherscan.io\",\n    rpcNode: \"https://kovan.infura.io/v3/ba1f4e263e8a497ab6982db5917462f9\",\n  },\n  eth: {\n    symbol: \"ETH\",\n    chainName: \"ETH\",\n    networkName: \"ERC20\",\n    icon: \"coin-eth2\",\n    blockBrowser: \"https://etherscan.io\",\n    rpcNode: \"https://rpc.ankr.com/eth\",\n  },\n  ethw: {\n    symbol: \"ETHW\",\n    chainName: \"EthereumPoW\",\n    networkName: \"ERC20\",\n    icon: \"ethw\",\n    blockBrowser: \"https://www.oklink.com/en/ethw/\",\n    rpcNode: \"https://mainnet.ethereumpow.org\",\n  },\n  ethf: {\n    symbol: \"ETHF\",\n    chainName: \"EthereumFair\",\n    networkName: \"ERC20\",\n    icon: \"ethf\",\n    blockBrowser: \"https://explorer.etherfair.org\",\n    rpcNode: \"https://rpc.etherfair.org/\",\n  },\n  eos: {\n    symbol: \"EOS\",\n    chainName: \"\",\n    networkName: \"\",\n    icon: \"coin-eos\",\n  },\n  xmr: {\n    symbol: \"XMR\",\n    chainName: \"XMR\",\n    networkName: \"\",\n    icon: \"coin-xmr\",\n  },\n  bnb: {\n    symbol: \"BNB\",\n    chainName: \"BSC\",\n    networkName: \"BEP20\",\n    icon: \"coin-bnb\",\n    blockBrowser: \"https://bscscan.com\",\n    rpcNode: \"https://rpc.ankr.com/bsc\",\n  },\n  bsc: {\n    symbol: \"BNB\",\n    chainName: \"BSC\",\n    networkName: \"BEP20\",\n    icon: \"coin-bnb\",\n    blockBrowser: \"https://bscscan.com\",\n    rpcNode: \"https://rpc.ankr.com/bsc\",\n  },\n  opbnb: {\n    symbol: \"opBNB\",\n    chainName: \"opBNB\",\n    networkName: \"BEP20\",\n    icon: \"opbnb\",\n    blockBrowser: \"https://opbnbscan.com\",\n    rpcNode: \"https://opbnb-mainnet-rpc.bnbchain.org\",\n  },\n  ht: {\n    symbol: \"HT\",\n    chainName: \"HECO\",\n    networkName: \"HRC20\",\n    icon: \"coin-heco\",\n    blockBrowser: \"https://hecoinfo.com\",\n    rpcNode: \"https://http-mainnet.hecochain.com\",\n  },\n  heco: {\n    symbol: \"HT\",\n    chainName: \"HECO\",\n    networkName: \"HRC20\",\n    icon: \"coin-heco\",\n    blockBrowser: \"https://hecoinfo.com\",\n    rpcNode: \"https://http-mainnet.hecochain.com\",\n  },\n  okt: {\n    symbol: \"OKT\",\n    chainName: \"OKX Chain\",\n    networkName: \"OEC20\",\n    icon: \"coin-okex\",\n    blockBrowser: \"https://www.oklink.com/okexchain\",\n    rpcNode: \"https://exchainrpc.okex.org\",\n  },\n  sui: {\n    symbol: \"SUI\",\n    chainName: \"SUI\",\n    networkName: \"SUI\",\n    icon: \"sui\",\n    blockBrowser: \"https://explorer.sui.io\",\n    rpcNode: \"https://explorer-rpc.mainnet.sui.io\",\n  },\n  sei: {\n    symbol: \"SEI\",\n    chainName: \"SEI\",\n    networkName: \"SEI\",\n    icon: \"sei\",\n    blockBrowser: \"https://www.seiscan.app\",\n    rpcNode: \"https://rpc.sei-apis.com\",\n  },\n  ftm: {\n    symbol: \"FTM\",\n    chainName: \"Fantom\",\n    networkName: \"FRC20\",\n    icon: \"coin-ftm\",\n    blockBrowser: \"https://ftmscan.com\",\n    rpcNode: \"https://rpc.ankr.com/fantom\",\n  },\n  kcc: {\n    symbol: \"KCS\",\n    chainName: \"KCC\",\n    networkName: \"KRC20\",\n    icon: \"coin-kcc\",\n    blockBrowser: \"https://explorer.kcc.io\",\n    rpcNode: \"https://rpc-mainnet.kcc.network\",\n  },\n  xdai: {\n    symbol: \"xDAI\",\n    chainName: \"xDAI\",\n    networkName: \"XRC20\",\n    icon: \"coin-xdai\",\n    blockBrowser: \"https://gnosis.blockscout.com\",\n    rpcNode: \"https://gnosis-mainnet.public.blastapi.io\",\n  },\n  matic: {\n    symbol: \"MATIC\",\n    chainName: \"Polygon\",\n    networkName: \"MRC20\",\n    icon: \"coin-matic\",\n    blockBrowser: \"https://polygonscan.com\",\n    rpcNode: \"https://rpc.ankr.com/polygon\",\n  },\n  dot: {\n    symbol: \"DOT\",\n    chainName: \"DOT\",\n    networkName: \"Polkadot\",\n    icon: \"coin-dot\",\n  },\n  sol: {\n    symbol: \"SOL\",\n    chainName: \"Solana\",\n    networkName: \"\",\n    icon: \"coin-sol\",\n    blockBrowser: \"https://solscan.io\",\n    rpcNode:\n      \"https://solana-mainnet.phantom.app/YBPpkkN4g91xDiAnTE9r0RcMkjg0sKUIWvAfoFVJ\",\n  },\n  aptos: {\n    symbol: \"APT\",\n    chainName: \"Aptos\",\n    networkName: \"Aptos\",\n    icon: \"apt\",\n    blockBrowser: \"https://explorer.aptoslabs.com/\",\n    rpcNode: \"https://fullnode.mainnet.aptoslabs.com/v1\",\n  },\n  avax: {\n    symbol: \"AVAX\",\n    chainName: \"Avalanche C\",\n    networkName: \"ARC20\",\n    icon: \"avax\",\n    blockBrowser: \"https://snowtrace.io\",\n    rpcNode: \"https://rpc.ankr.com/avalanche\",\n  },\n  arb: {\n    symbol: \"ETH\",\n    chainName: \"Arbitrum\",\n    networkName: \"ERC20\",\n    icon: \"arb\",\n    blockBrowser: \"https://arbiscan.io\",\n    rpcNode: \"https://rpc.ankr.com/arbitrum\",\n  },\n  arbnova: {\n    symbol: \"ETH\",\n    chainName: \"Arbitrum Nova\",\n    networkName: \"ERC20\",\n    icon: \"arbnova\",\n    blockBrowser: \"https://nova.arbiscan.io\",\n    rpcNode: \"https://nova.arbitrum.io/rpc\",\n  },\n  op: {\n    symbol: \"OETH\",\n    chainName: \"Optimism\",\n    networkName: \"OP20\",\n    icon: \"coin-op\",\n    blockBrowser: \"https://optimistic.etherscan.io\",\n    rpcNode: \"https://rpc.ankr.com/optimism\",\n  },\n  celo: {\n    symbol: \"CELO\",\n    chainName: \"Celo\",\n    networkName: \"CRC20\",\n    icon: \"celo\",\n    blockBrowser: \"https://explorer.celo.org\",\n    rpcNode: \"https://forno.celo.org\",\n  },\n  atom: {\n    symbol: \"ATOM\",\n    chainName: \"ATOM\",\n    networkName: \"ATOM\",\n    icon: \"atom\",\n    blockBrowser: \"https://explorer.celo.org\",\n    rpcNode: \"https://forno.celo.org\",\n  },\n  movr: {\n    symbol: \"MOVR\",\n    chainName: \"Moonriver\",\n    networkName: \"MRC20\",\n    icon: \"movr\",\n    blockBrowser: \"https://moonriver.moonscan.io\",\n    rpcNode: \"https://rpc.moonriver.moonbeam.network\",\n  },\n  cro: {\n    symbol: \"CRO\",\n    chainName: \"Cronos\",\n    networkName: \"CRC20\",\n    icon: \"cro\",\n    blockBrowser: \"https://cronoscan.com\",\n    rpcNode: \"https://gateway.nebkas.ro\",\n  },\n  vlx: {\n    symbol: \"VLX\",\n    chainName: \"Velas\",\n    networkName: \"VRC20\",\n    icon: \"vlx\",\n    blockBrowser: \"https://evmexplorer.velas.com\",\n    rpcNode: \"https://evmexplorer.velas.com/rpc\",\n  },\n  iotx: {\n    symbol: \"IOTX\",\n    chainName: \"IoTeX\",\n    networkName: \"XRC20\",\n    icon: \"iotex\",\n    blockBrowser: \"https://iotexscan.io\",\n    rpcNode: \"https://babel-api.mainnet.iotex.io\",\n  },\n  sbch: {\n    symbol: \"BCH\",\n    chainName: \"SmartBCH\",\n    networkName: \"SEP20\",\n    icon: \"bch\",\n    blockBrowser: \"https://www.smartscan.cash/\",\n    rpcNode: \"https://smartbch.greyh.at\",\n  },\n  glmr: {\n    symbol: \"GLMR\",\n    chainName: \"Moonbeam\",\n    networkName: \"GRC20\",\n    icon: \"glmr\",\n    blockBrowser: \"https://www.moonscan.io\",\n    rpcNode: \"https://rpc.api.moonbeam.network\",\n  },\n  xdc: {\n    symbol: \"XDC\",\n    chainName: \"XinFin\",\n    networkName: \"XRC20\",\n    icon: \"xdc\",\n    blockBrowser: \"https://explorer.xinfin.network\",\n    rpcNode: \"https://rpc.xinfin.network\",\n  },\n  sdn: {\n    symbol: \"SDN\",\n    chainName: \"Shiden\",\n    networkName: \"SRC20\",\n    icon: \"sdn\",\n    blockBrowser: \"https://blockscout.com/shiden\",\n    rpcNode: \"https://shiden.api.onfinality.io/public\",\n  },\n  fuse: {\n    symbol: \"FUSE\",\n    chainName: \"Fuse\",\n    networkName: \"FRC20\",\n    icon: \"fuse\",\n    blockBrowser: \"https://explorer.fuse.io\",\n    rpcNode: \"https://rpc.fuse.io/\",\n  },\n  aac: {\n    symbol: \"AAC\",\n    chainName: \"Double-A Chain\",\n    networkName: \"ARC20\",\n    icon: \"aac\",\n    blockBrowser: \"https://scan.acuteangle.com\",\n    rpcNode: \"https://rpc.acuteangle.com\",\n  },\n  klay: {\n    symbol: \"KLAY\",\n    chainName: \"Klaytn\",\n    networkName: \"KRC20\",\n    icon: \"klay\",\n    blockBrowser: \"https://scope.klaytn.com\",\n    rpcNode: \"https://rpc.ankr.com/klaytn\",\n  },\n  one: {\n    symbol: \"ONE\",\n    chainName: \"Harmony\",\n    networkName: \"HRC20\",\n    icon: \"one\",\n    blockBrowser: \"https://explorer.harmony.one\",\n    rpcNode: \"https://api.harmony.one\",\n  },\n  evmos: {\n    symbol: \"EVMOS\",\n    chainName: \"Evmos\",\n    networkName: \"ERC20\",\n    icon: \"evmos\",\n    blockBrowser: \"https://escan.live\",\n    rpcNode: \"https://evmos.lava.build\",\n  },\n  brise: {\n    symbol: \"BRISE\",\n    chainName: \"Bitgert\",\n    networkName: \"BRC20\",\n    icon: \"brise\",\n    blockBrowser: \"https://brisescan.com\",\n    rpcNode: \"https://mainnet-rpc.brisescan.com\",\n  },\n  dogechain: {\n    symbol: \"wDOGE\",\n    chainName: \"DogeChain\",\n    networkName: \"DRC20\",\n    icon: \"dogechain\",\n    blockBrowser: \"https://explorer.dogechain.dog\",\n    rpcNode: \"https://rpc01-sg.dogechain.dog\",\n  },\n  etc: {\n    symbol: \"ETC\",\n    chainName: \"Ethereum Classic\",\n    networkName: \"ERC20\",\n    icon: \"etc\",\n    blockBrowser: \"https://etc.blockscout.com\",\n    rpcNode: \"https://etc.rivet.link\",\n  },\n  syscoin: {\n    symbol: \"SYS\",\n    chainName: \"SysCoin\",\n    networkName: \"SRC20\",\n    icon: \"syscoin\",\n    blockBrowser: \"https://explorer.syscoin.org\",\n    rpcNode: \"https://rpc.ankr.com/syscoin\",\n  },\n  canto: {\n    symbol: \"Canto\",\n    chainName: \"Canto\",\n    networkName: \"CRC20\",\n    icon: \"canto\",\n    blockBrowser: \"https://tuber.build\",\n    rpcNode: \"https://canto.gravitychain.io\",\n  },\n  onus: {\n    symbol: \"Onus\",\n    chainName: \"ONUS Chain\",\n    networkName: \"ORC20\",\n    icon: \"onus\",\n    blockBrowser: \"https://explorer.onuschain.io\",\n    rpcNode: \"https://rpc.onuschain.io\",\n  },\n  core: {\n    symbol: \"CORE\",\n    chainName: \"CORE Chain\",\n    networkName: \"CRC20\",\n    icon: \"core\",\n    blockBrowser: \"https://scan.coredao.org/\",\n    rpcNode: \"https://rpc.coredao.org\",\n  },\n  cfx: {\n    symbol: \"CFX\",\n    chainName: \"CFX\",\n    networkName: \"CFX20\",\n    icon: \"cfx\",\n    blockBrowser: \"https://evm.confluxscan.net\",\n    rpcNode: \"https://evm.confluxrpc.com\",\n  },\n  fil: {\n    symbol: \"FIL\",\n    chainName: \"FIL\",\n    networkName: \"FRC20\",\n    icon: \"fil\",\n    blockBrowser: \"https://filfox.info/en\",\n    rpcNode: \"https://node.filutils.com/rpc/v1\",\n  },\n  \"zksync-era\": {\n    symbol: \"ETH\",\n    chainName: \"zkSync Era\",\n    networkName: \"ERC20\",\n    icon: \"zksync\",\n    blockBrowser: \"https://explorer.zksync.io\",\n    rpcNode: \"https://mainnet.era.zksync.io\",\n  },\n  \"zksync-lite\": {\n    symbol: \"ETH\",\n    chainName: \"zkSync Lite\",\n    networkName: \"ERC20\",\n    icon: \"zksync\",\n    blockBrowser: \"https://zkscan.io/\",\n    rpcNode: \"https://api.zksync.io/jsrpc\",\n  },\n  \"polygon-zkevm\": {\n    symbol: \"ETH\",\n    chainName: \"Polygon zkEVM\",\n    networkName: \"ERC20\",\n    icon: \"coin-matic\",\n    blockBrowser: \"https://zkevm.polygonscan.com\",\n    rpcNode: \"https://zkevm-rpc.com\",\n  },\n  pls: {\n    symbol: \"PLS\",\n    chainName: \"PulseChain\",\n    networkName: \"ERC20\",\n    icon: \"pls\",\n    blockBrowser: \"https://scan.pulsechain.com\",\n    rpcNode: \"https://rpc.pulsechain.com\",\n  },\n  base: {\n    symbol: \"ETH\",\n    chainName: \"Base\",\n    networkName: \"ERC20\",\n    icon: \"base\",\n    blockBrowser: \"https://basescan.org\",\n    rpcNode: \"https://mainnet.base.org\",\n  },\n  linea: {\n    symbol: \"ETH\",\n    chainName: \"Linea\",\n    networkName: \"ERC20\",\n    icon: \"linea\",\n    blockBrowser: \"https://lineascan.build\",\n    rpcNode: \"https://rpc.linea.build\",\n  },\n  shibarium: {\n    symbol: \"BONE\",\n    chainName: \"Shibarium\",\n    networkName: \"ERC20\",\n    icon: \"shib\",\n    blockBrowser: \"https://shibariumscan.io\",\n    rpcNode: \"https://www.shibrpc.com\",\n  },\n  scroll: {\n    symbol: \"ETH\",\n    chainName: \"Scroll\",\n    networkName: \"SRC20\",\n    icon: \"scroll\",\n    blockBrowser: \"https://scrollscan.com\",\n    rpcNode: \"https://scroll.blockpi.network/v1/rpc/public\",\n  },\n  noa: {\n    symbol: \"NOA\",\n    chainName: \"Nostr\",\n    networkName: \"\",\n    icon: \"noa\",\n    blockBrowser: \"mainnet.nostrassets.com\",\n    rpcNode: \"\",\n  },\n  telos: {\n    symbol: \"TLOS\",\n    chainName: \"Telos\",\n    networkName: \"TRC20\",\n    icon: \"telos\",\n    blockBrowser: \"https://www.teloscan.io\",\n    rpcNode: \"https://mainnet.telos.net/evm\",\n  },\n  humanode: {\n    symbol: \"eHMND\",\n    chainName: \"Humanode\",\n    networkName: \"HRC20\",\n    icon: \"humanode\",\n    blockBrowser: \"https://humanode.subscan.io/\",\n    rpcNode: \"https://explorer-rpc-http.mainnet.stages.humanode.io/\",\n  },\n  ton: {\n    symbol: \"TON\",\n    chainName: \"TON\",\n    networkName: \"TON20\",\n    icon: \"ton\",\n    blockBrowser: \" https://tonscan.org\",\n    rpcNode: \"https://toncenter.com/api/v2/jsonRPC\",\n  },\n  ace: {\n    symbol: \"ACE\",\n    chainName: \"Endurance(ACE)\",\n    networkName: \"ACE20\",\n    icon: \"ace\",\n    blockBrowser: \"https://explorer.endurance.fusionist.io\",\n    rpcNode: \"https://rpc-endurance.fusionist.io\",\n  },\n};\n"
  },
  {
    "path": "server/sample_schemas/crypto/tableConfig.ts",
    "content": "export type TableConfig = Record<\n  string,\n  {\n    /**\n     * Column names and sql definitions\n     * */\n    columns: Record<string, string>;\n  }\n>;\n\nexport const tableConfig: TableConfig = {\n  market_caps_import: {\n    columns: {\n      v: \"JSONB\",\n    },\n  },\n  market_caps: {\n    columns: {\n      image: \"TEXT\",\n      name: \"TEXT NOT NULL\",\n      id: \"TEXT PRIMARY KEY\",\n      market_cap: \"NUMERIC\",\n      symbol: \"TEXT NOT NULL\",\n      ath: \"NUMERIC\",\n      atl: \"NUMERIC\",\n      roi: \"TEXT\",\n      low_24h: \"NUMERIC\",\n      ath_date: \"TIMESTAMPTZ\",\n      atl_date: \"TIMESTAMPTZ\",\n      high_24h: \"NUMERIC\",\n      max_supply: \"NUMERIC\",\n      last_updated: \"TIMESTAMPTZ\",\n      total_supply: \"NUMERIC\",\n      total_volume: \"NUMERIC\",\n      current_price: \"NUMERIC\",\n      market_cap_rank: \"INTEGER\",\n      price_change_24h: \"NUMERIC\",\n      circulating_supply: \"NUMERIC\",\n      ath_change_percentage: \"NUMERIC\",\n      atl_change_percentage: \"NUMERIC\",\n      market_cap_change_24h: \"NUMERIC\",\n      fully_diluted_valuation: \"NUMERIC\",\n      price_change_percentage_24h: \"NUMERIC\",\n      market_cap_change_percentage_24h: \"NUMERIC\",\n    },\n  },\n  symbols: {\n    columns: {\n      pair: \"TEXT PRIMARY KEY\",\n    },\n  },\n  futures: {\n    columns: {\n      id: \"SERIAL PRIMARY KEY\",\n      symbol: \"TEXT NOT NULL REFERENCES symbols\",\n      price: \"NUMERIC NOT NULL\",\n      timestamp: \"TIMESTAMPTZ NOT NULL\",\n    },\n  },\n  futures_funding_rates: {\n    columns: {\n      id: `SERIAL PRIMARY KEY`,\n      symbol: \"TEXT NOT NULL REFERENCES symbols\",\n      funding_rate: `DECIMAL(18, 10) NOT NULL`,\n      mark_price: `DECIMAL(18, 8)`,\n      funding_time: `TIMESTAMP NOT NULL`,\n    },\n    // UNIQUE(symbol, funding_time)\n  },\n  markets: {\n    columns: {\n      id: \"TEXT PRIMARY KEY\",\n      current_price: \"NUMERIC\",\n      rpcNode: \"TEXT\",\n      logoLink: \"TEXT\",\n      fail_info: \"JSON\",\n      data: \"JSONB NOT NULL\",\n    },\n  },\n  gas_prices: {\n    columns: {\n      id: \"SERIAL PRIMARY KEY\",\n      market: \"TEXT NOT NULL REFERENCES markets\",\n      price_gwei: \"NUMERIC NOT NULL\",\n      timestamp: \"TIMESTAMPTZ NOT NULL DEFAULT NOW()\",\n    },\n  },\n};\n"
  },
  {
    "path": "server/sample_schemas/crypto/workspaceConfig.ts",
    "content": "// Using the below typescript definition return\n// a json workspace/dashboard config for a property management company ensuring all table and column names are snake case:\n\ntype WorkspaceConfig = {\n  workspaces: {\n    name: string;\n    options?: any;\n    layout?: any;\n    windows: {\n      type: \"table\";\n      table_name: string;\n      columns?: {\n        name: string;\n        show?: boolean;\n        width?: number;\n        nested?: any;\n        style?:\n          | {\n              type: \"Conditional\";\n              conditions: {\n                color: string;\n                operator: \"=\";\n                chipColor: string;\n                condition: string;\n                textColor: string;\n                borderColor: string;\n              }[];\n            }\n          | {\n              type: \"Barchart\";\n              barColor: string;\n              textColor: string;\n            };\n        format?: any;\n      }[];\n      options?: any;\n      sort?: { asc: boolean; key: string }[];\n    }[];\n  }[];\n};\n\nexport const workspaceConfig: WorkspaceConfig = {\n  workspaces: [\n    {\n      name: \"Crypto markets\",\n      options: {\n        hideCounts: false,\n        pinnedMenu: true,\n        tableListSortBy: \"extraInfo\",\n        tableListEndInfo: \"count\",\n        defaultLayoutType: \"tab\",\n      },\n      windows: [\n        {\n          type: \"table\",\n          table_name: \"market_caps\",\n          options: {\n            refresh: {\n              type: \"Realtime\",\n              intervalSeconds: 3,\n              throttleSeconds: 0,\n            },\n            maxCellChars: 500,\n          },\n          columns: [\n            {\n              name: \" \",\n              width: 50,\n            },\n            {\n              name: \"image\",\n              show: true,\n              width: 60,\n              format: {\n                type: \"Media\",\n                params: {\n                  type: \"fixed\",\n                  fixedContentType: \"image\",\n                },\n              },\n            },\n            {\n              name: \"name\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"id\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"market_cap\",\n              show: true,\n              style: {\n                type: \"Barchart\",\n                barColor: \"#05b0df\",\n                textColor: \"#646464\",\n              },\n              width: 100,\n            },\n            {\n              name: \"symbol\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"ath\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"atl\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"roi\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"low_24h\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"ath_date\",\n              show: true,\n              width: 100,\n              format: {\n                type: \"Age\",\n                params: {\n                  variant: {\n                    type: \"short\",\n                  },\n                },\n              },\n            },\n            {\n              name: \"atl_date\",\n              show: true,\n              width: 100,\n              format: {\n                type: \"Age\",\n                params: {\n                  variant: {\n                    type: \"short\",\n                  },\n                },\n              },\n            },\n            {\n              name: \"high_24h\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"max_supply\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"last_updated\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"total_supply\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"total_volume\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"current_price\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"market_cap_rank\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"price_change_24h\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"circulating_supply\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"ath_change_percentage\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"atl_change_percentage\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"market_cap_change_24h\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"fully_diluted_valuation\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"price_change_percentage_24h\",\n              show: true,\n              width: 100,\n            },\n            {\n              name: \"market_cap_change_percentage_24h\",\n              show: true,\n              width: 100,\n            },\n          ],\n          sort: [\n            {\n              asc: false,\n              key: \"market_cap\",\n            },\n          ],\n        },\n        {\n          type: \"table\",\n          table_name: \"markets\",\n          options: {\n            refresh: {\n              type: \"Realtime\",\n              intervalSeconds: 3,\n              throttleSeconds: 0,\n            },\n            maxCellChars: 500,\n          },\n          columns: [\n            {\n              name: \"gas_prices\",\n              show: true,\n              width: 250,\n              nested: {\n                path: [\n                  {\n                    table: \"gas_prices\",\n                    on: [\n                      {\n                        id: \"market\",\n                      },\n                    ],\n                  },\n                ],\n                chart: {\n                  type: \"time\",\n                  yAxis: {\n                    colName: \"price_gwei\",\n                    funcName: \"$avg\",\n                    isCountAll: false,\n                  },\n                  dateCol: \"timestamp\",\n                  renderStyle: \"smooth-line\",\n                },\n                limit: 200,\n                columns: [\n                  {\n                    name: \"id\",\n                    show: true,\n                  },\n                  {\n                    name: \"market\",\n                    show: true,\n                  },\n                  {\n                    name: \"price_gwei\",\n                    show: true,\n                  },\n                  {\n                    name: \"timestamp\",\n                    show: true,\n                  },\n                ],\n                joinType: \"left\",\n              },\n            },\n          ],\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "server/sample_schemas/financial.sql",
    "content": "\nCREATE TABLE users (\n  id SERIAL PRIMARY KEY,\n  email VARCHAR(100) NOT NULL UNIQUE,\n  password VARCHAR(100) NOT NULL,\n  first_name VARCHAR(50) NOT NULL,\n  last_name VARCHAR(50) NOT NULL,\n  phone_number VARCHAR(20) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE institutions (\n  id SERIAL PRIMARY KEY,\n  name VARCHAR(100) NOT NULL,\n  logo BYTEA,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE accounts (\n  id SERIAL PRIMARY KEY,\n  user_id INTEGER NOT NULL REFERENCES users(id),\n  institution_id INTEGER NOT NULL REFERENCES institutions(id), \n  access_token VARCHAR(100) NOT NULL,\n  account_type VARCHAR(20) NOT NULL,\n  balance NUMERIC(10,2) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE transactions (\n  id SERIAL PRIMARY KEY,\n  account_id INTEGER NOT NULL REFERENCES accounts(id),\n  transaction_id VARCHAR(100) NOT NULL,\n  transaction_type VARCHAR(20) NOT NULL,\n  amount NUMERIC(10,2) NOT NULL,\n  iso_currency_code CHAR(3) NOT NULL,\n  merchant_name VARCHAR(100) NOT NULL,\n  merchant_category VARCHAR(100) NOT NULL,\n  merchant_city VARCHAR(50) NOT NULL,\n  merchant_state CHAR(2) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n\nCREATE TABLE registrations (\n  id SERIAL PRIMARY KEY,\n  user_id INTEGER NOT NULL REFERENCES users(id),\n  email_verification_code VARCHAR(100) NOT NULL,\n  is_email_verified BOOLEAN NOT NULL DEFAULT FALSE,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);"
  },
  {
    "path": "server/sample_schemas/food_delivery/connection.ts",
    "content": "const table_options = {\n  users: {\n    icon: \"Account\",\n  },\n  orders: {\n    icon: \"FormatListBulletedSquare\",\n  },\n  ratings: {\n    icon: \"StarCheckOutline\",\n  },\n  addresses: {\n    icon: \"MapMarker\",\n  },\n  menu_items: {\n    icon: \"Food\",\n  },\n  order_items: {\n    icon: \"ClipboardListOutline\",\n  },\n  restaurants: {\n    icon: \"StoreMarker\",\n  },\n  user_addresses: {\n    icon: \"MapMarkerAccount\",\n  },\n  order_status_types: {\n    icon: \"TimerMarkerOutline\",\n  },\n  restaurant_managers: {\n    icon: \"AccountStar\",\n  },\n};\n\nexport default {\n  table_options,\n};\n"
  },
  {
    "path": "server/sample_schemas/food_delivery/databaseConfig.ts",
    "content": "const table_schema_positions = {\n  users: {\n    x: -1174.751805206928,\n    y: -598.113379359937,\n  },\n  orders: {\n    x: -420.92202582001323,\n    y: -1012.6362334972146,\n  },\n  ratings: {\n    x: -408.59482334284866,\n    y: -414.0255935352699,\n  },\n  customers: {\n    x: 968.6312623730562,\n    y: -424.00150372798294,\n  },\n  v_riders: {\n    x: 642,\n    y: 920,\n  },\n  addresses: {\n    x: -1149.9951277241566,\n    y: -991.5015729329936,\n  },\n  menu_items: {\n    x: -402.16855002029143,\n    y: -146.4190735982366,\n  },\n  user_types: {\n    x: -1484.0931586178083,\n    y: -566.8350226338544,\n  },\n  _menu_items: {\n    x: 700,\n    y: -83,\n  },\n  order_items: {\n    x: -51.12689031607691,\n    y: -856.1062361183423,\n  },\n  restaurants: {\n    x: -755.0858767099027,\n    y: -524.7197305176628,\n  },\n  customers_info: {\n    x: 883,\n    y: 682,\n  },\n  order_updates: {\n    x: -61.622649121343045,\n    y: -1126.1358619254,\n  },\n  v_restaurants: {\n    x: 282,\n    y: 920,\n  },\n  user_addresses: {\n    x: -750.203925184484,\n    y: -1095.6041244601697,\n  },\n  routes: {\n    x: 980,\n    y: 444,\n  },\n  spatial_ref_sys: {\n    x: 980,\n    y: 920,\n  },\n  geometry_columns: {\n    x: 414,\n    y: 637,\n  },\n  geography_columns: {\n    x: 693,\n    y: 429,\n  },\n  order_status_types: {\n    x: -758.6129589151711,\n    y: -901.934205261114,\n  },\n  restaurant_managers: {\n    x: -419.5108671726053,\n    y: -587.9239333420907,\n  },\n  customers_info_view: {\n    x: -30,\n    y: 920,\n  },\n  delivery_status_types: {\n    x: -105.19710678250252,\n    y: -373.49180690996263,\n  },\n  delivery_status_changes: {\n    x: -61.28934921994736,\n    y: -577.6752137133151,\n  },\n  '\"london_restaurants.geojson\"': {\n    x: 980,\n    y: 100,\n  },\n};\nconst table_schema_transform = {\n  scale: 0.917348432601495,\n  translate: { x: 1400, y: 1116 },\n};\n\nexport default {\n  table_schema_positions,\n  table_schema_transform,\n};\n"
  },
  {
    "path": "server/sample_schemas/food_delivery/onInit.sql",
    "content": "CREATE EXTENSION IF NOT EXISTS postgis;\n\nSET statement_timeout TO 220e3;\n\nDROP TABLE IF EXISTS  delivery_status_types , menu_items, menus, order_items, orders, \nratings, restaurants, user_types, users, order_status_types, user_addresses,\norder_updates, delivery_status_changes, addresses, _menu_items CASCADE;\n\nSELECT row_number() over() as id, *\nINTO _menu_items\nFROM (\n  VALUES \n    ('Drinks', 'Coke', '20 oz. Bottle', '4.09 USD'),\n    ('Entrees', 'Pasta Marinara', 'Penne pasta topped with marinara and Parmesan Cheese.', '11.99 USD'),\n    ('Veggie Cravings', 'Black Bean Chalupa Supreme®', 'A chewy chalupa shell filled with black beans, reduced-fat sour cream, lettuce, tomatoes, and three-cheese blend.', '5.03 USD'),\n    ('Sandwiches', 'Sweet Onion Chicken Teriyaki Footlong Regular Sub', 'The Sweet Onion Chicken Teriyaki sub is one sah-weeet sub. It starts with Hearty Multigrain bread, add perfectly cooked grilled chicken strips, marinated in our NEW Sweet Onion Teriyaki Sauce, then pile on the crunch with lettuce, spinach, tomatoes, cucumbers, green peppers, red onions and top with another pass of our NEW Sweet Onion Teriyaki sauce.', '11.49 USD'),\n    ('Breakfast Sides', 'Pancakes', '', '1.89 USD'),\n    ('Chicken', 'Almond Chicken', 'Served with steamed rice or brown rice.', '14.95 USD'),\n    ('Picked for you', 'Soft Drinks', 'Coca-Cola® products. ', '3.49 USD'),\n    ('Menu ', 'Dry Wok Spicy Beef', '', '14.95 USD'),\n    ('Hold-Me-Overs', 'Chili', 'cheddar, sour cream, scallions, garlic toast', '8.0 USD'),\n    ('Burgers', 'Pastrami', 'Pastrami, Swiss cheese, spiked horseradish, and bavarian mustard sauce.', '16.0 USD'),\n    ('Soup', 'Wonton Soup', 'Served with crispy noodles.', '5.99 USD'),\n    ('Mains', '9. Pork Loin Katsu Curry Udon or Rice', 'Pork Loin Katsu with Japanese Curry. Choice of Udon or Rice', '14.99 USD'),\n    ('Breakfast Sides', 'Smothered Home Fries', 'Served with onion and cheese.', '6.5 USD'),\n    ('Beverages', 'Cappuccino', 'Regular (130 Cal.), Large (160 Cal.) Espresso with steamed milk, topped with a cap of foam. Allergens: Contains Milk', '4.99 USD'),\n    ('Salad', 'Green Salad', '', '7.25 USD'),\n    ('Sides', 'Steak Fries', 'Thick cut and fried to perfection with seasoning.', '3.49 USD'),\n    ('Bowls', 'Supreme Fried Rice', 'Steak, chicken &amp; shrimp stir-fried with our signature fried rice recipe with carrots, green onions &amp; egg. ', '15.59 USD'),\n    ('Goat Entries', 'Goat Mandhakini(Mutton Mandhakini)', 'Semi gravy goat cooked with onions,chillies and chopped boiled egg. The entry is served with white rice on the side. Bread can add as on order.   ', '18.99 USD'),\n    ('Appetizers', 'French Fries', 'Deep fried extra crispy cut potatoes.', '5.0 USD'),\n    ('Starters', 'Veg Bonda', '', '7.0 USD'),\n    ('Fresh Melts®', 'Meatball Marinara Melt Footlong Melt', 'Add some melty goodness to your Meatball Marinara sub. Make it a Fresh Melt™ and get craveable meatballs and tangy marinara sauce topped with American and parmesan, all grilled to cheesy perfection. Freshly made in front of you.', '10.79 USD'),\n    ('Kids', 'Kids Crispy Chicken Tenders', 'Two or Three breaded chicken tenders.', '6.59 USD'),\n    ('OTC INTERNAL', 'JARABE COUGH CONGEST LQ 8Z', 'Vicks Jarabe Cough and Congestion Cold Syrup – the same powerful cough and congestion relief you used at home, now available in the United States and brought to you by VICKS, the world’s #1 selling cough and cold brand. Vicks Jarabe coats and soothes your throat by providing a refreshing sensation. Each dose contains a cough suppressant that helps control and relieve cough (Dextromethorphan HBr) and an expectorant (Guaifenesin) that helps loosen phlegm (mucus).', '10.49 USD'),\n    ('Dips &amp; Sauces', 'Ranch Dip (mild)', 'The coolest ranch deserves to go on a wing, not a chip. Luckily you already know how good it will be.', '0.5 USD'),\n    ('Large Pizzas', 'LG Donkey Dog', 'Whole wheat dough, black bean dip, whole beans, sweet corn, mozzarella and cheddar cheese. Topped with shredded lettuce, pico de gallo and avocado. Served with taco sauce on the side.', '23.95 USD'),\n    ('South/North Indian Tiffins', 'Pesarattu', 'Thin crepe made with green gram and rice.', '9.49 USD'),\n    ('Specials', 'Chicken Enchiladas + Favoritos® Special', 'Two chicken enchiladas, your choice Mexicanas or chipotle, served with rice and beans, plus a favoritos® of your choice.', '10.61 USD'),\n    ('Sandwiches &amp; Wraps', 'Tomato Pesto Grilled Cheese', 'Roasted tomatoes, pesto spread and melted white cheddar cheese between two pieces of toasted sourdough bread.', '0.0 USD'),\n    ('MEZZE', 'RED PEPPER BABAGANOUSH + ZA''ATAR PITA', 'Creamy and flavorful smoked eggplant, roasted red peppers, tahini &amp; a blend of spices.   Served with a za''atar pita. Pint includes 2 pitas and a quart comes with 4 pitas.', '6.3 USD'),\n    ('Dolce''s Stromboli', 'Regular Dolce Stromboli', 'Served with two toppings. Comes with one cup marinara sauce. Please contact the merchant for soda selection.', '8.99 USD'),\n    ('Rice Entrees', 'P32 Spicy Pad Ped Nor Mai ****', 'Stir-fried bamboo shoot strips with garlic, Thai chili, basil and herbs. Hot and Spicy!', '13.0 USD'),\n    ('LEGACY', 'FRY ME TO THE MOON', 'fries, waffle fries, cheddar, bacon, scallion (gringo dip or chipotle ranch)', '11.1 USD'),\n    ('Sashimi', 'Freshwater Eel Sashimi (3 pcs)', 'Unagi', '9.99 USD'),\n    ('Picked for you', 'Korean Tacos', '2 tacos.', '7.0 USD'),\n    ('Picked for you', '10 Pack Traditional Wings', 'Our traditional wings are lightly breaded and cooked to perfection then tossed in your favorite sauce or dry rub.', '17.69 USD'),\n    ('Breakfast', 'Egg White Omelet - Applewood Smoked Bacon', 'Contains: Cheddar, Applewood Smoked Bacon, Fresh Salsa, Spinach, Egg White Omelet, Tortilla Burrito', '4.59 USD'),\n    ('Curtains & Window', 'Umbra® Cafe 84 to 120-Inch Adjustable Curtain Rod in Bronze', 'Lend your window style a welcoming, homey feel with the Cafe Adjustable Curtain Rod from Umbra. Perfectly crafted to accent your drapery, these window treatments inject a modern look with great functionality into your home.', '24.99 USD'),\n    ('Popular Items', 'Nachos Tradicionales', 'Round yellow tortilla chips with nacho cheese.', '4.5 USD'),\n    ('Desserts', 'Cheesecake', '', '4.89 USD'),\n    ('Chinese Entrees Seafood Combo', 'Shrimp with Asparagus', 'Served with 2 crab cheese wonton, ham fried rice, and soup.', '15.95 USD'),\n    ('Dipping Sauces', 'Marinara Dip', '', '0.88 USD'),\n    (E'Enjoy A Treat', 'Banana Berry Treat®', 'Bananas, Strawberries*, Raspberries, Papaya Juice Blend, Dairy Whey Blend *contains added sugar  300 - 600 Calories  Allergens:    Dairy Whey Blend (milk, egg)', '6.54 USD'),\n    ('Fried Rice and Noodles', 'Thai Lo Mien', '', '12.0 USD'),\n    ('Picked for you', 'Cinnamon Sugar Churros', 'Chocolate- and caramel-filled churros with chocolate dipping sauce.', '5.99 USD'),\n    ('Picked for you', 'Cajun Pasta Combo with 6 Asian Wingz', 'Cajun Alfredo Pasta with sautéed trinity shrimp and crawfish tails and Texas toast. 6 Fried Wingz tossed in our Famous Asian Sauce. ', '27.49 USD'),\n    ('Desserts', 'One Personal Square Original DC Sweet Potato Cake', '', '6.0 USD'),\n    ('Bread & Bakery', 'Arnold · Potato Hot Dog Buns (8 buns)', '8 buns', '3.83 USD'),\n    ('Burritos', 'El Toro''s Grilled Chicken Burrito', 'A burrito filled with grilled chicken, rice, beans, lettuce, tomatoes, cheese and sour cream', '17.5 USD'),\n    ('Pizza', 'Loaded Tot-zza Pizza', 'Ranch sauce topped with 100% real Wisconsin mozzarella cheese, tater tots, applewood smoked bacon, green onions and drizzled with nacho cheese.', '0.0 USD'),\n    ('Soups', 'Tom Yum Soups cup', '', '4.99 USD'),\n    ('Regular FAVORITES', '#17 Ultimate Porker', 'HAM &amp; BACON lettuce, tomato &amp; mayo ', '8.39 USD'),\n    ('Energy &amp; Electrolyte Drinks', '5-Hour Energy Berry or Grape (1.93 oz)', '', '4.39 USD'),\n    ('Picked for you', 'Dumplings (饺子) (6 pcs)', 'Steamed or pan-fried.', '8.35 USD'),\n    ('Classic Roll or Hand Roll', 'Futo Maki', 'Crabstick, avocado, squash, cucumber, egg, and oshinko.', '9.0 USD'),\n    ('The Sushi', 'The Tuna Roll', 'Tuna roll with sashimi grade tuna.', '11.99 USD'),\n    ('Home Goods', 'Festive Voice Bird With Sound - 1.0 ea', 'Hear me Sing & Dance May be used indoors or in sheltered outdoor areas Motion sensor Requires two 1.5V LR03 (AAA) batteries Made in CHINA', '10.48 USD'),\n    ('BLIZZARD® Treats', 'Choco Brownie Extreme BLIZZARD® Treat', 'Chewy brownie pieces, choco chunks and cocoa fudge blended with creamy DQ® vanilla soft serve to BLIZZARD® perfection.', '5.11 USD'),\n    ('Full Menu', 'Trap Butter', 'Our special butter sauce with our gold seasoning at the bottom makes a great combination.', '1.2 USD'),\n    ('Sandwiches', 'Baja Turkey Avocado 6 Inch Regular Sub', 'Oven-Roasted Turkey, Smashed Avocado, and crisp veggies, topped with Baja Chipotle sauce: this one is all about that bold, smoky and spicy flavor!', '11.25 USD'),\n    ('Salad', 'Side Garden', '', '3.5 USD'),\n    ('Vodka', 'Grey Goose Essences Strawberry &amp; Lemongrass (ABV30%)', '0-Carbs  0-Sugar All Natural Ingredients', '36.99 USD'),\n    ('Home Health Care Solutions', 'Walgreens Diabetic Crew Socks for Women 6-10 - 1.0 pr', 'Dress For Comfort Walgreens Pharmacist Recommended√¢‚Ç¨¬† Non-Binding Extra Wide Top Helps Keep Feet Dry Smooth, Small Toe Seam Odor Resistant Women''s Shoe Size 6-10 Made in China √¢‚Ç¨¬†Walgreens Pharmacist Survey Study, November 2010.', '7.65 USD'),\n    ('Drinks', 'Jarritos Pineapple', ' [Cal 140]', '3.85 USD'),\n    ('Hamburgers', 'American Cheese Hamburger', 'Served with potatoes (servidos con papas).', '8.5 USD'),\n    ('Beverages', '1/2 Gallon Beverage Bucket', 'Select an ice-cold beverage.', '4.79 USD'),\n    ('Ice Cream to Share', 'Pre-Packed Quart', '24 oz. of your favorite ice cream flavor - enough to share...or not!', '0.0 USD'),\n    ('Noodles &amp; Fried Rice', 'Yaki Soba', '', '8.95 USD'),\n    ('Dessert', 'Hot Chocolate Cake', 'Served with mocha almond fudge ice cream and fennel seed brittle.', '11.0 USD'),\n    ('Desserts', 'Chocolate Dipped Banana', 'Whole frozen banana dipped in dark chocolate and decorated with your choice of white chocolate drizzle, chopped almonds, or rainbow sprinkles served on a popsicle stick. ', '0.0 USD'),\n    ('Tea &amp; Lemonade', 'Gold Peak Sweet Tea 18.5oz', 'Inspired by the comfort of home, to create delicious, naturally flavored iced teas and coffees. Explore Gold Peak® flavors and enjoy home-brewed taste today!', '0.0 USD'),\n    ('Bites', 'The Wings', 'Pub favorite, traditional can be gluten-friendly. Traditional or boneless wings cooked to perfection and tossed in your choice of buffalo, garlic buffalo, honey sriracha, stout bbq, sweet Asian, lemon pepper, butter garlic parmesan, mango habanero, or a hot dry rub.', '11.0 USD'),\n    ('Appetizers', 'Meatball sliders', '', '8.99 USD'),\n    ('Chips &amp; Snacks', 'Slim Jim Giant Slim 0.97 oz', 'When it comes to snacking, they say size matters. That''s why slim jim giant mild flavor smoked meat stick has a big, meaty flavor that will please the ginormous meat-lover in you; with 6 grams of protein in each serving, this meat stick will easily please your need for beef. Slim jim giant smoked meat stick satisfy even the beefiest appetites. Individually wrapped so you can enjoy a king-size snack anywhere you want. So, go ahead and fill your kitchen''s pantry and please your carnivorous palate.', '1.99 USD'),\n    ('SUB IN A BOWL', 'Double Chicken Cheese Steak', 'Double chicken &amp; provolone cheese with spring mix, onions, tomatoes, salt &amp; your choice of dressing', '14.35 USD'),\n    ('Tandoori Special', 'Beef Chapli Kebab', 'Minced meat with chopped vegetable and spices.', '11.99 USD'),\n    ('Rock and Raw', 'Red Tuna Nigiri', 'Comes with two pieces.', '4.45 USD'),\n    ('Salad', 'Larb Gai', 'Chicken salad. Minced chicken mixed with red onions and chili flakes stirred with roasted grounded rice and cilantro tossed with spicy lime dressing. ', '8.0 USD'),\n    ('Pesce', 'Cacciucco', 'Mussels, clams, prawns, calamari, fish sauteed in a spicy tomato sauce.', '36.0 USD'),\n    ('TO GO Sauce ', 'YUM YUM SAUCE', 'WHITE SAUCE FOR HIABCHI ', '0.25 USD'),\n    ('Burritos', 'Beefy 5-Layer Burrito', 'A warm tortilla is covered in a layer of warm nacho cheese and topped with seasoned beef, refried beans, cool sour cream and shredded cheddar cheese. Then it''s wrapped creating a layer of nacho cheese all around the outside of the burrito.', '3.95 USD'),\n    ('Traditional Wings', 'Traditional Bone-In Wings 6 Pcs', '6 pieces. Enjoy our juicy traditional wings in your choice of wing sauce. Comes with a side of ranch dipping sauce. ', '9.89 USD'),\n    ('Asian Street Noodles', 'Chow Fun with Chinese Broccoli', '', '15.95 USD'),\n    ('Snacks', 'Hostess Donettes Frosted Chocolate 6 Count', 'Mini donuts frosted with chocolate', '3.09 USD'),\n    ('Families, Groups &amp; Grocery', 'Project Sunrise Retail Coffee ', '12 oz bag of our Project Sunrise ground coffee. Buy a bag to support our female growers in Huila, Colombia!', '9.51 USD'),\n    ('Specials', 'Handmade Ravioli', 'Handmade ravioli filled with artisan ricotta and organic spinach served with sage-infused butter and Parmigiano 24 months.', '15.9 USD'),\n    ('Seafood', 'Fish Fajitas', 'Fish grilled with bell peppers, onions, and tomatoes. Served with a side of beans, guacamole salad, sour cream, and flour tortillas.', '14.99 USD'),\n    ('Burgers for Breakfast', 'Triple Whopper Meal', '', '15.19 USD'),\n    ('Picked for you', 'Vietnamese ice coffee', '', '4.95 USD'),\n    ('SIDE - EXTRA ORDER', 'RAITA - 8OZ ', 'YOGURT WITH ONION AND SPICES - 16 OZ ', '4.99 USD'),\n    ('Something Fried', 'Chicken Wing Basket (8 pcs)', '8 pieces. The basket comes with cajun fries.', '17.0 USD'),\n    ('No Bready Bowls™', 'Black Forest Ham', 'A Footlong’s worth of protein? Yup! When you make it a Protein Bowl you’ll get all of the Black Forest ham you’d get on your favorite Footlong, piled high atop fresh lettuce, tomato, cucumber and more veggies.', '9.49 USD'),\n    ('Diet & Nutrition', 'BOOST Original Nutritional Drink, Rich Chocolate, 6 CT', 'BOOST Original Rich Chocolate Flavored Nutritional Drink is a nutritional shake that provides balanced nutrition as part of your daily diet. This rich chocolate drink provides 10 grams of protein and 27 vitamins and minerals including vitamins C &amp; D, zinc, iron and selenium, key nutrients for immune support. Not only does this chocolate shake include calcium and Vitamin D to support strong bones, it is designed to provide energy with 240 nutrient-rich calories and B-vitamins to help convert food to energy. Each pack includes 6 nutritional drinks packaged in 8 fluid ounce reclosable bottles. Six pack of 8 fl oz bottles of BOOST Original Rich Chocolate Nutritional Drink Nutritional chocolate shake with a rich chocolate taste you&#39;ll love Nutrition shakes with 10 g of protein and 27 vitamins and minerals Designed to provide nutritional energy with 240 nutrient-rich calories and B-vitamins to help convert food to energy Nutritional shakes that provide essential nutrients with complete and balanced nutrition, and contain no artificial flavors, colors or sweeteners and are gluten free Now with 25%25 less sugars than the former drink, sugar content has been lowered from 20 g to 15 g per serving, all with great taste Packaged in easy to open and resealable plastic bottles ', '9.99 USD'),\n    ('Beverages', 'Sunjoy® (1/2 Sweet Tea, 1/2 Lemonade)', 'A refreshing combination of our classic Chick-fil-A® Lemonade and freshly-brewed Sweetened Iced Tea. Also available with combinations of Chick-fil-A® Diet Lemonade or Unsweetened Iced Tea.', '3.09 USD'),\n    ('Health & Beauty', 'Tylenol PM - 24 Count', '', '6.91 USD'),\n    ('Specialty Sandwiches', 'Hot and Spicy Signature Recipes - Southwest Chipotle Turkey', 'Contains: Pepper Jack, Spinach, Fresh Salsa, Creamy Chipotle, Panini Bread, Meat', '8.39 USD'),\n    ('Combinaciones', '#26 - 3 Crispy Tacos', 'Choice of meat: chicken, shredded beef or ground beef. Loaded with lettuce, tomato, and Cheddar cheese.', '14.99 USD'),\n    ('Fruit Smoothies', 'Mixed Berry', '', '4.79 USD'),\n    ('Allergy Season', 'Allegra Children''s 12HR Liquid Berry - 4.0 fl oz', 'Kid allergies are unpleasant. Taking their allergy medicine doesn''t have to be. Alleviate their worst allergy symptoms with Allegra Children''s Non-Drowsy Antihistamine Liquid Berry Flavor. Children''s Allegra for kids 2 years and older provides 12 full hours of non-drowsy relief from sneezing, runny nose, itchy and watery eyes, and itchy nose or throat. Use Allegra Children''s Liquid for indoor and outdoor allergies, including seasonal allergies and pet allergies. Best of all, this berry-flavored', '14.68 USD'),\n    ('PARTY PACKS', '40 BONELESS WINGS', '40 Boneless wings with your choice of Classic Buffalo, This is Q''d Up, Honey Trap Mustard or Birthday Suit (Naked).', '56.0 USD'),\n    ('Picked for you', 'Philly Chicken Wrap', '', '5.95 USD'),\n    ('Pollo', 'Pollo Pesto', 'Chicken, grape tomatoes, and mushrooms in a creamy pesto sauce over penne.', '19.18 USD'),\n    ('VITAMINS', 'ZARBEES CHLD SLEEP GUM50CT', 'Zarbee''s Naturals Children''s Melatonin 1mg is a drug-free, alcohol-free supplement for occasional sleeplessness in children ages 3 and up. Our natural kids’ supplement helps promote restful sleep without next day grogginess. Melatonin is a hormone the brain produces to help regulate sleep & wake cycles. Safe for occasional sleeplessness in children, Melatonin is non-habit forming and will help gently guide your child to sleep. We’ll BEE there! Check out our whole line of products made of handpicked wholesome ingredients.', '19.99 USD'),\n    ('Sandwiches', 'Buffalo Chicken Footlong Pro (Double Protein)', 'When you’re looking to spice things up, do it with Frank’s RedHot® and buffalo chicken. Our Buffalo Chicken Footlong is made with everyone’s favorite hot sauce – Frank’s RedHot® and topped with peppercorn ranch. Try it with lettuce, tomatoes and cucumbers! Frank’s RedHot® is a registered trademark of McCormick &amp; Co. and used under license by Subway Franchisee Advertising Fund Trust Ltd.®/© Subway IP LLC 2021.', '14.19 USD'),\n    ('Alternatives', 'Chai tea', '', '5.25 USD'),\n    ('Asian Fusion Selections', 'Beef Broccoli', 'Served with miso soup and salad.', '23.25 USD'),\n    ('Tenders &amp; Nuggets', '16 Tenders Family Bucket Meal', 'Feeds 7-8. 16 pieces of our freshly prepared Extra Crispy Tenders, 4 large sides of your choice, 8 biscuits and 8 dipping sauces.', '50.39 USD'),\n    ('Frijoles Antojitos / Beans Appetizers', 'Frijoles Grandes de Bistec / Large order of Beans with grilled Steak', '', '8.99 USD'),\n    ('Dinner Entrees', 'Fettucine Alfredo', 'A creamy Alfredo sauce over fettuccine noodles.', '7.99 USD'),\n    ('Hamburgers', 'Bourbon Bacon Cheeseburger', 'A quarter-pound* of fresh, never-frozen beef topped with Applewood smoked bacon, American cheese, crispy onions, and a sweet, smoky bourbon bacon sauce that is, essentially, a sauce made with real bourbon and real bacon. Read that part about the sauce again, and we’ll see you soon.', '6.92 USD'),\n    ('Salads', 'Baja Turkey Avocado', 'Spice up your salad with Oven-Roasted Turkey, and Smashed Avocado, on a bed of greens and crisp veggies and topped with Baja Chipotle sauce.', '11.99 USD'),\n    ('Sandwiches, Wraps &amp; Burgers', 'The Ultimate Burger', 'Savory, choice gourmet burger, a mix of melted cheese, topped with lettuce, tomatoes, pickles, onions and your favorite sauce. Then choose from the custom list of additional options.', '12.49 USD'),\n    ('Scotch Whiskey', 'BUCHANANS DELUXE 12YR 1L', '', '64.99 USD'),\n    ('Sides', 'Side Chorizo', '', '4.0 USD'),\n    ('Vegetarian Special ', 'Tofu Stir-Fried Vegetable', '', '14.99 USD'),\n    ('Sake', 'Bacardi Classic Cocktails Drinks Strawberry Daiquiri,1.75L shake (15%ABV)', '', '21.99 USD'),\n    ('Signature French Macarons', 'Birthday Cake French Macaron', '', '2.75 USD'),\n    ('TOYS', 'DOUBLE SIX WHITE DOMINOES', 'Bring some old-fashioned family fun to game night with this high quality Dominoes set. Double Six Dominoes is perfect for beginners and experts. The starter piece enhances gameplay while the bright colored dots make gameplay easier. This Double Six Dominoes set includes 28 durable, color-coded, ivory-colored dominoes that stack neatly into a durable storage tin and a starter piece. Learn to play different variations with the included instructions.', '9.99 USD'),\n    ('Spices', 'Derica 400g', '', '4.59 USD'),\n    ('House Favorites', '1 Honey-Butter Biscuit', 'Scratch-made in small batches all day long, and drizzled with honey-butter the second they come out of the oven.', '0.99 USD'),\n    ('Frozen', 'Stouffer''s', '', '5.48 USD'),\n    ('Prepared Foods', 'Blount''s Family Kitchen · Uncle Teddy s Beef Chili with Beans (12 oz)', '12 oz', '8.78 USD'),\n    ('FRUIT &amp; SIDES       ', 'Steamed Veggies ', 'A healthy blend of steamed broccoli, cauliflower, zucchini and organic baby carrots.', '2.95 USD'),\n    ('Salads', 'Roast Beef', 'Level up your salad with NEW Choice Angus Roast Beef and a whole lot of crisp veggies.', '9.29 USD'),\n    ('Burgers &amp; Sandwiches', 'Chili Cheese Dog', 'No one does hot-dogs better than your local DQ® restaurant! Order them plain or for the ultimate taste sensation, try our fabulous Chili Cheese dog.', '6.23 USD'),\n    ('Featured', 'The Dragon Fruit Lemonade Refresher 16 oz', 'Apple, Lemon, Honey, Dragon Fruit, Coconut Water - Blended with Frozen Strawberries', '10.74 USD'),\n    ('Drinks', 'FIJI - 1L', '', '5.04 USD'),\n    ('Desserts', 'Chocolate Chip Cookies', 'fresh warm cookies', '3.5 USD'),\n    ('A La Carte (pm)', 'Big Fish Sandwich', '', '4.79 USD'),\n    ('BEVVIES ', 'Ginger Ale', '', '2.5 USD'),\n    ('Candy', 'Kit Kat Duos Dark Chocolate Mint King Size 3oz', 'Turn your KIT KAT® break up a notch with New KIT KAT® Duos', '2.59 USD'),\n    ('Veggies  (All vegetarian orders are cooked when ordered one hour before closing time)', 'Cabbage Roti', 'Dal Poori roti served with a choice steamed cabbage, curry potatoes or chunks (soy). Can either be served wrapped or unwrapped. Wait time of 35 mins.', '12.63 USD'),\n    ('Appetizers', 'GB’s Chinese Chicken Salad', 'Chopped kale, marinated chicken, mandarin oranges, avocado, sliced peppers, roasted almonds, green onions, sesame seeds, and creamy sesame dressing (served on the side). (gluten-free)', '14.0 USD'),\n    ('Side Attractions', 'Suya', 'Seasoned with hot spices and grilled to perfection topped with onions and hot pepper sauce.', '13.0 USD'),\n    ('Lifestyle Bowls', 'Whole30® Salad Bowl', 'Supergreens Lettuce Blend, Chicken, Fajita Veggies, Fresh Tomato Salsa and Guacamole', '13.55 USD'),\n    ('Cognac', 'COURVOISIER VS 375ML', '', '19.99 USD'),\n    ('FILM/PHOTO', 'SANDISK 16G FLASH DRIVE', '16 GB USB Drive', '16.99 USD'),\n    ('Craft Beer 21+only', 'Independence Austin Amber, 6pk-12oz can beer (6.0% ABV)', '', '14.99 USD'),\n    ('Appetizers ', 'Zucchini Fritte', 'Hand-breaded, lightly fried and served with roasted garlic aioli ', '10.99 USD'),\n    ('Picked for you', 'Shakes', 'Jumbo 20 oz. shake Made with fresh Milwaukee frozen custard. Includes 1 topping.', '8.75 USD'),\n    ('Sides', 'Miss Vickie’s® Jalapeño', 'Made with jalapeño seasoning for enough heat to make things deliciously interesting. And every spicy bite is made with no artificial flavors or preservatives.', '1.29 USD'),\n    ('Melts &amp; Handhelds ', 'Cali Club Sandwich', 'Turkey breast, ham, bacon, Swiss cheese, fresh avocado, sun-dried tomato mayo, lettuce and tomato on toasted 7-grain bread. Served with wavy-cut fries. ', '14.34 USD'),\n    ('Ice Cream', 'Haagen Dazs Vanilla Milk Chocolate Bar 3oz', 'Discover A Sweet Reward Bite By Bite With Haagen-Dazs Ice Cream', '2.5 USD'),\n    ('Mariscos', 'Filete Empanizado', 'Breaded filet, rice, salad, and French fries.', '15.99 USD'),\n    ('Out of This World Pizza', 'Big Bang Pepperoni Jalapeno Pizza', 'Pizza with pepperoni, jalapenos, and bell peppers', '12.04 USD'),\n    ('Vodka', 'New Amsterdam 100 Proof, 750mL vodka (50.0% ABV)', 'New Amsterdam vodka was born from an uncompromising passion for great vodka. This commitment to excellence delivers a great-tasting vodka with unparalleled smoothness. 5 times distilled and 3 times filtered to deliver a clean crisp taste that is smooth enough to drink straight or complement any cocktail. Our 100 proof vodka has aromas of sweet frosting and light citrus with an impressively smooth, clean finish.', '19.19 USD'),\n    ('FOOD', 'HOST HOT DOG BUNS 8CT', 'No hotdog is complete without the perfect bun. These soft white hotdog buns keep the flavors in your dog and your hands mess-free.', '3.29 USD'),\n    ('Salads', 'Italian B.M.T. ® ', 'The Italian B.M.T. ® salad is the salad version of our popular sub. Crisp greens topped with Genoa salami, spicy pepperoni, and Black Forest ham. Meaty deliciousness, all in a salad. ', '8.79 USD'),\n    ('Drinks', 'Coke 16oz', 'Crisp, delicious soft drinks flavoried with vanilla, cinnamon, and citrus.', '2.09 USD'),\n    ('Drinks', 'Diet Coke 2L', 'Delicious, crisp tasting, no calorie sparkling cola.', '3.29 USD'),\n    ('No Bready Bowls™', 'Chicken &amp; Bacon Ranch', 'Fuel your day with every last bite of Rotisserie-Style Chicken, Monterrey Cheddar Cheese, and Hickory-Smoked Bacon you’d get in a Footlong, now in a bowl with veggies and Peppercorn Ranch.', '10.99 USD'),\n    ('Enchiladas', 'Charrito Enchiladas', 'Corn tortilla filled with your choice of meat ground beef, chicken, or picadillo. Topped with special enchilada sauce and cheese. Served with refried or rancho beans.', '12.5 USD'),\n    ('Herbal', 'Mullein (1 oz)', 'A resilient herb, mullein is a powerful respiratory aid. Helps rid mucus from the respiratory system. Smokeable alternative to cigarettes, and great for lung health.', '10.0 USD'),\n    ('Sandwiches', 'Premium Family Feast', 'A meal to feed the whole family. Includes 4 Half Sandwiches, 1 Whole Salad, 1 Quart of Soup, and 1 Whole French Baguette. Serves 4-6.', '49.19 USD'),\n    ('Brunch Specialties', 'Crab Cake Benedict', 'Jumbo lump crab cakes on an English muffin, topped with poached eggs, andouille-infused hollandaise, red peppers and green onions.', '19.99 USD'),\n    ('Grocery n'' Games', 'Pecan Log - 7oz.', 'An old-fashioned tradition. Our Pecan Logs are a handmade creation of rich nougat, dipped in creamy caramel, then hand-rolled in fresh chopped pecans. Great as a snack or slice a few to serve to guests.', '6.49 USD'),\n    ('No Bready Bowls™', 'Turkey Italiano', ' Oven Roasted Turkey, with an Italian kick. We add in pepperoni and Genoa Salami, plus Monterey cheddar cheese. You add in your favorite veggies and fixings. Mangia.', '10.19 USD'),\n    ('Frozen Treat - Italian Ice', 'Banana Italian Ice', '', '5.99 USD'),\n    ('🥄Rice Dishes🥢', 'Popcorn chicken Don', 'Popcorn chicken, Green onions, Tonkatsu sauce, Spicy mayo and Cabbage.', '14.0 USD'),\n    ('Combos', 'Smokehouse Combo', 'Two jumbo smoked sausage links served with 2 eggs* your way, hash browns &amp; 2 buttermilk pancakes. ', '14.75 USD'),\n    ('American Wagyu', 'Large American Wagyu Cheese Steak', 'Ultra-premium American wagyu beef, grilled with mushrooms, onions and white American cheese. Add hot or sweet peppers for even more flavor.', '23.99 USD'),\n    ('Cocina / Kitchen Products  ⏲', 'I-Chef Narrow Spatula', '1 ct.', '3.39 USD'),\n    ('Picked for you', 'General Tso''s Tofu', 'Hot and spicy. Served with white rice.', '8.75 USD'),\n    ('Shareables', 'BBQ Pork Sliders (3)', 'Pork shoulder slowly cooked overnight, pulled apart and tossed in our homemade barbeque sauce, topped with Asian slaw and seved on a pretzel bun', '14.3 USD'),\n    ('Melts &amp; Handhelds ', 'Nashville Hot Chicken Melt', 'A golden-fried chicken breast tossed in Nashville Hot sauce with Swiss cheese, tomato, pickles and mayo on grilled Texas toast.  Served with wavy-cut fries.', '15.38 USD'),\n    ('Diet & Nutrition', 'CVS Health Pulse Oximeter', 'HSA/FSA Eligible Read from all sides One-button operation Adjustable brightness Automatic power off Includes 2 AAA batteries Measures: oxygen level, pulse rate The CVS Health Pulse Oximeter is designed to help you get easy, fast readings for your oxygen levels and pulse rate to help monitor a variety of conditions. This lightweight and reliable device features a comfortable, slip-resistant fit so it stays put as you take the reading. The unit also boasts a high definition display that''s easy to read so you can quickly check your oxygen level and pulse rate as needed. The convenient design of this pulse oximeter allows you to read it from all sides, and the one-button operation makes it a cinch to use. Adjustable brightness helps you see the numbers clearly even in low light. An automatic power-off feature turns the unit off to save battery power. It requires two AAA batteries, which are included. This item is Flexible Spending Account (FSA) eligible. When using your CVS Health Pulse Oximeter, keep your hands still in order to get the most accurate results. Place one of your fingers inside to the end, then press down on the switch button once, located on the front panel, to turn the unit on. This device includes a handy lanyard so you can hang it up for storage or take it with you anywhere. Simply tie the lanyard through the hole in the rear of the unit. Always read the instructions fully and carefully before use to ensure that you''re receiving the most accurate readings possible. Please contact your healthcare provider if your are experiencing any unusual symptoms related to breathing or if your readings are outside of ranges established by your healthcare provider.', '49.99 USD'),\n    ('Salads &amp; Soups ', 'Wonton Soup Bowl', 'Savory broth, house-made pork wontons, shrimp, chicken', '10.5 USD'),\n    ('Bread Sticks', 'Jalapeño Feta Bread Sticks', 'Classic with Jalapeño, Feta', '16.4 USD'),\n    ('Pizza', 'Cheese Pizza', '', '9.0 USD'),\n    ('A La Carte', 'Kung Pao Chicken', '', '0.0 USD'),\n    ('Drinks', 'Soda', '', '1.99 USD'),\n    ('Island Famous Signature Combos (Alcoholic) - Must be 21+ to order.', 'Rip Tide', 'Piña Colada, Hypnotic.', '8.5 USD'),\n    ('Specialty Pizzas', 'Stampede Pizza-Small', 'Beef, pepperoni, Canadian bacon, Italian sausage, black olives, green olives, green peppers, mushrooms, and red onions.', '9.89 USD'),\n    ('Pizzas', 'Hawaiian', 'Canadian bacon, pineapple, and mozzarella cheese on a bed of red sauce. 10\" size.', '15.99 USD'),\n    ('PASTA', 'TAGLIATELLE CHICKEN ALFREDO', '', '20.0 USD'),\n    ('Chicken Dinners', 'Chicken Dinner (2 Pcs)', '', '8.49 USD'),\n    ('Slush', 'Mango T-Slush ', 'Made with fresh fruit. 16 oz.  590Cal', '6.99 USD'),\n    ('Beef', 'Pepper Steak (Entree)', '', '15.5 USD'),\n    ('Combination', 'Chicken Chow Mein', 'Served with vegetable spring roll and fried rice.', '9.35 USD'),\n    ('Breakfast', 'Egg &amp; Cheese 6 Inch with Regular Egg', 'A classic for a reason. Our Egg and Cheese is simply delicious. Enjoy a fluffy egg with melted cheese. Try it toasted - It''s unbeatable.', '6.09 USD'),\n    ('Health & Beauty', 'Oral-B Soft Toothbrush', '', '3.28 USD'),\n    ('Sides', 'Side of Sour Cream', '', '1.0 USD'),\n    ('Bakery', 'Glazed Doughnut', 'Old-fashioned cake doughnut glazed with sweet icing. - VEGETARIAN', '2.85 USD'),\n    ('Picked for you', 'Bacon King Sandwich', '', '11.49 USD'),\n    ('Beverages', 'Lemonade - Gallon', '', '11.0 USD'),\n    ('Drinks', 'Milk', '', '2.25 USD'),\n    ('Small Plate', 'Blue Crab Fried Rice', 'Fried rice, lump crab, and spicy cod roe.', '15.75 USD'),\n    ('Snacks', '7-Select Jack Link''s Beef & Cheese Stick 1.2oz', 'Crafted and seasoned beef and cheese sticks are individually wrapped for a quick and easy snack.', '2.09 USD'),\n    ('Candy &amp; Gum', 'Trident Tropical Twist (14 Count)', '', '1.99 USD'),\n    ('Other/ Liqueur', 'RumChata, 750mL liqueur (13.75% ABV)', '', '31.99 USD'),\n    ('Sides', 'Side of Hot Sauce', '', '0.99 USD'),\n    ('Burritos &amp; Bowls', 'New Mexico Chicken Burrito &amp; Bowl ', 'Grilled chicken, hickory-smoked bacon pieces, green peppers &amp; onions, tomatoes, queso sauce, shredded Jack &amp; Cheddar cheese, avocado and rice medley all wrapped up in a warm flour tortilla or layered in a bowl. Served with our salsa and choice of side.', '0.0 USD'),\n    ('Chicken ''n Biscuits', 'Southern Fried Chicken Bucket', '12 generous pieces of chicken, hand-breaded with our signature seasoning, perfectly crispy on the outside, perfectly juicy on the inside. Comes with honey for drizzling, two large sides, and hand rolled Buttermilk Biscuits.  ', '39.99 USD'),\n    ('Appetizers', 'Sushi Cake', 'Ahi Spicy tuna tower', '15.94 USD'),\n    ('Desserts', 'Ultimate Chocolate Chip Cookie', 'Pizza night just got a whole lot sweeter. Freshly baked and warm from the oven, our cookie is packed with semi-sweet chocolate chips that melt in your mouth.', '8.18 USD'),\n    ('Woodmont Famous Favorites', 'Shrimp Basket Platter', 'Served with French fries and coleslaw. ', '15.99 USD'),\n    ('Sides', 'Bacon', '', '3.99 USD'),\n    ('Boneless Wings', 'Buffalo Bites Entrée ', 'The full meal size of our Buffalo Bites appetizer. 12 oz. of hand-breaded, bite-sized versions of our boneless wings. Tossed in your favorite wing sauce and served with a side.', '13.5 USD'),\n    ('Beverages', 'Pepsi', '', '2.29 USD'),\n    ('Dinner Sides', 'Balsamic Portabellas', 'Flame grilled, sweet balsamic reduction, gluten-free, and vegetarian.', '6.0 USD'),\n    ('Hot Coffees', 'Clover® Starbucks Reserve® Guatemala Santa Clara Estate', 'Four generations ago, the Zelaya family established their farm in Guatemala’s renowned Antigua Valley. Today, Ricardo Zelaya proudly continues his legacy, mentoring his three daughters while producing award-winning coffee. For the family, farming is about cultivating lasting relationships, and their passion for the community is evident in the exceptional quality of this cup that features notes of red currant and coconut.', '6.25 USD'),\n    ('Tequila', 'Don Julio Blanco, 750mL tequila (40.0% ABV)', '', '67.58 USD'),\n    ('Picked for you', 'Butter Chicken Peshawari', 'Chargrilled marinated chicken chunks simmered to perfection in a thick, rich, and creamy cashew and tomato sauce,  with warm spices. Served with a side of steamed long-grain rice.', '19.8 USD'),\n    ('Stir-Fried', 'Vegetarian duck basil', 'Vegetarian mock duck, onion, bell pepper, and basil leave stir-fried in spicy chili and garlic sauce', '11.5 USD'),\n    ('Ice Cream', 'Ben & Jerry''s The Tonight Dough Pint', 'This is an ice cream and cookie dough party you don''t want to miss!', '7.29 USD'),\n    ('Salades &amp; Soupes', 'Romanoff Sauce (6 Ounces)', '', '5.15 USD'),\n    ('Beverages', 'Medium Frozen Fanta® Wild Cherry', '', '2.19 USD'),\n    ('Value Duets', 'Classic Grilled Cheese &amp; Creamy Tomato Soup', 'A half portion of our Classic Grilled Cheese served alongside a cup of Creamy Tomato Soup.', '6.99 USD'),\n    ('Picked for you', 'Chicken Wings', 'Chicken Wings', '8.48 USD'),\n    ('Salads &amp; Soups', 'Southern-Fried Chicken Tender Salad', 'With tomatoes, hard-boiled eggs, bacon and cheddar cheese with our Honey Mustard Dressing.', '12.99 USD'),\n    ('Starters', 'Wings', 'The option of house buffalo BBQ, or garlic Parmesan sauce.', '16.0 USD'),\n    ('Grocery', 'SmartSweets Sweet Fish - 1.8 oz', 'Sweet Fish are the #1 fish in the sea - for real they''re our most popular #KickSugar candy! Bursting with berry-goodness, its no wonder they''re the captain of Team Sweet. We''ve innovated plant-based Sweet Fish with our pinky promise: delicious candy free from sugar alcohols, artificial sweeteners and added sugar. NO GUESSWORK! ONE BAG = ONE SERVING, AND: 92%25 less sugar than other fish 3 grams of sugar for the whole bag! 44%25 less calories than the other fish 13 grams of plant-based fiber 3', '3.45 USD'),\n    ('Hand-Crafted Melts', 'Philly Cheese Steak Stacker', 'Philly comes to you with grilled sirloin steak &amp; onions topped with melted American cheese on a grilled roll.', '12.75 USD'),\n    ('McCafé', 'Medium French Vanilla Latte', '', '4.59 USD'),\n    ('Fried Rice and Noodles', 'Roast Pork Lo Mein', '', '7.99 USD'),\n    ('Fries', 'BBQ Chicken Fries', 'French fries topped with chicken, barbecue sauce, shredded cheese, sliced jalapenos and scallions.', '5.99 USD'),\n    ('Add-Ons', 'Lettuce', '', '0.25 USD'),\n    ('DL_SIDES', 'PICKLED RED ONION AND CABBAGE SIDE Regular', '', '7.0 USD'),\n    ('Ice Cream', 'Mars Twix Ice Cream (3 oz)', '', '3.75 USD'),\n    ('McCafé', 'Medium Caramel Hot Chocolate', '', '3.39 USD'),\n    ('Chop Suey', 'Seafood Chop Suey', '', '7.75 USD'),\n    ('Appetizers', 'Baby Corn Manchurian', 'Vegan &amp; Vegetarian- Crispy fried baby corn in a sweet and spicy thick Chinese sauce along with onions and bell pepper ', '12.99 USD'),\n    ('Dessert', '12 Chocolate Chip Cookies', '12 of our signature chocolate chip cookies.', '6.35 USD'),\n    ('Drinks', 'Strawberry Boba Smoothie', 'Fresh strawberry boba tea smoothie, served with tapioca boba balls.', '6.75 USD'),\n    ('Hamburgers', 'Bourbon Bacon Cheeseburger Triple', 'Three-quarters of a pound* of fresh, never-frozen beef topped with Applewood smoked bacon, American cheese, crispy onions, and a sweet, smoky bourbon bacon sauce. That’s actual bourbon and actual bacon actually in the sauce. Break this one out only for special occasions—like lunch.', '9.62 USD'),\n    ('Appetizers', 'The Rock Wings', 'Skillet baked and tossed with your choice of ranch season dry rub, hot sauce, sweet red chili sauce or BBQ sauce', '15.95 USD'),\n    ('SKIN CARE', 'CETAPHIL MOIST LOTION 16Z', 'Hydrates for 48 hours & restores the skin barrier.', '14.99 USD'),\n    ('Dessert', 'Coconut Cream Pie', '', '6.59 USD'),\n    ('Salads', 'House Salad', 'Cucumbers, grape tomatoes, Cheddar cheese and croutons atop a bed of iceberg mix. Served with choice of dressing. ', '7.78 USD'),\n    ('McCafé', 'Medium Strawberry Banana Smoothie', '', '4.19 USD'),\n    ('Dessert', 'Chocolate Baklava', 'A savory dessert, which features flaky phyllo dough wrapped around a blend of chopped walnuts and almonds in a sweet syrup mixture topped with ribbons of chocolate. Allergen: Wheat, Milk, and Tree Nuts.', '3.49 USD'),\n    ('Appetizers', 'Stuffed Grape Leaves', 'Five pieces. Dolmas filled with rice, tomatoes, and onions. ', '5.49 USD'),\n    ('Tenders', 'Handcrafted Tenders (8 Pcs)', 'Include three sauces', '25.19 USD'),\n    ('DESSERTS            ', 'Texas Chocolate Cake     ', 'Moist chocolate cake covered in chocolate icing, topped with chopped pecans.', '2.61 USD'),\n    ('Candy', 'Starburst Minis - Share Size', '', '1.52 USD'),\n    ('Picked for you', '10 pc Buffalo Wings', 'Our Chicken is Antibiotic and Hormone Free as well as Halal. Hand tossed in your favorite sauce and Always Fresh and made to order ', '11.99 USD'),\n    ('Desserts', 'Mango Ice Cream', '', '6.99 USD'),\n    ('Signature Platters', '6. Beef &amp; Lamb Gyro Platter', 'Beef and Lamb Gyro + Saffron Basmati Rice + Chickpeas + Fries', '16.8 USD'),\n    ('Breakfast Burritos', 'The One', '3 slices of crispy thick-cut bacon, sauteed chicken apple sausage, triple egg omelet, chili aioli, gooey American cheese, scallions, and fried potato tots seasoned with Nashville spices all encased in a toasted flour tortilla.  ', '14.01 USD'),\n    ('CHEESESTEAKS', 'VEGGIE DELIGHT', 'Veggie lovers rejoice! Our Veggie Delight cheesesteak features grilled sliced mushrooms, crispy green peppers and savory onion grilled to perfection and served on a toasted roll. We top our Veggie Delight with smooth melted provolone, mellow shredded cheddar cheese, and sharp Swiss to enhance the flavors of our featured veggies. This is the veggie sandwich you’ve been waiting for. All cheesesteaks come with optional creamy mayonnaise, juicy tomato, shredded lettuce, and crispy pickle.  The Veggie Delight pairs perfectly with Cheese Fries or an Original Real-Fruit Lemonade. ', '7.19 USD'),\n    ('Grocery', 'Quaker Express Maple & Brown Sugar 1.69oz', 'A breakfast that literally gives you sugar and spice and everything nice. This high-protein high-fiber meal is pure heaven!', '2.29 USD'),\n    ('Family Size Pig Outs', 'Gallon of Lemonade', '', '6.76 USD'),\n    ('BATTERIES', 'RA ALKALINE C 4PK', 'For your C battery powered needs choose Rite Aid Home Alkaline Batteries. They are dependable, long lasting, and affordable for whatever and whenever you need them.', '9.29 USD'),\n    ('Picked for you', 'General Tso''s Chicken', 'Deep fried white meat chicken pieces blended with chef’s sauce.', '18.38 USD'),\n    ('Specialty Sandwiches', 'Turkey Veggie Ranch', 'Contains: Multi Grain Bread, Swiss, Ranch Dressing, Tomato, Cucumbers, Spinach, Applewood Smoked Bacon, Oven Roasted Turkey', '6.89 USD'),\n    ('Frozen', 'White Castle', '', '7.68 USD'),\n    ('Starters and Salads', 'Grimaldi''s House Salad', 'Romaine Lettuce, Red Onion, Cherry Tomatoes, Oven Roasted Sweet Red Peppers, Mushrooms, Green Olives and Vinaigrette Dressing.', '10.0 USD'),\n    ('Picked for you', 'NEW! Mozzarella Sticks', 'Lightly battered Mozzarella cheese, fried to perfection and served with marinara sauce.', '9.89 USD'),\n    ('Picked for you', 'Spicy Chicken Sandwich Combo', 'Includes a choice of regular signature side and a drink.', '10.19 USD')\n) AS result( \"category\",\"name\",\"description\",\"price\");\n\nDROP FUNCTION IF EXISTS on_order_updates() CASCADE;\n\nDROP TABLE IF EXISTS customers_info CASCADE;\nSELECT first_name, last_name, email, '07' || round(random()*1e9) as phone_number\nINTO customers_info\nFROM (\n  VALUES \n    ('James', 'Butt', 'jbutt@gmail.com'),\n    ('Josephine', 'Darakjy', 'josephine_darakjy@darakjy.org'),\n    ('Art', 'Venere', 'art@venere.org'),\n    ('Lenna', 'Paprocki', 'lpaprocki@hotmail.com'),\n    ('Donette', 'Foller', 'donette.foller@cox.net'),\n    ('Simona', 'Morasca', 'simona@morasca.com'),\n    ('Mitsue', 'Tollner', 'mitsue_tollner@yahoo.com'),\n    ('Leota', 'Dilliard', 'leota@hotmail.com'),\n    ('Sage', 'Wieser', 'sage_wieser@cox.net'),\n    ('Kris', 'Marrier', 'kris@gmail.com'),\n    ('Minna', 'Amigon', 'minna_amigon@yahoo.com'),\n    ('Abel', 'Maclead', 'amaclead@gmail.com'),\n    ('Kiley', 'Caldarera', 'kiley.caldarera@aol.com'),\n    ('Graciela', 'Ruta', 'gruta@cox.net'),\n    ('Cammy', 'Albares', 'calbares@gmail.com'),\n    ('Mattie', 'Poquette', 'mattie@aol.com'),\n    ('Meaghan', 'Garufi', 'meaghan@hotmail.com'),\n    ('Gladys', 'Rim', 'gladys.rim@rim.org'),\n    ('Yuki', 'Whobrey', 'yuki_whobrey@aol.com'),\n    ('Fletcher', 'Flosi', 'fletcher.flosi@yahoo.com'),\n    ('Bette', 'Nicka', 'bette_nicka@cox.net'),\n    ('Veronika', 'Inouye', 'vinouye@aol.com'),\n    ('Willard', 'Kolmetz', 'willard@hotmail.com'),\n    ('Maryann', 'Royster', 'mroyster@royster.com'),\n    ('Alisha', 'Slusarski', 'alisha@slusarski.com'),\n    ('Allene', 'Iturbide', 'allene_iturbide@cox.net'),\n    ('Chanel', 'Caudy', 'chanel.caudy@caudy.org'),\n    ('Ezekiel', 'Chui', 'ezekiel@chui.com'),\n    ('Willow', 'Kusko', 'wkusko@yahoo.com'),\n    ('Bernardo', 'Figeroa', 'bfigeroa@aol.com'),\n    ('Ammie', 'Corrio', 'ammie@corrio.com'),\n    ('Francine', 'Vocelka', 'francine_vocelka@vocelka.com'),\n    ('Ernie', 'Stenseth', 'ernie_stenseth@aol.com'),\n    ('Albina', 'Glick', 'albina@glick.com'),\n    ('Alishia', 'Sergi', 'asergi@gmail.com'),\n    ('Solange', 'Shinko', 'solange@shinko.com'),\n    ('Jose', 'Stockham', 'jose@yahoo.com'),\n    ('Rozella', 'Ostrosky', 'rozella.ostrosky@ostrosky.com'),\n    ('Valentine', 'Gillian', 'valentine_gillian@gmail.com'),\n    ('Kati', 'Rulapaugh', 'kati.rulapaugh@hotmail.com'),\n    ('Youlanda', 'Schemmer', 'youlanda@aol.com'),\n    ('Dyan', 'Oldroyd', 'doldroyd@aol.com'),\n    ('Roxane', 'Campain', 'roxane@hotmail.com'),\n    ('Lavera', 'Perin', 'lperin@perin.org'),\n    ('Erick', 'Ferencz', 'erick.ferencz@aol.com'),\n    ('Fatima', 'Saylors', 'fsaylors@saylors.org'),\n    ('Jina', 'Briddick', 'jina_briddick@briddick.com'),\n    ('Kanisha', 'Waycott', 'kanisha_waycott@yahoo.com'),\n    ('Emerson', 'Bowley', 'emerson.bowley@bowley.org'),\n    ('Blair', 'Malet', 'bmalet@yahoo.com'),\n    ('Brock', 'Bolognia', 'bbolognia@yahoo.com'),\n    ('Lorrie', 'Nestle', 'lnestle@hotmail.com'),\n    ('Sabra', 'Uyetake', 'sabra@uyetake.org'),\n    ('Marjory', 'Mastella', 'mmastella@mastella.com'),\n    ('Karl', 'Klonowski', 'karl_klonowski@yahoo.com'),\n    ('Tonette', 'Wenner', 'twenner@aol.com'),\n    ('Amber', 'Monarrez', 'amber_monarrez@monarrez.org'),\n    ('Shenika', 'Seewald', 'shenika@gmail.com'),\n    ('Delmy', 'Ahle', 'delmy.ahle@hotmail.com'),\n    ('Deeanna', 'Juhas', 'deeanna_juhas@gmail.com'),\n    ('Blondell', 'Pugh', 'bpugh@aol.com'),\n    ('Jamal', 'Vanausdal', 'jamal@vanausdal.org'),\n    ('Cecily', 'Hollack', 'cecily@hollack.org'),\n    ('Carmelina', 'Lindall', 'carmelina_lindall@lindall.com'),\n    ('Maurine', 'Yglesias', 'maurine_yglesias@yglesias.com'),\n    ('Tawna', 'Buvens', 'tawna@gmail.com'),\n    ('Penney', 'Weight', 'penney_weight@aol.com'),\n    ('Elly', 'Morocco', 'elly_morocco@gmail.com'),\n    ('Ilene', 'Eroman', 'ilene.eroman@hotmail.com'),\n    ('Vallie', 'Mondella', 'vmondella@mondella.com'),\n    ('Kallie', 'Blackwood', 'kallie.blackwood@gmail.com'),\n    ('Johnetta', 'Abdallah', 'johnetta_abdallah@aol.com'),\n    ('Bobbye', 'Rhym', 'brhym@rhym.com'),\n    ('Micaela', 'Rhymes', 'micaela_rhymes@gmail.com'),\n    ('Tamar', 'Hoogland', 'tamar@hotmail.com'),\n    ('Moon', 'Parlato', 'moon@yahoo.com'),\n    ('Laurel', 'Reitler', 'laurel_reitler@reitler.com'),\n    ('Delisa', 'Crupi', 'delisa.crupi@crupi.com'),\n    ('Viva', 'Toelkes', 'viva.toelkes@gmail.com'),\n    ('Elza', 'Lipke', 'elza@yahoo.com'),\n    ('Devorah', 'Chickering', 'devorah@hotmail.com'),\n    ('Timothy', 'Mulqueen', 'timothy_mulqueen@mulqueen.org'),\n    ('Arlette', 'Honeywell', 'ahoneywell@honeywell.com'),\n    ('Dominque', 'Dickerson', 'dominque.dickerson@dickerson.org'),\n    ('Lettie', 'Isenhower', 'lettie_isenhower@yahoo.com'),\n    ('Myra', 'Munns', 'mmunns@cox.net'),\n    ('Stephaine', 'Barfield', 'stephaine@barfield.com'),\n    ('Lai', 'Gato', 'lai.gato@gato.org'),\n    ('Stephen', 'Emigh', 'stephen_emigh@hotmail.com'),\n    ('Tyra', 'Shields', 'tshields@gmail.com'),\n    ('Tammara', 'Wardrip', 'twardrip@cox.net'),\n    ('Cory', 'Gibes', 'cory.gibes@gmail.com'),\n    ('Danica', 'Bruschke', 'danica_bruschke@gmail.com'),\n    ('Wilda', 'Giguere', 'wilda@cox.net'),\n    ('Elvera', 'Benimadho', 'elvera.benimadho@cox.net'),\n    ('Carma', 'Vanheusen', 'carma@cox.net'),\n    ('Malinda', 'Hochard', 'malinda.hochard@yahoo.com'),\n    ('Natalie', 'Fern', 'natalie.fern@hotmail.com'),\n    ('Lisha', 'Centini', 'lisha@centini.org'),\n    ('Arlene', 'Klusman', 'arlene_klusman@gmail.com'),\n    ('Alease', 'Buemi', 'alease@buemi.com'),\n    ('Louisa', 'Cronauer', 'louisa@cronauer.com'),\n    ('Angella', 'Cetta', 'angella.cetta@hotmail.com'),\n    ('Cyndy', 'Goldammer', 'cgoldammer@cox.net'),\n    ('Rosio', 'Cork', 'rosio.cork@gmail.com'),\n    ('Celeste', 'Korando', 'ckorando@hotmail.com'),\n    ('Twana', 'Felger', 'twana.felger@felger.org'),\n    ('Estrella', 'Samu', 'estrella@aol.com'),\n    ('Donte', 'Kines', 'dkines@hotmail.com'),\n    ('Tiffiny', 'Steffensmeier', 'tiffiny_steffensmeier@cox.net'),\n    ('Edna', 'Miceli', 'emiceli@miceli.org'),\n    ('Sue', 'Kownacki', 'sue@aol.com'),\n    ('Jesusa', 'Shin', 'jshin@shin.com'),\n    ('Rolland', 'Francescon', 'rolland@cox.net'),\n    ('Pamella', 'Schmierer', 'pamella.schmierer@schmierer.org'),\n    ('Glory', 'Kulzer', 'gkulzer@kulzer.org'),\n    ('Shawna', 'Palaspas', 'shawna_palaspas@palaspas.org'),\n    ('Brandon', 'Callaro', 'brandon_callaro@hotmail.com'),\n    ('Scarlet', 'Cartan', 'scarlet.cartan@yahoo.com'),\n    ('Oretha', 'Menter', 'oretha_menter@yahoo.com'),\n    ('Ty', 'Smith', 'tsmith@aol.com'),\n    ('Xuan', 'Rochin', 'xuan@gmail.com'),\n    ('Lindsey', 'Dilello', 'lindsey.dilello@hotmail.com'),\n    ('Devora', 'Perez', 'devora_perez@perez.org'),\n    ('Herman', 'Demesa', 'hdemesa@cox.net'),\n    ('Rory', 'Papasergi', 'rpapasergi@cox.net'),\n    ('Talia', 'Riopelle', 'talia_riopelle@aol.com'),\n    ('Van', 'Shire', 'van.shire@shire.com'),\n    ('Lucina', 'Lary', 'lucina_lary@cox.net'),\n    ('Bok', 'Isaacs', 'bok.isaacs@aol.com'),\n    ('Rolande', 'Spickerman', 'rolande.spickerman@spickerman.com'),\n    ('Howard', 'Paulas', 'hpaulas@gmail.com'),\n    ('Kimbery', 'Madarang', 'kimbery_madarang@cox.net'),\n    ('Thurman', 'Manno', 'thurman.manno@yahoo.com'),\n    ('Becky', 'Mirafuentes', 'becky.mirafuentes@mirafuentes.com'),\n    ('Beatriz', 'Corrington', 'beatriz@yahoo.com'),\n    ('Marti', 'Maybury', 'marti.maybury@yahoo.com'),\n    ('Nieves', 'Gotter', 'nieves_gotter@gmail.com'),\n    ('Leatha', 'Hagele', 'lhagele@cox.net'),\n    ('Valentin', 'Klimek', 'vklimek@klimek.org'),\n    ('Melissa', 'Wiklund', 'melissa@cox.net'),\n    ('Sheridan', 'Zane', 'sheridan.zane@zane.com'),\n    ('Bulah', 'Padilla', 'bulah_padilla@hotmail.com'),\n    ('Audra', 'Kohnert', 'audra@kohnert.com'),\n    ('Daren', 'Weirather', 'dweirather@aol.com'),\n    ('Fernanda', 'Jillson', 'fjillson@aol.com'),\n    ('Gearldine', 'Gellinger', 'gearldine_gellinger@gellinger.com'),\n    ('Chau', 'Kitzman', 'chau@gmail.com'),\n    ('Theola', 'Frey', 'theola_frey@frey.com'),\n    ('Cheryl', 'Haroldson', 'cheryl@haroldson.org'),\n    ('Laticia', 'Merced', 'lmerced@gmail.com'),\n    ('Carissa', 'Batman', 'carissa.batman@yahoo.com'),\n    ('Lezlie', 'Craghead', 'lezlie.craghead@craghead.org'),\n    ('Ozell', 'Shealy', 'oshealy@hotmail.com'),\n    ('Arminda', 'Parvis', 'arminda@parvis.com'),\n    ('Reita', 'Leto', 'reita.leto@gmail.com'),\n    ('Yolando', 'Luczki', 'yolando@cox.net'),\n    ('Lizette', 'Stem', 'lizette.stem@aol.com'),\n    ('Gregoria', 'Pawlowicz', 'gpawlowicz@yahoo.com'),\n    ('Carin', 'Deleo', 'cdeleo@deleo.com'),\n    ('Chantell', 'Maynerich', 'chantell@yahoo.com'),\n    ('Dierdre', 'Yum', 'dyum@yahoo.com'),\n    ('Larae', 'Gudroe', 'larae_gudroe@gmail.com'),\n    ('Latrice', 'Tolfree', 'latrice.tolfree@hotmail.com'),\n    ('Kerry', 'Theodorov', 'kerry.theodorov@gmail.com'),\n    ('Dorthy', 'Hidvegi', 'dhidvegi@yahoo.com'),\n    ('Fannie', 'Lungren', 'fannie.lungren@yahoo.com'),\n    ('Evangelina', 'Radde', 'evangelina@aol.com'),\n    ('Novella', 'Degroot', 'novella_degroot@degroot.org'),\n    ('Clay', 'Hoa', 'choa@hoa.org'),\n    ('Jennifer', 'Fallick', 'jfallick@yahoo.com'),\n    ('Irma', 'Wolfgramm', 'irma.wolfgramm@hotmail.com'),\n    ('Eun', 'Coody', 'eun@yahoo.com'),\n    ('Sylvia', 'Cousey', 'sylvia_cousey@cousey.org'),\n    ('Nana', 'Wrinkles', 'nana@aol.com'),\n    ('Layla', 'Springe', 'layla.springe@cox.net'),\n    ('Joesph', 'Degonia', 'joesph_degonia@degonia.org'),\n    ('Annabelle', 'Boord', 'annabelle.boord@cox.net'),\n    ('Stephaine', 'Vinning', 'stephaine@cox.net'),\n    ('Nelida', 'Sawchuk', 'nelida@gmail.com'),\n    ('Marguerita', 'Hiatt', 'marguerita.hiatt@gmail.com'),\n    ('Carmela', 'Cookey', 'ccookey@cookey.org'),\n    ('Junita', 'Brideau', 'jbrideau@aol.com'),\n    ('Claribel', 'Varriano', 'claribel_varriano@cox.net'),\n    ('Benton', 'Skursky', 'benton.skursky@aol.com'),\n    ('Hillary', 'Skulski', 'hillary.skulski@aol.com'),\n    ('Merilyn', 'Bayless', 'merilyn_bayless@cox.net'),\n    ('Teri', 'Ennaco', 'tennaco@gmail.com'),\n    ('Merlyn', 'Lawler', 'merlyn_lawler@hotmail.com'),\n    ('Georgene', 'Montezuma', 'gmontezuma@cox.net'),\n    ('Jettie', 'Mconnell', 'jmconnell@hotmail.com'),\n    ('Lemuel', 'Latzke', 'lemuel.latzke@gmail.com'),\n    ('Melodie', 'Knipp', 'mknipp@gmail.com'),\n    ('Candida', 'Corbley', 'candida_corbley@hotmail.com'),\n    ('Karan', 'Karpin', 'karan_karpin@gmail.com'),\n    ('Andra', 'Scheyer', 'andra@gmail.com'),\n    ('Felicidad', 'Poullion', 'fpoullion@poullion.com'),\n    ('Belen', 'Strassner', 'belen_strassner@aol.com'),\n    ('Gracia', 'Melnyk', 'gracia@melnyk.com'),\n    ('Jolanda', 'Hanafan', 'jhanafan@gmail.com'),\n    ('Barrett', 'Toyama', 'barrett.toyama@toyama.org'),\n    ('Helga', 'Fredicks', 'helga_fredicks@yahoo.com'),\n    ('Ashlyn', 'Pinilla', 'apinilla@cox.net'),\n    ('Fausto', 'Agramonte', 'fausto_agramonte@yahoo.com'),\n    ('Ronny', 'Caiafa', 'ronny.caiafa@caiafa.org'),\n    ('Marge', 'Limmel', 'marge@gmail.com'),\n    ('Norah', 'Waymire', 'norah.waymire@gmail.com'),\n    ('Aliza', 'Baltimore', 'aliza@aol.com'),\n    ('Mozell', 'Pelkowski', 'mpelkowski@pelkowski.org'),\n    ('Viola', 'Bitsuie', 'viola@gmail.com'),\n    ('Franklyn', 'Emard', 'femard@emard.com'),\n    ('Willodean', 'Konopacki', 'willodean_konopacki@konopacki.org'),\n    ('Beckie', 'Silvestrini', 'beckie.silvestrini@silvestrini.com'),\n    ('Rebecka', 'Gesick', 'rgesick@gesick.org'),\n    ('Frederica', 'Blunk', 'frederica_blunk@gmail.com'),\n    ('Glen', 'Bartolet', 'glen_bartolet@hotmail.com'),\n    ('Freeman', 'Gochal', 'freeman_gochal@aol.com'),\n    ('Vincent', 'Meinerding', 'vincent.meinerding@hotmail.com'),\n    ('Rima', 'Bevelacqua', 'rima@cox.net'),\n    ('Glendora', 'Sarbacher', 'gsarbacher@gmail.com'),\n    ('Avery', 'Steier', 'avery@cox.net'),\n    ('Cristy', 'Lother', 'cristy@lother.com'),\n    ('Nicolette', 'Brossart', 'nicolette_brossart@brossart.com'),\n    ('Tracey', 'Modzelewski', 'tracey@hotmail.com'),\n    ('Virgina', 'Tegarden', 'virgina_tegarden@tegarden.com'),\n    ('Tiera', 'Frankel', 'tfrankel@aol.com'),\n    ('Alaine', 'Bergesen', 'alaine_bergesen@cox.net'),\n    ('Earleen', 'Mai', 'earleen_mai@cox.net'),\n    ('Leonida', 'Gobern', 'leonida@gobern.org'),\n    ('Ressie', 'Auffrey', 'ressie.auffrey@yahoo.com'),\n    ('Justine', 'Mugnolo', 'jmugnolo@yahoo.com'),\n    ('Eladia', 'Saulter', 'eladia@saulter.com'),\n    ('Chaya', 'Malvin', 'chaya@malvin.com'),\n    ('Gwenn', 'Suffield', 'gwenn_suffield@suffield.org'),\n    ('Salena', 'Karpel', 'skarpel@cox.net'),\n    ('Yoko', 'Fishburne', 'yoko@fishburne.com'),\n    ('Taryn', 'Moyd', 'taryn.moyd@hotmail.com'),\n    ('Katina', 'Polidori', 'katina_polidori@aol.com'),\n    ('Rickie', 'Plumer', 'rickie.plumer@aol.com'),\n    ('Alex', 'Loader', 'alex@loader.com'),\n    ('Lashon', 'Vizarro', 'lashon@aol.com'),\n    ('Lauran', 'Burnard', 'lburnard@burnard.com'),\n    ('Ceola', 'Setter', 'ceola.setter@setter.org'),\n    ('My', 'Rantanen', 'my@hotmail.com'),\n    ('Lorrine', 'Worlds', 'lorrine.worlds@worlds.com'),\n    ('Peggie', 'Sturiale', 'peggie@cox.net'),\n    ('Marvel', 'Raymo', 'mraymo@yahoo.com'),\n    ('Daron', 'Dinos', 'daron_dinos@cox.net'),\n    ('An', 'Fritz', 'an_fritz@hotmail.com'),\n    ('Portia', 'Stimmel', 'portia.stimmel@aol.com'),\n    ('Rhea', 'Aredondo', 'rhea_aredondo@cox.net'),\n    ('Benedict', 'Sama', 'bsama@cox.net'),\n    ('Alyce', 'Arias', 'alyce@arias.org'),\n    ('Heike', 'Berganza', 'heike@gmail.com'),\n    ('Carey', 'Dopico', 'carey_dopico@dopico.org'),\n    ('Dottie', 'Hellickson', 'dottie@hellickson.org'),\n    ('Deandrea', 'Hughey', 'deandrea@yahoo.com'),\n    ('Kimberlie', 'Duenas', 'kimberlie_duenas@yahoo.com'),\n    ('Martina', 'Staback', 'martina_staback@staback.com'),\n    ('Skye', 'Fillingim', 'skye_fillingim@yahoo.com'),\n    ('Jade', 'Farrar', 'jade.farrar@yahoo.com'),\n    ('Charlene', 'Hamilton', 'charlene.hamilton@hotmail.com'),\n    ('Geoffrey', 'Acey', 'geoffrey@gmail.com'),\n    ('Stevie', 'Westerbeck', 'stevie.westerbeck@yahoo.com'),\n    ('Pamella', 'Fortino', 'pamella@fortino.com'),\n    ('Harrison', 'Haufler', 'hhaufler@hotmail.com'),\n    ('Johnna', 'Engelberg', 'jengelberg@engelberg.org'),\n    ('Buddy', 'Cloney', 'buddy.cloney@yahoo.com'),\n    ('Dalene', 'Riden', 'dalene.riden@aol.com'),\n    ('Jerry', 'Zurcher', 'jzurcher@zurcher.org'),\n    ('Haydee', 'Denooyer', 'hdenooyer@denooyer.org'),\n    ('Joseph', 'Cryer', 'joseph_cryer@cox.net'),\n    ('Deonna', 'Kippley', 'deonna_kippley@hotmail.com'),\n    ('Raymon', 'Calvaresi', 'raymon.calvaresi@gmail.com'),\n    ('Alecia', 'Bubash', 'alecia@aol.com'),\n    ('Ma', 'Layous', 'mlayous@hotmail.com'),\n    ('Detra', 'Coyier', 'detra@aol.com'),\n    ('Terrilyn', 'Rodeigues', 'terrilyn.rodeigues@cox.net'),\n    ('Salome', 'Lacovara', 'slacovara@gmail.com'),\n    ('Garry', 'Keetch', 'garry_keetch@hotmail.com'),\n    ('Matthew', 'Neither', 'mneither@yahoo.com'),\n    ('Theodora', 'Restrepo', 'theodora.restrepo@restrepo.com'),\n    ('Noah', 'Kalafatis', 'noah.kalafatis@aol.com'),\n    ('Carmen', 'Sweigard', 'csweigard@sweigard.com'),\n    ('Lavonda', 'Hengel', 'lavonda@cox.net'),\n    ('Junita', 'Stoltzman', 'junita@aol.com'),\n    ('Herminia', 'Nicolozakes', 'herminia@nicolozakes.org'),\n    ('Casie', 'Good', 'casie.good@aol.com'),\n    ('Reena', 'Maisto', 'reena@hotmail.com'),\n    ('Mirta', 'Mallett', 'mirta_mallett@gmail.com'),\n    ('Cathrine', 'Pontoriero', 'cathrine.pontoriero@pontoriero.com'),\n    ('Filiberto', 'Tawil', 'ftawil@hotmail.com'),\n    ('Raul', 'Upthegrove', 'rupthegrove@yahoo.com'),\n    ('Sarah', 'Candlish', 'sarah.candlish@gmail.com'),\n    ('Lucy', 'Treston', 'lucy@cox.net'),\n    ('Judy', 'Aquas', 'jaquas@aquas.com'),\n    ('Yvonne', 'Tjepkema', 'yvonne.tjepkema@hotmail.com'),\n    ('Kayleigh', 'Lace', 'kayleigh.lace@yahoo.com'),\n    ('Felix', 'Hirpara', 'felix_hirpara@cox.net'),\n    ('Tresa', 'Sweely', 'tresa_sweely@hotmail.com'),\n    ('Kristeen', 'Turinetti', 'kristeen@gmail.com'),\n    ('Jenelle', 'Regusters', 'jregusters@regusters.com'),\n    ('Renea', 'Monterrubio', 'renea@hotmail.com'),\n    ('Olive', 'Matuszak', 'olive@aol.com'),\n    ('Ligia', 'Reiber', 'lreiber@cox.net'),\n    ('Christiane', 'Eschberger', 'christiane.eschberger@yahoo.com'),\n    ('Goldie', 'Schirpke', 'goldie.schirpke@yahoo.com'),\n    ('Loreta', 'Timenez', 'loreta.timenez@hotmail.com'),\n    ('Fabiola', 'Hauenstein', 'fabiola.hauenstein@hauenstein.org'),\n    ('Amie', 'Perigo', 'amie.perigo@yahoo.com'),\n    ('Raina', 'Brachle', 'raina.brachle@brachle.org'),\n    ('Erinn', 'Canlas', 'erinn.canlas@canlas.com'),\n    ('Cherry', 'Lietz', 'cherry@lietz.com'),\n    ('Kattie', 'Vonasek', 'kattie@vonasek.org'),\n    ('Lilli', 'Scriven', 'lilli@aol.com'),\n    ('Whitley', 'Tomasulo', 'whitley.tomasulo@aol.com'),\n    ('Barbra', 'Adkin', 'badkin@hotmail.com'),\n    ('Hermila', 'Thyberg', 'hermila_thyberg@hotmail.com'),\n    ('Jesusita', 'Flister', 'jesusita.flister@hotmail.com'),\n    ('Caitlin', 'Julia', 'caitlin.julia@julia.org'),\n    ('Roosevelt', 'Hoffis', 'roosevelt.hoffis@aol.com'),\n    ('Helaine', 'Halter', 'hhalter@yahoo.com'),\n    ('Lorean', 'Martabano', 'lorean.martabano@hotmail.com'),\n    ('France', 'Buzick', 'france.buzick@yahoo.com'),\n    ('Justine', 'Ferrario', 'jferrario@hotmail.com'),\n    ('Adelina', 'Nabours', 'adelina_nabours@gmail.com'),\n    ('Derick', 'Dhamer', 'ddhamer@cox.net'),\n    ('Jerry', 'Dallen', 'jerry.dallen@yahoo.com'),\n    ('Leota', 'Ragel', 'leota.ragel@gmail.com'),\n    ('Jutta', 'Amyot', 'jamyot@hotmail.com'),\n    ('Aja', 'Gehrett', 'aja_gehrett@hotmail.com'),\n    ('Kirk', 'Herritt', 'kirk.herritt@aol.com'),\n    ('Leonora', 'Mauson', 'leonora@yahoo.com'),\n    ('Winfred', 'Brucato', 'winfred_brucato@hotmail.com'),\n    ('Tarra', 'Nachor', 'tarra.nachor@cox.net'),\n    ('Corinne', 'Loder', 'corinne@loder.org'),\n    ('Dulce', 'Labreche', 'dulce_labreche@yahoo.com'),\n    ('Kate', 'Keneipp', 'kate_keneipp@yahoo.com'),\n    ('Kaitlyn', 'Ogg', 'kaitlyn.ogg@gmail.com'),\n    ('Sherita', 'Saras', 'sherita.saras@cox.net'),\n    ('Lashawnda', 'Stuer', 'lstuer@cox.net'),\n    ('Ernest', 'Syrop', 'ernest@cox.net'),\n    ('Nobuko', 'Halsey', 'nobuko.halsey@yahoo.com'),\n    ('Lavonna', 'Wolny', 'lavonna.wolny@hotmail.com'),\n    ('Lashaunda', 'Lizama', 'llizama@cox.net'),\n    ('Mariann', 'Bilden', 'mariann.bilden@aol.com'),\n    ('Helene', 'Rodenberger', 'helene@aol.com'),\n    ('Roselle', 'Estell', 'roselle.estell@hotmail.com'),\n    ('Samira', 'Heintzman', 'sheintzman@hotmail.com'),\n    ('Margart', 'Meisel', 'margart_meisel@yahoo.com'),\n    ('Kristofer', 'Bennick', 'kristofer.bennick@yahoo.com'),\n    ('Weldon', 'Acuff', 'wacuff@gmail.com'),\n    ('Shalon', 'Shadrick', 'shalon@cox.net'),\n    ('Denise', 'Patak', 'denise@patak.org'),\n    ('Louvenia', 'Beech', 'louvenia.beech@beech.com'),\n    ('Audry', 'Yaw', 'audry.yaw@yaw.org'),\n    ('Kristel', 'Ehmann', 'kristel.ehmann@aol.com'),\n    ('Vincenza', 'Zepp', 'vzepp@gmail.com'),\n    ('Elouise', 'Gwalthney', 'egwalthney@yahoo.com'),\n    ('Venita', 'Maillard', 'venita_maillard@gmail.com'),\n    ('Kasandra', 'Semidey', 'kasandra_semidey@semidey.com'),\n    ('Xochitl', 'Discipio', 'xdiscipio@gmail.com'),\n    ('Maile', 'Linahan', 'mlinahan@yahoo.com'),\n    ('Krissy', 'Rauser', 'krauser@cox.net'),\n    ('Pete', 'Dubaldi', 'pdubaldi@hotmail.com'),\n    ('Linn', 'Paa', 'linn_paa@paa.com'),\n    ('Paris', 'Wide', 'paris@hotmail.com'),\n    ('Wynell', 'Dorshorst', 'wynell_dorshorst@dorshorst.org'),\n    ('Quentin', 'Birkner', 'qbirkner@aol.com'),\n    ('Regenia', 'Kannady', 'regenia.kannady@cox.net'),\n    ('Sheron', 'Louissant', 'sheron@aol.com'),\n    ('Izetta', 'Funnell', 'izetta.funnell@hotmail.com'),\n    ('Rodolfo', 'Butzen', 'rodolfo@hotmail.com'),\n    ('Zona', 'Colla', 'zona@hotmail.com'),\n    ('Serina', 'Zagen', 'szagen@aol.com'),\n    ('Paz', 'Sahagun', 'paz_sahagun@cox.net'),\n    ('Markus', 'Lukasik', 'markus@yahoo.com'),\n    ('Jaclyn', 'Bachman', 'jaclyn@aol.com'),\n    ('Cyril', 'Daufeldt', 'cyril_daufeldt@daufeldt.com'),\n    ('Gayla', 'Schnitzler', 'gschnitzler@gmail.com'),\n    ('Erick', 'Nievas', 'erick_nievas@aol.com'),\n    ('Jennie', 'Drymon', 'jennie@cox.net'),\n    ('Mitsue', 'Scipione', 'mscipione@scipione.com'),\n    ('Ciara', 'Ventura', 'cventura@yahoo.com'),\n    ('Galen', 'Cantres', 'galen@yahoo.com'),\n    ('Truman', 'Feichtner', 'tfeichtner@yahoo.com'),\n    ('Gail', 'Kitty', 'gail@kitty.com'),\n    ('Dalene', 'Schoeneck', 'dalene@schoeneck.org'),\n    ('Gertude', 'Witten', 'gertude.witten@gmail.com'),\n    ('Lizbeth', 'Kohl', 'lizbeth@yahoo.com'),\n    ('Glenn', 'Berray', 'gberray@gmail.com'),\n    ('Lashandra', 'Klang', 'lashandra@yahoo.com'),\n    ('Lenna', 'Newville', 'lnewville@newville.com'),\n    ('Laurel', 'Pagliuca', 'laurel@yahoo.com'),\n    ('Mireya', 'Frerking', 'mireya.frerking@hotmail.com'),\n    ('Annelle', 'Tagala', 'annelle@yahoo.com'),\n    ('Dean', 'Ketelsen', 'dean_ketelsen@gmail.com'),\n    ('Levi', 'Munis', 'levi.munis@gmail.com'),\n    ('Sylvie', 'Ryser', 'sylvie@aol.com'),\n    ('Sharee', 'Maile', 'sharee_maile@aol.com'),\n    ('Cordelia', 'Storment', 'cordelia_storment@aol.com'),\n    ('Mollie', 'Mcdoniel', 'mollie_mcdoniel@yahoo.com'),\n    ('Brett', 'Mccullan', 'brett.mccullan@mccullan.com'),\n    ('Teddy', 'Pedrozo', 'teddy_pedrozo@aol.com'),\n    ('Tasia', 'Andreason', 'tasia_andreason@yahoo.com'),\n    ('Hubert', 'Walthall', 'hubert@walthall.org'),\n    ('Arthur', 'Farrow', 'arthur.farrow@yahoo.com'),\n    ('Vilma', 'Berlanga', 'vberlanga@berlanga.com'),\n    ('Billye', 'Miro', 'billye_miro@cox.net'),\n    ('Glenna', 'Slayton', 'glenna_slayton@cox.net'),\n    ('Mitzie', 'Hudnall', 'mitzie_hudnall@yahoo.com'),\n    ('Bernardine', 'Rodefer', 'bernardine_rodefer@yahoo.com'),\n    ('Staci', 'Schmaltz', 'staci_schmaltz@aol.com'),\n    ('Nichelle', 'Meteer', 'nichelle_meteer@meteer.com'),\n    ('Janine', 'Rhoden', 'jrhoden@yahoo.com'),\n    ('Ettie', 'Hoopengardner', 'ettie.hoopengardner@hotmail.com'),\n    ('Eden', 'Jayson', 'eden_jayson@yahoo.com'),\n    ('Lynelle', 'Auber', 'lynelle_auber@gmail.com'),\n    ('Merissa', 'Tomblin', 'merissa.tomblin@gmail.com'),\n    ('Golda', 'Kaniecki', 'golda_kaniecki@yahoo.com'),\n    ('Catarina', 'Gleich', 'catarina_gleich@hotmail.com'),\n    ('Virgie', 'Kiel', 'vkiel@hotmail.com'),\n    ('Jolene', 'Ostolaza', 'jolene@yahoo.com'),\n    ('Keneth', 'Borgman', 'keneth@yahoo.com'),\n    ('Rikki', 'Nayar', 'rikki@nayar.com'),\n    ('Elke', 'Sengbusch', 'elke_sengbusch@yahoo.com'),\n    ('Hoa', 'Sarao', 'hoa@sarao.org'),\n    ('Trinidad', 'Mcrae', 'trinidad_mcrae@yahoo.com'),\n    ('Mari', 'Lueckenbach', 'mari_lueckenbach@yahoo.com'),\n    ('Selma', 'Husser', 'selma.husser@cox.net'),\n    ('Antione', 'Onofrio', 'aonofrio@onofrio.com'),\n    ('Luisa', 'Jurney', 'ljurney@hotmail.com'),\n    ('Clorinda', 'Heimann', 'clorinda.heimann@hotmail.com'),\n    ('Dick', 'Wenzinger', 'dick@yahoo.com'),\n    ('Ahmed', 'Angalich', 'ahmed.angalich@angalich.com'),\n    ('Iluminada', 'Ohms', 'iluminada.ohms@yahoo.com'),\n    ('Joanna', 'Leinenbach', 'joanna_leinenbach@hotmail.com'),\n    ('Caprice', 'Suell', 'caprice@aol.com'),\n    ('Stephane', 'Myricks', 'stephane_myricks@cox.net'),\n    ('Quentin', 'Swayze', 'quentin_swayze@yahoo.com'),\n    ('Annmarie', 'Castros', 'annmarie_castros@gmail.com'),\n    ('Shonda', 'Greenbush', 'shonda_greenbush@cox.net'),\n    ('Cecil', 'Lapage', 'clapage@lapage.com'),\n    ('Jeanice', 'Claucherty', 'jeanice.claucherty@yahoo.com'),\n    ('Josphine', 'Villanueva', 'josphine_villanueva@villanueva.com'),\n    ('Daniel', 'Perruzza', 'dperruzza@perruzza.com'),\n    ('Cassi', 'Wildfong', 'cassi.wildfong@aol.com'),\n    ('Britt', 'Galam', 'britt@galam.org'),\n    ('Adell', 'Lipkin', 'adell.lipkin@lipkin.com'),\n    ('Jacqueline', 'Rowling', 'jacqueline.rowling@yahoo.com'),\n    ('Lonny', 'Weglarz', 'lonny_weglarz@gmail.com'),\n    ('Lonna', 'Diestel', 'lonna_diestel@gmail.com'),\n    ('Cristal', 'Samara', 'cristal@cox.net'),\n    ('Kenneth', 'Grenet', 'kenneth.grenet@grenet.org'),\n    ('Elli', 'Mclaird', 'emclaird@mclaird.com'),\n    ('Alline', 'Jeanty', 'ajeanty@gmail.com'),\n    ('Sharika', 'Eanes', 'sharika.eanes@aol.com'),\n    ('Nu', 'Mcnease', 'nu@gmail.com'),\n    ('Daniela', 'Comnick', 'dcomnick@cox.net'),\n    ('Cecilia', 'Colaizzo', 'cecilia_colaizzo@colaizzo.com'),\n    ('Leslie', 'Threets', 'leslie@cox.net'),\n    ('Nan', 'Koppinger', 'nan@koppinger.com'),\n    ('Izetta', 'Dewar', 'idewar@dewar.com'),\n    ('Tegan', 'Arceo', 'tegan.arceo@arceo.org'),\n    ('Ruthann', 'Keener', 'ruthann@hotmail.com'),\n    ('Joni', 'Breland', 'joni_breland@cox.net'),\n    ('Vi', 'Rentfro', 'vrentfro@cox.net'),\n    ('Colette', 'Kardas', 'colette.kardas@yahoo.com'),\n    ('Malcolm', 'Tromblay', 'malcolm_tromblay@cox.net'),\n    ('Ryan', 'Harnos', 'ryan@cox.net'),\n    ('Jess', 'Chaffins', 'jess.chaffins@chaffins.org'),\n    ('Sharen', 'Bourbon', 'sbourbon@yahoo.com'),\n    ('Nickolas', 'Juvera', 'nickolas_juvera@cox.net'),\n    ('Gary', 'Nunlee', 'gary_nunlee@nunlee.org'),\n    ('Diane', 'Devreese', 'diane@cox.net'),\n    ('Roslyn', 'Chavous', 'roslyn.chavous@chavous.org'),\n    ('Glory', 'Schieler', 'glory@yahoo.com'),\n    ('Rasheeda', 'Sayaphon', 'rasheeda@aol.com'),\n    ('Alpha', 'Palaia', 'alpha@yahoo.com'),\n    ('Refugia', 'Jacobos', 'refugia.jacobos@jacobos.com'),\n    ('Shawnda', 'Yori', 'shawnda.yori@yahoo.com'),\n    ('Mona', 'Delasancha', 'mdelasancha@hotmail.com'),\n    ('Gilma', 'Liukko', 'gilma_liukko@gmail.com'),\n    ('Janey', 'Gabisi', 'jgabisi@hotmail.com'),\n    ('Lili', 'Paskin', 'lili.paskin@cox.net'),\n    ('Loren', 'Asar', 'loren.asar@aol.com'),\n    ('Dorothy', 'Chesterfield', 'dorothy@cox.net'),\n    ('Gail', 'Similton', 'gail_similton@similton.com'),\n    ('Catalina', 'Tillotson', 'catalina@hotmail.com'),\n    ('Lawrence', 'Lorens', 'lawrence.lorens@hotmail.com'),\n    ('Carlee', 'Boulter', 'carlee.boulter@hotmail.com'),\n    ('Thaddeus', 'Ankeny', 'tankeny@ankeny.org'),\n    ('Jovita', 'Oles', 'joles@gmail.com'),\n    ('Alesia', 'Hixenbaugh', 'alesia_hixenbaugh@hixenbaugh.org'),\n    ('Lai', 'Harabedian', 'lai@gmail.com'),\n    ('Brittni', 'Gillaspie', 'bgillaspie@gillaspie.com'),\n    ('Raylene', 'Kampa', 'rkampa@kampa.org'),\n    ('Flo', 'Bookamer', 'flo.bookamer@cox.net'),\n    ('Jani', 'Biddy', 'jbiddy@yahoo.com'),\n    ('Chauncey', 'Motley', 'chauncey_motley@aol.com')\n) AS result(\"first_name\",\"last_name\",\"email\");\n\n\nCREATE OR REPLACE VIEW customers_info_view AS\nSELECT first_name, last_name, email, phone_number, row_number() over() as rnum\nFROM customers_info;\n\n\nCREATE OR REPLACE FUNCTION setof_fake_contacts(num int4) RETURNS SETOF customers_info_view AS $$\n  SELECT first_name, last_name, replace(email, '@', round(extract('epoch' from now()) - 1722705653) || '_' || rnum || '@') as email, phone_number, rnum\n  FROM (\n    SELECT fc.*, row_number() over() as rnum\n    FROM (\n      SELECT first_name, last_name, email, phone_number\n      FROM customers_info_view\n    ) fc,\n    generate_series(1, ceil(num::float/500)::int4) \n    ORDER BY random()\n    LIMIT num\n  ) t\n$$ LANGUAGE SQL; \n\nCREATE TABLE user_types (\n  id TEXT PRIMARY KEY, \n  description TEXT\n);  \n\nINSERT INTO user_types (id, description) \nVALUES ('customer', 'Places orders for delivery'), \n  ('restaurant_manager', 'Represents a restaurant and manages menu items and orders'),\n  ('support', 'Provides customer support for the food delivery service'),\n  ('rider', 'Delivers orders for the food delivery service');\n\nCREATE TABLE addresses (\n  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  street VARCHAR(255) NOT NULL,\n  city VARCHAR(255) NOT NULL,\n  state VARCHAR(255) NOT NULL,\n  postal_code VARCHAR(20) NOT NULL,\n  country VARCHAR(255) NOT NULL,\n  geog GEOGRAPHY\n);\n\nCREATE INDEX idx_addresses ON addresses USING gist (geog);\n\n\nCREATE TABLE users (\n  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  type TEXT NOT NULL REFERENCES user_types, \n  email VARCHAR(100) NOT NULL ,\n  password VARCHAR(100) NOT NULL,\n  first_name VARCHAR(50) NOT NULL,\n  last_name VARCHAR(50) NOT NULL,\n  phone_number VARCHAR(20) NOT NULL,\n  location GEOGRAPHY,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\ncreate index idx_users_email on users (email);\ncreate index idx_users_id_email on users (id, email);\ncreate index idx_users_id_type on users (id, type);\ncreate index idx_users_type on users (type);\n\nCREATE TABLE user_addresses (\n  user_id INTEGER NOT NULL REFERENCES users,\n  address_id BIGINT NOT NULL REFERENCES addresses,\n  UNIQUE(user_id, address_id)\n);\n \nCREATE TABLE restaurants (\n  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  name VARCHAR(100) NOT NULL,\n  address VARCHAR(100) NOT NULL,\n  logo BYTEA,\n  address_id BIGINT NOT NULL REFERENCES addresses,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX idx_restaurants_address_id ON restaurants ( address_id );\nCREATE INDEX idx_restaurants_id_address_id ON restaurants ( id, address_id );\n\nCREATE TABLE restaurant_managers (\n  restaurant_id INTEGER REFERENCES restaurants(id),\n  manager_id INTEGER REFERENCES users(id),\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n \nCREATE TABLE menu_items (\n  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  restaurant_id INTEGER NOT NULL REFERENCES restaurants(id),\n  name VARCHAR(100) NOT NULL,\n  category VARCHAR(100) NOT NULL,\n  price NUMERIC(8,2) NOT NULL,\n  description TEXT NOT NULL,\n  photo BYTEA,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE TABLE delivery_status_types (\n  id TEXT PRIMARY KEY,\n  description TEXT NOT NULL\n);\n\nINSERT INTO delivery_status_types(id, description)\nVALUES \n  ('started', 'A rider picked up the order'),\n  ('arriving', 'The rider is 2 minutes away from destination'),\n  ('delivered', 'The order has been delivered');\n\nCREATE TABLE order_status_types (\n  id TEXT PRIMARY KEY,\n  description TEXT NOT NULL\n);\n\nINSERT INTO order_status_types(id, description)\nVALUES \n  ('pending', 'Waiting for restaurant approval'),\n  ('preparing', 'The restaurant is preparing the order'),\n  ('prepared', 'The restaurant has finished preparing the order and is waiting for the deliverer to pick it up'),\n  ('picked_up', 'The order has been picked by the deliverer'),\n  ('delivered', 'The order has been delivered');\n\n \nCREATE TABLE orders (\n  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  restaurant_id INTEGER NOT NULL REFERENCES restaurants(id),\n  customer_id INTEGER NOT NULL REFERENCES users(id),\n  customer_address_id INTEGER NOT NULL,\n    FOREIGN KEY (customer_id, customer_address_id)  \n    REFERENCES user_addresses(user_id, address_id),\n  deliverer_id INTEGER REFERENCES users(id),\n  status TEXT NOT NULL DEFAULT 'pending' REFERENCES order_status_types,\n  delivery_fee NUMERIC(8,2) NOT NULL,\n  service_fee NUMERIC(8,2) NOT NULL,\n  total_price NUMERIC(8,2) NOT NULL,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX ON orders (restaurant_id);\nCREATE INDEX ON orders (customer_id);\nCREATE INDEX ON orders (deliverer_id);\nCREATE INDEX ON orders (created_at);\nCREATE INDEX ON orders (status, created_at);\nCREATE INDEX ON orders (customer_id, created_at);\nCREATE INDEX ON orders (deliverer_id, created_at);\n\nCREATE TABLE order_items (\n  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  order_id INTEGER NOT NULL REFERENCES orders(id),\n  menu_item_id INTEGER NOT NULL REFERENCES menu_items(id),\n  quantity INTEGER NOT NULL,\n  price NUMERIC(8,2) NOT NULL,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE TABLE order_updates (\n  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  order_id INTEGER NOT NULL REFERENCES orders,\n  change JSONB NOT NULL,\n  changed_by INTEGER REFERENCES users,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE TABLE delivery_status_changes (\n  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  order_id INTEGER NOT NULL REFERENCES orders,\n  delivery_status TEXT NOT NULL REFERENCES delivery_status_types,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE TABLE ratings (\n  id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  restaurant_id INTEGER NOT NULL REFERENCES restaurants(id),\n  customer_id INTEGER NOT NULL REFERENCES users(id),\n  rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),\n  review TEXT NOT NULL,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\n-- CREATE FUNCTION on_order_updates()\n--   RETURNS TRIGGER AS $func$ \n-- BEGIN\n--   INSERT INTO order_updates (order_id, change, changed_by, created_at)\n--   SELECT new.id, \n--     json_object_agg(pre.key, post.value) as change,\n--     old.customer_id,\n--     now()\n--   FROM jsonb_each(to_jsonb(OLD)) AS pre\n--   CROSS JOIN jsonb_each(to_jsonb(NEW)) AS post\n--   WHERE pre.key = post.key \n--     AND pre.value IS DISTINCT FROM post.value;\n--   -- VALUES(new.id)\n--   RETURN NULL;\n-- END;\n-- $func$ LANGUAGE plpgsql;\n\n-- CREATE TRIGGER order_updates \n-- AFTER UPDATE\n-- ON orders\n-- REFERENCING NEW TABLE AS new OLD TABLE AS old\n-- FOR EACH ROW \n-- EXECUTE PROCEDURE on_order_updates();\n \n-- DROP TABLE IF EXISTS temp_json;\n-- CREATE TABLE temp_json (values text);\n/** \n  // Roads\n  const body = `[out:json];(way(51.31087184032102,-0.33782958984375,51.723200166800346,0.053558349609375)[highway=motorway];); out;`;\n  await (await fetch(`http://overpass-api.de/api/interpreter`, { method: \"POST\", body })).json();\n\n  https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL\n  Use 'out count;' to get count \n\n  To get full data\n  curl -d \"[out:json];(node(51.31087184032102,-0.33782958984375,51.723200166800346,0.053558349609375)[amenity=pub];); out;\"  -X POST http://overpass-api.de/api/interpreter\n  \n  to get csv:\n  curl -d \"[out:csv(::id, ::type, ::lat, ::lon, name)];(node(51.31087184032102,-0.33782958984375,51.723200166800346,0.053558349609375)[amenity=pub];); out;\"  -X POST http://overpass-api.de/api/interpreter\n*/\n\nDROP TABLE IF EXISTS \"london_restaurants.geojson\";\nCREATE TABLE IF NOT EXISTS \"london_restaurants.geojson\" (\n  id BIGINT PRIMARY KEY,\n  type TEXT NOT NULL,\n  lat NUMERIC NOT NULL,\n  lon NUMERIC NOT NULL,\n  /* calculated */\n  name TEXT,\n  postcode TEXT,\n  amenity TEXT,\n  website TEXT,\n  geometry GEOMETRY\n);\n\nCREATE INDEX IF NOT EXISTS idx_london \nON \"london_restaurants.geojson\" USING gist (geometry);\n\n/* \n  To include tag values in the CSV must ensure add \"out meta\" or \"out body\"\n  https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#CSV_output_mode \n\n  Retired due to intermittent failures due to Overpass API server load\n\nCOPY \"london_restaurants.geojson\" (id, \"type\", lat, lon, name, postcode, amenity, website) FROM PROGRAM $$\n  curl --retry 5 --retry-all-errors --retry-delay 10 -d \"\n    [out:csv(\n      ::id, \n      ::type, \n      ::lat, \n      ::lon, \n      name, \n      \\\"addr:postcode\\\", \n      amenity, \n      website; \n      false\n    )];\n    (\n      node(\n        51.31087184032102,\n        -0.33782958984375,\n        51.723200166800346,\n        0.053558349609375\n      )[amenity~\\\"pub|restaurant|bar|cafe|fast_food\\\"][name];\n    ); out meta;\"  -X POST http://overpass-api.de/api/interpreter\n$$;\n*/\n\nCOPY \"london_restaurants.geojson\" (id, \"type\", lat, lon, name, postcode, amenity, website) FROM PROGRAM $$\n  curl -f --silent --retry 5 --retry-all-errors --retry-delay 10 https://prostgles.com/static/london_restaurants.csv\n$$;\n\nUPDATE \"london_restaurants.geojson\"\nSET geometry = st_point(lon, lat, 4326);\n\n\nCREATE TABLE IF NOT EXISTS routes (\n  id BIGINT PRIMARY KEY,\n  geog GEOGRAPHY,\n  deliverer_id BIGINT REFERENCES users(id) ON DELETE CASCADE,\n  geometry JSONB\n);\n\n/* Create ~14k restaurants in London */\nWITH raddr as (\n  SELECT name, st_centroid(geometry)::GEOGRAPHY AS geog, 'restaurant' as type, postcode\n  FROM \"london_restaurants.geojson\"\n  WHERE name IS NOT NULL\n  -- AND postcode IS NOT NULL\n),  \niaddr as (\n  INSERT INTO addresses (street, city, country, state, postal_code, geog)\n  SELECT name, 'London', 'UK', 'England', postcode, geog\n  FROM raddr\n  RETURNING *\n), \nires AS (\n  INSERT INTO restaurants (name, address, address_id)\n  SELECT r.name, 'London', a.id\n  FROM iaddr a\n  INNER JOIN raddr r \n    ON a.geog::TEXT = r.geog::TEXT\n  RETURNING *, id as restaurant_id\n),\nnew_users AS (\n  SELECT fk.*, 'pwd' as pwd, 'restaurant_manager' as type\n  FROM (\n    SELECT *, row_number() OVER() as rnum\n    FROM ires\n  ) r\n  INNER JOIN setof_fake_contacts((SELECT COUNT(*) FROM ires)::int4) fk\n    ON fk.rnum = r.rnum\n),\nuins AS (\n  INSERT INTO users (email, password, first_name, last_name, phone_number, type, created_at)\n  SELECT email, pwd, first_name, last_name, phone_number, type,  now() - (random() * \"interval\"('1 year'))\n  FROM new_users\n  RETURNING *\n)\nSELECT *\nFROM uins;\n\n\nCREATE OR REPLACE PROCEDURE mock_users(number_of_users INTEGER DEFAULT 1e3, period INTERVAL DEFAULT '1 day')\nLANGUAGE plpgsql\nAS $$  \n  DECLARE \n    number_of_riders INTEGER;\nBEGIN\n\n  WITH uadd as (\n    INSERT INTO addresses (street, city, state, postal_code, country, geog)\n    SELECT 'West street', 'London', 'UK', 'England', postal_code,\n      (\n        st_dump( \n          st_generatepoints(\n            st_buffer(geog::GEOMETRY, 0.05, 'quad_segs=8'), \n            100\n          )\n        )\n      ).geom::GEOGRAPHY\n    FROM addresses a\n    WHERE EXISTS (\n      SELECT * \n      FROM restaurants r\n      WHERE r.address_id = a.id\n    )\n    ORDER BY random()\n    LIMIT number_of_users\n    RETURNING *\n  ), \n  new_users AS (\n    SELECT fk.*, 'pwd' as pwd, 'customer' as type, \n      geog, id as address_id\n    FROM (\n      SELECT *, row_number() OVER() as rnum\n      FROM uadd\n    ) u\n    INNER JOIN setof_fake_contacts(number_of_users) fk\n      ON fk.rnum = u.rnum\n  ),\n  uins AS (\n    INSERT INTO users (email, password, first_name, last_name, phone_number, type, created_at)\n    SELECT email, pwd, first_name, last_name, phone_number, type,  now() - (random() * period)\n    FROM new_users\n    RETURNING *\n  )\n  -- , \n  -- uains AS (\n    INSERT INTO user_addresses (user_id, address_id)\n    SELECT u.id, nu.address_id \n    FROM uins u \n    INNER JOIN new_users nu\n      ON u.email = nu.email\n  --   RETURNING *\n  -- )\n  -- SELECT * FROM uains\n  ;\n\n  number_of_riders := GREATEST(CEIL(number_of_users / 5), 1);\n\n  /* Create 20% riders */\n  INSERT INTO users (email, password, first_name, last_name, phone_number, type, created_at)\n  SELECT email, 'pwd', first_name, last_name, phone_number, 'rider', now() - (random() * period)\n  FROM setof_fake_contacts(number_of_riders)\n  LIMIT number_of_riders;\nEND $$;\n\nCALL mock_users(1e5::integer, '1 year');\n\nCREATE OR REPLACE VIEW customers AS\nWITH order_stats AS (\n  SELECT customer_id, max(created_at) as last_order, count(*) as total_orders\n  FROM orders\n  GROUP BY 1 -- customer_id, deliverer_id\n)\n  SELECT u.*, geog, a.id as address_id\n    , oc.*\n  FROM users u\n  LEFT JOIN user_addresses ua\n    ON u.id = ua.user_id\n  LEFT JOIN addresses a\n    ON a.id = ua.address_id\n  LEFT JOIN order_stats oc\n    ON oc.customer_id = u.id\n  WHERE u.type = 'customer';\n\nCREATE OR REPLACE VIEW v_riders AS\nWITH order_stats AS (\n  SELECT deliverer_id, max(created_at) as last_order, count(*) as total_orders\n  FROM orders\n  GROUP BY 1 \n)\n  SELECT u.*  \n    , o_r.last_order as last_delivery\n    , o_r.total_orders as total_delivered\n  FROM users u\n  LEFT JOIN order_stats o_r\n    ON o_r.deliverer_id = u.id\n  WHERE u.type = 'rider';\n\nCREATE OR REPLACE  VIEW v_restaurants AS\n  SELECT r.*, geog \n  FROM restaurants r\n  INNER JOIN addresses a\n    ON r.address_id = a.id;\n\n\n/* Create 50 menu items per restaurant */\nINSERT INTO menu_items ( restaurant_id, name, category, description, price )\nSELECT restaurant_id,  name, category, description, replace(price, 'USD', '')::numeric\nFROM (\n  SELECT \n    r.id as restaurant_id,   \n    row_number() OVER( partition by r.id order by random() ) as rnum, mi.*\n  FROM restaurants r\n  CROSS JOIN (\n    SELECT *--, row_number() OVER( order by random()) as rnum\n    FROM _menu_items\n    order by random()\n    LIMIT 100\n  ) mi\n) t\nWHERE rnum < 50; --number of menu items per restaurant\n\n\n\nCREATE OR REPLACE PROCEDURE mock_orders(number_of_orders INTEGER DEFAULT 1e4, period INTERVAL DEFAULT '1 second')\nLANGUAGE plpgsql\nAS $$  \nBEGIN\n \n  INSERT INTO orders (\n    customer_id, \n    customer_address_id, \n    restaurant_id, \n    deliverer_id, \n    status, \n    delivery_fee, \n    service_fee, \n    total_price, \n    created_at\n  )\n  SELECT \n    u.id as customer_id, \n    u.address_id as customer_address_id, \n    r.id as restaurant_id, \n    rider.id as deliverer_id,  \n    CASE WHEN random() < 0.5 THEN 'preparing' ELSE 'picked_up' END as order_status,\n    round(r.dist/1000) as delivery_fee, \n    1 as service_fee, \n    round(random() * 100) as total_price, \n    now() - (random() * period) as created_at\n  FROM (\n    SELECT *, row_number() over() as rnum\n    FROM (\n      SELECT *\n      FROM customers\n      ORDER BY last_order\n      LIMIT number_of_orders\n    ) unested\n  ) u\n  INNER JOIN ( \n    SELECT *, row_number() over(ORDER BY random()) as rnum\n    FROM v_riders, \n      generate_series(\n        1, \n        greatest(ceil(number_of_orders/(select count(*) from v_riders)), 1)::BIGINT\n      )\n    LIMIT number_of_orders\n  ) rider\n  ON rider.rnum = u.rnum\n  LEFT JOIN LATERAL (\n    SELECT *\n      , st_distance(u.geog, ri.geog) as dist\n    FROM v_restaurants ri\n    WHERE st_distance(u.geog, ri.geog) < 7000\n    ORDER BY u.geog <-> ri.geog\n    LIMIT 1\n  ) r ON TRUE; \n \n\n  /* Create 5 items per order  */\n  INSERT INTO order_items (order_id, menu_item_id, quantity, price, created_at)\n  SELECT id, item_id, 1, price, now() - (random() * period)\n  FROM (\n    SELECT  o.id, o.restaurant_id, mi.id as item_id, mi.price, row_number() over(PARTITION BY o.id ) as urnum\n    FROM orders o\n    INNER JOIN (\n      SELECT id, restaurant_id, price\n      FROM menu_items\n      ORDER BY random()\n    ) mi\n    ON mi.restaurant_id = o.restaurant_id\n    WHERE NOT EXISTS (\n      SELECT 1\n      FROM order_items\n      WHERE o.id = order_id\n    )\n  ) tt\n  WHERE urnum < 6;\n\n  /* Prepared */\n  UPDATE orders\n  SET status = 'prepared'\n  WHERE status = 'preparing'\n  AND age(now(), created_at) > '15 minutes'::INTERVAL;\n\n  /* Picked up  */\n  UPDATE orders\n  SET status = 'picked_up'\n  WHERE status = 'prepared'\n  AND deliverer_id IS NOT NULL\n  AND age(now(), created_at) > '2 minutes'::INTERVAL;\n\n  /* Delivered  */\n  UPDATE orders\n  SET status = 'delivered'\n  WHERE status = 'picked_up'\n  AND age(now(), created_at) > '10 minutes'::INTERVAL;\n\nEND $$;\n\nCALL mock_orders(1e5::INTEGER, '1 year'::INTERVAL);\nCALL mock_orders(35e3::INTEGER, '1 hour'::INTERVAL);\n\n\n\n\n\nCREATE OR REPLACE PROCEDURE mock_locations()\nLANGUAGE plpgsql\nAS $$ \n  DECLARE\n    end_time timestamptz := now() + '60 seconds'::INTERVAL;\n    progress NUMERIC := 0.1;\nBEGIN\n\n  WITH \n    ordrs  AS (\n      SELECT o.id,\n        o.deliverer_id, \n        a.geog as user_geog\n      FROM orders o\n      INNER JOIN user_addresses ua\n        ON ua.user_id = o.customer_id \n        AND ua.address_id = o.customer_address_id  \n      INNER JOIN addresses a\n        ON a.id = ua.address_id\n      WHERE o.status = 'picked_up'\n    ), \n    locations AS (\n      SELECT DISTINCT ON (o.id)\n        o.id as order_id, \n        o.deliverer_id, \n        r.id as road_id, \n        r.geog <-> o.user_geog \n      FROM ordrs o\n      JOIN LATERAL (\n        SELECT *\n        FROM routes r\n        -- WHERE properties::TEXT not ilike '%footway%'\n        -- AND st_isempty(geog::GEOMETRY) = false \n        -- AND st_length(geog) > 100\n        ORDER BY r.geog <-> o.user_geog\n        LIMIT 1\n      ) as r\n      ON TRUE\n      ORDER BY o.id, \n        o.deliverer_id, \n        r.id, \n        r.geog <-> o.user_geog\n    )\n  UPDATE routes r\n  SET deliverer_id = l.deliverer_id\n  FROM locations l\n  WHERE r.id = l.road_id;\n\n  WHILE now() < end_time AND progress < 1 LOOP\n\n    UPDATE users u\n    SET location = st_lineinterpolatepoint(r.geog, progress - (random() * 0.1), true)\n    FROM routes r\n    WHERE u.id = r.deliverer_id;\n    \n    COMMIT;\n\n    progress := progress + random() * 0.1;\n    PERFORM pg_sleep(random() * 1);  \n  END LOOP;\nEND $$; "
  },
  {
    "path": "server/sample_schemas/food_delivery/onMount.ts",
    "content": "export const onMount: ProstglesOnMount = async ({ dbo }) => {\n  const roadTableHandler = dbo.routes;\n  if (!roadTableHandler) return;\n\n  const count = await roadTableHandler.count();\n  if (count) {\n    await dbo.sql(`\n      VACUUM;\n    `);\n\n    const mockLocations = async () => {\n      try {\n        await dbo.sql(`CALL mock_locations(); /* from fork */`);\n      } catch (error) {\n        console.error(\"Error calling mock_locations\", error);\n        const funcs = await dbo.sql(\n          `\n          SELECT proname, probin, pg_get_function_arguments(oid), current_database(), (SELECT string_agg(extname, '; ') FROM pg_catalog.pg_extension) as extensions\n          FROM pg_catalog.pg_proc\n          WHERE proname = 'st_lineinterpolatepoint'\n        `,\n          {},\n          { returnType: \"rows\" },\n        );\n        console.error(funcs);\n        throw error;\n      }\n      mockLocations();\n    };\n    mockLocations();\n\n    setInterval(async () => {\n      const hourOfDayAverageOrders = {\n        0: 2,\n        1: 1,\n        2: 1,\n        3: 1,\n        4: 1,\n        5: 1,\n        6: 2,\n        7: 3,\n        8: 5,\n        9: 8,\n        10: 10,\n        11: 12,\n        12: 12,\n        13: 12,\n        14: 10,\n        15: 10,\n        16: 10,\n        17: 12,\n        18: 15,\n        19: 15,\n        20: 12,\n        21: 8,\n        22: 5,\n        23: 3,\n      };\n      const hourOfDay =\n        new Date().getHours() as keyof typeof hourOfDayAverageOrders;\n      const orderRatePerSecond = hourOfDayAverageOrders[hourOfDay] ?? 1;\n      await dbo.sql(`CALL mock_orders(\\${orderRatePerSecond}::integer)`, {\n        orderRatePerSecond,\n      });\n    }, 3e3);\n\n    return;\n  }\n\n  // const { elements } = await fetch(\"http://overpass-api.de/api/interpreter\", {\n  //   method: \"POST\",\n  //   body: \"[out:json];(way(51.31087184032102,-0.33782958984375,51.723200166800346,0.053558349609375)[highway];); out 200000 ids geom;\",\n  //   headers: {\n  //     \"Content-Type\": \"application/json\",\n  //   },\n  // }).then((res) => res.json());\n  const { elements } = await fetch(\n    \"https://prostgles.com/static/routes.json\",\n  ).then((res) => res.json());\n\n  await dbo.routes.insert(\n    elements.map((d) => ({\n      id: d.id,\n      geometry: {\n        type: \"LineString\",\n        coordinates: d.geometry.map(({ lat, lon }) => [lon, lat]),\n      },\n    })),\n  );\n\n  await dbo.sql(`\n    UPDATE routes\n    SET geog = ST_SetSRID(ST_GeomFromGeoJSON(geometry), 4326);\n\n    DELETE FROM routes\n    WHERE st_isempty(geog::GEOMETRY) = true\n    OR st_length(geog) < 100\n    OR id IS NULL;\n\n    CREATE INDEX IF NOT EXISTS idx_roads \n    ON routes USING gist (geog);\n  `);\n};\n"
  },
  {
    "path": "server/sample_schemas/food_delivery/workspaceConfig.ts",
    "content": "// Using the below typescript definition return\n// a json workspace/dashboard config for a property management company ensuring all table and column names are snake case:\n\ntype WorkspaceConfig = {\n  workspaces: {\n    name: string;\n    options?: any;\n    windows: (\n      | {\n          type: \"sql\";\n          name: string;\n          sql: string;\n        }\n      | {\n          type: \"table\";\n          table_name: string;\n          columns?: {\n            name: string;\n            show?: boolean;\n            width?: number;\n            nested?: any;\n            style?:\n              | {\n                  type: \"Conditional\";\n                  conditions: {\n                    color: string;\n                    operator: \"=\";\n                    chipColor: string;\n                    condition: string;\n                    textColor: string;\n                    borderColor: string;\n                  }[];\n                }\n              | {\n                  type: \"Barchart\";\n                  barColor: string;\n                  textColor: string;\n                };\n            format?: any;\n          }[];\n          options?: any;\n          sort?: { asc: boolean; key: string }[];\n        }\n    )[];\n  }[];\n};\n\nexport const workspaceConfig: WorkspaceConfig = {\n  workspaces: [\n    {\n      name: \"Main\",\n      options: {\n        hideCounts: false,\n        pinnedMenu: true,\n        tableListSortBy: \"extraInfo\",\n        tableListEndInfo: \"count\",\n        defaultLayoutType: \"tab\",\n      },\n      windows: [\n        {\n          type: \"sql\",\n          name: \"Stats\",\n          sql: `/* Most popular menu items */\nSELECT mi.name, COUNT(*) as order_count\nFROM order_items oi\nLEFT JOIN menu_items mi\n  ON mi.id = oi.menu_item_id\nLEFT JOIN orders o\n  ON o.id = oi.order_id\nGROUP BY 1\nORDER BY 2 desc\n\n/* Busiest delivery times */\nSELECT EXTRACT(HOUR FROM created_at) as hour, COUNT(*) as order_count\nFROM orders\nGROUP BY hour\nORDER BY order_count DESC\n\n/* Top-performing restaurants */\nSELECT restaurants.name, SUM(orders.total_price) as total_revenue\nFROM orders\nJOIN restaurants ON orders.restaurant_id = restaurants.id\nGROUP BY restaurants.id, restaurants.name\nORDER BY total_revenue DESC\nLIMIT 10;\n\n/* Customer retention rate */\nWITH customer_orders AS (\n  SELECT customer_id, MIN(created_at) as first_order, MAX(created_at) as last_order\n  FROM orders\n  GROUP BY customer_id\n)\nSELECT \n  (COUNT(CASE WHEN last_order >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' THEN 1 END) * 100.0 / COUNT(*))::NUMERIC(8,2) as retention_rate\nFROM customer_orders;`,\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "server/sample_schemas/lodging.sql",
    "content": " \nCREATE TABLE users (\n  id SERIAL PRIMARY KEY,\n  email VARCHAR(100) NOT NULL UNIQUE,\n  password VARCHAR(100) NOT NULL,\n  first_name VARCHAR(50) NOT NULL,\n  last_name VARCHAR(50) NOT NULL,\n  profile_picture BYTEA,\n  phone_number VARCHAR(20) NOT NULL,\n  is_host BOOLEAN NOT NULL DEFAULT FALSE,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE listings (\n  id SERIAL PRIMARY KEY,\n  user_id INTEGER NOT NULL REFERENCES users(id),\n  title VARCHAR(100) NOT NULL,\n  description TEXT NOT NULL,\n  price NUMERIC(8,2) NOT NULL,\n  num_guests INTEGER NOT NULL,\n  num_bedrooms INTEGER NOT NULL,\n  num_bathrooms INTEGER NOT NULL,\n  address VARCHAR(100) NOT NULL,\n  lat NUMERIC(9,6) NOT NULL,\n  lon NUMERIC(9,6) NOT NULL,\n  cover_photo BYTEA,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE reservations (\n  id SERIAL PRIMARY KEY,\n  user_id INTEGER NOT NULL REFERENCES users(id),\n  listing_id INTEGER NOT NULL REFERENCES listings(id),\n  start_date DATE NOT NULL,\n  end_date DATE NOT NULL,\n  num_guests INTEGER NOT NULL,\n  message TEXT,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE reviews (\n  id SERIAL PRIMARY KEY,\n  user_id INTEGER NOT NULL REFERENCES users(id),\n  listing_id INTEGER NOT NULL REFERENCES listings(id),\n  rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),\n  review TEXT NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);"
  },
  {
    "path": "server/sample_schemas/maps.sql",
    "content": "CREATE TABLE users (\n  id SERIAL PRIMARY KEY,\n  name VARCHAR(255) NOT NULL,\n  email VARCHAR(255) NOT NULL,\n  password VARCHAR(255) NOT NULL\n);\n\nINSERT INTO users (name, email, password) VALUES\n  ('John Doe', 'johndoe@example.com', 'password123'),\n  ('Jane Doe', 'janedoe@example.com', 'password456');\n\nCREATE TABLE places (\n  id SERIAL PRIMARY KEY,\n  name VARCHAR(255) NOT NULL,\n  address VARCHAR(255) NOT NULL,\n  latitude FLOAT NOT NULL,\n  longitude FLOAT NOT NULL,\n  opening_hours VARCHAR(255) NOT NULL\n);\n\nINSERT INTO places (id, name, address, latitude, longitude\n, opening_hours\n) VALUES\n  (1, 'restaurant 1', 'here', 51, 0, 'here'),\n  (2, 'restaurant 2', 'here', 51, 0, 'here'),\n  (3, 'hotel 1', 'here', 51, 0, 'here'),\n  (4, 'hotel 2', 'here', 51, 0, 'here');\n\nCREATE TABLE opening_hours (\n  id SERIAL PRIMARY KEY,\n  place_id INTEGER NOT NULL REFERENCES places,\n  week_day INTEGER NOT NULL CHECK(week_day >= 0 AND week_day <= 6),  --(0=Monday - 6=Sunday)\n  opens_at TIME,\n  closes_at TIME,\n  UNIQUE(place_id, week_day)\n);\n\nCREATE TABLE place_types (\n  id TEXT PRIMARY KEY \n);\n\nINSERT INTO place_types (id) VALUES\n  ('Restaurant'),\n  ('Hotel');\n\nCREATE TABLE place_type_icons (\n  id SERIAL PRIMARY KEY,\n  place_type_id TEXT NOT NULL REFERENCES place_types,\n  url TEXT NOT NULL \n);\n \n\nCREATE TABLE photos (\n  id SERIAL PRIMARY KEY,\n  place_id INTEGER NOT NULL REFERENCES places,\n  file_name VARCHAR(255) NOT NULL\n);\n\nINSERT INTO photos (place_id, file_name) VALUES\n  (1, 'restaurant_photo_1.jpg'),\n  (1, 'restaurant_photo_2.jpg'),\n  (2, 'hotel_photo_1.jpg'),\n  (2, 'hotel_photo_2.jpg');\n\nCREATE TABLE reviews (\n  id SERIAL PRIMARY KEY,\n  place_id INTEGER NOT NULL,\n  user_id INTEGER NOT NULL,\n  rating INTEGER NOT NULL,\n  review_text TEXT NOT NULL,\n  FOREIGN KEY (place_id) REFERENCES places(id),\n  FOREIGN KEY (user_id) REFERENCES users(id)\n);\n"
  },
  {
    "path": "server/sample_schemas/property_management/onMount.ts",
    "content": "export const onMount: ProstglesOnMount = async ({ dbo }) => {\n  const createData = async () => {\n    await dbo.sql(\n      `\n    -- Create Property Listings Table\n    CREATE TABLE property_listings (\n        property_id SERIAL PRIMARY KEY,\n        address VARCHAR(255) NOT NULL,\n        property_type VARCHAR(100) NOT NULL,\n        status VARCHAR(100) NOT NULL,\n        price NUMERIC(12, 2) NOT NULL\n    );\n    \n    -- Create Tenant Information Table\n    CREATE TABLE tenant_information (\n        tenant_id SERIAL PRIMARY KEY,\n        name VARCHAR(255) NOT NULL,\n        lease_start DATE NOT NULL,\n        lease_end DATE NOT NULL,\n        monthly_rent NUMERIC(12, 2) NOT NULL\n    );\n    \n    -- Create Maintenance Requests Table\n    CREATE TABLE maintenance_requests (\n        request_id SERIAL PRIMARY KEY,\n        property_id INT NOT NULL REFERENCES property_listings(property_id),\n        issue VARCHAR(255) NOT NULL,\n        status VARCHAR(100) NOT NULL,\n        reported_date DATE NOT NULL\n    );\n    \n    -- Insert Dummy Data into Property Listings\n    INSERT INTO property_listings (address, property_type, status, price) VALUES \n    ('123 Main St, Anytown, USA', 'Apartment', 'Available', 1200.00),\n    ('456 Elm St, Anytown, USA', 'House', 'Rented', 1500.00),\n    ('789 Oak St, Anytown, USA', 'Condo', 'Available', 1100.00);\n    \n    -- Insert Dummy Data into Tenant Information\n    INSERT INTO tenant_information (name, lease_start, lease_end, monthly_rent) VALUES \n    ('John Doe', '2023-01-01', '2023-12-31', 1200.00),\n    ('Jane Smith', '2023-02-01', '2023-12-31', 1500.00),\n    ('Bob Johnson', '2023-03-01', '2023-12-31', 1100.00);\n    \n    -- Insert Dummy Data into Maintenance Requests\n\n    \n    `,\n      {},\n      { returnType: \"rows\" },\n    );\n  };\n  await createData();\n};\n"
  },
  {
    "path": "server/sample_schemas/property_management/workspaceConfig.ts",
    "content": "// Using the below typescript definition return:\n//  1) a json workspace/dashboard config for a property management company ensuring all table and column names are snake case:\n//  2) a postgres sql script (with postgis if required) to create and populate the tables with dummy data\n\ntype WorkspaceConfig = {\n  workspaces: {\n    name: string;\n    options?: any;\n    windows: {\n      type: \"table\";\n      table_name: string;\n      columns?: {\n        name: string;\n        show?: boolean;\n        width?: number;\n        nested?: any;\n        style?: {\n          type: \"Conditional\";\n          conditions: {\n            color: string;\n            operator: \"=\";\n            chipColor: string;\n            condition: string;\n            textColor: string;\n            borderColor: string;\n          }[];\n        };\n        format?: any;\n      }[];\n      options?: any;\n      sort?: { asc: boolean; key: string }[];\n    }[];\n  }[];\n};\n\nexport const workspaceConfig: WorkspaceConfig = {\n  workspaces: [\n    {\n      name: \"Property Management Dashboard\",\n      options: {\n        hideCounts: false,\n        pinnedMenu: true,\n        tableListSortBy: \"extraInfo\",\n        tableListEndInfo: \"count\",\n        defaultLayoutType: \"tab\",\n      },\n      windows: [\n        {\n          type: \"table\",\n          table_name: \"property_listings\",\n          columns: [\n            { name: \"property_id\", show: true, width: 100 },\n            { name: \"address\", show: true, width: 200 },\n            { name: \"property_type\", show: true, width: 100 },\n            { name: \"status\", show: true, width: 100 },\n            { name: \"price\", show: true, width: 120, format: \"currency\" },\n          ],\n          sort: [{ asc: true, key: \"price\" }],\n        },\n        {\n          type: \"table\",\n          table_name: \"tenant_information\",\n          columns: [\n            { name: \"tenant_id\", show: true, width: 100 },\n            { name: \"name\", show: true, width: 150 },\n            { name: \"lease_start\", show: true, width: 120, format: \"date\" },\n            { name: \"lease_end\", show: true, width: 120, format: \"date\" },\n            {\n              name: \"monthly_rent\",\n              show: true,\n              width: 120,\n              format: \"currency\",\n            },\n          ],\n          sort: [{ asc: true, key: \"lease_start\" }],\n        },\n        {\n          type: \"table\",\n          table_name: \"maintenance_requests\",\n          columns: [\n            { name: \"request_id\", show: true, width: 100 },\n            { name: \"property_id\", show: true, width: 100 },\n            { name: \"issue\", show: true, width: 200 },\n            { name: \"status\", show: true, width: 100 },\n            { name: \"reported_date\", show: true, width: 120, format: \"date\" },\n          ],\n          sort: [{ asc: false, key: \"reported_date\" }],\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "server/sample_schemas/sales.sql",
    "content": " \nCREATE TABLE users (\n  id SERIAL PRIMARY KEY,\n  email VARCHAR(100) NOT NULL UNIQUE,\n  password VARCHAR(100) NOT NULL,\n  first_name VARCHAR(50) NOT NULL,\n  last_name VARCHAR(50) NOT NULL,\n  phone_number VARCHAR(20) NOT NULL,\n  user_type VARCHAR(20) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE accounts (\n  id SERIAL PRIMARY KEY,\n  name VARCHAR(100) NOT NULL,\n  owner_id INTEGER NOT NULL REFERENCES users(id),\n  industry VARCHAR(100) NOT NULL,\n  website VARCHAR(100) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE contacts (\n  id SERIAL PRIMARY KEY,\n  account_id INTEGER NOT NULL REFERENCES accounts(id),\n  first_name VARCHAR(50) NOT NULL,\n  last_name VARCHAR(50) NOT NULL,\n  email VARCHAR(100) NOT NULL,\n  phone_number VARCHAR(20) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE opportunities (\n  id SERIAL PRIMARY KEY,\n  account_id INTEGER NOT NULL REFERENCES accounts(id),\n  contact_id INTEGER NOT NULL REFERENCES contacts(id),\n  name VARCHAR(100) NOT NULL,\n  amount NUMERIC(10,2) NOT NULL,\n  stage VARCHAR(20) NOT NULL,\n  close_date TIMESTAMP WITH TIME ZONE NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n \nCREATE TABLE tasks (\n  id SERIAL PRIMARY KEY,\n  opportunity_id INTEGER NOT NULL REFERENCES opportunities(id),\n  assigned_to INTEGER NOT NULL REFERENCES users(id),\n  due_date TIMESTAMP WITH TIME ZONE NOT NULL,\n  subject VARCHAR(100) NOT NULL,\n  status VARCHAR(20) NOT NULL,\n  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n);\n"
  },
  {
    "path": "server/sample_schemas/sample.sql",
    "content": "\n\nDROP TABLE IF EXISTS account_statuses CASCADE;\nCREATE TABLE account_statuses (\n  id TEXT PRIMARY KEY \n);\n\nINSERT INTO account_statuses  (id)\nVALUES ('active'), ('disabled'), ('deleted');\n\nDROP TABLE IF EXISTS customers CASCADE;\nCREATE TABLE customers (\n  id SERIAL PRIMARY KEY,\n  first_name  VARCHAR(150) NOT NULL ,\n  last_name  VARCHAR(150) NOT NULL ,\n  job_title VARCHAR(150) NOT NULL ,\n  email  VARCHAR(255)  ,\n  address_line1  VARCHAR(250) ,\n  city  VARCHAR(150) NOT NULL,\n  country  VARCHAR(150) NOT NULL ,\n  status TEXT REFERENCES account_statuses,\n  created_on  TIMESTAMP  NOT NULL DEFAULT now(),\n  created_by  UUID NOT NULL DEFAULT gen_random_uuid()\n);\n\nINSERT INTO customers  (\n  first_name, \n  last_name, \n  job_title, \n  email, \n  address_line1, \n  city, \n  country\n)\nSELECT \n  data->'name'->>'firstName' as firstName\n, data->'name'->>'lastName' as lastName\n, data->'name'->>'jobTitle' as jobTitle\n, data->'internet'->>'email' as email\n, data->'addres'->>'streetAddress' as streetAddress\n, data->'addres'->>'city' as city\n, data->'addres'->>'country' as country\nFROM fake_data;\n\n\n-- ALTER TABLE customers \n-- ADD COLUMN photo_id UUID REFERENCES files;\n\nDROP TABLE IF EXISTS products CASCADE;\nCREATE TABLE products (\n  id    SERIAL PRIMARY KEY,\n  name  TEXT,\n  description TEXT,\n  price   NUMERIC(10,2)\n); \n\nDROP TABLE IF EXISTS orders CASCADE;\nCREATE TABLE orders (\n  id SERIAL PRIMARY KEY,\n  customer_id INTEGER REFERENCES customers,\n  total_price DECIMAL,\n  date TIMESTAMP\n);\n\nDROP TABLE IF EXISTS order_items CASCADE;\nCREATE TABLE order_items (\n  id SERIAL PRIMARY KEY,\n  order_id INTEGER REFERENCES orders,\n  product_id INTEGER REFERENCES products,\n  quantity INTEGER,\n  price NUMERIC(10,2)\n);"
  },
  {
    "path": "server/sample_schemas/testschema.ts",
    "content": "export const tableConfig: TableConfig = {\n  market_caps: {\n    columns: {\n      image: \"TEXT\",\n    },\n  },\n};\n\nexport const dashboardConfig: DashboardConfig = {\n  workspaces: [\n    {\n      name: \"Main\",\n      windows: [\n        {\n          type: \"table\",\n          table_name: \"\",\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "server/sample_schemas/weather/onMount.ts",
    "content": "export const onMount: ProstglesOnMount = async ({ dbo }) => {\n  if (!dbo.cities) {\n    console.warn(\"Creating tables...\");\n    await dbo.sql(`\n      CREATE EXTENSION IF NOT EXISTS postgis;\n\n      CREATE TABLE airports (\n        id BIGINT PRIMARY KEY,                -- Unique OSM ID\n        name TEXT,                            -- Name of the airport\n        iata_code TEXT,                 -- IATA code (3-letter code)\n        icao_code TEXT,                 -- ICAO code (4-letter code)\n        latitude DOUBLE PRECISION NOT NULL,            \n        longitude DOUBLE PRECISION NOT NULL,           \n        elevation TEXT,                       -- Elevation (in meters)\n        operator TEXT,                        -- Airport operator\n        capacity numeric,                     -- Passenger capacity (if available)\n        opened_date TEXT,                     -- Date the airport opened\n        country TEXT,                         -- Country where the airport is located\n        runway_count numeric,                 -- Number of runways\n        type TEXT ,                           -- Airport type (e.g., international, regional)\n        geog GEOGRAPHY(POINT, 4326) generated always as (ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)) stored\n      );\n\n      CREATE INDEX airports_geog_idx ON airports USING GIST (geog);\n      \n      CREATE TABLE cities (\n        id BIGINT PRIMARY KEY,\n        name VARCHAR(100),\n        name_en VARCHAR(100),\n        capital TEXT,\n        admin_level TEXT,\n        official_status TEXT,\n        population TEXT,\n        population_date TEXT,\n        start_date TEXT,\n\n        note TEXT,\n        website TEXT,\n        addr_country TEXT,\n        country_code TEXT,\n        \n        latitude DECIMAL(9,6) NOT NULL,\n        longitude DECIMAL(9,6) NOT NULL,\n        geog GEOGRAPHY(POINT, 4326) generated always as (ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)) stored,\n        CONSTRAINT unique_city UNIQUE (latitude, longitude)\n      );\n\n      CREATE INDEX cities_geog_idx ON airports USING GIST (geog);\n\n      CREATE TABLE weather_forecasts (\n        id SERIAL PRIMARY KEY,\n        city_id INT REFERENCES cities(id) ON DELETE CASCADE,\n        forecast_time TIMESTAMPTZ NOT NULL,\n        temperature DECIMAL(5,2),\n        wind_speed DECIMAL(5,2),\n        precipitation DECIMAL(5,2),\n        pressure DECIMAL(7,2),\n        humidity DECIMAL(5,2),\n        created_at TIMESTAMPTZ DEFAULT NOW(),\n        CONSTRAINT unique_forecast_per_city UNIQUE (city_id, forecast_time)\n      );\n\n      CREATE INDEX weather_forecasts_city_id_idx ON weather_forecasts(city_id);\n    `);\n    console.warn(\"Created sample schema 'weather'\");\n  }\n\n  const addForecasts = async () => {\n    if (!dbo.weather_forecasts) return;\n    const citiesCount = await dbo.cities.count();\n    console.log(\"Cities in DB:\", citiesCount);\n    if (!+citiesCount) {\n      console.log(\"Adding cities and airports...\");\n      await dbo.sql(`\n        TRUNCATE weather_forecasts RESTART IDENTITY CASCADE;\n        TRUNCATE airports RESTART IDENTITY CASCADE; \n      `);\n      const airports = await fetchAirports();\n      await dbo.airports.insert(airports);\n\n      const cities = await fetchCities();\n      await dbo.cities.insert(cities);\n    }\n    const cities = await dbo.cities.find({\n      name_en: {\n        $in: [\n          \"London\",\n          \"Amsterdam\",\n          \"Athens\",\n          \"Valencia\",\n          \"Malaga\",\n          \"Zagreb\",\n          \"Valencia\",\n          \"Copenhagen\",\n        ],\n      },\n    });\n\n    const lastAdded = await dbo.weather_forecasts.findOne(\n      {},\n      { orderBy: [{ forecast_time: -1 }] },\n    );\n    if (\n      lastAdded &&\n      new Date().getTime() - new Date(lastAdded.forecast_time).getTime() < DAY\n    ) {\n      console.log(\"Forecasts are up to date, skipping update\");\n      return;\n    }\n    console.log(\"Adding forecasts for\", cities.length, \"cities\");\n    for (const city of cities) {\n      console.log(\"Fetched weather for\", city.name_en || city.name);\n      const weather = await getWeatherForCity(city.latitude, city.longitude);\n      const forecasts = weather.properties.timeseries.map((entry) => ({\n        city_id: city.id,\n        // forecast_time: new Date(entry.time),\n        forecast_time: entry.time,\n        temperature: entry.data.instant.details.air_temperature,\n        wind_speed: entry.data.instant.details.wind_speed,\n        precipitation:\n          entry.data.next_1_hours?.details.precipitation_amount ?? 0,\n        pressure: entry.data.instant.details.air_pressure_at_sea_level,\n        humidity: entry.data.instant.details.relative_humidity,\n      }));\n\n      await dbo.weather_forecasts.insert(forecasts);\n    }\n  };\n\n  const DAY = 1000 * 3600 * 24;\n  setTimeout(addForecasts, DAY); // Run every day\n  await addForecasts();\n};\n\ntype ForecastDetails = {\n  air_temperature: number;\n  wind_speed: number;\n  precipitation_amount: number;\n  air_pressure_at_sea_level: number;\n  relative_humidity: number;\n};\n\ntype TimeSeriesEntry = {\n  time: string; // The timestamp\n  data: {\n    instant: {\n      details: ForecastDetails;\n    };\n    next_1_hours?: {\n      details: {\n        precipitation_amount: number;\n      };\n      summary: {\n        symbol_code: string;\n      };\n    };\n  };\n};\n\ntype ForecastResponse = {\n  properties: {\n    timeseries: TimeSeriesEntry[];\n  };\n};\n\nconst getWeatherForCity = async (\n  lat: number | string,\n  lon: number | string,\n) => {\n  const response = await fetch(\n    `https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${lat}&lon=${lon}`,\n    {\n      headers: {\n        \"User-Agent\": \"Prostgles-Demo\", // YR requires a user agent header\n      },\n    },\n  );\n\n  if (!response.ok) {\n    throw new Error(`Failed to fetch weather for coordinates: ${lat}, ${lon}`);\n  }\n\n  const data: ForecastResponse = await response.json();\n  return data;\n};\n\nconst EUROPE_BBOX = \"(34,-25,72,45)\";\nconst fetchCities = async (): Promise<\n  {\n    id: number;\n    lat: number;\n    lon: number;\n    name: string | null;\n    name_en: string | null;\n    capital: string | null;\n    admin_level: string | null;\n    official_status: string | null;\n    population: string | null;\n    population_date: string | null;\n    start_date: string | null;\n  }[]\n> => {\n  const overpassQuery = `\n  [out:json];\n  node[\"place\"=\"city\"]${EUROPE_BBOX};\n  out body;\n  `;\n\n  const overpassUrl =\n    \"https://overpass-api.de/api/interpreter?data=\" +\n    encodeURIComponent(overpassQuery);\n\n  return fetch(overpassUrl)\n    .then((response) => {\n      if (!response.ok) {\n        throw new Error(\"Failed to fetch cities\");\n      }\n      return response;\n    })\n    .then((response) => response.json())\n    .then((data) => {\n      const cities = data.elements.map((element) => ({\n        id: element.id,\n        latitude: element.lat,\n        longitude: element.lon,\n        name: element.tags.name || null,\n        name_en:\n          element.tags[\"name:en\"] ||\n          (element.tags.name || \"\")\n            .normalize(\"NFD\")\n            .replace(/[\\u0300-\\u036f]/g, \"\"),\n        capital: element.tags.capital || null,\n        admin_level: element.tags.admin_level || null,\n        official_status: element.tags.official_status || null,\n        population: element.tags.population || null,\n        population_date: element.tags[\"population:date\"] || null,\n        start_date: element.tags.start_date || null,\n        note: element.tags.note || null,\n        website: element.tags.website || null,\n        addr_country: element.tags[\"addr:country\"] || null,\n        country_code: element.tags[\"is_in:country_code\"] || null,\n      }));\n\n      return cities;\n    });\n};\n\nconst fetchAirports = async () => {\n  const overpassQuery = `\n    [out:json][timeout:180]; \n    way[\"aeroway\"=\"aerodrome\"][\"iata\"]${EUROPE_BBOX}; \n    out center;\n  `;\n  const overpassUrl =\n    \"https://overpass-api.de/api/interpreter?data=\" +\n    encodeURIComponent(overpassQuery);\n  const response = await fetch(overpassUrl);\n  if (!response.ok) {\n    throw new Error(`Failed to fetch Airports`);\n  }\n  const data = await response.json();\n\n  return data.elements.map((element) => {\n    return {\n      id: element.id,\n      name: element.tags.name || null,\n      iata_code: element.tags[\"iata\"] || null,\n      icao_code: element.tags[\"icao\"] || null,\n      latitude: element.lat || (element.center ? element.center.lat : null),\n      longitude: element.lon || (element.center ? element.center.lon : null),\n      elevation: element.tags.ele || null,\n      operator: element.tags.operator || null,\n      capacity: element.tags.capacity || null,\n      opened_date: element.tags[\"start_date\"] || null,\n      country: element.tags[\"addr:country\"] || null,\n      runway_count: element.tags.runways || null,\n      type: element.tags[\"aeroway\"] || null,\n    };\n  });\n};\n"
  },
  {
    "path": "server/src/BackupManager/BackupManager.ts",
    "content": "import path from \"path\";\nimport { PassThrough } from \"stream\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { getInstalledPsqlVersions } from \"./getInstalledPrograms\";\nimport { pgDump } from \"./pgDump\";\nimport { pgRestore } from \"./pgRestore\";\nimport { getBkp, getFileMgr } from \"./utils\";\n\nexport type Backups = Required<DBGeneratedSchema[\"backups\"]>[\"columns\"];\ntype DumpOpts = Backups[\"options\"];\nexport type DumpOptsServer = DumpOpts & { initiator: string };\n\nexport type Users = Required<DBGeneratedSchema[\"users\"][\"columns\"]>;\nexport type Connections = Required<DBGeneratedSchema[\"connections\"][\"columns\"]>;\ntype DBS = DBOFullyTyped<DBGeneratedSchema>;\n\nimport checkDiskSpace from \"check-disk-space\";\nimport type { Request, Response } from \"express\";\nimport type { Filter } from \"prostgles-server/dist/DboBuilder/DboBuilderTypes\";\nimport { bytesToSize } from \"prostgles-server/dist/FileManager/FileManager\";\nimport type { DB } from \"prostgles-server/dist/Prostgles\";\nimport type { FilterItem, SubscriptionHandler } from \"prostgles-types\";\nimport type { InstalledPrograms } from \"@common/electronInitTypes\";\nimport { ROUTES } from \"@common/utils\";\nimport type { SUser } from \"../authConfig/sessionUtils\";\nimport type { ConnectionManager } from \"../ConnectionManager/ConnectionManager\";\nimport { getRootDir } from \"../electronConfig\";\nimport { checkAutomaticBackup } from \"./checkAutomaticBackup\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\n\nexport const HOUR = 3600 * 1000;\n\nexport default class BackupManager {\n  tempStreams: Record<string, { lastChunk: number; stream: PassThrough }> = {};\n\n  installedPrograms: InstalledPrograms | undefined;\n\n  dbs: DBS;\n  db: DB;\n  automaticBackupInterval: NodeJS.Timeout;\n  connMgr: ConnectionManager;\n  dbConfSub?: SubscriptionHandler;\n\n  constructor(\n    db: DB,\n    dbs: DBS,\n    connMgr: ConnectionManager,\n    installedPrograms: InstalledPrograms | undefined,\n  ) {\n    this.db = db;\n    this.dbs = dbs;\n    this.connMgr = connMgr;\n    this.installedPrograms = installedPrograms;\n\n    const checkAutomaticBackupsForEachConnection = async () => {\n      const connections = await this.dbs.connections.find({\n        $existsJoined: {\n          database_configs: { \"backups_config->>enabled\": \"true\" },\n        },\n      } as FilterItem);\n      for (const con of connections) {\n        await this.checkAutomaticBackup(con);\n      }\n    };\n    this.automaticBackupInterval = setInterval(() => {\n      void checkAutomaticBackupsForEachConnection();\n    }, HOUR / 4);\n    void (async () => {\n      await this.dbConfSub?.unsubscribe();\n      this.dbConfSub = await dbs.database_configs.subscribe(\n        {},\n        { select: { backups_config: 1 }, limit: 0 },\n        checkAutomaticBackupsForEachConnection,\n      );\n    })();\n  }\n\n  getCmd = (cmd: \"pg_dump\" | \"pg_restore\" | \"pg_dumpall\" | \"psql\") => {\n    if (!this.installedPrograms) throw new Error(\"No installed programs\");\n    const { filePath, os } = this.installedPrograms;\n    if (os === \"Windows\") {\n      if (!filePath) throw new Error(\"No file path\");\n      return `${filePath}${cmd}.exe`;\n    }\n    return `${filePath}${cmd}`;\n  };\n\n  static create = async (db: DB, dbs: DBS, connMgr: ConnectionManager) => {\n    const installedPrograms = await getInstalledPsqlVersions(db);\n    return new BackupManager(db, dbs, connMgr, installedPrograms);\n  };\n\n  async destroy() {\n    await this.dbConfSub?.unsubscribe();\n    clearInterval(this.automaticBackupInterval);\n  }\n\n  checkIfEnoughSpace = async (conId: string) => {\n    const dbSizeInBytes = await this.getDBSizeInBytes(conId);\n    const diskSpace = await checkDiskSpace(getRootDir());\n    const minLimin = 100 * 1e6;\n    if (diskSpace.free < minLimin) {\n      const err = `There is not enough space on server for local backups:\\nTotal: ${bytesToSize(diskSpace.size)} \\nRemaning: ${bytesToSize(diskSpace.free)} \\nRequired: ${bytesToSize(minLimin)}`;\n      return { ok: false, err, diskSpace, dbSizeInBytes };\n    } else if (diskSpace.free - 1.1 * dbSizeInBytes < 0) {\n      const err = `There is not enough space on server for local backups:\\nTotal: ${bytesToSize(diskSpace.size)} \\nRemaning: ${bytesToSize(diskSpace.free)} \\nRequired: 1.1*DB size on disk (${bytesToSize(dbSizeInBytes)})`;\n      return { ok: false, err, diskSpace, dbSizeInBytes };\n    } else {\n      return { ok: true, diskSpace, dbSizeInBytes };\n    }\n  };\n\n  getDBSizeInBytes = async (conId: string) => {\n    const db = await this.connMgr.getNewConnectionDb(conId, {\n      allowExitOnIdle: true,\n    });\n    const { size: result } = await db.one<{ size: number }>(\n      \"SELECT pg_database_size(current_database()) as size  \",\n    );\n    await db.$pool.end();\n    return Number.isFinite(+result) ? result : 0;\n  };\n\n  pgDump = pgDump.bind(this);\n\n  pgRestore = pgRestore.bind(this);\n\n  pgRestoreStream = async (\n    fileName: string,\n    conId: string,\n    stream: PassThrough,\n    sizeBytes: number,\n    restore_options: Backups[\"restore_options\"],\n  ) => {\n    const con = await this.dbs.connections.findOne({ id: conId });\n    if (!con) throw new Error(\"Could not find the connection\");\n\n    const bkp = await this.dbs.backups.insert(\n      {\n        created: new Date(),\n        dbSizeInBytes: await this.getDBSizeInBytes(conId),\n        sizeInBytes: sizeBytes.toString(),\n        initiator: \"manual_restore_from_file: \" + fileName,\n        connection_id: con.id,\n        credential_id: null,\n        destination: \"None (temp stream)\",\n        dump_command: \"pg_dump --format=c --clean --if-exists\",\n        options: {\n          command: \"pg_dump\",\n          clean: true,\n          format: \"c\",\n        },\n        status: { ok: new Date().toISOString() },\n      },\n      { returning: \"*\" },\n    );\n\n    let lastChunk = Date.now();\n    let chunkSum = 0;\n    stream.on(\"data\", (chunk) => {\n      chunkSum += chunk.length;\n      if (Date.now() - lastChunk > 1000) {\n        lastChunk = Date.now();\n        void this.dbs.backups.update(\n          { id: bkp.id },\n          {\n            restore_status: { loading: { total: sizeBytes, loaded: chunkSum } },\n          },\n        );\n      }\n    });\n\n    return this.pgRestore({ bkpId: bkp.id }, stream, restore_options);\n  };\n\n  bkpDelete = async (bkpId: string, force = false) => {\n    const { fileMgr, bkp } = await getBkp(this.dbs, bkpId);\n\n    try {\n      await fileMgr.deleteFile(bkp.id);\n    } catch (err) {\n      if (!force) throw err;\n    }\n    await this.dbs.backups.delete({ id: bkp.id });\n\n    return bkp.id;\n  };\n\n  onRequestBackupFile = async (\n    res: Response,\n    userData: SUser | undefined,\n    req: Request,\n  ) => {\n    if (userData?.user.type !== \"admin\") {\n      res.sendStatus(401);\n      return;\n    }\n    const backupId = req.path.slice(ROUTES.BACKUPS.length + 1);\n    if (!backupId) {\n      res.sendStatus(404);\n      return;\n    }\n    const backup = await this.dbs.backups.findOne({ id: backupId });\n    if (!backup) {\n      res.sendStatus(404);\n      return;\n    }\n    const { fileMgr } = await getFileMgr(this.dbs, backup.credential_id);\n    if (backup.credential_id) {\n      /* Allow access to file for a period equivalent to a download rate of 50KBps */\n      const presignedURL = await fileMgr.getFileCloudDownloadURL(\n        backup.id,\n        1 * 60, // 1 minute\n      );\n      if (!presignedURL) {\n        res.sendStatus(404);\n      } else {\n        res.redirect(presignedURL);\n      }\n    } else {\n      try {\n        res.type(backup.content_type);\n        res.sendFile(\n          path.resolve(\n            path.join(getRootDir() + ROUTES.BACKUPS + \"/\" + backup.id),\n          ),\n        );\n      } catch (err) {\n        res.sendStatus(404);\n      }\n    }\n  };\n\n  timeout?: NodeJS.Timeout;\n  closeStream = (streamId: string) => {\n    const s = this.tempStreams[streamId];\n    if (!s) throw new Error(\"Stream not found\");\n    return s.stream;\n  };\n  pushToStream = (streamId: string, chunk: any, cb: (err: any) => void) => {\n    const s = this.tempStreams[streamId];\n    if (!s) throw new Error(\"Stream not found\");\n\n    if (this.timeout) clearTimeout(this.timeout);\n\n    /** Delete stale streams */\n    this.timeout = setTimeout(() => {\n      Object.keys(this.tempStreams).forEach((key) => {\n        const v = this.tempStreams[key];\n        if (v && Date.now() - v.lastChunk > 60 * 1000) {\n          v.stream.destroy();\n          delete this.tempStreams[key];\n        }\n      });\n    }, 60 * 1000);\n\n    s.lastChunk = Date.now();\n    s.stream.write(chunk, cb);\n  };\n\n  getTempFileStream = (fileName: string, userId: string) => {\n    // const filePath = localFolderPath + \"/temp/\" + fileName;\n    // const writeStream = fs.createWriteStream(filePath);\n    const stream = new PassThrough();\n    const streamId = `${userId}-${fileName}`;\n    this.tempStreams[streamId] = {\n      lastChunk: Date.now(),\n      stream,\n    };\n    stream.on(\"error\", (err) => {\n      console.error(err);\n      stream.end();\n      if (this.tempStreams[streamId]) {\n        delete this.tempStreams[streamId];\n      }\n    });\n    return {\n      streamId,\n      stream,\n    };\n  };\n\n  getCurrentBackup = (conId: string) =>\n    this.dbs.backups.findOne({\n      connection_id: conId,\n      \"status->loading.<>\": null,\n      /* If not updated in last 5 minutes then consider it dead */\n      // last_updated: { \">\": new Date(Date.now() - HOUR/12)  }\n      $filter: [{ $ageNow: [\"last_updated\"] }, \"<\", \"2 seconds\"],\n    } as Filter);\n\n  checkAutomaticBackup = checkAutomaticBackup.bind(this);\n}\n"
  },
  {
    "path": "server/src/BackupManager/checkAutomaticBackup.ts",
    "content": "import type { Connections } from \"..\";\nimport { getDatabaseConfigFilter } from \"../ConnectionManager/connectionManagerUtils\";\nimport type BackupManager from \"./BackupManager\";\nimport { HOUR } from \"./BackupManager\";\n\nexport async function checkAutomaticBackup(\n  this: BackupManager,\n  con: Connections,\n) {\n  const AUTO_INITIATOR = \"automatic_backups\";\n  const dbConf = await this.dbs.database_configs.findOne(\n    getDatabaseConfigFilter(con),\n  );\n  const bkpConf = dbConf?.backups_config;\n  if (!bkpConf?.dump_options) return;\n\n  const bkpFilter = { connection_id: con.id, initiator: AUTO_INITIATOR };\n\n  const dump = async () => {\n    const lastBackup = await this.dbs.backups.findOne(bkpFilter, {\n      orderBy: { created: -1 },\n    });\n    if (bkpConf.err)\n      await this.dbs.database_configs.update(\n        { $existsJoined: { connections: { id: con.id } } },\n        { backups_config: { ...bkpConf, err: null } },\n      );\n\n    const hourIsOK = () => {\n      if (Number.isInteger(bkpConf.hour)) {\n        if (now.getHours() >= bkpConf.hour!) {\n          return true;\n        }\n      } else {\n        return true;\n      }\n\n      return false;\n    };\n    const dowIsOK = () => {\n      if (Number.isInteger(bkpConf.dayOfWeek)) {\n        if ((now.getDay() || 7) >= bkpConf.dayOfWeek!) {\n          return true;\n        }\n      } else {\n        return true;\n      }\n\n      return false;\n    };\n    const dateIsOK = () => {\n      const date = new Date();\n\n      const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);\n      if (bkpConf.dayOfMonth && Number.isInteger(bkpConf.dayOfMonth)) {\n        if (\n          now.getDate() >= bkpConf.dayOfMonth ||\n          (bkpConf.dayOfMonth > lastDay.getDate() &&\n            now.getDate() === lastDay.getDate())\n        ) {\n          return true;\n        }\n      } else {\n        return true;\n      }\n\n      return false;\n    };\n\n    let shouldDump = false;\n    const now = new Date();\n    const currentBackup = await this.getCurrentBackup(con.id);\n    if (currentBackup) {\n      shouldDump = false;\n    } else if (\n      bkpConf.frequency === \"hourly\" &&\n      (!lastBackup ||\n        new Date(lastBackup.created) < new Date(Date.now() - HOUR))\n    ) {\n      shouldDump = true;\n    } else if (\n      hourIsOK() &&\n      bkpConf.frequency === \"daily\" &&\n      (!lastBackup ||\n        new Date(lastBackup.created) < new Date(Date.now() - 24 * HOUR))\n    ) {\n      shouldDump = true;\n    } else if (\n      dowIsOK() &&\n      hourIsOK() &&\n      bkpConf.frequency === \"weekly\" &&\n      (!lastBackup ||\n        new Date(lastBackup.created) < new Date(Date.now() - 7 * 24 * HOUR))\n    ) {\n      shouldDump = true;\n    } else if (\n      dateIsOK() &&\n      dowIsOK() &&\n      hourIsOK() &&\n      bkpConf.frequency === \"monthly\" &&\n      (!lastBackup ||\n        new Date(lastBackup.created) < new Date(Date.now() - 28 * 24 * HOUR))\n    ) {\n      shouldDump = true;\n    }\n\n    if (shouldDump) {\n      await this.pgDump(con.id, null, {\n        options: { ...bkpConf.dump_options },\n        destination: bkpConf.cloudConfig ? \"Cloud\" : \"Local\",\n        credentialID: bkpConf.cloudConfig?.credential_id,\n        initiator: AUTO_INITIATOR,\n      });\n      if (bkpConf.keepLast && bkpConf.keepLast > 0) {\n        const toKeepIds: string[] = (\n          await this.dbs.backups.find(bkpFilter, {\n            select: { id: 1 },\n            orderBy: { created: -1 },\n            limit: bkpConf.keepLast,\n          })\n        ).map((c) => c.id);\n        await this.dbs.backups.delete({ \"id.$nin\": toKeepIds, ...bkpFilter });\n      }\n    }\n  };\n\n  /** Local backup, check if enough space */\n  if (!bkpConf.cloudConfig?.credential_id) {\n    const space = await this.checkIfEnoughSpace(con.id);\n    if (space.err) {\n      if (bkpConf.err !== space.err) {\n        await this.dbs.database_configs.update(\n          { id: dbConf!.id },\n          { backups_config: { ...bkpConf, err: space.err } },\n        );\n      }\n    } else {\n      await dump();\n    }\n  } else {\n    await dump();\n  }\n}\n"
  },
  {
    "path": "server/src/BackupManager/getInstalledPrograms.ts",
    "content": "import { execSync } from \"child_process\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport type { DB } from \"prostgles-server/dist/Prostgles\";\nimport { programList, type InstalledPrograms } from \"@common/electronInitTypes\";\nimport type { WithUndef } from \"@common/utils\";\nimport { isDefined } from \"prostgles-types\";\nimport { EOL } from \"os\";\n\nlet installedPrograms: WithUndef<InstalledPrograms> | undefined = {\n  psql: undefined,\n  pg_dump: undefined,\n  pg_restore: undefined,\n  docker: undefined,\n  filePath: undefined,\n  os: undefined,\n};\n\nconst getDataDirectory = async (db: DB) => {\n  const dataDir = (\n    await db.one<{ data_directory: string }>(\"SHOW data_directory\")\n  ).data_directory;\n  const binDir =\n    dataDir.endsWith(\"data\") ? dataDir.slice(0, -4) + \"bin/\" : undefined;\n  return {\n    binDir,\n    dataDir,\n  };\n};\n\nconst getWindowsPsqlBinPath = async (db: DB) => {\n  let filePath = \"\";\n  const { ProgramFiles } = process.env;\n  try {\n    const pgPath =\n      ProgramFiles ?\n        `${ProgramFiles}/PostgreSQL`\n      : \"C:/Program Files/PostgreSQL\";\n    if (fs.existsSync(pgPath)) {\n      const installedVersions = fs.readdirSync(pgPath).map((v) => Number(v));\n      const latestVersion = installedVersions.sort((a, b) => b - a)[0];\n      if (latestVersion) {\n        filePath = path.resolve(`${pgPath}/${latestVersion}/bin/`) + \"/\";\n        if (fs.existsSync(`${filePath}psql.exe`)) {\n          return filePath;\n        }\n      }\n    }\n  } catch (e: any) {\n    console.warn(e);\n  }\n\n  if (!filePath) {\n    const { binDir } = await getDataDirectory(db);\n    if (binDir && fs.existsSync(`${binDir}psql.exe`)) {\n      return binDir;\n    }\n\n    try {\n      const psqlPath = execSync(\"where psql\").toString().split(EOL)[0];\n      if (psqlPath) {\n        filePath = path.resolve(psqlPath + \"/../\") + \"/\";\n        if (fs.existsSync(`${filePath}psql.exe`)) {\n          return filePath;\n        }\n      }\n    } catch (e: any) {\n      console.warn(e);\n    }\n  }\n\n  return \"\";\n};\n\nconst tryExecSync = (command: string): string | undefined => {\n  try {\n    return execSync(command).toString();\n  } catch (e: any) {\n    if (command.startsWith(\"which \")) {\n      console.warn(e.message);\n    } else {\n      console.warn(\"Error executing command:\", command, e);\n    }\n  }\n};\nexport const getInstalledPsqlVersions = async (\n  db: DB,\n): Promise<InstalledPrograms | undefined> => {\n  const os =\n    process.platform === \"win32\" ? \"Windows\"\n    : process.platform === \"linux\" ? \"Linux\"\n    : process.platform === \"darwin\" ? \"Mac\"\n    : \"\";\n  let filePath = \"\";\n  try {\n    if (os === \"Windows\") {\n      filePath = await getWindowsPsqlBinPath(db);\n      const ext = \".exe\";\n      installedPrograms = {\n        os,\n        filePath,\n        psql: tryExecSync(\n          JSON.stringify(`${filePath}psql${ext}`) + ` --version`,\n        ),\n        pg_dump: tryExecSync(\n          JSON.stringify(`${filePath}pg_dump${ext}`) + ` --version`,\n        ),\n        pg_restore: tryExecSync(\n          JSON.stringify(`${filePath}pg_restore${ext}`) + ` --version`,\n        ),\n        docker: tryExecSync(\"docker --version\"),\n      };\n\n      /** Linux/MacOS */\n    } else {\n      if (os === \"Mac\") {\n        /**\n         * Option 1 - PG was installed through EDB\n         * Expecting something like this:\n         *    /Library/PostgreSQL/16/bin/psql\n         */\n        const { binDir } = await getDataDirectory(db);\n        if (binDir && fs.existsSync(`${binDir}psql`)) {\n          filePath = binDir;\n\n          /**\n           * Option 2 - PG was installed through Homebrew\n           * Expecting something like this:\n           *    /opt/homebrew/opt/postgresql/bin/psql\n           * OR\n           *    /opt/homebrew/opt/postgresql@13/bin/psql\n           */\n        } else {\n          const brewProgramFolders = fs.readdirSync(\"/opt/homebrew/opt/\");\n          let maxVersion = 0;\n          const postgresFolders = brewProgramFolders\n            .map((folder) => {\n              if (folder === \"postgresql\") {\n                return { version: undefined };\n              } else if (folder.startsWith(\"postgresql@\")) {\n                const version = Number(folder.split(\"@\")[1]!);\n                maxVersion = Math.max(maxVersion, version);\n                return { version };\n              }\n            })\n            .filter(isDefined);\n\n          if (postgresFolders.length) {\n            filePath = \"/opt/homebrew/opt/postgresql/bin/\";\n            if (maxVersion) {\n              filePath = `/opt/homebrew/opt/postgresql@${maxVersion}/bin/`;\n            }\n          }\n        }\n        if (filePath) {\n          installedPrograms = {\n            os,\n            filePath,\n            psql: tryExecSync(`${filePath}psql --version`),\n            pg_dump: tryExecSync(`${filePath}pg_dump --version`),\n            pg_restore: tryExecSync(`${filePath}pg_restore --version`),\n            docker: tryExecSync(\"docker --version\"),\n          };\n        }\n      } else {\n        installedPrograms = {\n          os,\n          filePath,\n          ...getLinuxInstalledPrograms(programList),\n        };\n      }\n    }\n  } catch (e: any) {\n    console.warn(e);\n    installedPrograms = undefined;\n  }\n\n  const { pg_dump, pg_restore, psql, docker } = installedPrograms ?? {};\n\n  return {\n    psql,\n    pg_dump,\n    pg_restore,\n    filePath,\n    docker,\n    os,\n  };\n};\n\nconst getLinuxInstalledPrograms = <ProgramList extends readonly string[]>(\n  programs: ProgramList,\n): Record<ProgramList[number], string | undefined> => {\n  const getInstalledVersion = (program: string) =>\n    tryExecSync(\"which \" + program) && tryExecSync(program + \" --version\");\n\n  return programs.reduce(\n    (acc, program) => {\n      acc[program as ProgramList[number]] = getInstalledVersion(program);\n      return acc;\n    },\n    {} as Record<ProgramList[number], string | undefined>,\n  );\n};\n"
  },
  {
    "path": "server/src/BackupManager/pgDump.ts",
    "content": "import type { ChildProcess } from \"child_process\";\nimport type { Filter } from \"prostgles-server/dist/DboBuilder/DboBuilderTypes\";\nimport { isDefined, omitKeys, pickKeys } from \"prostgles-types\";\nimport type { DumpOpts, PGDumpParams } from \"@common/utils\";\nimport { getSSLEnvVars } from \"../ConnectionManager/saveCertificates\";\nimport type BackupManager from \"./BackupManager\";\nimport { envToStr, pipeFromCommand } from \"./pipeFromCommand\";\nimport {\n  addOptions,\n  getConnectionEnvVars,\n  getConnectionUri,\n  getFileMgr,\n  makeLogs,\n} from \"./utils\";\n\nexport async function pgDump(\n  this: BackupManager,\n  conId: string,\n  credId: number | null,\n  {\n    options: o,\n    destination,\n    credentialID,\n    initiator = \"manual_backup\",\n    name,\n  }: PGDumpParams,\n) {\n  if (isDefined(name) && typeof name !== \"string\") {\n    throw new Error(\"Backup name must be a string\");\n  }\n  const con = await this.dbs.connections.findOne({ id: conId });\n  if (!con) throw new Error(\"Could not find the connection\");\n  let proc: ChildProcess | undefined;\n\n  const setError = (err: any) => {\n    console.error(\n      new Date().toISOString() + \" pg_dump err for connection \" + conId,\n      err,\n    );\n    proc?.kill();\n    if (backup_id) {\n      void this.dbs.backups.update(\n        { id: backup_id },\n        { status: { err }, last_updated: new Date() },\n      );\n    } else {\n      throw err;\n    }\n  };\n\n  const { fileMgr } = await getFileMgr(this.dbs, credId);\n\n  if (!credId) {\n    const space = await this.checkIfEnoughSpace(conId);\n    if (space.err) throw space.err;\n  }\n\n  const currentBackup = await this.getCurrentBackup(con.id);\n  if (currentBackup) {\n    throw \"Cannot backup while another backup is in progress\";\n  }\n\n  const SSL_ENV_VARS = getSSLEnvVars(con);\n\n  let backup_id: string | undefined;\n  const uri = getConnectionUri(con);\n  const ConnectionEnvVars = getConnectionEnvVars(con);\n  const ENV_VARS = { ...SSL_ENV_VARS, ...ConnectionEnvVars };\n  const dumpAll = o.command === \"pg_dumpall\";\n  const dumpCommand =\n    dumpAll ?\n      {\n        command: this.getCmd(\"pg_dumpall\"),\n        args: addOptions(\n          [\"-d\", uri],\n          [\n            [o.clean, \"--clean\"],\n            [o.ifExists, \"--if-exists\"],\n            [o.globalsOnly, \"--globals-only\"],\n            [o.rolesOnly, \"--roles-only\"],\n            [o.dataOnly, \"--data-only\"],\n            [o.schemaOnly, \"--schema-only\"],\n            [!!o.encoding, [\"--encoding\", o.encoding!]],\n            [true, \"-v\"],\n          ],\n        ),\n      }\n    : {\n        command: this.getCmd(\"pg_dump\"),\n        // opts: addOptions([uri_NOT_SAFE_-> VISIBLE TO ps aux], [\n        args: addOptions(\n          [],\n          [\n            [!!o.format, [\"--format\", o.format]],\n            [o.clean, \"--clean\"],\n            [o.create, \"--create\"],\n            [o.noOwner, \"--no-owner\"],\n            [o.ifExists, \"--if-exists\"],\n            [o.dataOnly, \"--data-only\"],\n            [!!o.encoding, [\"--encoding\", o.encoding!]],\n            [o.schemaOnly, \"--schema-only\"],\n            [!!o.excludeSchema, [\"--exclude-schema\", o.excludeSchema!]],\n            [\n              Number.isInteger(o.compressionLevel),\n              [\"--compress\", o.compressionLevel!],\n            ],\n            [Number.isInteger(o.numberOfJobs), [\"--jobs\", o.numberOfJobs!]],\n            [true, \"-v\"],\n          ],\n        ),\n      };\n  try {\n    const content_type =\n      dumpAll || o.format === \"p\" ? \"text/sql\" : \"application/gzip\";\n    const backup = await this.dbs.backups.insert(\n      {\n        name,\n        created: new Date(),\n        dbSizeInBytes: await this.getDBSizeInBytes(conId),\n        initiator,\n        connection_id: con.id,\n        credential_id: credId ?? null,\n        destination: credId ? \"Cloud\" : \"Local\",\n        dump_command:\n          envToStr(ENV_VARS) +\n          dumpCommand.command +\n          \" \" +\n          dumpCommand.args.join(\" \"),\n        status: { loading: { loaded: 0, total: 0 } },\n        options: omitKeys(o as any, [\"credentialID\"]) as DumpOpts,\n        content_type,\n      },\n      { returning: \"*\" },\n    );\n\n    const bkpForId = await this.dbs.backups.findOne(\n      { id: backup.id },\n      { select: { created: \"$datetime_\" } },\n    );\n    if (!bkpForId) throw \"Internal error\";\n    backup_id = `${(con.db_name || \"\").replace(/[\\W]+/g, \"_\")}__${bkpForId.created}_pg_dump${dumpAll ? \"all\" : \"\"}_${backup.id}.${content_type === \"text/sql\" ? \"sql\" : \"dump\"}`;\n    await this.dbs.backups.update({ id: backup.id }, { id: backup_id });\n\n    const getBkp = () => this.dbs.backups.findOne({ id: backup_id });\n\n    const destStream = fileMgr.uploadStream(\n      backup_id,\n      content_type,\n      (loadingRaw) => {\n        /** S3 is adding some extra fields while total is missing */\n        const loading = pickKeys(loadingRaw, [\"total\", \"loaded\"]);\n        void this.dbs.backups.update(\n          {\n            $and: [\n              { id: backup_id },\n              { \"status->>ok\": null },\n              { \"status->>err\": null },\n            ] as Filter[],\n          },\n          { status: { loading }, last_updated: new Date() },\n        );\n      },\n      setError,\n      (item) => {\n        void getBkp().then(async (bkp) => {\n          if (bkp && \"err\" in bkp.status) {\n            try {\n              await fileMgr.deleteFile(bkp.id);\n            } catch {}\n          } else {\n            void this.dbs.backups.update(\n              { id: backup_id },\n              {\n                sizeInBytes: item.content_length,\n                uploaded: new Date(),\n                status: { ok: \"1\" },\n                last_updated: new Date(),\n                local_filepath: item.filePath,\n              },\n            );\n          }\n        });\n      },\n    );\n\n    // const res = child.spawnSync(dumpCommand.command, dumpCommand.opts.concat([\"-f\", \"-l\"]) as any, { env: ENV_VARS });\n\n    /** Will not show pg_dump TOC list progress because generating a TOC list takes as long as the actual pg_dump in some cases */\n    proc = pipeFromCommand({\n      ...dumpCommand,\n      envVars: ENV_VARS,\n      destination: destStream,\n      onEnd: (err) => {\n        if (err) {\n          setError(err);\n        }\n      },\n      onStdout:\n        !o.keepLogs ? undefined : (\n          async ({ chunk: _dump_logs }, isStdErr) => {\n            if (!isStdErr) return;\n            const currBkp = await this.dbs.backups.findOne({ id: backup_id });\n            if (!currBkp || \"err\" in currBkp.status) {\n              proc?.kill();\n              return;\n            }\n            const dump_logs = makeLogs(\n              _dump_logs,\n              currBkp.dump_logs,\n              currBkp.created,\n            );\n            void this.dbs.backups.update(\n              { id: backup_id, \"status->>ok\": null } as Filter,\n              {\n                dump_logs,\n                last_updated: new Date(),\n              },\n            );\n          }\n        ),\n    });\n\n    // eslint-disable-next-line @typescript-eslint/no-misused-promises\n    const interval = setInterval(async () => {\n      const bkp = await this.dbs.backups.findOne({ id: backup_id });\n      if (!bkp || \"err\" in bkp.status) {\n        destStream.end();\n        clearInterval(interval);\n      } else if (bkp.uploaded) {\n        clearInterval(interval);\n      }\n    }, 2000);\n\n    return backup_id;\n  } catch (err) {\n    setError(err);\n  }\n}\n"
  },
  {
    "path": "server/src/BackupManager/pgRestore.ts",
    "content": "import { asName, omitKeys } from \"prostgles-types\";\nimport type { Readable } from \"stream\";\nimport { throttle } from \"@common/utils\";\nimport type BackupManager from \"./BackupManager\";\nimport type { Backups } from \"./BackupManager\";\nimport { envToStr } from \"./pipeFromCommand\";\nimport { pipeToCommand } from \"./pipeToCommand\";\nimport { addOptions, getBkp, getConnectionEnvVars, makeLogs } from \"./utils\";\nimport { getSSLEnvVars } from \"../ConnectionManager/saveCertificates\";\n\nexport async function pgRestore(\n  this: BackupManager,\n  arg1: { bkpId: string; connId?: string },\n  stream: Readable | undefined,\n  o: Backups[\"restore_options\"],\n) {\n  const { bkpId, connId } = arg1;\n  const { fileMgr, bkp } = await getBkp(this.dbs, bkpId);\n  if (!bkp.id && !connId)\n    throw \"Must provide a connection id if backup does not have a connection_id\";\n  const connection_id = connId ?? bkp.connection_id!;\n  const con = await this.dbs.connections.findOne({ id: connection_id });\n  if (!con) throw \"Connection not found\";\n  if (!o) throw \"Restore options missing\";\n\n  const setError = async (err: any) => {\n    const currBkp = await this.dbs.backups.findOne({ id: bkpId });\n    if (currBkp) {\n      void this.dbs.backups.update(\n        { id: bkpId },\n        {\n          restore_status: {\n            ...omitKeys(currBkp.restore_status as any, [\"ok\"]),\n            err: (err ?? \"\").toString(),\n          },\n          last_updated: new Date(),\n        },\n      );\n    }\n  };\n  if (o.newDbName) {\n    if (o.create)\n      throw \"Cannot use 'newDbName' together with 'create'. --create option will still restore into the database specified within the dump file\";\n    try {\n      await this.dbs.sql(`CREATE DATABASE ${asName(o.newDbName)}`);\n    } catch (err) {\n      void setError(err);\n    }\n  }\n\n  const isWin = process.platform === \"win32\";\n  const byBassStreamDueToWindowsUnrecognisedBlockTypeError = !!(\n    isWin && bkp.local_filepath\n  );\n  if (\n    byBassStreamDueToWindowsUnrecognisedBlockTypeError &&\n    !bkp.local_filepath\n  ) {\n    throw \"Cannot restore from cloud on Windows through the Desktop version\";\n  }\n\n  try {\n    const SSL_ENV_VARS = getSSLEnvVars(con);\n    const ConnectionEnvVars = getConnectionEnvVars(con);\n    const ENV_VARS = { ...SSL_ENV_VARS, ...ConnectionEnvVars };\n    const bkpStream = stream ?? (await fileMgr.getFileStream(bkp.id));\n    const restoreCmd =\n      o.command === \"psql\" || o.format === \"p\" ?\n        {\n          command: this.getCmd(\"psql\"),\n          // opts: [getConnectionUri(con as any)] // NOT SAFE ps aux\n          opts: [],\n        }\n      : {\n          command: this.getCmd(\"pg_restore\"),\n          opts: addOptions(\n            // [\"-d\", getConnectionUri(con as any) NOT SAFE FROM ps aux],\n            [],\n            [\n              [true, \"--dbname=\" + ConnectionEnvVars.PGDATABASE], // Prevent error: \"d -f/--file must be specified\"\n              [true, \"-w\"], // Do not ask for password\n              [o.clean, \"--clean\"],\n              [o.create, \"--create\"],\n              [o.noOwner, \"--no-owner\"],\n              [!!o.format, [\"--format\", o.format]],\n              [o.dataOnly, \"--data-only\"],\n              [o.ifExists, \"--if-exists\"],\n              [!!o.excludeSchema, [\"--exclude-schema\", o.excludeSchema!]],\n              [Number.isInteger(o.numberOfJobs), \"--jobs\"],\n              [true, \"-v\"],\n              [\n                byBassStreamDueToWindowsUnrecognisedBlockTypeError,\n                bkp.local_filepath!,\n              ],\n            ],\n          ),\n        };\n    await this.dbs.backups.update(\n      { id: bkpId },\n      {\n        restore_logs: \"\",\n        restore_start: new Date(),\n        restore_command:\n          envToStr(ENV_VARS) +\n          restoreCmd.command +\n          \" \" +\n          restoreCmd.opts.join(\" \"),\n        restore_status: { loading: { loaded: 0, total: 0 } },\n        last_updated: new Date(),\n      },\n    );\n\n    let chunkSum = 0;\n    const throttledUpdate = throttle(async () => {\n      if (!(await this.dbs.backups.findOne({ id: bkpId }))) {\n        bkpStream.emit(\"error\", \"Backup file not found\");\n      } else {\n        const finished = chunkSum >= +(bkp.sizeInBytes ?? bkp.dbSizeInBytes);\n        void this.dbs.backups.update(\n          { id: bkpId },\n          {\n            restore_status:\n              finished ?\n                {\n                  ok: `${new Date()}`,\n                }\n              : {\n                  loading: {\n                    loaded: chunkSum,\n                    total: +(bkp.sizeInBytes ?? bkp.dbSizeInBytes),\n                  },\n                },\n            ...(finished && !(bkp.status as any)?.ok ?\n              { status: { ok: `${new Date()}` } }\n            : {}),\n          },\n        );\n\n        if (finished) {\n          const dummyViewToReloadSchema =\n            \"prostgles_dummy_view_to_reload_schema\";\n          void this.connMgr\n            .getConnection(con.id)\n            .prgl._db.any(\n              `\n            CREATE VIEW ${dummyViewToReloadSchema} AS SELECT 1;\n          `,\n            )\n            .then(() => {\n              void this.connMgr.getConnection(con.id).prgl._db.any(`\n              DROP VIEW ${dummyViewToReloadSchema};\n            `);\n            });\n        }\n      }\n    }, 1000);\n\n    bkpStream.on(\"data\", (chunk) => {\n      chunkSum += chunk.length;\n      // console.log(chunk.toString(), { chunk })\n      throttledUpdate();\n    });\n\n    const proc = pipeToCommand(\n      restoreCmd.command,\n      restoreCmd.opts,\n      ENV_VARS,\n      bkpStream,\n      (err) => {\n        if (err) {\n          console.error(\"pipeToCommand ERR:\", err);\n          bkpStream.destroy();\n          void setError(err);\n        } else {\n          void this.dbs.backups.update(\n            { id: bkpId },\n            {\n              restore_end: new Date(),\n              restore_status: { ok: `${new Date()}` },\n              last_updated: new Date(),\n            },\n          );\n        }\n      },\n      async ({ chunk: _restore_logs }, isStdErr) => {\n        /** Full logs are always provided */\n        if (!isStdErr) return;\n        const currBkp = await this.dbs.backups.findOne({ id: bkpId });\n        if ((currBkp as any)?.restore_status.err) {\n          proc.kill();\n          return;\n        }\n        if (!currBkp) {\n          bkpStream.emit(\"error\", \"Backup file not found\");\n          bkpStream.destroy();\n        } else {\n          const restore_logs = makeLogs(\n            _restore_logs,\n            currBkp.restore_logs,\n            currBkp.restore_start as any,\n          );\n          void this.dbs.backups.update(\n            { id: bkpId },\n            { restore_end: new Date(), restore_logs, last_updated: new Date() },\n          );\n        }\n      },\n    );\n  } catch (err) {\n    void setError(err);\n  }\n}\n"
  },
  {
    "path": "server/src/BackupManager/pipeFromCommand.ts",
    "content": "import child from \"child_process\";\nimport { getKeys } from \"prostgles-types\";\nimport type internal from \"stream\";\n\nexport type EnvVars = Record<string, string> | {};\nexport const envToStr = (vars: EnvVars) => {\n  return (\n    getKeys(vars)\n      .map((k) => `${k}=${JSON.stringify(vars[k])}`)\n      .join(\" \") + \" \"\n  );\n};\n\nexport function pipeFromCommand(params: {\n  command: string;\n  // opts: SpawnOptionsWithoutStdio | string[],\n  args: string[];\n  envVars: EnvVars;\n  destination: internal.Writable;\n  onEnd: (err: Error | string | undefined, fullLog: string) => void;\n  onStdout?: (\n    data: { full: string; chunk: string; pipedLength: number },\n    isStdErr?: boolean,\n  ) => void | Promise<void>;\n  onChunk?: (chunk: any, streamSize: number) => void;\n}) {\n  const {\n    envVars = {},\n    onEnd,\n    command,\n    destination,\n    args,\n    onStdout,\n    onChunk,\n  } = params;\n\n  const env: NodeJS.ProcessEnv = envVars;\n  const proc = child.spawn(command, args, { env });\n  const getUTFText = (v: string) => v.toString(); //.replaceAll(/[^\\x00-\\x7F]/g, \"\"); //.replaceAll(\"\\\\\", \"[escaped backslash]\");   // .replaceAll(\"\\\\u0000\", \"\");\n\n  let fullLog = \"\";\n  // const lastSent = Date.now();\n  let log: string;\n  let streamSize = 0;\n  proc.stderr.on(\"data\", (data) => {\n    log = getUTFText(data);\n    fullLog += log;\n    // const now = Date.now();\n    // if(lastSent > now - 1000){\n\n    /** These are the pg_dump logs */\n    void onStdout?.(\n      { full: fullLog, chunk: log, pipedLength: streamSize },\n      true,\n    );\n\n    // }\n  });\n  proc.stdout.on(\"data\", (data) => {\n    streamSize += data.length;\n\n    /** These is the pg_dump actual data */\n    onChunk?.(data, streamSize);\n    // onStdout?.({ chunk: getUTFText(data), full: fullLog });\n  });\n\n  proc.stdout.on(\"error\", function (err) {\n    onEnd(err, fullLog);\n  });\n  proc.stdin.on(\"error\", function (err) {\n    onEnd(err, fullLog);\n  });\n  proc.on(\"error\", function (err) {\n    onEnd(err, fullLog);\n  });\n\n  proc.stdout.pipe(destination, { end: false });\n\n  proc.on(\"exit\", function (code, signal) {\n    if (code) {\n      console.error({\n        code,\n        signal,\n        logs: fullLog.slice(fullLog.length - 1100),\n      });\n      onEnd(log, fullLog);\n    } else {\n      onEnd(undefined, fullLog);\n      destination.end();\n    }\n  });\n\n  return proc;\n}\n"
  },
  {
    "path": "server/src/BackupManager/pipeToCommand.ts",
    "content": "import child from \"child_process\";\nimport type internal from \"stream\";\nimport type { EnvVars } from \"./pipeFromCommand\";\n\nexport const pipeToCommand = (\n  command: string,\n  args: string[],\n  envVars: EnvVars = {},\n  source: internal.Readable,\n  onEnd: (err?: any) => void | Promise<void>,\n  onStdout?: (\n    data: { full: any; chunk: any },\n    isStdErr?: boolean,\n  ) => void | Promise<void>,\n) => {\n  const env: NodeJS.ProcessEnv = envVars;\n  const proc = child.spawn(command, args, { env });\n\n  let log: string;\n  let fullLog = \"\";\n  proc.stderr.on(\"data\", (data: Buffer) => {\n    log = data.toString();\n    fullLog += log;\n    void onStdout?.({ full: fullLog, chunk: log }, true);\n  });\n  proc.stdout.on(\"data\", (data: Buffer) => {\n    const log = data.toString();\n    fullLog += log;\n    void onStdout?.({ full: fullLog, chunk: log });\n  });\n  proc.stdout.on(\"error\", function (err) {\n    void onEnd(err);\n  });\n  proc.stdin.on(\"error\", function (err) {\n    void onEnd(err);\n  });\n  proc.on(\"error\", function (err) {\n    void onEnd(err);\n  });\n\n  source.pipe(proc.stdin);\n\n  proc.on(\"exit\", function (code, signal) {\n    const err = fullLog\n      .split(\"\\n\")\n      .filter((l) => l)\n      .at(-1);\n    /**\n     * Some ignorablea warnings can generate a code 1\n     */\n    const isMaybeError = (code || 0) > 1;\n    if (isMaybeError) {\n      console.error({\n        code,\n        signal,\n        execCommandErr: err,\n        logs: fullLog.slice(fullLog.length - 100),\n      });\n      source.destroy();\n    }\n\n    void onEnd(isMaybeError ? (err ?? \"Error\") : undefined);\n  });\n\n  return proc;\n};\n"
  },
  {
    "path": "server/src/BackupManager/utils.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport path from \"path\";\nimport { FileManager } from \"prostgles-server/dist/FileManager/FileManager\";\n\nimport { getAge, ROUTES } from \"@common/utils\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\nimport type { Connections, DBS } from \"..\";\nimport { getCloudClient } from \"../cloudClients/cloudClients\";\nimport { getConnectionDetails } from \"../connectionUtils/getConnectionDetails\";\nimport { getRootDir } from \"../electronConfig\";\n\nexport const getConnectionUri = (c: Connections) =>\n  c.db_conn ||\n  `postgres://${c.db_user}:${c.db_pass || \"\"}@${c.db_host || \"localhost\"}:${c.db_port || \"5432\"}/${c.db_name}`;\n\nexport async function getFileMgr(dbs: DBS, credId: number | null) {\n  const localFolderPath = path.resolve(getRootDir() + ROUTES.BACKUPS);\n\n  let cred;\n  if (credId) {\n    cred = await dbs.credentials.findOne({ id: credId });\n    if (!cred) throw new Error(\"Could not find the credentials\");\n  }\n  const fileMgr = new FileManager(\n    cred ?\n      getCloudClient({\n        accessKeyId: cred.key_id,\n        secretAccessKey: cred.key_secret,\n        Bucket: cred.bucket!,\n        region: cred.region || \"auto\",\n        endpoint: cred.endpoint_url,\n      })\n    : { localFolderPath },\n  );\n  return { fileMgr, cred };\n}\n\nexport async function getBkp(\n  dbs: DBOFullyTyped<DBGeneratedSchema>,\n  bkpId: string,\n) {\n  const bkp = await dbs.backups.findOne({ id: bkpId });\n  if (!bkp) throw new Error(\"Could not find the backup\");\n\n  const { cred, fileMgr } = await getFileMgr(dbs, bkp.credential_id);\n\n  return {\n    bkp,\n    cred,\n    fileMgr,\n  };\n}\n\nexport function bytesToSize(bytes: number) {\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  if (bytes == 0) return \"0 Byte\";\n  const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)) + \"\");\n  return Math.round(bytes / Math.pow(1024, i)) + \" \" + sizes[i];\n}\n\ntype Basics = string | number | boolean;\nexport function addOptions(\n  opts: string[],\n  extra: [add: boolean | undefined, val: Basics | Basics[]][],\n): string[] {\n  return opts.concat(\n    extra\n      .filter((e) => e[0])\n      .flatMap((e) => e[1])\n      .map((e) => e.toString()),\n  );\n}\n\n// process.stdout.on(\"error\", (err) => {\n//   debugger\n// });\n// process.on('uncaughtException', function (err) {\n//   console.error(err);\n//   console.log(\"Node NOT Exiting...\");\n// });\n\ntype ConnectionEnvVars = {\n  PGHOST: string;\n  PGPORT: string;\n  PGDATABASE: string;\n  PGUSER: string;\n  PGPASSWORD: string;\n};\n\nexport const getConnectionEnvVars = (c: Connections): ConnectionEnvVars => {\n  const conDetails = getConnectionDetails(c);\n  return {\n    PGHOST: conDetails.host,\n    PGPORT: conDetails.port + \"\",\n    PGDATABASE: conDetails.database,\n    PGUSER: conDetails.user,\n    PGPASSWORD: conDetails.password,\n  };\n};\n\nexport const makeLogs = (\n  newLogs: string,\n  oldLogs: string | null | undefined,\n  startTimeStr: string | undefined,\n) => {\n  let restore_logs = newLogs;\n  if (startTimeStr) {\n    const startTime = new Date(startTimeStr);\n    const age = getAge(+startTime, Date.now(), true);\n    const padd = (v: number, len = 2) =>\n      v.toFixed(0).toString().padStart(len, \"0\");\n    restore_logs = newLogs\n      .split(\"\\n\")\n      .filter((v) => v)\n      .map((v) =>\n        v.includes(\"T+\") ? v : (\n          `T+ ${[age.hours, age.minutes, age.seconds].map((v) => padd(v)).join(\":\") + \".\" + padd(age.milliseconds, 3)}   ${v}`\n        ),\n      )\n      .join(\"\\n\");\n  }\n  return (oldLogs || \"\") + \"\\n\" + restore_logs;\n};\n"
  },
  {
    "path": "server/src/ConnectionManager/ConnectionManager.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { ROUTES } from \"@common/utils\";\nimport type { Express } from \"express\";\nimport type { Server as httpServer } from \"http\";\nimport path from \"path\";\nimport type pg from \"pg-promise/typescript/pg-subset\";\nimport type { Filter } from \"prostgles-server/dist/DboBuilder/DboBuilderTypes\";\nimport type { DB } from \"prostgles-server/dist/Prostgles\";\nimport type {\n  FileTableConfig,\n  ProstglesInitOptions,\n} from \"prostgles-server/dist/ProstglesTypes\";\nimport type { InitResult } from \"prostgles-server/dist/initProstgles\";\nimport type { SubscriptionHandler } from \"prostgles-types\";\nimport { pickKeys } from \"prostgles-types\";\nimport type { DefaultEventsMap, Server } from \"socket.io\";\nimport type { SUser } from \"../authConfig/sessionUtils\";\nimport type { AuthSetupDataListener } from \"../authConfig/subscribeToAuthSetupChanges\";\nimport { getDbConnection } from \"../connectionUtils/testDBConnection\";\nimport { getRootDir } from \"../electronConfig\";\nimport type { Connections, DBS, DatabaseConfigs } from \"../index\";\nimport { connMgr } from \"../index\";\nimport { UNIQUE_DB_COLS } from \"../tableConfig/tableConfig\";\nimport { ForkedPrglProcRunner } from \"./ForkedPrglProcRunner/ForkedPrglProcRunner\";\nimport {\n  getCompiledTS,\n  getRestApiConfig,\n  getTableConfig,\n  parseTableConfig,\n} from \"./connectionManagerUtils\";\nimport { initConnectionManager } from \"./initConnectionManager\";\nimport { startConnection } from \"./startConnection\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\nexport type Unpromise<T extends Promise<any>> =\n  T extends Promise<infer U> ? U : never;\n\nexport type ConnectionTableConfig = Pick<FileTableConfig, \"referencedTables\"> &\n  Omit<Exclude<DatabaseConfigs[\"file_table_config\"], null>, \"referencedTables\">;\n\nexport type User = DBSSchema[\"users\"];\n\nexport const getACRules = async (\n  dbs: DBOFullyTyped<DBGeneratedSchema>,\n  user: Pick<User, \"type\">,\n): Promise<DBSSchema[\"access_control\"][]> => {\n  return await dbs.access_control.find({\n    $existsJoined: { access_control_user_types: { user_type: user.type } },\n  });\n};\n\ntype DBWithUsers = { users?: Partial<DBS[\"users\"]> };\n\ntype PRGLInstance = {\n  socket_path: string;\n  io:\n    | Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>\n    | undefined;\n  con: Connections;\n  dbConf: DatabaseConfigs;\n  prgl?: InitResult<void, SUser>;\n  error?: any;\n  connectionInfo: pg.IConnectionParameters<pg.IClient>;\n  methodRunner: ForkedPrglProcRunner | undefined;\n  tableConfigRunner: ForkedPrglProcRunner | undefined;\n  onMountRunner: ForkedPrglProcRunner | undefined;\n  isReady: boolean;\n  lastRestart: number;\n  isSuperUser: boolean | undefined;\n  authSetupDataListener: AuthSetupDataListener | undefined;\n};\n\nexport type HotReloadConfigOptions = Pick<\n  ProstglesInitOptions,\n  \"fileTable\" | \"restApi\" | \"schemaFilter\" // \"tableConfig\" |\n>;\nexport const getHotReloadConfigs = async (\n  conMgr: ConnectionManager,\n  c: Connections,\n  conf: DatabaseConfigs,\n  dbs: DBS,\n): Promise<HotReloadConfigOptions> => {\n  const restApi = getRestApiConfig(conMgr, c, conf);\n  const { fileTable } = await parseTableConfig({\n    type: \"saved\",\n    dbs,\n    con: c,\n    conMgr,\n  });\n  return {\n    restApi,\n    fileTable,\n    /** TODO */\n    schemaFilter: c.db_schema_filter ?? { public: 1 },\n  };\n};\n\nexport class ConnectionManager {\n  prglConnections: Record<string, PRGLInstance> = {};\n  http: httpServer;\n  app: Express;\n  // wss?: WebSocket.Server<WebSocket.WebSocket>;\n  dbs?: DBS;\n  db?: DB;\n  connections?: Connections[];\n  database_configs?: DatabaseConfigs[];\n\n  constructor(http: httpServer, app: Express) {\n    this.http = http;\n    this.app = app;\n\n    this.setUpWSS();\n  }\n\n  destroy = async () => {\n    await this.conSub?.unsubscribe();\n    await this.dbConfSub?.unsubscribe();\n    await this.userSub?.unsubscribe();\n    Object.values(this.prglConnections).forEach((c) => {\n      c.methodRunner?.destroy();\n      c.tableConfigRunner?.destroy();\n      c.onMountRunner?.destroy();\n      void c.prgl?.destroy();\n    });\n    await Promise.all(\n      this.accessControlListeners?.map((l) => l.unsubscribe()) ?? [],\n    );\n  };\n\n  getConnectionsWithPublicAccess = () => {\n    return this.dbConfigs.filter((c) =>\n      c.access_control_user_types.some((u) => u.user_type === \"public\"),\n    );\n  };\n\n  /**\n   * If a connection was reloaded due to permissions change (revoke/grant) then\n   * restart all other related connections that did not get this event\n   *\n   */\n  onConnectionReload = (conId: string, dbConfId: number) => {\n    const delay = 1000;\n    setTimeout(() => {\n      Object.entries(this.prglConnections).forEach(([_conId, prglCon]) => {\n        if (\n          conId !== _conId &&\n          prglCon.dbConf.id === dbConfId &&\n          prglCon.lastRestart < Date.now() - delay\n        ) {\n          void prglCon.prgl?.restart();\n        }\n      });\n    }, delay);\n  };\n\n  setTableConfig = async (\n    conId: string,\n    table_config_ts: string | undefined | null,\n    disabled: boolean | null,\n  ) => {\n    const prglCon = this.prglConnections[conId];\n    if (!prglCon) throw \"Connection not found\";\n    if (!this.dbs) throw \"Dbs not ready\";\n    if (\n      !disabled &&\n      prglCon.tableConfigRunner?.opts.type === \"tableConfig\" &&\n      prglCon.tableConfigRunner.opts.table_config_ts === table_config_ts\n    )\n      return;\n    prglCon.tableConfigRunner?.destroy();\n    prglCon.tableConfigRunner = undefined;\n    if (disabled) return;\n    await this.dbs.database_config_logs.update(\n      { id: prglCon.dbConf.id },\n      { table_config_logs: null },\n    );\n    if (table_config_ts) {\n      const tableConfig = getTableConfig({\n        table_config_ts,\n        table_config: null,\n      });\n      prglCon.tableConfigRunner = await ForkedPrglProcRunner.create({\n        dbs: this.dbs,\n        type: \"tableConfig\",\n        pass_process_env_vars_to_server_side_functions: false,\n        table_config_ts,\n        dbConfId: prglCon.dbConf.id,\n        prglInitOpts: {\n          dbConnection: {\n            ...prglCon.connectionInfo,\n            application_name: \"tableConfig\",\n          },\n          tableConfig,\n        },\n      });\n      return 1;\n    }\n  };\n\n  setOnMount = async (\n    conId: string,\n    on_mount_ts: string | undefined | null,\n    disabled: boolean | null,\n  ) => {\n    const prglCon = this.prglConnections[conId];\n    if (!prglCon) throw \"Connection not found\";\n    if (!this.dbs) throw \"Dbs not ready\";\n    if (\n      !disabled &&\n      prglCon.onMountRunner?.opts.type === \"onMount\" &&\n      prglCon.onMountRunner.opts.on_mount_ts === on_mount_ts\n    ) {\n      return;\n    }\n    prglCon.onMountRunner?.destroy();\n    prglCon.onMountRunner = undefined;\n\n    if (disabled) return;\n    await this.dbs.database_config_logs.update(\n      { id: prglCon.dbConf.id },\n      { on_mount_logs: null },\n    );\n    if (on_mount_ts) {\n      const compiledCode = getCompiledTS(on_mount_ts);\n      prglCon.onMountRunner = await ForkedPrglProcRunner.create({\n        dbs: this.dbs,\n        type: \"onMount\",\n        on_mount_ts,\n        on_mount_ts_compiled: compiledCode,\n        pass_process_env_vars_to_server_side_functions: false,\n        dbConfId: prglCon.dbConf.id,\n        prglInitOpts: {\n          dbConnection: {\n            ...prglCon.connectionInfo,\n            application_name: \"onMount\",\n          },\n        },\n      });\n      /** Not awaited not block opening the connection */\n      void prglCon.onMountRunner.run({\n        type: \"onMount\",\n        code: compiledCode,\n      });\n    }\n  };\n\n  syncUsers = async (\n    db: DBWithUsers,\n    userTypes: DBSSchema[\"users\"][\"type\"][],\n    syncableColumns: (keyof DBSSchema[\"users\"])[],\n  ) => {\n    if (!db.users || !this.dbs || !syncableColumns.length) return;\n    const lastUpdateDb = await db.users.findOne?.(\n      {},\n      { select: { last_updated: 1 }, orderBy: { last_updated: -1 } },\n    );\n    const lastUpdateDbs = await this.dbs.users.findOne(\n      { \"type.$in\": userTypes },\n      { select: { last_updated: 1 }, orderBy: { last_updated: -1 } },\n    );\n    if (\n      (lastUpdateDbs?.last_updated && !lastUpdateDb?.last_updated) ||\n      (lastUpdateDbs?.last_updated &&\n        +(lastUpdateDb?.last_updated || 0) < +lastUpdateDbs.last_updated)\n    ) {\n      const newUsers = await this.dbs.users.find(\n        {\n          \"type.$in\": userTypes,\n          \"last_updated.>\": lastUpdateDb?.last_updated ?? 0,\n        } as Filter,\n        { limit: 1000, orderBy: { last_updated: 1 } },\n      );\n      if (newUsers.length) {\n        await db.users.insert?.(\n          newUsers.map((u) => pickKeys(u, syncableColumns)),\n          { onConflict: \"DoUpdate\" },\n        );\n        void this.syncUsers(db, userTypes, syncableColumns);\n      }\n    }\n  };\n\n  userSub?: SubscriptionHandler | undefined;\n  setSyncUserSub = async () => {\n    await this.userSub?.unsubscribe();\n    this.userSub = await this.dbs?.users.subscribe(\n      {},\n      { throttle: 1e3 },\n      async (_users) => {\n        for (const prglCon of Object.values(this.prglConnections)) {\n          const db = prglCon.prgl?.db as DBWithUsers | undefined;\n          const dbUsersHandler = db?.users;\n          const dbConf = await this.dbs?.database_configs.findOne({\n            id: prglCon.dbConf.id,\n          });\n          if (dbUsersHandler && dbConf?.sync_users) {\n            const userTypes = await this.dbs?.access_control_user_types.find(\n              {\n                $existsJoined: {\n                  [\"**.connections\" as \"connections\"]: { id: prglCon.con.id },\n                },\n              },\n              {\n                select: { user_type: 1 },\n                returnType: \"values\",\n              },\n            );\n            const dbCols = await dbUsersHandler.getColumns?.();\n            const dbsCols = await this.dbs?.users.getColumns();\n            if (!dbCols || !dbsCols) return;\n            const requiredColumns = [\"id\", \"last_updated\"] as const;\n            const excludedColumns = [\"password\"];\n            const syncableColumns = dbsCols\n              .filter((c) =>\n                dbCols.some(\n                  (dc) =>\n                    dc.insert &&\n                    dc.name === c.name &&\n                    dc.udt_name === c.udt_name,\n                ),\n              )\n              .map((c) => c.name)\n              .filter(\n                (c) => !excludedColumns.includes(c),\n              ) as (keyof DBSSchema[\"users\"])[];\n            if (\n              userTypes &&\n              requiredColumns.every((c) => syncableColumns.includes(c))\n            ) {\n              void this.syncUsers(\n                db,\n                userTypes as DBSSchema[\"users\"][\"type\"][],\n                syncableColumns,\n              );\n            }\n          }\n        }\n      },\n    );\n  };\n  conSub?: SubscriptionHandler | undefined;\n  dbConfSub?: SubscriptionHandler | undefined;\n  dbConfigs: (DBSSchema[\"database_configs\"] & {\n    connections: { id: string }[];\n    access_control_user_types: {\n      user_type: string;\n      access_control_id: number;\n    }[];\n  })[] = [];\n  init = initConnectionManager.bind(this);\n\n  accessControlSkippedFirst = false;\n  accessControlListeners?: SubscriptionHandler[];\n  accessControlHotReload = async () => {\n    if (!this.dbs || this.accessControlListeners?.length) return;\n    const onAccessChange = (connIds: string[]) => {\n      if (!this.accessControlSkippedFirst) {\n        this.accessControlSkippedFirst = true;\n        return;\n      }\n      console.log(\"onAccessChange\");\n      return Promise.all(\n        connIds.map(async (connection_id) => {\n          return this.prglConnections[connection_id]?.prgl?.restart();\n        }),\n      );\n    };\n    this.accessControlListeners = [\n      await this.dbs.access_control.subscribe(\n        {},\n        {\n          select: {\n            database_id: 1,\n            access_control_user_types: { access_control_id: 1 },\n            access_control_methods: { access_control_id: 1 },\n          },\n          throttle: 1000,\n          throttleOpts: {\n            skipFirst: true,\n          },\n        },\n        async (connections) => {\n          const dbIds = Array.from(\n            new Set(connections.map((c) => c.database_id)),\n          );\n          const d: { connIds?: string[] } | undefined =\n            await this.dbs?.connections.findOne(\n              { $existsJoined: { database_configs: { id: { $in: dbIds } } } },\n              { select: { connIds: { $array_agg: [\"id\"] } } },\n            );\n          await onAccessChange(d?.connIds ?? []);\n          await this.setSyncUserSub();\n        },\n      ),\n    ];\n  };\n\n  setUpWSS() {\n    // if(!this.wss){\n    //   this.wss = new WebSocket.Server({ port: 3004, path: \"/here\" });\n    // }\n    // const clients = new Map();\n    // this.wss.on('connection', (ws) => {\n    //   const id = Date.now() + \".\" + Math.random()\n    //   const color = Math.floor(Math.random() * 360);\n    //   const metadata = { id, color };\n    //   clients.set(ws, metadata);\n    //   ws.on(\"message\", console.log)\n    //   ws.on(\"close\", () => {\n    //     clients.delete(ws);\n    //   });\n    // });\n    // return this.wss;\n  }\n\n  getFileFolderPath(conId?: string) {\n    const rootPath = path.resolve(`${getRootDir()}${ROUTES.STORAGE}`);\n    if (!conId) return rootPath;\n    const conn = this.connections?.find((c) => c.id === conId);\n    if (!conn) throw \"Connection not found\";\n    const conPath = UNIQUE_DB_COLS.map((f) => conn[f]).join(\"_\");\n    return `${rootPath}/${conPath}`;\n  }\n\n  getConnectionDb(\n    conId: string,\n  ): Required<PRGLInstance>[\"prgl\"][\"db\"] | undefined {\n    return this.prglConnections[conId]?.prgl?.db;\n  }\n\n  async getNewConnectionDb(\n    connId: string,\n    opts?: pg.IConnectionParameters<pg.IClient>,\n  ) {\n    return getDbConnection(await this.getConnectionData(connId), opts);\n  }\n\n  getConnection(\n    conId: string,\n  ): PRGLInstance & Pick<Required<PRGLInstance>, \"prgl\"> {\n    const c = this.prglConnections[conId];\n    if (!c?.prgl) {\n      throw \"Connection not found\";\n    }\n    return c as PRGLInstance & Pick<Required<PRGLInstance>, \"prgl\">;\n  }\n\n  getConnections() {\n    return this.prglConnections;\n  }\n\n  async disconnect(conId: string): Promise<boolean> {\n    await cdbCache[conId]?.destroy();\n    const conn = this.prglConnections[conId];\n    if (conn) {\n      conn.methodRunner?.destroy();\n      conn.tableConfigRunner?.destroy();\n      conn.onMountRunner?.destroy();\n      //TODO: fix re-started connection not working. Might need to use ws instead of socket.io\n      await conn.prgl?.destroy();\n      delete this.prglConnections[conId];\n      return true;\n    }\n    return false;\n  }\n\n  async getConnectionData(connection_id: string) {\n    const con = await this.dbs?.connections.findOne({ id: connection_id });\n    if (!con) throw \"Connection not found\";\n\n    return con;\n  }\n\n  setFileTable = async (\n    con: DBSSchema[\"connections\"],\n    newTableConfig: DatabaseConfigs[\"file_table_config\"],\n  ) => {\n    const prgl = this.prglConnections[con.id]?.prgl;\n    const dbs = this.dbs;\n    if (!dbs || !prgl) return;\n\n    const { fileTable } = await parseTableConfig({\n      type: \"new\",\n      dbs,\n      con,\n      conMgr: this,\n      newTableConfig,\n    });\n    await prgl.update({ fileTable });\n  };\n\n  startConnection = startConnection.bind(this);\n}\n\nexport const cdbCache: Record<\n  string,\n  {\n    db: DB;\n    isSuperUser?: boolean;\n    hasSuperUser?: boolean;\n    destroy: () => Promise<void>;\n  }\n> = {};\nexport const getCDB = async (\n  connId: string,\n  opts?: pg.IConnectionParameters<pg.IClient>,\n  isTemporary = false,\n) => {\n  if (!cdbCache[connId] || cdbCache[connId].db.$pool.ending || isTemporary) {\n    const destroy: () => Promise<void> = async () => {\n      await db.$pool.end();\n      delete cdbCache[connId];\n    };\n    const db = await connMgr.getNewConnectionDb(connId, {\n      application_name: \"prostgles getCDB\",\n      ...opts,\n    });\n    if (isTemporary) return { db, destroy };\n    cdbCache[connId] = {\n      db,\n      destroy,\n    };\n  }\n\n  return cdbCache[connId];\n};\nexport const getSuperUserCDB = async (connId: string, dbs: DBS) => {\n  const dbInfo = await getCDB(connId);\n  if (dbInfo.isSuperUser) return dbInfo;\n  const connIdSuperUser = `${connId}_super_user`;\n  if (dbInfo.hasSuperUser === false) {\n    return dbInfo;\n  } else if (dbInfo.hasSuperUser === true) {\n    const su = cdbCache[connIdSuperUser];\n    if (!su) throw \"No super user db found\";\n    return su;\n  }\n  const _superUsers: { usename: string; is_current_user: boolean }[] =\n    await dbInfo.db.any(\n      `\n    SELECT usename, \"current_user\"() = usename as is_current_user\n    FROM pg_user WHERE usesuper = true\n    `,\n      {},\n    );\n\n  if (_superUsers.some((s) => s.is_current_user)) {\n    cdbCache[connId]!.isSuperUser = true;\n    cdbCache[connId]!.hasSuperUser = true;\n    return dbInfo;\n  }\n\n  const superUsers = _superUsers.map((u) => u.usename);\n\n  const conn = await dbs.connections.findOne({ id: connId });\n  const connsWithSuperUser = await dbs.connections.find({\n    db_host: conn!.db_host,\n    db_port: conn!.db_port,\n    db_user: { $in: superUsers },\n  });\n\n  const firstConn = connsWithSuperUser[0];\n  if (firstConn) {\n    const dbSu = await getCDB(\n      connId,\n      { user: firstConn.db_user, password: firstConn.db_pass! },\n      true,\n    );\n    cdbCache[connIdSuperUser] = {\n      ...dbSu,\n      isSuperUser: true,\n      hasSuperUser: true,\n    };\n    cdbCache[connId]!.hasSuperUser = true;\n    return cdbCache[connIdSuperUser];\n  }\n  cdbCache[connId]!.hasSuperUser = false;\n  cdbCache[connId]!.isSuperUser = false;\n\n  return dbInfo;\n};\n"
  },
  {
    "path": "server/src/ConnectionManager/ForkedPrglProcRunner/ForkedPrglProcRunner.ts",
    "content": "import type { ChildProcess, ForkOptions } from \"child_process\";\nimport { fork } from \"child_process\";\nimport * as path from \"path\";\nimport pidusage from \"pidusage\";\nimport type { ProstglesInitOptions } from \"prostgles-server/dist/ProstglesTypes\";\nimport { type AnyObject, isObject } from \"prostgles-types\";\nimport type { DBS } from \"../..\";\nimport { FORKED_PROC_ENV_NAME, type ProcStats } from \"@common/utils\";\nimport { getError } from \"./forkedProcess\";\n\ntype ForkedProcMessageCommon = {\n  id: string;\n};\n\ntype PrglInitOptions = Omit<ProstglesInitOptions, \"onReady\">;\n\ntype ForkedProcMessageStart = ForkedProcMessageCommon & {\n  type: \"start\";\n  prglInitOpts: PrglInitOptions;\n};\ntype ForkedProcRunArgs =\n  | {\n      type: \"run\";\n      code: string;\n      validatedArgs?: AnyObject;\n      user?: any;\n    }\n  | {\n      type: \"onMount\";\n      code: string;\n    };\ntype ForkedProcMessageRun = ForkedProcMessageCommon & ForkedProcRunArgs;\ntype ForkedProcMCPResult = ForkedProcMessageCommon & {\n  type: \"mcpResult\";\n  callId: number;\n  error: any;\n  result: any;\n};\n\nexport type ForkedProcMessage =\n  | ForkedProcMessageStart\n  | ForkedProcMessageRun\n  | ForkedProcMCPResult;\n\nexport type ForkedProcMessageError = {\n  lastMsgId: string;\n  type: \"error\";\n  error: any;\n};\n\nexport type ForkedProcMessageResult =\n  | {\n      id: string;\n      result: any;\n      error?: any;\n    }\n  | ForkedProcMessageError;\n// | {\n//     id: string;\n//     callId: number;\n//     type: \"toolCall\";\n//     serverName: string;\n//     toolName: string;\n//     args?: any;\n//   };\n\ntype Opts = {\n  prglInitOpts: PrglInitOptions;\n  dbs: DBS;\n  dbConfId: number;\n  forkOpts?: Pick<ForkOptions, \"cwd\">;\n  pass_process_env_vars_to_server_side_functions: boolean;\n} & (\n  | {\n      type: \"run\";\n    }\n  | {\n      type: \"onMount\";\n      on_mount_ts: string;\n      on_mount_ts_compiled: string;\n    }\n  | {\n      type: \"tableConfig\";\n      table_config_ts: string;\n    }\n);\n\n/**\n * This class is used to run onMount/method TS code in a forked process.\n */\nexport class ForkedPrglProcRunner {\n  currentRunId = 1;\n  opts: Opts;\n  proc: ChildProcess;\n  runQueue: Record<\n    string,\n    ForkedProcMessageCommon & {\n      cb: (err?: any, result?: any) => void;\n    }\n  > = {};\n\n  stdout: any[] = [];\n  stderr: any[] = [];\n  logs: any[] = [];\n\n  private constructor(proc: ChildProcess, opts: Opts) {\n    this.proc = proc;\n    this.opts = opts;\n    this.initProc();\n  }\n\n  databaseNotFound = false;\n  destroyed = false;\n  destroy = (databaseNotFound = false) => {\n    this.destroyed = true;\n    this.databaseNotFound = databaseNotFound;\n    this.proc.kill(\"SIGKILL\");\n  };\n\n  isRestarting = false;\n  restartProc = debounce((error: any) => {\n    if (this.isRestarting || this.databaseNotFound || this.destroyed) return;\n    const logName = `ForkedPrglProcRunner (${this.opts.type})`;\n    this.isRestarting = true;\n    console.error(`${logName} restartProc. error:`, error);\n    Object.entries(this.runQueue).forEach(([id, { cb }]) => {\n      cb(\n        \"Forked process error. Check logs: \\n\" + this.logs.slice(-3).join(\"\\n\"),\n      );\n      delete this.runQueue[id];\n    });\n    console.log(`${logName} restarting ...`);\n    // eslint-disable-next-line @typescript-eslint/no-misused-promises\n    setTimeout(async () => {\n      console.log(`${logName} restarted`);\n      const newProc = await ForkedPrglProcRunner.createProc(this.opts);\n      this.proc = newProc;\n      this.initProc();\n      if (this.opts.type === \"onMount\") {\n        void this.run({\n          type: \"onMount\",\n          code: this.opts.on_mount_ts_compiled,\n        });\n      }\n      this.isRestarting = false;\n    }, 1e3);\n  }, 400);\n\n  private initProc = () => {\n    const updateLogs = (\n      dataOrError: Buffer | Error | string | any[] | AnyObject,\n    ) => {\n      const stringMessage =\n        Buffer.isBuffer(dataOrError) ? dataOrError.toString()\n        : isObject(dataOrError) ? JSON.stringify(dataOrError, null, 2) + \"\\n\"\n        : dataOrError;\n      this.logs.push(`${new Date().toISOString()} ${stringMessage}`);\n      this.logs = this.logs.slice(-500);\n      const { type } = this.opts;\n      const logs = this.logs.map((v) => v.toString()).join(\"\");\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      this.opts.dbs.database_config_logs.update(\n        { id: this.opts.dbConfId },\n        {\n          [type === \"onMount\" ? \"on_mount_logs\"\n          : type === \"tableConfig\" ? \"table_config_logs\"\n          : \"on_run_logs\"]: logs,\n        },\n      );\n    };\n    this.proc.on(\"exit\", (code) => {\n      this.restartProc(code);\n    });\n    this.proc.on(\"message\", (msg: ForkedProcMessageResult) => {\n      if (\"type\" in msg) {\n        // if (msg.type === \"toolCall\") {\n        //   const { id, callId, serverName, toolName, args } = msg;\n        //   (async () => {\n        //     const result = await tryCatchV2(\n        //       async () =>\n        //         await callMCPServerTool(\n        //           this.opts.dbs,\n        //           serverName,\n        //           toolName,\n        //           args,\n        //         ),\n        //     );\n\n        //     this.proc.send({\n        //       callId,\n        //       result: result.data,\n        //       error: getErrorAsObject(result.error),\n        //       type: \"mcpResult\",\n        //       id,\n        //     } satisfies ForkedProcMCPResult);\n        //   })();\n        //   return;\n        // }\n\n        console.error(\n          \"ForkedPrglProc error \",\n          this.opts.prglInitOpts.dbConnection,\n          msg.error,\n        );\n        updateLogs(getError(msg.error));\n        /** Database was dropped */\n        if (msg.error.code === \"3D000\") {\n          this.destroy(true);\n        }\n        return;\n      }\n\n      /** This is the onReady/onReady reload callback */\n      if (msg.id === \"1\" && msg.result === \"reload\") {\n        if (this.opts.type !== \"tableConfig\") {\n          // Reload schema;\n          this.proc.kill();\n        }\n        return;\n      }\n\n      const req = this.runQueue[msg.id];\n      if (!req) {\n        console.error(\n          \"ForkedPrglProcRunner queue item not found\",\n          msg,\n          process.pid,\n        );\n      }\n      req?.cb(msg.error, msg.result);\n      delete this.runQueue[msg.id];\n    });\n\n    this.proc.on(\"error\", (error) => {\n      this.restartProc(error);\n    });\n\n    void this.opts.dbs.database_config_logs.insert(\n      { id: this.opts.dbConfId },\n      { onConflict: \"DoNothing\" },\n    );\n\n    this.proc.stdout?.on(\"data\", updateLogs);\n    this.proc.stdout?.on(\"error\", updateLogs);\n    this.proc.stderr?.on(\"data\", updateLogs);\n    this.proc.stderr?.on(\"error\", updateLogs);\n  };\n\n  private static createProc = ({\n    prglInitOpts,\n    pass_process_env_vars_to_server_side_functions,\n    forkOpts,\n  }: Opts): Promise<ChildProcess> => {\n    return new Promise((resolve, reject) => {\n      const forkedPath = path.join(__dirname, \"forkedProcess.js\");\n      const proc = fork(forkedPath, {\n        ...forkOpts,\n        /** Prevent inheriting execArgv */\n        execArgv: [],\n        // execArgv: console.error(\"REMOVE\") || [\"--inspect-brk\"],\n        silent: true,\n        env: {\n          ...(pass_process_env_vars_to_server_side_functions && process.env),\n          [FORKED_PROC_ENV_NAME]: \"true\",\n        },\n      });\n      proc.on(\"error\", reject);\n      const onMessage = (message: ForkedProcMessageResult) => {\n        proc.off(\"error\", reject);\n        const error = \"error\" in message && message.error;\n        if (error || !(\"id\" in message) || message.id !== \"1\") {\n          reject(error ?? \"Something is wrong with the forked process\");\n        } else {\n          resolve(proc);\n        }\n        proc.off(\"message\", onMessage);\n      };\n      proc.on(\"message\", onMessage);\n\n      proc.send({\n        id: \"1\",\n        type: \"start\",\n        prglInitOpts,\n      } satisfies ForkedProcMessageStart);\n    });\n  };\n\n  static create = async (opts: Opts): Promise<ForkedPrglProcRunner> => {\n    const proc = await ForkedPrglProcRunner.createProc(opts);\n    return new ForkedPrglProcRunner(proc, opts);\n  };\n\n  run = async <T>(runProps: ForkedProcRunArgs): Promise<T> => {\n    this.currentRunId++;\n    const id = this.currentRunId.toString();\n    return new Promise((resolve, reject) => {\n      this.runQueue[id] = {\n        id,\n        ...runProps,\n        cb: (err, res) => {\n          if (err) reject(err);\n          else resolve(res);\n        },\n      };\n      try {\n        if (!this.proc.connected) {\n          throw \"Forked process not connected\";\n        }\n        this.proc.send({ id, ...runProps } satisfies ForkedProcMessageRun);\n      } catch (error: any) {\n        reject(error);\n      }\n    });\n  };\n\n  getProcStats = async (): Promise<ProcStats> => {\n    const { pid } = this.proc;\n    if (!pid) {\n      throw new Error(\"Process PID is not available\");\n    }\n    const res = await pidusage(pid);\n    return {\n      cpu: res.cpu,\n      mem: res.memory,\n      pid: res.pid,\n      uptime: res.elapsed / 1000, // Convert milliseconds to seconds\n    };\n  };\n}\n\nexport function debounce<Params extends any[]>(\n  func: (...args: Params) => any,\n  timeout: number,\n): (...args: Params) => void {\n  let timer: NodeJS.Timeout;\n  return (...args: Params) => {\n    clearTimeout(timer);\n    timer = setTimeout(() => {\n      func(...args);\n    }, timeout);\n  };\n}\n"
  },
  {
    "path": "server/src/ConnectionManager/ForkedPrglProcRunner/createProc.ts",
    "content": ""
  },
  {
    "path": "server/src/ConnectionManager/ForkedPrglProcRunner/forkedProcess.ts",
    "content": "import prostgles from \"prostgles-server\";\nimport type { OnReadyParamsBasic } from \"prostgles-server/dist/initProstgles\";\nimport { getSerialisableError } from \"prostgles-types\";\nimport { FORKED_PROC_ENV_NAME } from \"@common/utils\";\nimport type {\n  ForkedProcMessage,\n  ForkedProcMessageError,\n  ForkedProcMessageResult,\n} from \"./ForkedPrglProcRunner\";\n\nexport const getError = (rawError: any) => {\n  return getSerialisableError(rawError) || \"Unknown error\";\n};\nconst initForkedProc = () => {\n  let _prglParams: OnReadyParamsBasic | undefined;\n  let prglParams: OnReadyParamsBasic | undefined;\n\n  const lastToolCallId = 0;\n  const toolCalls: Record<number, { cb: (err: any, res: any) => void }> = {};\n  const setProxy = (params: OnReadyParamsBasic) => {\n    _prglParams = params;\n    prglParams ??= new Proxy(params, {\n      get(target, prop: keyof OnReadyParamsBasic, receiver) {\n        return _prglParams![prop];\n      },\n    });\n  };\n\n  let lastMsgId = \"\";\n  const sendError = (error: any) => {\n    if (!process.connected) {\n      console.error(\"Process not connected, exiting\");\n      process.exit(1);\n    }\n    process.send?.({\n      lastMsgId,\n      type: \"error\",\n      error: getSerialisableError(error),\n    } satisfies ForkedProcMessageError);\n  };\n  process.on(\"unhandledRejection\", (reason: any, p) => {\n    console.error(\"Unhandled Rejection at: Promise\", p, \"reason:\", reason);\n    sendError(reason);\n  });\n  process.on(\"uncaughtException\", (error: any, origin) => {\n    console.error(\"Uncaught Exception: \", error, \"origin:\", origin);\n    sendError(error);\n  });\n\n  if (!process.send) {\n    console.error(\"No process.send\");\n  }\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n  process.on(\"message\", async (msg: ForkedProcMessage) => {\n    try {\n      if (\"id\" in msg) lastMsgId = msg.id;\n      const cb = (error?: any, result?: any) => {\n        process.send!({\n          id: msg.id,\n          error,\n          result,\n        } satisfies ForkedProcMessageResult);\n      };\n      if (msg.type === \"start\") {\n        if (prglParams) throw \"Already started\";\n\n        //@ts-ignore\n        await prostgles({\n          ...msg.prglInitOpts,\n          watchSchema: \"*\",\n          transactions: true,\n          onReady: (params) => {\n            if (prglParams) {\n              console.log(\"reload\", params.reason);\n              cb(undefined, \"reload\");\n            } else {\n              cb(undefined, \"ready\");\n            }\n            setProxy(params as any);\n          },\n        });\n      } else {\n        if (!prglParams) throw \"prgl not ready\";\n\n        try {\n          if (msg.type === \"mcpResult\") {\n            const { callId, error, result } = msg;\n            toolCalls[callId]?.cb(error, result);\n            delete toolCalls[callId];\n          } else if (msg.type === \"run\") {\n            const { code, validatedArgs, user, id } = msg;\n            const { run } = eval(code + \"\\n\\n exports;\");\n            // const callMCPServerTool = async (\n            //   serverName: string,\n            //   toolName: string,\n            //   args?: any,\n            // ) => {\n            //   return new Promise((resolve, reject) => {\n            //     const callId = lastToolCallId++;\n            //     toolCalls[callId] = {\n            //       cb: (err, res) => (err ? reject(err) : resolve(res)),\n            //     };\n            //     process.send?.({\n            //       id,\n            //       callId,\n            //       type: \"toolCall\",\n            //       serverName,\n            //       toolName,\n            //       args,\n            //     } satisfies ForkedProcMessageResult);\n            //   });\n            // };\n            const methodResult = await run(validatedArgs, {\n              ...prglParams,\n              user,\n              // callMCPServerTool,\n            });\n            cb(undefined, methodResult);\n          } else {\n            const { code } = msg;\n            const { onMount } = eval(code + \"\\n\\n exports;\");\n\n            const methodResult = await onMount(prglParams);\n            cb(undefined, methodResult);\n          }\n        } catch (rawError: any) {\n          const error = getSerialisableError(rawError);\n          console.error(\"forkedProcess error\", error);\n          cb(error);\n        }\n      }\n    } catch (error) {\n      console.error(error);\n      sendError(error);\n    }\n  });\n};\n\nif (process.env[FORKED_PROC_ENV_NAME]) {\n  initForkedProc();\n  checkForImportBug();\n}\n\nfunction checkForImportBug() {\n  const ESMFiles = module.children.map((m) => ({\n    type: \"ESM\" as const,\n    filename: m.filename,\n  }));\n  const mainProcess = ESMFiles.find((f) =>\n    f.filename\n      .replaceAll(\"\\\\\", \"/\")\n      .endsWith(\"ui/server/dist/server/src/index.js\"),\n  );\n  if (mainProcess) {\n    throw new Error(\n      \"Forked process should not import main process file. It will trigger this bug: listen EADDRINUSE: address already in use 127.0.0.1:3004\",\n    );\n  }\n  return ESMFiles;\n}\n"
  },
  {
    "path": "server/src/ConnectionManager/connectionManagerUtils.ts",
    "content": "import type { CloudClient } from \"prostgles-server/dist/FileManager/FileManager\";\nimport type {\n  FileTableConfig,\n  ProstglesInitOptions,\n} from \"prostgles-server/dist/ProstglesTypes\";\nimport type { DbTableInfo } from \"prostgles-server/dist/PublishParser/publishTypesAndUtils\";\nimport type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport type { DB, OnInitReason } from \"prostgles-server/dist/initProstgles\";\nimport type { FileColumnConfig } from \"prostgles-types\";\nimport { pickKeys } from \"prostgles-types\";\nimport ts, { ModuleKind, ModuleResolutionKind, ScriptTarget } from \"typescript\";\nimport type { Connections, DatabaseConfigs, DBS } from \"..\";\nimport { getConnectionPaths, ROUTES } from \"@common/utils\";\nimport { getCloudClient } from \"../cloudClients/cloudClients\";\nimport type { ConnectionManager } from \"./ConnectionManager\";\n\nexport const getDatabaseConfigFilter = (c: Connections) =>\n  pickKeys(c, [\"db_name\", \"db_host\", \"db_port\"]);\n\ntype ParseTableConfigArgs = {\n  dbs: DBS;\n  conMgr: ConnectionManager;\n  con: Connections;\n} & (\n  | {\n      type: \"saved\";\n      newTableConfig?: undefined;\n    }\n  | {\n      type: \"new\";\n      newTableConfig: DatabaseConfigs[\"file_table_config\"];\n    }\n);\n\nexport const parseTableConfig = async ({\n  con,\n  conMgr,\n  dbs,\n  type,\n  newTableConfig,\n}: ParseTableConfigArgs): Promise<{\n  fileTable?: FileTableConfig;\n  tableConfigOk: boolean;\n}> => {\n  const connectionId = con.id;\n  let tableConfigOk = false;\n  let tableConfig:\n    | (DatabaseConfigs[\"file_table_config\"] &\n        Pick<FileTableConfig, \"referencedTables\">)\n    | null = null;\n  if (type === \"saved\") {\n    const database_config = await dbs.database_configs.findOne(\n      getDatabaseConfigFilter(con),\n    );\n    if (!database_config) {\n      return {\n        fileTable: undefined,\n        tableConfigOk: true,\n      };\n    }\n    tableConfig = database_config.file_table_config;\n  } else {\n    tableConfig = newTableConfig;\n  }\n  let cloudClient: CloudClient | undefined;\n  if (tableConfig && tableConfig.storageType.type !== \"local\") {\n    if (tableConfig.storageType.credential_id) {\n      const s3Creds = await dbs.credentials.findOne({\n        id: tableConfig.storageType.credential_id,\n      });\n      if (s3Creds) {\n        tableConfigOk = true;\n        cloudClient = getCloudClient({\n          accessKeyId: s3Creds.key_id,\n          secretAccessKey: s3Creds.key_secret,\n          Bucket: s3Creds.bucket!,\n          region: s3Creds.region || \"auto\",\n          endpoint: s3Creds.endpoint_url,\n        });\n      }\n    }\n    if (!tableConfigOk) {\n      console.error(\n        \"Could not find cloud credentials for fileTable config. File storage will not be set up \",\n      );\n    }\n  } else if (\n    tableConfig?.storageType.type === \"local\" &&\n    tableConfig.fileTable\n  ) {\n    tableConfigOk = true;\n  }\n\n  const fileTable =\n    !tableConfig?.fileTable || !tableConfigOk ?\n      undefined\n    : ({\n        tableName: tableConfig.fileTable,\n        expressApp: conMgr.app,\n        fileServePath: `${ROUTES.STORAGE}/${connectionId}`,\n        ...(tableConfig.storageType.type === \"local\" ?\n          {\n            localConfig: {\n              /* Use path.resolve when using a relative path. Otherwise will get 403 forbidden */\n              localFolderPath: conMgr.getFileFolderPath(connectionId),\n            },\n          }\n        : { cloudClient }),\n        referencedTables: tableConfig.referencedTables,\n      } satisfies FileTableConfig);\n\n  return { tableConfigOk, fileTable };\n};\n\nexport const getCompiledTS = (code: string) => {\n  const sourceCode = ts.transpile(\n    code,\n    {\n      noEmit: false,\n      target: ScriptTarget.ES2022,\n      lib: [\"ES2022\"],\n      module: ModuleKind.CommonJS,\n      moduleResolution: ModuleResolutionKind.Node16,\n    },\n    \"input.ts\",\n  );\n\n  return sourceCode;\n};\n\nexport const getRestApiConfig = (\n  conMgr: ConnectionManager,\n  con: Connections,\n  dbConf: DatabaseConfigs,\n) => {\n  const res: ProstglesInitOptions[\"restApi\"] =\n    dbConf.rest_api_enabled ?\n      {\n        expressApp: conMgr.app,\n        path: getConnectionPaths(con).rest,\n      }\n    : undefined;\n\n  return res;\n};\nexport const getEvaledExports = <T>(\n  code: string | undefined,\n): T | undefined => {\n  if (!code) return undefined;\n  /**\n   * This is needed to ensure all named exports are returned in eval\n   */\n  const ending = \"\\n\\nexports;\";\n  const sourceCode = getCompiledTS(code + ending);\n  // eslint-disable-next-line security/detect-eval-with-expression\n  const result = eval(sourceCode) as T;\n  return result;\n};\n\ntype TableDbConfig = Pick<DatabaseConfigs, \"table_config\" | \"table_config_ts\">;\ntype CompiledTableConfig = { tableConfig: TableConfig; dashboardConfig?: any };\nconst getCompiledTableConfig = ({\n  table_config,\n  table_config_ts,\n}: TableDbConfig): undefined | CompiledTableConfig => {\n  if (table_config) return { tableConfig: table_config as TableConfig };\n  if (!table_config_ts) return undefined;\n\n  const res = getEvaledExports<CompiledTableConfig>(table_config_ts);\n  if (!res?.tableConfig)\n    throw \"A table_config_ts must export a const named 'tableConfig' \";\n  return res;\n};\n\nexport const getTableConfig = (dbConf: TableDbConfig) => {\n  return getCompiledTableConfig(dbConf)?.tableConfig;\n};\n\nexport type FileTableConfigReferences = Record<\n  string,\n  { referenceColumns: Record<string, FileColumnConfig> }\n>;\n\ntype AlertIfReferencedFileColumnsRemovedArgs = {\n  reason: OnInitReason;\n  tables: DbTableInfo[];\n  connId: string;\n  db: DB;\n};\nexport const alertIfReferencedFileColumnsRemoved = async function (\n  this: ConnectionManager,\n  { connId, reason, tables }: AlertIfReferencedFileColumnsRemovedArgs,\n) {\n  /** Remove dropped referenced file columns */\n  const { dbConf, isSuperUser } = this.prglConnections[connId] ?? {};\n  const referencedTables = dbConf?.file_table_config?.referencedTables as\n    | FileTableConfigReferences\n    | undefined;\n  if (\n    isSuperUser &&\n    dbConf &&\n    this.dbs &&\n    referencedTables &&\n    (reason.type === \"schema change\" || reason.type === \"TableConfig\")\n  ) {\n    const droppedFileColumns: { tableName: string; missingCols: string[] }[] =\n      [];\n    Object.entries(referencedTables).map(\n      ([tableName, { referenceColumns }]) => {\n        const table = tables.find((t) => t.name === tableName);\n        const missingCols = Object.keys(referenceColumns).filter(\n          (colName) => !table?.columns.find((c) => c.name === colName),\n        );\n        if (missingCols.length) {\n          droppedFileColumns.push({ tableName, missingCols });\n        }\n      },\n    );\n    if (\n      droppedFileColumns.length &&\n      !(await this.dbs.alerts.findOne({\n        database_config_id: dbConf.id,\n        data: droppedFileColumns,\n      }))\n    ) {\n      await this.dbs.alerts.insert({\n        severity: \"warning\",\n        title: \"Storage columns missing\",\n        message: `Some file column configs are missing from database schema: ${droppedFileColumns.map(({ tableName, missingCols }) => `${tableName}: ${missingCols.join(\", \")}`).join(\", \")}`,\n        database_config_id: dbConf.id,\n        data: droppedFileColumns,\n      });\n    }\n  }\n};\n"
  },
  {
    "path": "server/src/ConnectionManager/getConnectionPublish.ts",
    "content": "import type {\n  Publish,\n  PublishObject,\n} from \"prostgles-server/dist/PublishParser/publishTypesAndUtils\";\nimport { isDefined, omitKeys } from \"prostgles-types\";\nimport type { DBS } from \"..\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { parseTableRules } from \"@common/publishUtils\";\nimport { getEntries } from \"@common/utils\";\nimport type { SUser } from \"../authConfig/sessionUtils\";\nimport { getAccessRule } from \"./startConnection\";\nimport { publish } from \"../publish/publish\";\n\ntype Args = {\n  dbs: DBS;\n  dbConf: DBSSchema[\"database_configs\"];\n  connection: DBSSchema[\"connections\"];\n};\n\nexport const getConnectionPublish = ({\n  dbs,\n  dbConf,\n  connection,\n}: Args): Publish<void, SUser> | undefined => {\n  if (connection.is_state_db) {\n    // throw new Error(\n    //   \"Cannot publish state database. Must be used from useDBSConnection\",\n    // );\n    return publish as Publish<void, SUser>;\n  }\n  const connectionId = connection.id;\n  const connectionPublish: Publish<void, SUser> = async ({\n    user,\n    dbo,\n    tables,\n  }) => {\n    if (!user) {\n      return null;\n    }\n\n    if (user.type === \"admin\") {\n      return \"*\";\n    }\n\n    const accessRule = await getAccessRule(dbs, user, dbConf.id, connectionId);\n    if (!accessRule) {\n      return null;\n    }\n\n    const { dbPermissions } = accessRule;\n\n    if (dbPermissions.type === \"Run SQL\" && dbPermissions.allowSQL) {\n      return \"*\" as const;\n    } else if (\n      dbPermissions.type === \"All views/tables\" &&\n      dbPermissions.allowAllTables.length\n    ) {\n      const { allowAllTables } = dbPermissions;\n      const res = getEntries(dbo)\n        .filter((entry) => {\n          const [_, t] = entry;\n          return Boolean(\"find\" in t && t.find);\n        })\n        .reduce(\n          (acc, [tableName, tableHandler]) => ({\n            ...acc,\n            [tableName]: {\n              select: allowAllTables.includes(\"select\") ? \"*\" : undefined,\n              ...(!(tableHandler.is_view as boolean) && {\n                update: allowAllTables.includes(\"update\") ? \"*\" : undefined,\n                insert: allowAllTables.includes(\"insert\") ? \"*\" : undefined,\n                delete: allowAllTables.includes(\"delete\") ? \"*\" : undefined,\n              }),\n            } satisfies PublishObject[string],\n          }),\n          {} as PublishObject,\n        );\n      return res;\n    } else if (dbPermissions.type === \"Custom\") {\n      const customTableList = dbPermissions.customTables\n        .map((rule) => {\n          const tableHandler = dbo[rule.tableName];\n          if (!tableHandler) return undefined;\n          const table = tables.find(({ name }) => name === rule.tableName);\n          if (!table) return undefined;\n          return {\n            rule,\n            table,\n            tableHandler,\n          };\n        })\n        .filter(isDefined);\n\n      const publish: PublishObject = customTableList.reduce(\n        (acc, { table, rule, tableHandler }) => {\n          const parsedRule = parseTableRules(\n            omitKeys(rule, [\"tableName\"]),\n            tableHandler.is_view,\n            table.columns.map((c) => c.name),\n            { user },\n          );\n\n          if (!parsedRule) return acc;\n\n          const ptr = {\n            ...acc,\n            [rule.tableName]: parsedRule,\n          };\n          return ptr;\n        },\n        {} as PublishObject,\n      );\n\n      return publish;\n    } else {\n      console.error(\"Unexpected access control rule: \", dbPermissions);\n    }\n    return null;\n  };\n\n  return connectionPublish;\n};\n"
  },
  {
    "path": "server/src/ConnectionManager/getConnectionPublishMethods.ts",
    "content": "import type { PublishMethods } from \"prostgles-server/dist/PublishParser/publishTypesAndUtils\";\nimport type { SUser } from \"../authConfig/sessionUtils\";\nimport {\n  omitKeys,\n  pickKeys,\n  type AnyObject,\n  type MethodFullDef,\n} from \"prostgles-types\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { getCompiledTS } from \"./connectionManagerUtils\";\nimport { getAccessRule } from \"./startConnection\";\nimport type { DBS } from \"..\";\nimport type { JSONBColumnDef } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport type { DB } from \"prostgles-server/dist/initProstgles\";\nimport type { ForkedPrglProcRunner } from \"./ForkedPrglProcRunner/ForkedPrglProcRunner\";\n\ntype Args = {\n  dbs: DBS;\n  dbConf: DBSSchema[\"database_configs\"];\n  con: DBSSchema[\"connections\"];\n  _dbs: DB;\n  getForkedProcRunner: () => Promise<ForkedPrglProcRunner>;\n};\n\nexport const getConnectionPublishMethods = ({\n  dbConf,\n  dbs,\n  con,\n  _dbs,\n  getForkedProcRunner,\n}: Args): PublishMethods<void, SUser> => {\n  const publishMethods: PublishMethods<void, SUser> = async ({ user }) => {\n    const result: Record<string, MethodFullDef> = {};\n\n    /** Admin has access to all methods */\n    let allowedMethods: DBSSchema[\"published_methods\"][] = [];\n    if (user?.type === \"admin\") {\n      allowedMethods = await dbs.published_methods.find({\n        connection_id: con.id,\n      });\n    } else {\n      const ac = await getAccessRule(dbs, user, dbConf.id, con.id);\n      if (ac) {\n        allowedMethods = await dbs.published_methods.find({\n          connection_id: con.id,\n          $existsJoined: {\n            access_control_methods: { access_control_id: ac.id },\n          },\n        });\n      }\n    }\n\n    allowedMethods.forEach((m) => {\n      result[m.name] = {\n        input: m.arguments.reduce((a, v) => ({ ...a, [v.name]: v }), {}),\n        outputTable: m.outputTable ?? undefined,\n        run: async (args) => {\n          const sourceCode = getCompiledTS(m.run);\n\n          try {\n            let validatedArgs: AnyObject | undefined = undefined;\n            if (m.arguments.length) {\n              /**\n               * Validate args\n               */\n              for (const arg of m.arguments) {\n                let argType = omitKeys(arg, [\"name\"]);\n                if (arg.type === \"Lookup\" || arg.type === \"Lookup[]\") {\n                  argType = {\n                    ...omitKeys(arg, [\"type\", \"name\", \"optional\"]),\n                    //@ts-ignore\n                    lookup: {\n                      ...arg.lookup,\n                      type: \"data\",\n                    },\n                  };\n                }\n                const partialArgSchema: JSONBColumnDef[\"jsonbSchema\"] = {\n                  //@ts-ignore\n                  type: { [arg.name]: argType },\n                };\n                const partialValue = pickKeys(args, [arg.name]);\n\n                try {\n                  if (arg.type !== \"any\") {\n                    await _dbs.any(\n                      \"SELECT validate_jsonb_schema(${argSchema}::TEXT, ${args})\",\n                      { args: partialValue, argSchema: partialArgSchema },\n                    );\n                  }\n                } catch (error) {\n                  throw {\n                    message: \"Could not validate argument against schema\",\n                    argument: arg.name,\n                    error,\n                  };\n                }\n              }\n              validatedArgs = args;\n            }\n\n            const forkedPrglProcRunner = await getForkedProcRunner();\n            return forkedPrglProcRunner.run({\n              type: \"run\",\n              code: sourceCode,\n              validatedArgs,\n              user,\n            });\n          } catch (err: any) {\n            return Promise.reject(err);\n          }\n        },\n      };\n    });\n\n    return result;\n  };\n  return publishMethods;\n};\n"
  },
  {
    "path": "server/src/ConnectionManager/getInitiatedPostgresqlPIDs.ts",
    "content": "import { execSync } from \"child_process\";\n\n// TODO: just use a custom pg-client that can return the PIDs of all active connections\nexport const getInitiatedPostgresqlPIDs = (parentPid: number) => {\n  const tcpConnections = execSync(`ss -tpn | grep ${parentPid}`)\n    .toString()\n    .split(\"\\n\")\n    .filter(Boolean)\n    .map((line) => {\n      const [state, recvQ, sendQ, localAddress, peerAddress, procInfo] = line\n        .trim()\n        .split(/\\s+/);\n      return {\n        state,\n        recvQ,\n        sendQ,\n        localAddress,\n        peerAddress,\n        procInfo,\n      };\n    });\n  console.log(`TCP connections for parent PID ${parentPid}:`, tcpConnections);\n  // then just ` sudo lsof -i :localPort `\n  // try {\n  //   // Get network connections for the parent process\n  //   const netContent = readFileSync(`/proc/${parentPid}/net/tcp`, \"utf8\");\n  //   const connections = netContent.split(\"\\n\").slice(1).filter(Boolean);\n\n  //   const remotePids: number[] = [];\n\n  //   connections.forEach((line) => {\n  //     const parts = line.trim().split(/\\s+/);\n  //     const localAddr = parts[1];\n  //     const state = parts[3];\n  //     const remoteAddr = parts[2];\n  //     if (state === \"01\" && localAddr && remoteAddr) {\n  //       // ESTABLISHED connection\n\n  //       // Find which process has the remote address as local\n  //       const allPids = readdirSync(\"/proc\").filter((name) =>\n  //         /^\\d+$/.test(name),\n  //       );\n\n  //       for (const pid of allPids) {\n  //         if (pid === parentPid.toString()) continue;\n\n  //         try {\n  //           const pidNetContent = readFileSync(`/proc/${pid}/net/tcp`, \"utf8\");\n  //           if (\n  //             pidNetContent.includes(remoteAddr.split(\":\").reverse().join(\":\"))\n  //           ) {\n  //             remotePids.push(parseInt(pid));\n  //             console.log(`Parent pid: ${parentPid} connected to ${pid}`);\n  //             break;\n  //           }\n  //         } catch (e) {\n  //           // Process might have disappeared or no permission\n  //         }\n  //       }\n  //     }\n  //   });\n\n  //   console.log(remotePids);\n  //   return remotePids;\n  // } catch (error) {\n  //   console.error(\"Error reading /proc filesystem:\", error);\n  //   return [];\n  // }\n  // const procs = execSync(`netstat -tep | grep ${parentPid} | awk '{print $4}'`)\n  //   .toString()\n  //   .split(\"\\n\")\n  //   .filter(Boolean);\n  // procs.forEach((proc, index) => {\n  //   const [host, port] = proc.split(\":\");\n  //   if (port) {\n  //     const pid = execSync(\n  //       `sudo lsof -i:${port} | grep -v ${parentPid} | awk 'NR>1 {print $2}' | head -1`,\n  //     )\n  //       .toString()\n  //       .trim();\n  //     console.log(`Parent pid: ${parentPid} connected to ${pid}`);\n  //   }\n  // });\n};\n"
  },
  {
    "path": "server/src/ConnectionManager/initConnectionManager.ts",
    "content": "import { API_ENDPOINTS, getConnectionPaths } from \"@common/utils\";\nimport type { DB } from \"prostgles-server/dist/Prostgles\";\nimport { tout, type DBS } from \"../index\";\nimport {\n  getHotReloadConfigs,\n  type ConnectionManager,\n} from \"./ConnectionManager\";\nimport { saveCertificates } from \"./saveCertificates\";\n\nexport async function initConnectionManager(\n  this: ConnectionManager,\n  dbs: DBS,\n  db: DB,\n) {\n  this.dbs = dbs;\n  this.db = db;\n\n  await this.conSub?.unsubscribe();\n  this.conSub = await this.dbs.connections.subscribe({}, {}, (connections) => {\n    saveCertificates(connections);\n    connections.forEach((updatedConnection) => {\n      const prglCon = this.prglConnections[updatedConnection.id];\n      const currentConnection = this.connections?.find(\n        (ccon) => ccon.id === updatedConnection.id,\n      );\n      if (\n        prglCon?.io &&\n        currentConnection &&\n        currentConnection.url_path !== updatedConnection.url_path\n      ) {\n        prglCon.io.path(getConnectionPaths(updatedConnection).ws);\n      }\n    });\n    this.connections = connections;\n  });\n\n  await this.dbConfSub?.unsubscribe();\n  this.dbConfSub = await this.dbs.database_configs.subscribe(\n    {},\n    {\n      select: {\n        \"*\": 1,\n        connections: { id: 1 },\n        access_control_user_types: \"*\",\n      },\n    },\n    async (dbConfigs: typeof this.dbConfigs) => {\n      this.dbConfigs = dbConfigs;\n      for (const conf of dbConfigs) {\n        for (const c of conf.connections) {\n          const prglCon = this.prglConnections[c.id];\n          if (prglCon?.prgl && !prglCon.con.is_state_db) {\n            const con = await this.getConnectionData(c.id);\n            const hotReloadConfig = await getHotReloadConfigs(\n              this,\n              con,\n              conf,\n              dbs,\n            );\n            /** Can happen due to error in onMount */\n            await prglCon.prgl.update(hotReloadConfig).catch((e) => {\n              console.error(\n                `Error updating connection ${con.id} with hot reload config`,\n                e,\n                { hotReloadConfig },\n              );\n            });\n            await this.setSyncUserSub();\n          }\n        }\n      }\n      this.database_configs = dbConfigs;\n    },\n  );\n\n  /** Start connections if accessed. TODO This should be a 404 error request handler */\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n  this.app.use(async (req, res, next) => {\n    const { url } = req;\n    if (this.dbs && this.db && this.connections) {\n      let validOfflineConnectionId: string | undefined;\n\n      const connectionIdOrPath = url.split(\"/\")[2];\n      if (\n        connectionIdOrPath &&\n        (url.startsWith(API_ENDPOINTS.WS_DB) ||\n          url.startsWith(API_ENDPOINTS.REST))\n      ) {\n        validOfflineConnectionId = this.connections.find(\n          (c) =>\n            !this.prglConnections[c.id] &&\n            [c.id, c.url_path].includes(connectionIdOrPath),\n        )?.id;\n      }\n      if (validOfflineConnectionId) {\n        await this.startConnection(validOfflineConnectionId, this.dbs, this.db);\n        await tout(1000);\n        return res.redirect(307, req.originalUrl);\n      }\n    }\n    next();\n  });\n\n  await this.accessControlHotReload();\n}\n"
  },
  {
    "path": "server/src/ConnectionManager/saveCertificates.ts",
    "content": "import * as fs from \"fs\";\nimport path from \"path\";\nimport { getRootDir } from \"../electronConfig\";\nimport type { Connections } from \"..\";\nimport type { ConnectionManager } from \"./ConnectionManager\";\nimport type { EnvVars } from \"../BackupManager/pipeFromCommand\";\n\nexport const saveCertificates = (connections: Connections[]) => {\n  connections.forEach((c) => {\n    const hasCerts =\n      c.ssl_certificate ||\n      c.ssl_client_certificate_key ||\n      c.ssl_client_certificate;\n    if (hasCerts) {\n      const folder = getCertPath(c.id);\n      try {\n        fs.rmSync(folder, { recursive: true });\n        fs.mkdirSync(folder, { recursive: true, mode: 0o600 });\n        const utfOpts: fs.WriteFileOptions = {\n          encoding: \"utf-8\",\n          mode: 0o600,\n        }; //\n        if (c.ssl_certificate) {\n          fs.writeFileSync(getCertPath(c.id, \"ca\"), c.ssl_certificate, utfOpts);\n        }\n        if (c.ssl_client_certificate) {\n          fs.writeFileSync(\n            getCertPath(c.id, \"cert\"),\n            c.ssl_client_certificate,\n            utfOpts,\n          );\n        }\n        if (c.ssl_client_certificate_key) {\n          fs.writeFileSync(\n            getCertPath(c.id, \"key\"),\n            c.ssl_client_certificate_key,\n            utfOpts,\n          );\n        }\n      } catch (err) {\n        console.error(\"Failed writing ssl certificates:\", err);\n      }\n    }\n  });\n};\nconst PROSTGLES_CERTS_FOLDER = \"prostgles_certificates\";\n\nconst getCertPath = (conId: string, type?: \"ca\" | \"cert\" | \"key\") => {\n  return path.resolve(\n    `${getRootDir()}/${PROSTGLES_CERTS_FOLDER}/${conId}` +\n      (type ? `/${type}.pem` : \"\"),\n  );\n};\n\nexport function getSSLEnvVars(c: Connections): EnvVars {\n  const result: Record<string, string> = {};\n  if ((c as any).db_ssl) {\n    result.PGSSLMODE = c.db_ssl;\n  }\n  if (c.db_pass) {\n    result.PGPASSWORD = c.db_pass;\n  }\n  if (c.ssl_client_certificate) {\n    result.PGSSLCERT = getCertPath(c.id, \"cert\");\n  }\n  if (c.ssl_client_certificate_key) {\n    result.PGSSLKEY = getCertPath(c.id, \"key\");\n  }\n  if (c.ssl_certificate) {\n    result.PGSSLROOTCERT = getCertPath(c.id, \"ca\");\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "server/src/ConnectionManager/startConnection.ts",
    "content": "import type e from \"express\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { getConnectionPaths } from \"@common/utils\";\nimport prostgles from \"prostgles-server\";\nimport type { AuthConfig } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\nimport type { PRGLIOSocket } from \"prostgles-server/dist/DboBuilder/DboBuilder\";\nimport { getErrorAsObject } from \"prostgles-server/dist/DboBuilder/dboBuilderUtils\";\nimport { getIsSuperUser, type DB } from \"prostgles-server/dist/Prostgles\";\nimport { pickKeys, type AnyObject } from \"prostgles-types\";\nimport { Server } from \"socket.io\";\nimport { addLog } from \"../Logger\";\nimport { getAuth, withOrigin } from \"../authConfig/getAuth\";\nimport type { SUser } from \"../authConfig/sessionUtils\";\nimport {\n  getAuthSetupData,\n  subscribeToAuthSetupChanges,\n  type AuthSetupData,\n} from \"../authConfig/subscribeToAuthSetupChanges\";\nimport { testDBConnection } from \"../connectionUtils/testDBConnection\";\nimport { log, restartProc, type DBS } from \"../index\";\nimport type { ConnectionManager, User } from \"./ConnectionManager\";\nimport { getHotReloadConfigs } from \"./ConnectionManager\";\nimport { ForkedPrglProcRunner } from \"./ForkedPrglProcRunner/ForkedPrglProcRunner\";\nimport { alertIfReferencedFileColumnsRemoved } from \"./connectionManagerUtils\";\nimport { getConnectionPublish } from \"./getConnectionPublish\";\nimport { getConnectionPublishMethods } from \"./getConnectionPublishMethods\";\n\nexport const startConnection = async function (\n  this: ConnectionManager,\n  con_id: string,\n  dbs: DBOFullyTyped<DBGeneratedSchema>,\n  _dbs: DB,\n  socket?: PRGLIOSocket,\n  restartIfExists = false,\n): Promise<string | undefined> {\n  const { http } = this;\n\n  if (this.prglConnections[con_id]) {\n    if (restartIfExists) {\n      await this.prglConnections[con_id].prgl?.destroy();\n      delete this.prglConnections[con_id];\n    } else {\n      if (this.prglConnections[con_id].error) {\n        throw this.prglConnections[con_id].error;\n      }\n      return this.prglConnections[con_id].socket_path;\n    }\n  }\n\n  const con = await dbs.connections.findOne({ id: con_id }).catch((e) => {\n    console.error(\"Could not fetch connection\", e);\n    return undefined;\n  });\n  if (!con) throw \"Connection not found\";\n  const dbConf = await dbs.database_configs.findOne({\n    $existsJoined: { connections: { id: con.id } },\n  });\n  if (!dbConf) throw \"dbConf not found\";\n\n  const { connectionInfo, isSSLModeFallBack } = await testDBConnection(con);\n  log(\n    \"testDBConnection ok\" +\n      (isSSLModeFallBack ? \". (sslmode=prefer fallback)\" : \"\"),\n  );\n\n  const socket_path = getConnectionPaths(con).ws;\n\n  const creatingPref = \"connecting to \" + con.db_name;\n  try {\n    const prglInstance = this.prglConnections[con.id];\n    if (prglInstance) {\n      console.error(\"socket_path changed\");\n      if (prglInstance.socket_path !== socket_path) {\n        restartProc(() => {\n          socket?.emit(\"server-restart-request\", true);\n        });\n\n        if (prglInstance.prgl) {\n          log(\"disconnecting from \", Object.keys(prglInstance.con.db_name));\n          await prglInstance.prgl.destroy();\n        }\n      } else {\n        log(\"reusing \", Object.keys(prglInstance));\n        if (prglInstance.error) throw prglInstance.error;\n        return socket_path;\n      }\n    }\n    log(creatingPref);\n    this.prglConnections[con.id] = {\n      io: undefined,\n      socket_path,\n      con,\n      dbConf,\n      isReady: false,\n      connectionInfo,\n      methodRunner: undefined,\n      onMountRunner: undefined,\n      tableConfigRunner: undefined,\n      lastRestart: 0,\n      isSuperUser: undefined,\n      authSetupDataListener: undefined,\n    };\n  } catch (e) {\n    console.error(e);\n    throw e;\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n  return new Promise<string>(async (resolve, reject) => {\n    const global_settings = await dbs.global_settings.findOne();\n    if (!global_settings) {\n      throw new Error(\"global_settings not found\");\n    }\n    const _io = new Server(http, {\n      path: socket_path,\n      maxHttpBufferSize: 1e8,\n      cors: withOrigin,\n    });\n\n    try {\n      const hotReloadConfig = await getHotReloadConfigs(this, con, dbConf, dbs);\n      const watchSchema = con.db_watch_shema ? \"*\" : false;\n      const getForkedProcRunner = async () => {\n        if (!this.prglConnections[con.id]?.methodRunner) {\n          const methodRunner = await ForkedPrglProcRunner.create({\n            type: \"run\",\n            dbConfId: dbConf.id,\n            pass_process_env_vars_to_server_side_functions:\n              global_settings.pass_process_env_vars_to_server_side_functions,\n            dbs,\n            prglInitOpts: {\n              dbConnection: {\n                ...connectionInfo,\n                application_name: \"methodRunner\",\n              },\n              watchSchema,\n            },\n          });\n          this.prglConnections[con.id]!.methodRunner = methodRunner;\n        }\n        const forkedPrglProcRunner =\n          this.prglConnections[con.id]!.methodRunner!;\n        return forkedPrglProcRunner;\n      };\n      await this.setTableConfig(\n        con.id,\n        dbConf.table_config_ts,\n        dbConf.table_config_ts_disabled,\n      ).catch((e) => {\n        void dbs.alerts.insert({\n          severity: \"error\",\n          message: \"Table config was disabled due to error\",\n          database_config_id: dbConf.id,\n          connection_id: con.id,\n          section: \"table_config\",\n        });\n        void dbs.database_configs.update(\n          { id: dbConf.id },\n          { table_config_ts_disabled: true },\n        );\n      });\n      await this.setOnMount(\n        con.id,\n        con.on_mount_ts,\n        con.on_mount_ts_disabled,\n      ).catch((e) => {\n        void dbs.alerts.insert({\n          severity: \"error\",\n          message:\n            \"On mount was disabled due to error\" +\n            `\\n\\n${JSON.stringify(getErrorAsObject(e))}`,\n          database_config_id: dbConf.id,\n          connection_id: con.id,\n          section: \"methods\",\n        });\n        void dbs.connections.update(\n          { id: con.id },\n          { on_mount_ts_disabled: true },\n        );\n      });\n\n      const prgl = await prostgles({\n        dbConnection: connectionInfo,\n        io: _io,\n        ...hotReloadConfig,\n        auth: await getConnectionAuth(this.app, dbs, _dbs, getAuthSetupData()),\n        watchSchema,\n        disableRealtime: con.disable_realtime ?? undefined,\n        transactions: true,\n        joins: \"inferred\",\n        publish: getConnectionPublish({ dbs, dbConf, connection: con }),\n        publishMethods: getConnectionPublishMethods({\n          dbConf,\n          dbs,\n          con,\n          _dbs,\n          getForkedProcRunner,\n        }),\n        // DEBUG_MODE: true,\n        onConnectionError: (error) => {\n          const nonReconnectableErrorCodes = {\n            \"3D000\": \"Database does not exist\",\n            \"28P01\": \"Invalid authentication credentials\",\n          };\n          const errorCode = (error as AnyObject | undefined)?.code as string;\n          if (errorCode && errorCode in nonReconnectableErrorCodes) {\n            // void this.startConnection(con.id, dbs, _dbs, undefined, true);\n            void this.disconnect(con.id);\n          }\n        },\n        publishRawSQL: async ({ user }) => {\n          if (user?.type === \"admin\") {\n            return true;\n          }\n          const ac = await getAccessRule(dbs, user, dbConf.id, con.id);\n          if (\n            ac?.dbPermissions.type === \"Run SQL\" &&\n            ac.dbPermissions.allowSQL\n          ) {\n            return true;\n          }\n          return false;\n        },\n        onLog: (e) => {\n          addLog(e, con_id);\n        },\n        onReady: (params) => {\n          const { dbo: db, db: _db, reason, tables } = params;\n\n          const newAuthSetupDataListener = subscribeToAuthSetupChanges(\n            dbs,\n            async (authData) => {\n              const auth = await getConnectionAuth(\n                this.app,\n                dbs,\n                _dbs,\n                authData,\n              );\n              void prgl.update({\n                auth,\n              });\n            },\n            this.prglConnections[con.id]?.authSetupDataListener,\n          );\n          this.prglConnections[con.id]!.authSetupDataListener =\n            newAuthSetupDataListener;\n          if (this.prglConnections[con.id]) {\n            if (this.prglConnections[con.id]!.prgl) {\n              this.prglConnections[con.id]!.prgl!._db = _db;\n              this.prglConnections[con.id]!.prgl!.db = db;\n            }\n            this.prglConnections[con.id]!.lastRestart = Date.now();\n          }\n          if (reason.type !== \"prgl.restart\" && reason.type !== \"init\") {\n            this.onConnectionReload(con.id, dbConf.id);\n          }\n\n          void alertIfReferencedFileColumnsRemoved.bind(this)({\n            reason,\n            tables,\n            connId: con.id,\n            db: _db,\n          });\n\n          /**\n           * In some cases watchSchema does not work as expected (GRANT/REVOKE will not be observable to a less privileged db user)\n           */\n          const refreshSamedatabaseForOtherUsers = async () => {\n            const sameDbs = await dbs.connections.find({\n              \"id.<>\": con.id,\n              ...pickKeys(con, [\"db_host\", \"db_port\", \"db_name\"]),\n            });\n            sameDbs.forEach(({ id }) => {\n              if (this.prglConnections[id]) {\n                this.prglConnections[id].isReady = false;\n                void this.prglConnections[id].prgl?.restart();\n              }\n            });\n          };\n          //@ts-ignore\n          const isNotRecursive = reason.type !== \"prgl.restart\";\n          if (this.prglConnections[con.id]?.isReady && isNotRecursive) {\n            void refreshSamedatabaseForOtherUsers();\n          }\n\n          resolve(socket_path);\n          if (this.prglConnections[con.id]) {\n            this.prglConnections[con.id]!.isReady = true;\n          }\n          console.log(\"dbProj ready\", con.db_name);\n        },\n      });\n      this.prglConnections[con.id] = {\n        io: _io,\n        prgl,\n        dbConf,\n        connectionInfo,\n        socket_path,\n        con,\n        isReady: false,\n        methodRunner: undefined,\n        onMountRunner: this.prglConnections[con.id]?.onMountRunner,\n        tableConfigRunner: this.prglConnections[con.id]?.tableConfigRunner,\n        isSuperUser: await getIsSuperUser(prgl._db),\n        lastRestart: Date.now(),\n        authSetupDataListener:\n          this.prglConnections[con.id]?.authSetupDataListener,\n      };\n      void this.setSyncUserSub();\n    } catch (e) {\n      reject(e);\n      this.prglConnections[con.id] = {\n        io: _io,\n        error: e,\n        connectionInfo,\n        dbConf,\n        socket_path,\n        con,\n        isReady: false,\n        methodRunner: undefined,\n        onMountRunner: undefined,\n        tableConfigRunner: undefined,\n        lastRestart: 0,\n        isSuperUser: undefined,\n        authSetupDataListener: undefined,\n      };\n    }\n  });\n};\n\nexport const getAccessRule = async (\n  dbs: DBOFullyTyped<DBGeneratedSchema>,\n  user: User | undefined,\n  database_id: number,\n  connection_id: string,\n): Promise<DBSSchema[\"access_control\"] | undefined> => {\n  if (!user) return undefined;\n  return await dbs.access_control.findOne({\n    $and: [\n      {\n        database_id,\n        $existsJoined: {\n          access_control_user_types: {\n            user_type: user.type,\n          },\n        },\n      },\n      {\n        $existsJoined: {\n          access_control_connections: {\n            connection_id,\n          },\n        },\n      },\n    ],\n  });\n};\n\nconst getConnectionAuth = async (\n  app: e.Express,\n  dbs: DBS,\n  _dbs: DB,\n  authData: AuthSetupData,\n) => {\n  const auth = await getAuth(app, dbs, authData);\n  return {\n    sidKeyName: auth.sidKeyName,\n    getUser: (sid, __, _, cl, reqInfo) =>\n      auth.getUser(sid, dbs, _dbs, cl, reqInfo),\n    cacheSession: {\n      getSession: (sid) => auth.cacheSession.getSession(sid, dbs),\n    },\n  } satisfies AuthConfig<void, SUser>;\n};\n"
  },
  {
    "path": "server/src/Logger.ts",
    "content": "import type { EventInfo } from \"prostgles-server/dist/Logging\";\nimport type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport { pickKeys } from \"prostgles-types\";\nimport { type DBS } from \".\";\nimport { getAuthSetupData } from \"./authConfig/subscribeToAuthSetupChanges\";\n\nexport const loggerTableConfig: TableConfig<{ en: 1 }> = {\n  logs: {\n    columns: {\n      id: `BIGSERIAL PRIMARY KEY`,\n      connection_id: `UUID`,\n      type: \"TEXT\",\n      command: \"TEXT\",\n      table_name: \"TEXT\",\n      sid: \"TEXT\",\n      tx_info: \"JSONB\",\n      socket_id: \"TEXT\",\n      duration: \"NUMERIC\",\n      data: \"JSONB\",\n      error: \"JSON\",\n      has_error: \"BOOLEAN\",\n      created: \"TIMESTAMPTZ DEFAULT NOW()\",\n    },\n  },\n};\n\nlet loggerConfig:\n  | {\n      dbs: DBS;\n    }\n  | undefined;\nexport const setLoggerDBS = (dbs: DBS) => {\n  loggerConfig = { dbs };\n};\n\nconst shouldExclude = (e: EventInfo, isStateDb: boolean) => {\n  if (!getAuthSetupData().globalSettings?.enable_logs) return true;\n  if (\n    isStateDb &&\n    e.type === \"table\" &&\n    [\"logs\", \"windows\"].includes(e.tableName)\n  ) {\n    return true;\n  }\n  return false;\n};\n\nconst logRecords: {\n  e: EventInfo;\n  connection_id: string | null;\n  created: Date;\n}[] = [];\nconst isPlaywright = process.env.PLAYWRIGHT_TEST === \"true\";\n\nexport const addLog = (e: EventInfo, connection_id: string | null) => {\n  if (isPlaywright) {\n    console.log(\n      //@ts-ignore\n      e.command,\n      //@ts-ignore\n      e.table_name || e.tableName,\n      //@ts-ignore\n      e.filter || e.data?.filter || e.condition,\n      //@ts-ignore\n      e.channel_name,\n    );\n  }\n  if (shouldExclude(e, connection_id === null)) return;\n  logRecords.push({ e, connection_id, created: new Date() });\n  const batchSize = 20;\n  const { dbs } = loggerConfig ?? {};\n  if (dbs && logRecords.length > batchSize) {\n    const getSid = (e: EventInfo): string | null | undefined => {\n      if (e.type === \"table\" || e.type === \"sync\") {\n        const { clientReq } = e.localParams ?? {};\n        return (\n          clientReq?.socket ? clientReq.socket.__prglCache?.session.sid\n          : clientReq?.httpReq ?\n            (clientReq.httpReq.cookies as Record<string, string>)[\"sid\"]\n          : null\n        );\n      }\n      if (e.type === \"connect\") {\n        return e.sid;\n      }\n      if (e.type === \"disconnect\") {\n        return e.sid;\n      }\n      if (e.type === \"method\") {\n        return \"not implemented\";\n      }\n      return null;\n    };\n    const data =\n      (\n        e.type === \"sync\" &&\n        (e.command === \"pushData\" || e.command === \"upsertData\")\n      ) ?\n        pickKeys(e, [\"connectedSocketIds\", \"rows\"])\n      : e.type === \"connect\" || e.type === \"disconnect\" ?\n        pickKeys(e, [\"connectedSocketIds\"])\n      : e.type === \"method\" ? pickKeys(e, [\"args\"])\n      : undefined;\n    const batch = logRecords.splice(0, batchSize);\n    void dbs.logs.insert(\n      batch.map(({ connection_id, created, e }) => ({\n        connection_id,\n        created,\n        type: e.type,\n        command: \"command\" in e ? e.command : null,\n        table_name: \"tableName\" in e ? e.tableName : null,\n        sid: getSid(e),\n        tx_info: e.type === \"table\" ? e.txInfo : null,\n        error: \"error\" in e ? e.error : null,\n        duration: \"duration\" in e ? e.duration : null,\n        has_error: \"error\" in e && e.error !== undefined ? true : false,\n        data,\n      })),\n      {},\n      //@ts-ignore\n      { noLog: true },\n    );\n  }\n};\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/McpHub.ts",
    "content": "import { McpToolCallResponse } from \"@common/mcp\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport {\n  CallToolResultSchema,\n  ReadResourceResultSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { getSerialisableError, isEqual, tryCatchV2 } from \"prostgles-types\";\nimport {\n  connectToMCPServer,\n  type MCPServerInitInfo,\n} from \"./connectToMCPServer\";\nimport { fetchMCPResourcesList } from \"./fetchMCPResourcesList\";\nimport { fetchMCPResourceTemplatesList } from \"./fetchMCPResourceTemplatesList\";\nimport { fetchMCPToolsList } from \"./fetchMCPToolsList\";\nimport { McpResourceResponse, McpServer, ServersConfig } from \"./McpTypes\";\n\nexport type McpConnection = {\n  /**\n   * Actual MCP server name.\n   * server.name is concatenated with config id instance\n   */\n  server_name: string;\n  server: McpServer;\n  client: Client;\n  transport: StdioClientTransport | StreamableHTTPClientTransport;\n  destroy: () => Promise<void>;\n};\n\nexport class McpHub {\n  connections: Record<string, McpConnection> = {};\n  isConnecting = false;\n\n  constructor() {}\n\n  getClient = (serverName: string): Client | undefined => {\n    return Object.values(this.connections).find(\n      (conn) => conn.server_name === serverName,\n    )?.client;\n  };\n\n  getServers(): McpServer[] {\n    // Only return enabled servers\n    return Object.values(this.connections)\n      .filter((conn) => !conn.server.disabled)\n      .map((conn) => conn.server);\n  }\n\n  private async connectToServer(initInfo: MCPServerInitInfo): Promise<void> {\n    const { name } = initInfo;\n    delete this.connections[name];\n    const { data: connection, error } = await tryCatchV2(\n      async () => await connectToMCPServer(initInfo),\n    );\n    if (connection) {\n      connection.server.tools = await fetchMCPToolsList(connection.client);\n      connection.server.resources = await fetchMCPResourcesList(\n        connection.client,\n      );\n      connection.server.resourceTemplates = await fetchMCPResourceTemplatesList(\n        connection.client,\n      );\n      this.connections[name] = connection;\n    } else {\n      delete this.connections[name];\n      throw error;\n    }\n  }\n\n  private async destroyConnection(name: string): Promise<void> {\n    const connection = this.connections[name];\n    if (connection) {\n      delete this.connections[name];\n      await connection.destroy();\n    }\n  }\n\n  async setServerConnections(serversConfig: ServersConfig): Promise<void> {\n    this.isConnecting = true;\n    const currentNames = Object.keys(this.connections);\n    const newNames = new Set(Object.keys(serversConfig));\n\n    // Delete removed servers\n    for (const name of currentNames) {\n      if (!newNames.has(name)) {\n        await this.destroyConnection(name);\n        console.log(`Destroyed MCP server: ${name}`);\n      }\n    }\n\n    // Update or add servers\n    for (const [name, { onLog, ...config }] of Object.entries(serversConfig)) {\n      const currentConnection = this.connections[name];\n      const isRunningDifferentConfig =\n        currentConnection && !isEqual(currentConnection.server.config, config);\n      if (isRunningDifferentConfig) {\n        await this.destroyConnection(name);\n      }\n\n      if (!currentConnection || isRunningDifferentConfig) {\n        try {\n          const eventOptions = {\n            onLog,\n            onTransportClose: () => {\n              delete this.connections[name];\n            },\n          };\n          await this.connectToServer({\n            name,\n            config,\n            server_name: config.server_name,\n            ...eventOptions,\n          });\n        } catch (error) {\n          void onLog(\"error\", JSON.stringify(getSerialisableError(error)), \"\");\n          if (isRunningDifferentConfig) {\n            console.error(\n              `Failed to connect to new MCP server ${name}:`,\n              error,\n            );\n          } else {\n            console.error(`Failed to reconnect MCP server ${name}:`, error);\n          }\n        }\n      }\n    }\n    this.isConnecting = false;\n  }\n\n  async readResource(\n    serverName: string,\n    uri: string,\n  ): Promise<McpResourceResponse> {\n    const connection = this.connections[serverName];\n    if (!connection) {\n      throw new Error(\n        `No connection found for MCP server: ${serverName}. Make sure it is enabled`,\n      );\n    }\n    if (connection.server.disabled) {\n      throw new Error(`Server \"${serverName}\" is disabled`);\n    }\n    return await connection.client.request(\n      {\n        method: \"resources/read\",\n        params: {\n          uri,\n        },\n      },\n      ReadResourceResultSchema,\n    );\n  }\n\n  async callTool(\n    serverName: string,\n    toolName: string,\n    toolArguments?: Record<string, unknown>,\n  ): Promise<McpToolCallResponse> {\n    const connection = this.connections[serverName];\n    if (!connection) {\n      throw new Error(\n        `No connection found for MCP server: ${serverName}. Please make sure it is enabled`,\n      );\n    }\n\n    if (connection.server.disabled) {\n      throw new Error(\n        `MCP Server \"${serverName}\" is disabled and cannot be used`,\n      );\n    }\n\n    const toolResult = await connection.client.request(\n      {\n        method: \"tools/call\",\n        params: {\n          name: toolName,\n          arguments: toolArguments,\n        },\n      },\n      CallToolResultSchema,\n    );\n    return toolResult;\n  }\n\n  async destroy(): Promise<void> {\n    for (const connection of Object.values(this.connections)) {\n      try {\n        await this.destroyConnection(connection.server.name);\n      } catch (error) {\n        console.error(\n          `Failed to close connection for ${connection.server.name}:`,\n          error,\n        );\n      }\n    }\n    this.connections = {};\n  }\n}\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/McpTypes.ts",
    "content": "import type { StdioServerParameters } from \"@modelcontextprotocol/sdk/client/stdio\";\nimport type { ListToolsResult } from \"@modelcontextprotocol/sdk/types\";\n\nexport type McpMode = \"full\" | \"server-use-only\" | \"off\";\n\nexport type McpServer = {\n  name: string;\n  config: StdioServerParameters;\n  status: \"connected\" | \"connecting\" | \"disconnected\";\n  error?: string;\n  tools?: McpTool[];\n  resources?: McpResource[];\n  resourceTemplates?: McpResourceTemplate[];\n  disabled?: boolean;\n};\n\nexport type McpTool = ListToolsResult[\"tools\"][number];\n\nexport type McpResource = {\n  uri: string;\n  name: string;\n  mimeType?: string;\n  description?: string;\n};\n\nexport type McpResourceTemplate = {\n  uriTemplate: string;\n  name: string;\n  description?: string;\n  mimeType?: string;\n};\n\nexport type McpResourceResponse = {\n  _meta?: Record<string, any>;\n  contents: Array<{\n    uri: string;\n    mimeType?: string;\n    text?: string;\n    blob?: string;\n  }>;\n};\n\nexport interface McpMarketplaceItem {\n  mcpId: string;\n  githubUrl: string;\n  name: string;\n  author: string;\n  description: string;\n  codiconIcon: string;\n  logoUrl: string;\n  category: string;\n  tags: string[];\n  requiresApiKey: boolean;\n  readmeContent?: string;\n  llmsInstallationContent?: string;\n  isRecommended: boolean;\n  githubStars: number;\n  downloadCount: number;\n  createdAt: string;\n  updatedAt: string;\n  lastGithubSync: string;\n}\n\nexport interface McpMarketplaceCatalog {\n  items: McpMarketplaceItem[];\n}\n\nexport interface McpDownloadResponse {\n  mcpId: string;\n  githubUrl: string;\n  name: string;\n  author: string;\n  description: string;\n  readmeContent: string;\n  llmsInstallationContent: string;\n  requiresApiKey: boolean;\n}\n\nexport type McpServerEvents = {\n  onLog: (\n    type: \"stderr\" | \"error\",\n    data: string,\n    fullLog: string,\n  ) => void | Promise<void>;\n  onTransportClose: () => void;\n};\n\nexport type McpConfigWithEvents = StdioServerParameters & McpServerEvents;\n\nexport type ServersConfig = Record<\n  string,\n  Omit<McpConfigWithEvents, \"onTransportClose\"> & {\n    server_name: string;\n  }\n>;\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/connectToMCPServer.ts",
    "content": "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport {\n  StdioClientTransport,\n  type StdioServerParameters,\n} from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport { getSerialisableError } from \"prostgles-types\";\nimport { z } from \"zod\";\nimport { tout } from \"../..\";\nimport type { McpConnection } from \"./McpHub\";\nimport type { McpServerEvents } from \"./McpTypes\";\n\nconst AutoApproveSchema = z.array(z.string()).default([]);\n\nconst StdioConfigSchema = z.object({\n  command: z.string(),\n  args: z.array(z.string()).optional(),\n  env: z.record(z.string(), z.string()).optional(),\n  autoApprove: AutoApproveSchema.optional(),\n  disabled: z.boolean().optional(),\n});\n\nexport type MCPServerInitInfo = McpServerEvents & {\n  name: string;\n  server_name: string;\n  config: StdioServerParameters;\n};\n\nexport const connectToMCPServer = (\n  { name, server_name, config, onLog, onTransportClose }: MCPServerInitInfo,\n  // { onLog, onTransportClose }: McpServerEvents,\n): Promise<McpConnection> => {\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n  return new Promise(async (resolve, reject) => {\n    let log = \"\";\n    try {\n      const parsedConfig = StdioConfigSchema.safeParse(config);\n      if (!parsedConfig.success)\n        throw new Error(\n          parsedConfig.error.issues.map((e) => e.message).join(\"\\n\"),\n        );\n      /** Clear previous logs and errors */\n      await onLog(\"stderr\", \"\", log);\n      await onLog(\"error\", \"\", log);\n\n      // Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling.\n      // Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.\n      const client = new Client(\n        {\n          name: \"Prostgles\",\n          version: \"1.0.0\",\n        },\n        {\n          capabilities: {},\n        },\n      );\n\n      const transport = new StdioClientTransport({\n        command: config.command,\n        args: config.args,\n        env: {\n          ...config.env,\n          ...(process.env.PATH ? { PATH: process.env.PATH } : {}),\n          // ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),\n        },\n        cwd: config.cwd,\n        stderr: \"pipe\", // necessary for stderr to be available\n      });\n      // transport.onmessage = (message) => {\n      //   console.log(`MCP Server ${name} message:`, message);\n      // };\n\n      transport.onerror = (error) => {\n        const errMsg = `Transport error: ${error.message}`;\n        log += errMsg;\n        void onLog(\"error\", errMsg, log);\n        reject(errMsg);\n      };\n      transport.onclose = () => {\n        onTransportClose();\n        reject(new Error(`Transport closed`));\n      };\n\n      const connection: McpConnection = {\n        server_name,\n        server: {\n          name,\n          config,\n          status: \"connecting\",\n          disabled: parsedConfig.data.disabled,\n        },\n        client,\n        transport,\n        destroy: async () => {\n          try {\n            await transport.close();\n            await client.close();\n          } catch (error) {\n            console.error(`Failed to close transport for ${name}:`, error);\n          }\n        },\n      };\n\n      // transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.\n      // As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.\n      await transport.start();\n      const stderrStream = transport.stderr;\n      if (stderrStream) {\n        stderrStream.on(\"data\", (data: Buffer) => {\n          const errorOutput = data.toString();\n          log += errorOutput;\n          void onLog(\"stderr\", errorOutput, log);\n        });\n      } else {\n        console.error(`No stderr stream for ${name}`);\n      }\n      transport.start = async () => {}; // No-op now, .connect() won't fail\n\n      // Connect\n      await client.connect(transport).catch(async (error) => {\n        await tout(1000); // wait for connection to be established\n        return Promise.reject(error);\n      });\n      connection.server.status = \"connected\";\n      connection.server.error = \"\";\n      resolve(connection);\n    } catch (error) {\n      // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors\n      reject({ error: getSerialisableError(error), log });\n    }\n  });\n};\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/fetchMCPResourceTemplatesList.ts",
    "content": "import type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { ListResourceTemplatesResultSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport type { McpResourceTemplate } from \"./McpTypes\";\n\nexport const fetchMCPResourceTemplatesList = async (\n  client: Client,\n): Promise<McpResourceTemplate[]> => {\n  try {\n    const response = await client.request(\n      { method: \"resources/templates/list\" },\n      ListResourceTemplatesResultSchema,\n    );\n    return response.resourceTemplates;\n  } catch (_error) {\n    return [];\n  }\n};\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/fetchMCPResourcesList.ts",
    "content": "import type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport type { McpResource } from \"./McpTypes\";\nimport { ListResourcesResultSchema } from \"@modelcontextprotocol/sdk/types.js\";\n\nexport const fetchMCPResourcesList = async (\n  client: Client,\n): Promise<McpResource[]> => {\n  try {\n    const response = await client.request(\n      { method: \"resources/list\" },\n      ListResourcesResultSchema,\n    );\n    return response.resources;\n  } catch (_error) {\n    /** Error ignored because for some reason servers withour resources trigger a McpError: MCP error -32601: Method not found */\n    return [];\n  }\n};\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/fetchMCPToolsList.ts",
    "content": "import type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { ListToolsResultSchema } from \"@modelcontextprotocol/sdk/types.js\";\n\nexport const fetchMCPToolsList = async (client: Client) => {\n  try {\n    const response = await client.request(\n      { method: \"tools/list\" },\n      ListToolsResultSchema,\n    );\n\n    const autoApproveConfig: string[] = [];\n    const tools = response.tools.map((tool) => ({\n      ...tool,\n      description: tool.description ?? \"\",\n      autoApprove: autoApproveConfig.includes(tool.name),\n    }));\n    return tools;\n  } catch (_error) {\n    return [];\n  }\n};\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/installMCPServer.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { simpleGit, type SimpleGit } from \"simple-git\";\nimport type { DBS } from \"../..\";\nimport { getEntries } from \"@common/utils\";\nimport { getRootDir } from \"../../electronConfig\";\nimport { runShellCommand } from \"./runShellCommand\";\n\nlet createdMCPDirectory = \"\";\nexport const getMCPDirectory = () => {\n  if (!createdMCPDirectory) {\n    const MCP_DIR = path.resolve(path.join(getRootDir(), `/prostgles_mcp`));\n    fs.mkdirSync(MCP_DIR, { recursive: true });\n    createdMCPDirectory = MCP_DIR;\n  }\n  return createdMCPDirectory;\n};\n\nconst getMCPServerInstallationPath = (name: string) => {\n  const installationPath = path.join(getMCPDirectory(), name);\n  return installationPath;\n};\n\nexport const installMCPServer = async (dbs: DBS, name: string) => {\n  const serverInfo = await dbs.mcp_servers.findOne({ name });\n  if (!serverInfo?.source) {\n    const allServers = await dbs.mcp_servers.find(\n      {},\n      { select: { name: 1 }, returnType: \"values\" },\n    );\n    throw (\n      \"Server not found. Available servers: \" +\n      Object.keys(allServers).join(\", \")\n    );\n  }\n  const { source } = serverInfo;\n\n  const logTypeFilter = { server_name: name };\n  await dbs.mcp_server_logs.delete(logTypeFilter);\n  let log = \"Installing MCP server\";\n  let firstError: string | undefined;\n  const addLog = (logChunk: string, error?: string) => {\n    firstError ??= error;\n    log += `\\n${logChunk}`;\n    return dbs.mcp_server_logs.update(logTypeFilter, {\n      install_log: log,\n      install_error: error,\n      last_updated: new Date(),\n    });\n  };\n\n  try {\n    await dbs.mcp_server_logs.insert({\n      ...logTypeFilter,\n      log,\n    });\n\n    await addLog(\"Creating MCP servers folder...\");\n    if (source.type === \"github\") {\n      throw new Error(\"Not implemented\");\n      // await addLog(\"Cloning MCP servers...\");\n      // const res1 = await runShellCommand(\n      //   \"git\",\n      //   [\"clone\", source.repoUrl],\n      //   { cwd: MCP_DIR },\n      //   (chunk) => addLog(chunk),\n      // );\n      // if (res1.err) {\n      //   return addLog(\"Failed to clone MCP servers.\", true, res1.err);\n      // }\n      // await addLog(\"MCP servers cloned\\nInstalling MCP servers...\");\n\n      // const res2 = await runShellCommand(\n      //   \"npm\",\n      //   [\"install\"],\n      //   { cwd: MCP_DIR },\n      //   (chunk) => addLog(chunk),\n      // );\n      // if (res2.err) {\n      //   return addLog(\"Failed to install MCP servers\", true, res2.err);\n      // }\n    } else {\n      const installationPath = getMCPServerInstallationPath(name);\n      if (fs.existsSync(installationPath)) {\n        await addLog(\"MCP servers already installed. Reinstalling...\");\n        fs.rmSync(installationPath, { recursive: true });\n      }\n      fs.mkdirSync(installationPath, { recursive: true });\n      const { packageJson, tsconfigJson, files } = source;\n      for (const [fileName, content] of getEntries({\n        \"package.json\": packageJson,\n        \"tsconfig.json\": tsconfigJson,\n      })) {\n        const destination = path.join(installationPath, fileName);\n        fs.writeFileSync(destination, content, \"utf-8\");\n      }\n      for (const [fileName, content] of getEntries(files)) {\n        const destination = path.join(installationPath, \"src\", fileName);\n        fs.mkdirSync(path.join(installationPath, \"src\"), { recursive: true });\n        fs.writeFileSync(destination, content, \"utf-8\");\n      }\n      const npmI = await runShellCommand(\n        \"npm\",\n        [\"install\", \"--include=dev\"],\n        { cwd: installationPath },\n        (chunk) => {\n          void addLog(chunk);\n        },\n      );\n      if (npmI.err) {\n        await addLog(\n          \"Failed to install MCP server\",\n          npmI.err instanceof Error ? npmI.err.message : npmI.err,\n        );\n      } else {\n        await addLog(npmI.fullLog);\n      }\n    }\n  } catch (error) {\n    await addLog(\n      \"Failed to install MCP server\",\n      error instanceof Error ? error.message : String(error),\n    );\n  }\n\n  if (firstError === undefined) {\n    await dbs.mcp_servers.update(\n      {\n        name,\n      },\n      {\n        installed: new Date(),\n      },\n    );\n    await addLog(`MCP server ${JSON.stringify(name)} installed`);\n  }\n};\n\nexport const getMCPServersStatus = async (\n  dbs: DBS,\n  serverName: string,\n): Promise<{\n  ok: boolean;\n  message?: string;\n}> => {\n  const folderExists = fs.existsSync(getMCPDirectory());\n  if (!folderExists) {\n    return { ok: false, message: \"No MCP servers installed\" };\n  }\n  const serverInfo = await dbs.mcp_servers.findOne({ name: serverName });\n  if (!serverInfo) {\n    throw new Error(\"Server not found\");\n  }\n  const source = serverInfo.source;\n  if (!source) {\n    return { ok: false, message: `Installation not required` };\n  }\n  const server = await dbs.mcp_servers.findOne({ name: serverName });\n  const cwd = server?.cwd;\n  if (!cwd) return { ok: false, message: `Server cwd missing` };\n\n  const installationPath = getMCPServerInstallationPath(serverName);\n  if (source.type === \"github\") {\n    const git: SimpleGit = simpleGit(installationPath);\n    const status = await git.status();\n\n    return status.behind > 0 ?\n        {\n          ok: false,\n          message: `Repository is behind by ${status.behind} commits`,\n        }\n      : {\n          ok: true,\n          message: `Repository is up to date`,\n        };\n  }\n\n  const wasInstalled = fs.existsSync(installationPath);\n\n  return wasInstalled ?\n      { ok: true }\n    : { ok: false, message: `Server not installed` };\n};\n\nexport const getMcpHostInfo = async () => {\n  const platform = process.platform;\n  const os = platform === \"win32\" ? \"windows\" : platform;\n  const npmVersion = await runShellCommand(\"npm\", [\"--version\"], {}, () => {});\n  const uvxVersion = await runShellCommand(\"git\", [\"--version\"], {}, () => {});\n  return {\n    os,\n    npmVersion: npmVersion.fullLog,\n    uvxVersion: uvxVersion.fullLog,\n  };\n};\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/runShellCommand.ts",
    "content": "import { spawn, type SpawnOptions } from \"child_process\";\n\nexport const runShellCommand = (\n  command: string,\n  args: ReadonlyArray<string>,\n  opts: SpawnOptions,\n  onData: (data: string, source: \"stdout\" | \"stderr\") => void,\n): Promise<{\n  err: Error | string | undefined;\n  fullLog: string;\n  code?: number;\n}> => {\n  const proc = spawn(command, args, opts);\n  const getUTFText = (v: string) => v.toString();\n\n  let fullLog = \"\";\n  let log: string;\n  proc.stderr!.on(\"data\", (data) => {\n    log = getUTFText(data);\n    fullLog += log;\n    onData(getUTFText(data), \"stderr\");\n  });\n  proc.stdout!.on(\"data\", (data) => {\n    log = getUTFText(data);\n    fullLog += log;\n    onData(getUTFText(data), \"stdout\");\n  });\n\n  return new Promise((resolve) => {\n    proc.stdout!.on(\"error\", function (err) {\n      resolve({ err, fullLog });\n    });\n    proc.stdin!.on(\"error\", function (err) {\n      resolve({ err, fullLog });\n    });\n    proc.on(\"error\", function (err) {\n      resolve({ err, fullLog });\n    });\n\n    proc.on(\"exit\", function (code, signal) {\n      if (code) {\n        // console.error({\n        //   code,\n        //   signal,\n        //   logs: fullLog.slice(fullLog.length - 1100),\n        // });\n        resolve({ err: log || \"Unknow error\", code, fullLog });\n      } else {\n        resolve({ err: undefined, fullLog });\n      }\n    });\n  });\n};\n"
  },
  {
    "path": "server/src/McpHub/AnthropicMcpHub/startMcpHub.ts",
    "content": "import type { DBS } from \"@src/index\";\nimport { McpHub } from \"./McpHub\";\nimport { fetchMCPServerConfigs } from \"../fetchMCPServerConfigs\";\nimport { updateMcpServerTools } from \"../reloadMcpServerTools\";\nimport type { SubscriptionHandler } from \"prostgles-types\";\nimport { insertServerList } from \"../insertServerList\";\nimport type { DBSSchema } from \"@common/publishUtils\";\n\nconst mcpHub = new McpHub();\n\nlet mcpHubInitPromise: Promise<McpHub> | undefined;\nexport const startMcpHub = async (\n  dbs: DBS,\n  restart = false,\n): Promise<McpHub> => {\n  if (mcpHubInitPromise) {\n    await mcpHubInitPromise;\n    if (!restart) {\n      const res = await mcpHubInitPromise;\n      return res;\n    } else {\n      mcpHubInitPromise = undefined;\n    }\n  }\n\n  mcpHubInitPromise = (async () => {\n    if (restart) {\n      await mcpHub.destroy();\n    }\n    const serversConfig = await fetchMCPServerConfigs(dbs);\n    const serverNames = Array.from(\n      new Set(Object.values(serversConfig).map((s) => s.server_name)),\n    );\n    await mcpHub.setServerConnections(serversConfig);\n    if (serverNames.length) {\n      const serverNamesWithConfig = Object.keys(serversConfig);\n      console.log(\n        `McpHub started. Enabled servers (${serverNamesWithConfig.length}): ${serverNamesWithConfig.join()}`,\n      );\n    }\n    return mcpHub;\n  })();\n\n  return mcpHubInitPromise;\n};\n\nconst loadMissingTools = async (\n  dbs: DBS,\n  mcpHub: McpHub,\n  enabledMcpServers: DBSSchema[\"mcp_servers\"][],\n) => {\n  for (const { name: server_name } of enabledMcpServers) {\n    const toolCount = await dbs.mcp_server_tools.count({\n      server_name,\n    });\n    if (!toolCount) {\n      await updateMcpServerTools(dbs, server_name, mcpHub);\n    }\n  }\n};\n\nconst mcpSubscriptions: Record<string, SubscriptionHandler | undefined> = {\n  globalSettings: undefined,\n  servers: undefined,\n};\n\nexport const setupMCPServerHub = async (dbs: DBS) => {\n  await insertServerList(dbs);\n  for (const sub of Object.values(mcpSubscriptions)) {\n    await sub?.unsubscribe();\n  }\n\n  let enabledMcpServers: DBSSchema[\"mcp_servers\"][] | undefined;\n  let globalSettings: DBSSchema[\"global_settings\"] | undefined;\n  const onCallback = () => {\n    if (enabledMcpServers && globalSettings) {\n      void startMcpHub(dbs, true).then(async (mcpHub) => {\n        if (!enabledMcpServers) {\n          throw new Error(\"enabledMcpServers is undefined\");\n        }\n        await loadMissingTools(dbs, mcpHub, enabledMcpServers);\n      });\n    }\n  };\n\n  mcpSubscriptions.servers = await dbs.mcp_servers.subscribe(\n    { enabled: true },\n    { select: { \"*\": 1, mcp_server_configs: \"*\" } },\n    (servers) => {\n      enabledMcpServers = servers;\n      onCallback();\n    },\n  );\n  mcpSubscriptions.globalSettings = await dbs.global_settings.subscribeOne(\n    {},\n    { limit: 1 },\n    (settings) => {\n      globalSettings = settings;\n      onCallback();\n    },\n  );\n};\n"
  },
  {
    "path": "server/src/McpHub/DefaultMCPServers/DefaultMCPServers.ts",
    "content": "import type { DEFAULT_MCP_SERVER_NAMES, MCPServerInfo } from \"@common/mcp\";\nimport { mcpGithub } from \"./mcpGithub\";\nimport { ProstglesMCPServers } from \"../ProstglesMcpHub/ProstglesMCPServers\";\nimport { fromEntries, getEntries } from \"@common/utils\";\n\nexport const getDefaultMCPServers = (): Record<\n  (typeof DEFAULT_MCP_SERVER_NAMES)[number],\n  MCPServerInfo\n> => ({\n  filesystem: {\n    icon_path: \"FolderOutline\",\n    command: \"npx\",\n    args: [\n      \"-y\",\n      \"@modelcontextprotocol/server-filesystem\",\n      \"${dir:/path/to/other/allowed/dir}\",\n    ],\n    config_schema: {\n      allowedDir: {\n        title: \"Allowed Directory\",\n        description: \"Directory path to allow access to\",\n        type: \"arg\",\n        renderWithComponent: \"FileBrowser\",\n      },\n    },\n  },\n  fetch: {\n    icon_path: \"Web\",\n    command: \"uvx\",\n    args: [\"mcp-server-fetch\"],\n  },\n  git: {\n    icon_path: \"Git\",\n    command: \"uvx\",\n    args: [\"mcp-server-git\", \"--repository\", \"${path:path/to/git/repo}\"],\n    config_schema: {\n      repoPath: {\n        title: \"Repository Path\",\n        description: \"Path to the git repository\",\n        type: \"arg\",\n      },\n    },\n  },\n  github: mcpGithub,\n  \"google-maps\": {\n    icon_path: \"GoogleMaps\",\n    command: \"npx\",\n    args: [\"-y\", \"@modelcontextprotocol/server-google-maps\"],\n    env: {\n      GOOGLE_MAPS_API_KEY: \"<YOUR_API_KEY>\",\n    },\n    config_schema: {\n      GOOGLE_MAPS_API_KEY: {\n        title: \"Google Maps API Key\",\n        description: \"API key for Google Maps\",\n        type: \"env\",\n      },\n    },\n  },\n  memory: {\n    command: \"npx\",\n    args: [\"-y\", \"@modelcontextprotocol/server-memory\"],\n  },\n  playwright: {\n    icon_path: \"Web\",\n    command: \"npx\",\n    args: [\"@playwright/mcp@latest\"],\n  },\n  slack: {\n    icon_path: \"Slack\",\n    command: \"npx\",\n    args: [\"-y\", \"@modelcontextprotocol/server-slack\"],\n    env: {\n      SLACK_BOT_TOKEN: \"xoxb-your-bot-token\",\n      SLACK_TEAM_ID: \"T01234567\",\n    },\n    config_schema: {\n      SLACK_BOT_TOKEN: {\n        title: \"Slack Bot Token\",\n        description: \"Bot token for Slack\",\n        type: \"env\",\n      },\n      SLACK_TEAM_ID: {\n        title: \"Slack Team ID\",\n        description: \"Team ID for Slack\",\n        type: \"env\",\n      },\n    },\n  },\n  ...fromEntries(\n    getEntries(ProstglesMCPServers).map(\n      ([\n        serverName,\n        {\n          definition: { icon_path, tools },\n        },\n      ]) => [\n        serverName,\n        {\n          command: \"prostgles-local\",\n          config_schema: undefined,\n          icon_path,\n          mcp_server_tools: getEntries(tools).map(\n            ([name, { schema, description }]) => ({\n              name,\n              description,\n              inputSchema: schema,\n            }),\n          ),\n        } satisfies MCPServerInfo,\n      ],\n    ),\n  ),\n});\n"
  },
  {
    "path": "server/src/McpHub/DefaultMCPServers/mcpGithub.ts",
    "content": "import type { MCPServerInfo } from \"@common/mcp\";\n\nexport const mcpGithub: MCPServerInfo = {\n  icon_path: \"Github\",\n  command: \"docker\",\n  args: [\n    \"run\",\n    \"-i\",\n    \"--rm\",\n    \"-e\",\n    \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n    \"ghcr.io/github/github-mcp-server\",\n  ],\n  config_schema: {\n    GITHUB_PERSONAL_ACCESS_TOKEN: {\n      type: \"env\",\n      title: \"GitHub Personal Access Token\",\n      description:\n        \"GitHub Personal Access Token to access private repositories. https://github.com/settings/personal-access-tokens\",\n      optional: false,\n    },\n  },\n  mcp_server_tools: [\n    {\n      name: \"add_issue_comment\",\n      description: \"Add a comment to an existing issue\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"issue_number\", \"body\"],\n        properties: {\n          body: {\n            type: \"string\",\n            description: \"Comment text\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          issue_number: {\n            type: \"number\",\n            description: \"Issue number to comment on\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"create_branch\",\n      description: \"Create a new branch in a GitHub repository\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"branch\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          branch: {\n            type: \"string\",\n            description: \"Name for new branch\",\n          },\n          from_branch: {\n            type: \"string\",\n            description: \"Source branch (defaults to repo default)\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"create_issue\",\n      description: \"Create a new issue in a GitHub repository\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"title\"],\n        properties: {\n          body: {\n            type: \"string\",\n            description: \"Issue body content\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          title: {\n            type: \"string\",\n            description: \"Issue title\",\n          },\n          labels: {\n            type: \"array\",\n            items: {\n              type: \"string\",\n            },\n            description: \"Labels to apply to this issue\",\n          },\n          assignees: {\n            type: \"array\",\n            items: {\n              type: \"string\",\n            },\n            description: \"Usernames to assign to this issue\",\n          },\n          milestone: {\n            type: \"number\",\n            description: \"Milestone number\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"create_or_update_file\",\n      description: \"Create or update a single file in a GitHub repository\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"path\", \"content\", \"message\", \"branch\"],\n        properties: {\n          sha: {\n            type: \"string\",\n            description: \"SHA of file being replaced (for updates)\",\n          },\n          path: {\n            type: \"string\",\n            description: \"Path where to create/update the file\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner (username or organization)\",\n          },\n          branch: {\n            type: \"string\",\n            description: \"Branch to create/update the file in\",\n          },\n          content: {\n            type: \"string\",\n            description: \"Content of the file\",\n          },\n          message: {\n            type: \"string\",\n            description: \"Commit message\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"create_pull_request\",\n      description: \"Create a new pull request in a GitHub repository\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"title\", \"head\", \"base\"],\n        properties: {\n          base: {\n            type: \"string\",\n            description: \"Branch to merge into\",\n          },\n          body: {\n            type: \"string\",\n            description: \"PR description\",\n          },\n          head: {\n            type: \"string\",\n            description: \"Branch containing changes\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          draft: {\n            type: \"boolean\",\n            description: \"Create as draft PR\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          title: {\n            type: \"string\",\n            description: \"PR title\",\n          },\n          maintainer_can_modify: {\n            type: \"boolean\",\n            description: \"Allow maintainer edits\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"create_pull_request_review\",\n      description: \"Create a review on a pull request\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"pullNumber\", \"event\"],\n        properties: {\n          body: {\n            type: \"string\",\n            description: \"Review comment text\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          event: {\n            type: \"string\",\n            description:\n              \"Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT')\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          comments: {\n            type: \"array\",\n            items: {\n              type: \"object\",\n              required: [\"path\", \"position\", \"body\"],\n              properties: {\n                body: {\n                  type: \"string\",\n                  description: \"comment body\",\n                },\n                path: {\n                  type: \"string\",\n                  description: \"path to the file\",\n                },\n                position: {\n                  type: \"number\",\n                  description: \"line number in the file\",\n                },\n              },\n              additionalProperties: false,\n            },\n            description:\n              \"Line-specific comments array of objects, each object with path (string), position (number), and body (string)\",\n          },\n          commitId: {\n            type: \"string\",\n            description: \"SHA of commit to review\",\n          },\n          pullNumber: {\n            type: \"number\",\n            description: \"Pull request number\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"create_repository\",\n      description: \"Create a new GitHub repository in your account\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"name\"],\n        properties: {\n          name: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          private: {\n            type: \"boolean\",\n            description: \"Whether repo should be private\",\n          },\n          autoInit: {\n            type: \"boolean\",\n            description: \"Initialize with README\",\n          },\n          description: {\n            type: \"string\",\n            description: \"Repository description\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"fork_repository\",\n      description:\n        \"Fork a GitHub repository to your account or specified organization\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          organization: {\n            type: \"string\",\n            description: \"Organization to fork to\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_code_scanning_alert\",\n      description:\n        \"Get details of a specific code scanning alert in a GitHub repository.\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"alertNumber\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"The name of the repository.\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"The owner of the repository.\",\n          },\n          alertNumber: {\n            type: \"number\",\n            description: \"The number of the alert.\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_file_contents\",\n      description:\n        \"Get the contents of a file or directory from a GitHub repository\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"path\"],\n        properties: {\n          path: {\n            type: \"string\",\n            description: \"Path to file/directory\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner (username or organization)\",\n          },\n          branch: {\n            type: \"string\",\n            description: \"Branch to get contents from\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_issue\",\n      description: \"Get details of a specific issue in a GitHub repository.\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"issue_number\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"The name of the repository.\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"The owner of the repository.\",\n          },\n          issue_number: {\n            type: \"number\",\n            description: \"The number of the issue.\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_me\",\n      description:\n        'Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...',\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          reason: {\n            type: \"string\",\n            description: \"Optional: reason the session was created\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_pull_request\",\n      description: \"Get details of a specific pull request\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"pullNumber\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          pullNumber: {\n            type: \"number\",\n            description: \"Pull request number\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_pull_request_comments\",\n      description: \"Get the review comments on a pull request\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"pullNumber\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          pullNumber: {\n            type: \"number\",\n            description: \"Pull request number\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_pull_request_files\",\n      description: \"Get the list of files changed in a pull request\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"pullNumber\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          pullNumber: {\n            type: \"number\",\n            description: \"Pull request number\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_pull_request_reviews\",\n      description: \"Get the reviews on a pull request\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"pullNumber\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          pullNumber: {\n            type: \"number\",\n            description: \"Pull request number\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"get_pull_request_status\",\n      description:\n        \"Get the combined status of all status checks for a pull request\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"pullNumber\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          pullNumber: {\n            type: \"number\",\n            description: \"Pull request number\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"list_code_scanning_alerts\",\n      description: \"List code scanning alerts in a GitHub repository.\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\"],\n        properties: {\n          ref: {\n            type: \"string\",\n            description: \"The Git reference for the results you want to list.\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"The name of the repository.\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"The owner of the repository.\",\n          },\n          state: {\n            type: \"string\",\n            default: \"open\",\n            description:\n              \"State of the code scanning alerts to list. Set to closed to list only closed code scanning alerts. Default: open\",\n          },\n          severity: {\n            type: \"string\",\n            description:\n              \"Only code scanning alerts with this severity will be returned. Possible values are: critical, high, medium, low, warning, note, error.\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"list_commits\",\n      description: \"Get list of commits of a branch in a GitHub repository\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\"],\n        properties: {\n          sha: {\n            type: \"string\",\n            description: \"Branch name\",\n          },\n          page: {\n            type: \"number\",\n            description: \"Page number\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          perPage: {\n            type: \"number\",\n            description: \"Number of records per page\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"list_issues\",\n      description: \"List issues in a GitHub repository with filtering options\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\"],\n        properties: {\n          page: {\n            type: \"number\",\n            description: \"Page number\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          sort: {\n            enum: [\"created\", \"updated\", \"comments\"],\n            type: \"string\",\n            description: \"Sort by ('created', 'updated', 'comments')\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          since: {\n            type: \"string\",\n            description: \"Filter by date (ISO 8601 timestamp)\",\n          },\n          state: {\n            enum: [\"open\", \"closed\", \"all\"],\n            type: \"string\",\n            description: \"Filter by state ('open', 'closed', 'all')\",\n          },\n          labels: {\n            type: \"array\",\n            items: {\n              type: \"string\",\n            },\n            description: \"Filter by labels\",\n          },\n          per_page: {\n            type: \"number\",\n            description: \"Results per page\",\n          },\n          direction: {\n            enum: [\"asc\", \"desc\"],\n            type: \"string\",\n            description: \"Sort direction ('asc', 'desc')\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"list_pull_requests\",\n      description: \"List and filter repository pull requests\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\"],\n        properties: {\n          base: {\n            type: \"string\",\n            description: \"Filter by base branch\",\n          },\n          head: {\n            type: \"string\",\n            description: \"Filter by head user/org and branch\",\n          },\n          page: {\n            type: \"number\",\n            description: \"Page number\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          sort: {\n            type: \"string\",\n            description:\n              \"Sort by ('created', 'updated', 'popularity', 'long-running')\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          state: {\n            type: \"string\",\n            description: \"Filter by state ('open', 'closed', 'all')\",\n          },\n          per_page: {\n            type: \"number\",\n            description: \"Results per page (max 100)\",\n          },\n          direction: {\n            type: \"string\",\n            description: \"Sort direction ('asc', 'desc')\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"merge_pull_request\",\n      description: \"Merge a pull request\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"pullNumber\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          pullNumber: {\n            type: \"number\",\n            description: \"Pull request number\",\n          },\n          commit_title: {\n            type: \"string\",\n            description: \"Title for merge commit\",\n          },\n          merge_method: {\n            type: \"string\",\n            description: \"Merge method ('merge', 'squash', 'rebase')\",\n          },\n          commit_message: {\n            type: \"string\",\n            description: \"Extra detail for merge commit\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"push_files\",\n      description:\n        \"Push multiple files to a GitHub repository in a single commit\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"branch\", \"files\", \"message\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          files: {\n            type: \"array\",\n            items: {\n              type: \"object\",\n              required: [\"path\", \"content\"],\n              properties: {\n                path: {\n                  type: \"string\",\n                  description: \"path to the file\",\n                },\n                content: {\n                  type: \"string\",\n                  description: \"file content\",\n                },\n              },\n              additionalProperties: false,\n            },\n            description:\n              \"Array of file objects to push, each object with path (string) and content (string)\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          branch: {\n            type: \"string\",\n            description: \"Branch to push to\",\n          },\n          message: {\n            type: \"string\",\n            description: \"Commit message\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"search_code\",\n      description: \"Search for code across GitHub repositories\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"q\"],\n        properties: {\n          q: {\n            type: \"string\",\n            description: \"Search query using GitHub code search syntax\",\n          },\n          page: {\n            type: \"number\",\n            minimum: 1,\n            description: \"Page number\",\n          },\n          sort: {\n            type: \"string\",\n            description: \"Sort field ('indexed' only)\",\n          },\n          order: {\n            enum: [\"asc\", \"desc\"],\n            type: \"string\",\n            description: \"Sort order ('asc' or 'desc')\",\n          },\n          per_page: {\n            type: \"number\",\n            maximum: 100,\n            minimum: 1,\n            description: \"Results per page (max 100)\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"search_issues\",\n      description:\n        \"Search for issues and pull requests across GitHub repositories\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"q\"],\n        properties: {\n          q: {\n            type: \"string\",\n            description: \"Search query using GitHub issues search syntax\",\n          },\n          page: {\n            type: \"number\",\n            minimum: 1,\n            description: \"Page number\",\n          },\n          sort: {\n            enum: [\n              \"comments\",\n              \"reactions\",\n              \"reactions-+1\",\n              \"reactions--1\",\n              \"reactions-smile\",\n              \"reactions-thinking_face\",\n              \"reactions-heart\",\n              \"reactions-tada\",\n              \"interactions\",\n              \"created\",\n              \"updated\",\n            ],\n            type: \"string\",\n            description: \"Sort field (comments, reactions, created, etc.)\",\n          },\n          order: {\n            enum: [\"asc\", \"desc\"],\n            type: \"string\",\n            description: \"Sort order ('asc' or 'desc')\",\n          },\n          per_page: {\n            type: \"number\",\n            maximum: 100,\n            minimum: 1,\n            description: \"Results per page (max 100)\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"search_repositories\",\n      description: \"Search for GitHub repositories\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"query\"],\n        properties: {\n          page: {\n            type: \"number\",\n            description: \"Page number for pagination\",\n          },\n          query: {\n            type: \"string\",\n            description: \"Search query\",\n          },\n          perPage: {\n            type: \"number\",\n            description: \"Results per page (max 100)\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"search_users\",\n      description: \"Search for GitHub users\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"q\"],\n        properties: {\n          q: {\n            type: \"string\",\n            description: \"Search query using GitHub users search syntax\",\n          },\n          page: {\n            type: \"number\",\n            minimum: 1,\n            description: \"Page number\",\n          },\n          sort: {\n            enum: [\"followers\", \"repositories\", \"joined\"],\n            type: \"string\",\n            description: \"Sort field (followers, repositories, joined)\",\n          },\n          order: {\n            enum: [\"asc\", \"desc\"],\n            type: \"string\",\n            description: \"Sort order ('asc' or 'desc')\",\n          },\n          per_page: {\n            type: \"number\",\n            maximum: 100,\n            minimum: 1,\n            description: \"Results per page (max 100)\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"update_issue\",\n      description: \"Update an existing issue in a GitHub repository\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"issue_number\"],\n        properties: {\n          body: {\n            type: \"string\",\n            description: \"New description\",\n          },\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          state: {\n            enum: [\"open\", \"closed\"],\n            type: \"string\",\n            description: \"New state ('open' or 'closed')\",\n          },\n          title: {\n            type: \"string\",\n            description: \"New title\",\n          },\n          labels: {\n            type: \"array\",\n            items: {\n              type: \"string\",\n            },\n            description: \"New labels\",\n          },\n          assignees: {\n            type: \"array\",\n            items: {\n              type: \"string\",\n            },\n            description: \"New assignees\",\n          },\n          milestone: {\n            type: \"number\",\n            description: \"New milestone number\",\n          },\n          issue_number: {\n            type: \"number\",\n            description: \"Issue number to update\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n    {\n      name: \"update_pull_request_branch\",\n      description:\n        \"Update a pull request branch with the latest changes from the base branch\",\n      inputSchema: {\n        type: \"object\",\n        required: [\"owner\", \"repo\", \"pullNumber\"],\n        properties: {\n          repo: {\n            type: \"string\",\n            description: \"Repository name\",\n          },\n          owner: {\n            type: \"string\",\n            description: \"Repository owner\",\n          },\n          pullNumber: {\n            type: \"number\",\n            description: \"Pull request number\",\n          },\n          expectedHeadSha: {\n            type: \"string\",\n            description: \"The expected SHA of the pull request's HEAD ref\",\n          },\n        },\n      },\n      autoApprove: false,\n    },\n  ],\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServerTypes.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { type DBS } from \"@src/index\";\nimport { type JSONB } from \"prostgles-types\";\nimport type { McpTool } from \"../AnthropicMcpHub/McpTypes\";\nimport type { AuthClientRequest } from \"prostgles-server/dist/Auth/AuthTypes\";\n\nexport type ProstglesMcpServerDefinition = {\n  icon_path: string;\n  label: string;\n  description: string;\n  tools: Record<\n    string,\n    {\n      description: string;\n      schema: JSONB.FieldTypeObj | undefined;\n      outputSchema: JSONB.FieldType | undefined;\n    }\n  >;\n  // config_schema: JSONB.FieldType | undefined;\n};\n\nexport type JSONBTypeIfDefined<Schema extends JSONB.FieldType | undefined> =\n  Schema extends JSONB.FieldType ? JSONB.GetType<Schema> : undefined;\n\ntype MaybePromise<T> = T | Promise<T>;\n\nexport type McpCallContext = {\n  chat_id: DBSSchema[\"llm_chats\"][\"id\"];\n  user_id: DBSSchema[\"users\"][\"id\"];\n  clientReq: AuthClientRequest;\n};\n\nexport type ProstglesMcpServerHandler = {\n  start: (\n    // config: unknown,\n    dbs: DBS,\n  ) => MaybePromise<ProstglesMcpServerHandlerInstance>;\n};\nexport type ProstglesMcpServerHandlerInstance = {\n  stop: () => MaybePromise<void>;\n  fetchTools: (\n    dbs: DBS,\n    context: McpCallContext,\n  ) => MaybePromise<\n    {\n      name: string;\n      description: string;\n      inputSchema: McpTool[\"inputSchema\"];\n    }[]\n  >;\n  tools: Record<\n    string,\n    (toolArguments: unknown, context: McpCallContext) => MaybePromise<unknown>\n  >;\n};\n\nexport type ProstglesMcpServerHandlerTyped<\n  ServerDefinition extends Omit<\n    ProstglesMcpServerDefinition,\n    \"handler\"\n  > = ProstglesMcpServerDefinition,\n> = {\n  start: (\n    // config: JSONBTypeIfDefined<ServerDefinition[\"config_schema\"]>,\n    dbs: DBS,\n  ) => MaybePromise<{\n    stop: () => MaybePromise<void>;\n    fetchTools: (\n      dbs: DBS,\n      context: McpCallContext,\n    ) => MaybePromise<\n      {\n        name: string;\n        description: string;\n        inputSchema: McpTool[\"inputSchema\"];\n      }[]\n    >;\n\n    tools: {\n      [ToolName in keyof ServerDefinition[\"tools\"]]: (\n        toolArguments: JSONBTypeIfDefined<\n          ServerDefinition[\"tools\"][ToolName][\"schema\"]\n        >,\n        context: McpCallContext,\n      ) => MaybePromise<unknown>;\n    };\n    // JSONBTypeIfDefined<ServerDefinition[\"tools\"][ToolName][\"outputSchema\"]>\n  }>;\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/createContainer.spec.ts",
    "content": "import { strict } from \"assert\";\nimport { test } from \"node:test\";\nimport { createContainer } from \"./createContainer.js\";\nimport type { CreateContainerParams } from \"./fetchTools.js\";\n\nconst testContainerName = `test-container-${Date.now()}`;\nvoid test(\"createContainer build error\", async () => {\n  const config = {\n    files: {\n      ...filesObject,\n      Dockerfile: Dockerfile.replace(\n        \"WORKDIR\",\n        \"WORKDIR_INVALID\", // Introduce an error\n      ),\n    },\n    networkMode: \"bridge\",\n  } satisfies CreateContainerParams;\n  const sandbox = await createContainer(testContainerName, config);\n  strict.equal(sandbox.state, \"build-error\");\n  const stderr = sandbox.log\n    .filter((l) => l.type === \"stderr\")\n    .map((l) => l.text)\n    .join(\"\");\n  strict.equal(stderr.includes(\"| >>> WORKDIR_INVALID\"), true);\n});\n\nvoid test(\"createContainer run error\", async () => {\n  const config = {\n    files: {\n      ...filesObject,\n      \"index.js\": indexJsContent.replace(\n        `require`,\n        `requireInvalid`, // Introduce an error\n      ),\n    },\n    networkMode: \"bridge\",\n  } satisfies CreateContainerParams;\n  const sandbox = await createContainer(testContainerName, config);\n  strict.equal(sandbox.state, \"error\");\n  const stderr = sandbox.log\n    .filter((l) => l.type === \"stderr\")\n    .map((l) => l.text)\n    .join(\"\");\n  strict.equal(stderr.includes(\"requireInvalid is not defined\"), true);\n});\n\nvoid test(\"createContainer run timeout\", async () => {\n  const config = {\n    files: {\n      ...filesObject,\n      \"index.js\": indexJsContent.replace(\n        `try { `,\n        `try { \\nawait new Promise(resolve => setTimeout(resolve, 120_000));\\n `,\n      ),\n    },\n    timeout: 3_000,\n  } satisfies CreateContainerParams;\n  const sandbox = await createContainer(testContainerName, config);\n  strict.equal(sandbox.state, \"timed-out\");\n  const stdout = sandbox.log\n    .filter((l) => l.type === \"stdout\")\n    .map((l) => l.text)\n    .join(\"\");\n  strict.equal(\n    stdout.includes(\"Fetch app started. Will attempt to fetch from\"),\n    true,\n  );\n  strict.equal(stdout.includes(\"Will attempt to fetch from\"), true);\n});\n\nvoid test(\"createContainer stdout response\", async () => {\n  const expectedOutput = \"Hello from index.js\";\n  const config = {\n    files: {\n      ...filesObject,\n      \"index.js\": `console.log('${expectedOutput}');`,\n    },\n    timeout: 3_000,\n  } satisfies CreateContainerParams;\n  const sandbox = await createContainer(testContainerName, config);\n  strict.equal(sandbox.state, \"finished\");\n  const stdout = sandbox.log\n    .filter((l) => l.type === \"stdout\")\n    .map((l) => l.text)\n    .join(\"\");\n  const stderr = sandbox.log\n    .filter((l) => l.type === \"stderr\")\n    .map((l) => l.text)\n    .join(\"\");\n  strict.equal(stdout, expectedOutput + \"\\n\");\n  strict.equal(stderr, \"\");\n});\n\n// void test(\"createContainer host network\", async () => {\n//   const { address, route, server } = await dockerMCPRouter(() => {\n//     return {\n//       connection_id: \"1\",\n//       db_data_permissions: { Mode: \"Run commited SQL\" },\n//       db_schema_permissions: { type: \"Full\" },\n//     };\n//   });\n//   const config = {\n//     files: {\n//       ...filesObject,\n//       \"index.js\": indexJsContent.replace(\n//         `http://127.0.0.1:3004/robots.txt`,\n//         `http://${address.address}:${address.port}${route}`, // Use the MCP router address\n//       ),\n//     },\n//     timeout: 4_000,\n//     networkMode: \"host\",\n//   } satisfies CreateContainerParams;\n//   const sandbox = await createContainer(testContainerName, config);\n//   strict.equal(sandbox.state, \"finished\");\n//   strict.equal(sandbox.stdout.includes(\"<title>Prostgles UI</title>\"), true);\n//   strict.equal(sandbox.stderr, \"\");\n//   server.close();\n// });\n\nvoid test(\"createContainer stderr response\", async () => {\n  const expectedOutput = \"Hello from index.js\";\n  const config = {\n    files: {\n      ...filesObject,\n      \"index.js\": `console.error('${expectedOutput}');`,\n    },\n    timeout: 3_000,\n  } satisfies CreateContainerParams;\n  const sandbox = await createContainer(testContainerName, config);\n  strict.equal(sandbox.state, \"finished\");\n  const stdout = sandbox.log\n    .filter((l) => l.type === \"stdout\")\n    .map((l) => l.text)\n    .join(\"\");\n  const stderr = sandbox.log\n    .filter((l) => l.type === \"stderr\")\n    .map((l) => l.text)\n    .join(\"\");\n  strict.equal(stdout, \"\");\n  strict.equal(stderr, expectedOutput + \"\\n\");\n});\n\nconst packageJson = {\n  name: \"fetch-app\",\n  version: \"1.0.0\",\n  description: \"Basic Node.js app that fetches from host port 3004\",\n  main: \"index.js\",\n  scripts: { start: \"node index.js\" },\n  dependencies: { axios: \"^1.6.0\" },\n};\n\nconst indexJsContent = `\nconst axios = require('axios');\nconst HOST_URL = 'http://127.0.0.1:3004/robots.txt'; // This resource will not redirect\nasync function fetchFromHost() {  \n  try { \n    console.log(\\`Attempting to fetch from \\${HOST_URL}\\`);\n    const response = await axios.post(HOST_URL, {\n      timeout: 3_000  \n    });    \n    console.log('Response status:', response.status); console.log('Response data:', response.data);  \n  } catch (error) {    \n    if (error.code === 'ECONNREFUSED') {\n      console.log('Connection refused - make sure service is running on host port 3004');    \n    } else if (error.code === 'ETIMEDOUT') {\n      console.log('Request timed out');\n    } else {\n      console.log('Error fetching from host:', error.message);\n    }\n  }\n}\nfetchFromHost();\n//setInterval(fetchFromHost, 30000);\nconsole.log('Fetch app started. Will attempt to fetch from host:3004 every 30 seconds...');\n`;\n\nconst Dockerfile = `\nFROM node:18-alpine\n\nWORKDIR /workspace\n\nCOPY package*.json ./\n\nRUN npm install  \n\nCOPY . .\n\nCMD [\"node\", \"index.js\"]\n`;\n\nconst filesObject = {\n  \"package.json\": JSON.stringify(packageJson, null, 2),\n  \"index.js\": indexJsContent,\n  Dockerfile: Dockerfile,\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/createContainer.ts",
    "content": "import type { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport type { JSONBTypeIfDefined } from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServerTypes\";\nimport { existsSync, mkdirSync, rmSync, writeFileSync } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { dirname, join } from \"path\";\nimport { executeDockerCommand } from \"./executeDockerCommand\";\nimport { getDockerRunArgs } from \"./getDockerRunArgs\";\nimport type { CreateContainerParams } from \"./fetchTools\";\n\ntype CreateContainerResult = JSONBTypeIfDefined<\n  (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"docker-sandbox\"][\"create_container\"][\"outputSchema\"]\n>;\n\nexport const createContainer = async (\n  name: string,\n  params: CreateContainerParams,\n): Promise<CreateContainerResult> => {\n  let localDir = \"\";\n  try {\n    const { files } = params;\n    localDir = join(tmpdir(), name);\n\n    mkdirSync(localDir, { recursive: true });\n    const dockerFileName = \"Dockerfile\";\n    const dockerFile = files[dockerFileName];\n    if (!dockerFile) {\n      throw new Error(\"Dockerfile is required in the files array\");\n    }\n    if (dockerFile.toLowerCase().includes(\"expose\")) {\n      throw new Error(\"Dockerfile should not contain EXPOSE instruction\");\n    }\n\n    for (const [name, content] of Object.entries(files)) {\n      const tempFile = join(localDir, name);\n      const dir = dirname(tempFile);\n      if (!existsSync(dir)) {\n        mkdirSync(dir, { recursive: true });\n      }\n      writeFileSync(tempFile, content);\n    }\n\n    const buildArgs = [\n      \"build\",\n      \"-t\",\n      name,\n      \"-f\",\n      join(localDir, dockerFileName),\n      localDir,\n    ];\n    const startTime = Date.now();\n    const buildResult = await executeDockerCommand(buildArgs, {\n      timeout: 300_000,\n    });\n    const buildDuration = Date.now() - startTime;\n\n    if (buildResult.exitCode !== 0) {\n      return {\n        name,\n        state: \"build-error\",\n        command: [\"docker\", ...buildArgs].join(\" \"),\n        log: buildResult.log,\n        buildDuration,\n        runDuration: -1,\n        exitCode: buildResult.exitCode,\n      };\n    }\n\n    const { runArgs, config } = getDockerRunArgs({\n      ...params,\n      name,\n      localDir,\n    });\n\n    const runStartTime = Date.now();\n    const runResult = await executeDockerCommand(runArgs, {\n      timeout: 30_000,\n      ...config,\n      ...params,\n    });\n\n    /** Cleanup */\n    if (runResult.state === \"timed-out\") {\n      await executeDockerCommand([\"kill\", name], { timeout: 60_000 });\n    }\n    await executeDockerCommand([\"image\", \"rm\", name], { timeout: 60_000 });\n\n    return {\n      command: [\"docker\", ...runArgs].join(\" \"),\n      state: runResult.state === \"close\" ? \"finished\" : runResult.state,\n      ...config,\n      log: runResult.log,\n      exitCode: runResult.exitCode,\n      runDuration: Date.now() - runStartTime,\n      buildDuration: Date.now() - startTime,\n    };\n  } finally {\n    if (localDir) {\n      rmSync(localDir, { recursive: true });\n    }\n  }\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/dockerMCPServerProxy/dockerContainerAuthRegistry.ts",
    "content": "import { execSync } from \"child_process\";\nimport { getKeys, isDefined, pickKeys } from \"prostgles-types\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { GetAuthContext } from \"./dockerMCPServerProxy\";\nexport const DOCKER_CONTAINER_NAME_PREFIX = \"prostgles-docker-mcp-sandbox\";\n\nexport type CreateContainerContext = {\n  userId: string;\n  chatId: number;\n};\n\nexport type ContainerAuthInfo = {\n  chat: DBSSchema[\"llm_chats\"];\n  sid_token: string;\n};\n\nconst containers: Record<string, ContainerAuthInfo> = {};\n\nconst setContainerInfo = (name: string, info: ContainerAuthInfo) => {\n  containers[name] = info;\n};\n\nconst deleteContainerInfo = (name: string) => {\n  delete containers[name];\n};\n\nconst containerIpCache = {\n  containerNames: \"\",\n  ipToContainerName: new Map<string, string>(),\n};\nconst getIPToContainerName = () => {\n  const containerNames = getKeys(containers).sort().join();\n  if (!containerNames) {\n    throw new Error(\"No containers available\");\n  }\n  if (containerIpCache.containerNames === containerNames) {\n    return containerIpCache.ipToContainerName;\n  }\n  const containerNamesToIPs = execSync(\n    \"docker inspect   -f '{{.Name}} {{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq)\",\n  )\n    .toString()\n    .split(\"\\n\")\n    .map((line) => {\n      // Skip slash at the beginning of the container name\n      const [name, ip] = line.slice(1).trim().split(\" \");\n      if (!name || !name.startsWith(DOCKER_CONTAINER_NAME_PREFIX) || !ip)\n        return;\n      return { name, ip };\n    })\n    .filter(isDefined);\n\n  const containerNamesWithIPs = containerNamesToIPs\n    .map((c) => c.name)\n    .sort()\n    .join();\n  containerIpCache.containerNames = containerNamesWithIPs;\n  containerIpCache.ipToContainerName = new Map(\n    containerNamesToIPs.map((c) => [c.ip, c.name]),\n  );\n  return containerIpCache.ipToContainerName;\n};\n\nconst getContainerFromIP: GetAuthContext = (ip: string) => {\n  const containerName = getIPToContainerName().get(ip);\n\n  if (!containerName) throw new Error(`No container found for IP ${ip}`);\n  const containerInfo = containerName ? containers[containerName] : undefined;\n  if (!containerName || !containerInfo) {\n    return;\n  }\n  return pickKeys(containerInfo, [\"chat\", \"sid_token\"]);\n};\n\nexport const dockerContainerAuthRegistry = {\n  getContainerFromIP,\n  setContainerInfo,\n  deleteContainerInfo,\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/dockerMCPServerProxy/dockerMCPServerProxy.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { execSync } from \"child_process\";\nimport express, { json, Request, Response, urlencoded } from \"express\";\nimport _http from \"http\";\nimport type { AddressInfo } from \"net\";\nimport { HTTP_FAIL_CODES } from \"prostgles-server/dist/Auth/AuthHandler\";\nimport { getSerialisableError, isObject } from \"prostgles-types\";\nimport { dockerContainerAuthRegistry } from \"./dockerContainerAuthRegistry\";\nimport { getDockerGatewayIP } from \"../getDockerGatewayIP\";\nimport { isPortFree } from \"./isPortFree\";\nimport { getProstglesState } from \"@src/init/tryStartProstgles\";\nimport { isDocker } from \"@src/index\";\nimport { runProstglesDBTool } from \"@src/publishMethods/askLLM/prostglesLLMTools/runProstglesDBTool\";\n\nconst PREFERRED_PORT = 3009;\nexport const DOCKER_MCP_ENDPOINT = \"/db\";\nconst ROUTE = `${DOCKER_MCP_ENDPOINT}/:endpoint`;\n\nexport type ChatPermissions = Pick<\n  DBSSchema[\"llm_chats\"],\n  \"db_data_permissions\" | \"connection_id\"\n>;\n\ntype AuhtContext = {\n  sid_token: string;\n  chat: ChatPermissions;\n};\n\nexport type GetAuthContext = (ip: string) => AuhtContext | undefined;\n\n/**\n * A separate server is used to improve security because we need to bind it to 0.0.0.0 to ensure docker containers can access it.\n */\nexport const dockerMCPServerProxy = async () => {\n  const dockerVersion = execSync(\"docker --version\").toString();\n  if (!dockerVersion) throw new Error(\"Docker not installed\");\n  const app = express();\n\n  app.use(json({ limit: \"1000mb\" }));\n  app.use(urlencoded({ extended: true, limit: \"1000mb\" }));\n  app.post(ROUTE, (req, res) => {\n    return requestHandler(req, res);\n  });\n  const http = _http.createServer(app);\n  const usePreferredPort = await isPortFree(PREFERRED_PORT);\n\n  const dockerGatewayIP = getDockerGatewayIP();\n  const hostname =\n    isDocker || getProstglesState().isElectron ? \"0.0.0.0\" : dockerGatewayIP;\n\n  return new Promise<{\n    app: express.Express;\n    server: _http.Server;\n    address: AddressInfo;\n    api_url: string;\n    destroy: () => void;\n  }>((resolve, reject) => {\n    const server = http.listen(\n      usePreferredPort ? PREFERRED_PORT : undefined,\n      hostname,\n      () => {\n        const address = server.address();\n        console.log(\"Docker MCP Router listening on\", address);\n        if (!isObject(address)) {\n          reject(new Error(\"Server address is not an object\"));\n        } else {\n          const actualPort = address.port;\n          const api_url =\n            isDocker ?\n              `http://prostgles-ui-docker-mcp:${actualPort}${ROUTE}`\n            : `http://${dockerGatewayIP}:${actualPort}${ROUTE}`;\n          resolve({\n            app,\n            server,\n            address,\n            api_url,\n            destroy: () => server.close(),\n          });\n        }\n      },\n    );\n  });\n};\n\nconst requestHandler = (req: Request, res: Response) => {\n  const { endpoint = \"\" } = req.params;\n  try {\n    const ip = req.ip || req.socket.remoteAddress || \"\";\n\n    const authContext = dockerContainerAuthRegistry.getContainerFromIP(ip);\n    if (!authContext) {\n      return res.status(HTTP_FAIL_CODES.UNAUTHORIZED).json({\n        error:\n          \"Container and/or Chat not found for the given IP address: \" + ip,\n      });\n    }\n    const { chat, sid_token } = authContext;\n    req.cookies ??= {};\n    req.cookies.sid_token = sid_token;\n    runProstglesDBTool(chat, { httpReq: req, res }, req.body, endpoint)\n      .then((result) => {\n        res.json(result);\n      })\n      .catch((error) => {\n        res.status(400).json({ error: getSerialisableError(error) });\n      });\n  } catch (error) {\n    console.error(\"Error in request handler:\", error);\n    return res.status(500).json({ error: \"Internal server error\" });\n  }\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/dockerMCPServerProxy/isPortFree.ts",
    "content": "import { createServer } from \"net\";\n\nexport const isPortFree = async (port: number): Promise<boolean> => {\n  const result = await tryPort(port);\n  return result === port;\n};\n\nexport const getFreePort = async (preferredPort?: number): Promise<number> => {\n  if (preferredPort) {\n    const isFree = await isPortFree(preferredPort);\n    if (isFree) return preferredPort;\n  }\n  const port = await tryPort(undefined);\n  if (!port) throw new Error(\"Could not find a free port\");\n  return port;\n};\n\nconst tryPort = (port: number | undefined): Promise<number | undefined> => {\n  /**\n   * We must check all interfaces\n   */\n  const host = \"0.0.0.0\";\n  return new Promise((resolve) => {\n    const server = createServer();\n\n    server.listen(port, host, () => {\n      const address = server.address();\n      const actualPort =\n        typeof address === \"object\" && address ? address.port : undefined;\n      server.once(\"close\", () => {\n        resolve(actualPort);\n      });\n      server.close();\n    });\n\n    server.on(\"error\", () => {\n      resolve(undefined);\n    });\n  });\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/executeDockerCommand.ts",
    "content": "import { spawn, type SpawnOptionsWithoutStdio } from \"child_process\";\n\nexport type ProcessLog = {\n  type: \"stdout\" | \"stderr\" | \"error\";\n  text: string;\n};\nexport interface ExecutionResult {\n  state: \"close\" | \"error\" | \"timed-out\" | \"aborted\";\n  command: string;\n  exitCode: number;\n  timedOut: boolean;\n  executionTime: number;\n  log: ProcessLog[];\n}\n\n/**\n * Execute a command inside the container\n */\nexport const executeDockerCommand = async (\n  args: string[] = [],\n  options: Pick<SpawnOptionsWithoutStdio, \"cwd\" | \"env\" | \"timeout\" | \"signal\">,\n  onLogs?: (logs: ProcessLog[]) => void,\n): Promise<ExecutionResult> => {\n  const startTime = Date.now();\n  const timeout = options.timeout;\n\n  return new Promise((resolve) => {\n    // Check if already aborted before starting\n    if (options.signal?.aborted) {\n      resolve({\n        state: \"error\",\n        command: `docker ${args.join(\" \")}`,\n        exitCode: -1,\n        timedOut: false,\n        executionTime: 0,\n        log: [{ type: \"error\", text: \"Execution aborted\" }],\n      });\n      return;\n    }\n    const child = spawn(\"docker\", args, {\n      ...options,\n      stdio: [\"pipe\", \"pipe\", \"pipe\"],\n    });\n    const command = `docker ${args.join(\" \")}`;\n    // TODO: move timeout handling to spawn options\n    let timedOut = false;\n    let ended = false;\n\n    const log: ProcessLog[] = [];\n\n    const timeoutId =\n      timeout === undefined ? undefined : (\n        setTimeout(() => {\n          timedOut = true;\n          child.kill(\"SIGKILL\");\n          onEnd({ type: \"timed-out\", error: new Error(\"Execution timed out\") });\n        }, timeout)\n      );\n\n    const onEnd = (\n      reason:\n        | { type: \"close\"; code: number | null }\n        | { type: \"error\" | \"timed-out\" | \"aborted\"; error: Error },\n    ) => {\n      if (ended) return;\n      ended = true;\n      if (timeoutId !== undefined) {\n        clearTimeout(timeoutId);\n      }\n\n      const executionTime = Date.now() - startTime;\n      if (reason.type === \"error\") {\n        log.push({\n          type: \"error\",\n          text: reason.error.message,\n        });\n      }\n      resolve({\n        state: reason.type,\n        command,\n        exitCode: reason.type === \"close\" ? reason.code || 0 : -1,\n        timedOut,\n        executionTime,\n        log,\n      });\n\n      if (!child.killed) {\n        child.kill();\n      }\n    };\n\n    child.stdout.on(\"data\", (data: Buffer) => {\n      log.push({ type: \"stdout\", text: data.toString() });\n      onLogs?.(log);\n    });\n\n    child.stderr.on(\"data\", (data: Buffer) => {\n      log.push({ type: \"stderr\", text: data.toString() });\n      onLogs?.(log);\n    });\n\n    child.on(\"close\", (code) => {\n      if (timedOut) return;\n      if (code !== 0) {\n        const stderr = log\n          .filter((l) => l.type === \"stderr\")\n          .map((l) => l.text)\n          .join(\"\");\n        onEnd({ type: \"error\", error: new Error(stderr) });\n      } else {\n        onEnd({ type: \"close\", code: 0 });\n      }\n    });\n\n    child.on(\"error\", (error) => {\n      if (options.signal?.aborted) {\n        onEnd({ type: \"aborted\", error });\n        // If aborted, kill the process\n        child.kill(\"SIGKILL\");\n      } else {\n        onEnd({ type: \"error\", error });\n      }\n    });\n  });\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/fetchTools.ts",
    "content": "import { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport type { DBS } from \"@src/index\";\nimport type { McpTool } from \"@src/McpHub/AnthropicMcpHub/McpTypes\";\nimport { getProstglesDBTools } from \"@src/publishMethods/askLLM/prostglesLLMTools/getProstglesDBTools\";\nimport {\n  getJSONBSchemaAsJSONSchema,\n  omitKeys,\n  type JSONB,\n} from \"prostgles-types\";\nimport type { McpCallContext } from \"../../ProstglesMCPServerTypes\";\n\nconst createContainerToolInfo =\n  PROSTGLES_MCP_SERVERS_AND_TOOLS[\"docker-sandbox\"][\"create_container\"];\n\nexport const fetchTools = async (\n  apiUrl: string,\n  dbs: DBS,\n  context: McpCallContext,\n) => {\n  const chat = await dbs.llm_chats.findOne({ id: context.chat_id });\n  const dbTools = getProstglesDBTools(chat);\n  const isDocker = Boolean(process.env.IS_DOCKER);\n\n  const databaseQueryDescription =\n    !dbTools.length ?\n      \"Access to the database is not allowed. If user wants to run queries, they need to set the Mode to Custom or SQL.\"\n    : [\n        `To run queries against the database you need to POST JSON body parameters to ${apiUrl}`,\n        `The following endpoints are available:\\n\\n`,\n        ...dbTools.map((t) => {\n          const argTSSchema = JSON.stringify(\n            omitKeys(getJSONBSchemaAsJSONSchema(\"\", \"\", t.schema), [\n              \"$id\",\n              \"$schema\",\n            ]),\n          );\n          return ` - /${t.tool_name} - ${t.description}. JSON body input schema: ${argTSSchema}  `;\n        }),\n        isDocker ?\n          \"DO NOT USE WORKHOST to connect to prostgles-ui-docker-mcp. Just specify network mode 'bridge' and it will work.\"\n        : \"\",\n      ].join(\"\\n\");\n\n  return [\n    {\n      name: \"create_container\",\n      description: `${createContainerToolInfo.description}. ${databaseQueryDescription}`,\n      inputSchema: omitKeys(\n        getJSONBSchemaAsJSONSchema(\"\", \"\", createContainerSchema),\n        [\"$id\", \"$schema\"],\n      ) as McpTool[\"inputSchema\"],\n    },\n  ];\n};\n\nexport type CreateContainerParams = JSONB.GetSchemaType<\n  typeof createContainerSchema\n>;\n\nconst createContainerSchema = PROSTGLES_MCP_SERVERS_AND_TOOLS[\"docker-sandbox\"][\n  \"create_container\"\n].schema satisfies JSONB.JSONBSchema;\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/getContainerLogs.ts",
    "content": "import { executeDockerCommand } from \"./executeDockerCommand\";\n\nexport const getContainerLogs = async (containerId: string) => {\n  const result = await executeDockerCommand(\n    [\n      \"logs\",\n      // \"--timestamps\",\n      \"--details\",\n      // \"--tail\",\n      // \"all\",\n      containerId,\n    ],\n    { timeout: 5000 },\n  );\n\n  return result;\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/getDockerGatewayIP.ts",
    "content": "import { execSync } from \"child_process\";\n\nexport const getDockerGatewayIP = () => {\n  let dockerGatewayIP = \"172.17.0.1\";\n  try {\n    const actualDockerGatewayIP = execSync(\n      `docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}'`,\n    )\n      .toString()\n      .trim();\n    if (actualDockerGatewayIP) {\n      dockerGatewayIP = actualDockerGatewayIP;\n    }\n  } catch (error) {\n    console.error(\"Failed to get Docker gateway IP, using default: \", error);\n  }\n\n  return dockerGatewayIP;\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/getDockerRunArgs.ts",
    "content": "import type { CreateContainerParams } from \"./fetchTools\";\n\nconst CUSTOM_BRIDGE_NETWORK_NAME = \"prostgles-bridge-net\";\nconst LABEL = \"prostgles-docker-sandbox\";\n\ntype LocalDockerParams = {\n  user?: string;\n  workingDir?: string;\n  volumes?: Array<{\n    host: string;\n    container: string;\n    readOnly?: boolean;\n  }>;\n  localDir: string;\n  name: string;\n};\n\nconst isDocker = Boolean(process.env.IS_DOCKER);\n\nexport const getDockerRunArgs = ({\n  cpus = \"1\",\n  memory = \"512m\",\n  networkMode = \"none\",\n  user = \"nobody\",\n  workingDir = \"/workspace\",\n  environment = {},\n  volumes,\n  localDir,\n  name,\n}: CreateContainerParams & LocalDockerParams) => {\n  const runArgs = [\"run\", \"--rm\", \"--interactive\"];\n\n  // Resource limits\n  if (memory) {\n    runArgs.push(\"--memory\", memory);\n  }\n\n  if (cpus) {\n    runArgs.push(\"--cpus\", cpus);\n  }\n\n  // Network settings\n  if (networkMode === \"bridge\" && isDocker) {\n    runArgs.push(\"--network\", CUSTOM_BRIDGE_NETWORK_NAME);\n  } else {\n    runArgs.push(\"--network\", networkMode);\n  }\n\n  // User\n  if (user) {\n    runArgs.push(\"--user\", user);\n  }\n\n  runArgs.push(\"--read-only\");\n\n  // Environment variables\n  Object.entries(environment).forEach(([key, value]) => {\n    runArgs.push(\"--env\", `${key}=${value}`);\n  });\n\n  // Volumes\n  // runArgs.push(\"-v\", `${localDir}:${workingDir}`);\n  // if (volumes) {\n  //   volumes.forEach((volume) => {\n  //     const volumeStr =\n  //       volume.readOnly ?\n  //         `${volume.host}:${volume.container}:ro`\n  //       : `${volume.host}:${volume.container}`;\n  //     runArgs.push(\"-v\", volumeStr);\n  //   });\n  // }\n\n  runArgs.push(\"--label\", LABEL, \"--name\", name);\n\n  // Security options\n  runArgs.push(\"--security-opt\", \"no-new-privileges\");\n  runArgs.push(\"--cap-drop\", \"ALL\");\n\n  runArgs.push(name);\n\n  return {\n    runArgs,\n    config: { user, workingDir, volumes, localDir, name },\n  };\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox.mcp.ts",
    "content": "import { DOCKER_USER_AGENT } from \"@common/OAuthUtils\";\nimport { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport { upsertSession } from \"@src/authConfig/upsertSession\";\nimport { randomUUID } from \"crypto\";\nimport type {\n  ProstglesMcpServerDefinition,\n  ProstglesMcpServerHandler,\n  ProstglesMcpServerHandlerTyped,\n} from \"../ProstglesMCPServerTypes\";\nimport { createContainer } from \"./DockerSandbox/createContainer\";\nimport {\n  DOCKER_CONTAINER_NAME_PREFIX,\n  dockerContainerAuthRegistry,\n} from \"./DockerSandbox/dockerMCPServerProxy/dockerContainerAuthRegistry\";\nimport { dockerMCPServerProxy } from \"./DockerSandbox/dockerMCPServerProxy/dockerMCPServerProxy\";\nimport { fetchTools } from \"./DockerSandbox/fetchTools\";\n\nconst definition = {\n  icon_path: \"Docker\",\n  label: \"Docker Sandbox\",\n  description: \"Run code in isolated Docker containers\",\n  tools: PROSTGLES_MCP_SERVERS_AND_TOOLS[\"docker-sandbox\"],\n} as const satisfies ProstglesMcpServerDefinition;\n\nlet mcpRequestRouter: ReturnType<typeof dockerMCPServerProxy> | undefined;\n\nconst handler = {\n  start: async (dbs) => {\n    mcpRequestRouter ??= dockerMCPServerProxy();\n    const { api_url, destroy } = await mcpRequestRouter;\n\n    return {\n      stop: () => {\n        destroy();\n      },\n      tools: {\n        create_container: async (args, context) => {\n          const name = `${DOCKER_CONTAINER_NAME_PREFIX}-${Date.now()}-${randomUUID()}`;\n          const chat = await dbs.llm_chats.findOne({ id: context.chat_id });\n          const user = await dbs.users.findOne({ id: context.user_id });\n          if (!chat) {\n            throw new Error(`Chat with id ${context.chat_id} not found`);\n          }\n          if (!user) {\n            throw new Error(`User with id ${context.user_id} not found`);\n          }\n          const tokenForMCP = await upsertSession({\n            db: dbs,\n            ip: \"127.0.0.1\",\n            user,\n            user_agent: DOCKER_USER_AGENT,\n          });\n          const sid_token = tokenForMCP.sid;\n          if (!sid_token) {\n            throw new Error(\"Failed to create session for Docker MCP\");\n          }\n          dockerContainerAuthRegistry.setContainerInfo(name, {\n            chat,\n            sid_token,\n          });\n          const containerResult = await createContainer(name, args).catch(\n            (error) => {\n              console.error(\"Error creating container:\", error);\n            },\n          );\n          dockerContainerAuthRegistry.deleteContainerInfo(name);\n          return containerResult;\n        },\n      },\n      fetchTools: (dbs, context) => fetchTools(api_url, dbs, context),\n    };\n  },\n} satisfies ProstglesMcpServerHandlerTyped<typeof definition>;\n\nexport const DockerSandboxMCPServer = {\n  definition,\n  handler: handler as ProstglesMcpServerHandler,\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers/WebSearch.mcp.ts",
    "content": "import { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport { getEntries } from \"@common/utils\";\nimport type { McpTool } from \"@src/McpHub/AnthropicMcpHub/McpTypes\";\nimport { getServiceManager } from \"@src/ServiceManager/ServiceManager\";\nimport { getJSONBSchemaAsJSONSchema } from \"prostgles-types\";\nimport type {\n  ProstglesMcpServerDefinition,\n  ProstglesMcpServerHandler,\n  ProstglesMcpServerHandlerTyped,\n} from \"../ProstglesMCPServerTypes\";\nimport { McpHub } from \"@src/McpHub/AnthropicMcpHub/McpHub\";\n\nconst definition = {\n  icon_path: \"Web\",\n  label: \"Web Search\",\n  description: \"Search the web for information\",\n  tools: PROSTGLES_MCP_SERVERS_AND_TOOLS[\"websearch\"],\n} as const satisfies ProstglesMcpServerDefinition;\n\nconst handler = {\n  start: async (dbs) => {\n    const searXngService = getServiceManager(dbs);\n    await searXngService\n      .enableService(\"webSearchSearxng\", () => {})\n      .catch(console.error);\n\n    const serviceInstance = searXngService.getService(\"webSearchSearxng\");\n    if (serviceInstance?.status !== \"running\") {\n      throw new Error(\n        \"Failed to start SearXNG service for Web Search MCP Server\",\n      );\n    }\n\n    return {\n      stop: () => {\n        searXngService.stopService(\"webSearchSearxng\");\n      },\n      tools: {\n        websearch: async (toolArguments, { clientReq }) => {\n          const clientIp =\n            clientReq.httpReq?.ip ||\n            clientReq.socket?.handshake.address ||\n            \"127.0.0.1\";\n          const result = await serviceInstance.endpoints[\"/search\"](\n            { ...toolArguments, format: \"json\" },\n            {\n              headers: {\n                \"X-Forwarded-For\": clientIp,\n                \"X-Real-IP\": clientIp,\n              },\n            },\n          );\n\n          return result.results;\n        },\n        get_snapshot: async (toolArguments) => {\n          const mcpHub = new McpHub();\n          await mcpHub.setServerConnections({\n            fetch: {\n              command: \"uvx\",\n              args: [\"mcp-server-fetch\"],\n              server_name: \"fetch\",\n              onLog: () => {},\n            },\n            playwright: {\n              command: \"npx\",\n              args: [\"@playwright/mcp@latest\"],\n              onLog: () => {},\n              server_name: \"playwright\",\n            },\n          });\n          const result1 = await mcpHub.callTool(\n            \"playwright\",\n            \"browser_navigate\",\n            toolArguments,\n          );\n          if (result1.isError) {\n            await mcpHub.destroy();\n            throw new Error(\n              `Failed to get snapshot: ${JSON.stringify(result1.content)}`,\n            );\n          }\n          const result2 = await mcpHub.callTool(\n            \"playwright\",\n            \"browser_snapshot\",\n          );\n          if (result2.isError) {\n            await mcpHub.destroy();\n            throw new Error(\n              `Failed to get snapshot: ${JSON.stringify(result2.content)}`,\n            );\n          }\n          await mcpHub.destroy();\n          return (\n            result2.content\n              .map((item) => (item.type === \"text\" ? item.text : \"\"))\n              .join(\"\\n\") || \"\"\n          );\n        },\n      },\n      fetchTools: () => {\n        return getEntries(PROSTGLES_MCP_SERVERS_AND_TOOLS[\"websearch\"]).map(\n          ([name, { schema, description }]) => ({\n            name,\n            description,\n            inputSchema: getJSONBSchemaAsJSONSchema(\n              \"\",\n              \"\",\n              schema,\n            ) as McpTool[\"inputSchema\"],\n          }),\n        );\n      },\n    };\n  },\n} satisfies ProstglesMcpServerHandlerTyped<typeof definition>;\n\nexport const WebSearchMCPServer = {\n  definition,\n  handler: handler as ProstglesMcpServerHandler,\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMCPServers.ts",
    "content": "import { getKeys, includes } from \"prostgles-types\";\nimport { DockerSandboxMCPServer } from \"./ProstglesMCPServers/DockerSandbox.mcp\";\nimport { WebSearchMCPServer } from \"./ProstglesMCPServers/WebSearch.mcp\";\nimport type {\n  ProstglesMcpServerDefinition,\n  ProstglesMcpServerHandler,\n} from \"./ProstglesMCPServerTypes\";\n\nexport const ProstglesMCPServers = {\n  \"docker-sandbox\": DockerSandboxMCPServer,\n  websearch: WebSearchMCPServer,\n} as const satisfies Record<\n  string,\n  {\n    definition: ProstglesMcpServerDefinition;\n    handler: ProstglesMcpServerHandler;\n  }\n>;\nexport const getProstglesMCPServer = (serverName: string) => {\n  if (includes(getKeys(ProstglesMCPServers), serverName)) {\n    return ProstglesMCPServers[serverName];\n  }\n  return undefined;\n};\n"
  },
  {
    "path": "server/src/McpHub/ProstglesMcpHub/ProstglesMcpHub.ts",
    "content": "import type { McpToolCallResponse } from \"@common/mcp\";\nimport type { DBS } from \"@src/index\";\nimport {\n  getJSONBSchemaValidationError,\n  getKeys,\n  getSerialisableError,\n  includes,\n  tryCatchV2,\n} from \"prostgles-types\";\nimport type {\n  McpCallContext,\n  ProstglesMcpServerDefinition,\n  ProstglesMcpServerHandler,\n} from \"./ProstglesMCPServerTypes\";\nimport { getProstglesMCPServer } from \"./ProstglesMCPServers\";\n\nconst servers: Map<\n  string,\n  Awaited<ReturnType<ProstglesMcpServerHandler[\"start\"]>>\n> = new Map();\n\nconst init = async (dbs: DBS) => {\n  const sub = await dbs.mcp_servers.subscribe(\n    {\n      command: \"prostgles-local\",\n    },\n    {\n      select: {\n        \"*\": 1,\n        mcp_server_configs: \"*\",\n      },\n    },\n    (serverRecords) => {\n      for (const serverRecord of serverRecords) {\n        const serverInstance = servers.get(serverRecord.name);\n        if (!serverInstance && serverRecord.enabled) {\n          const serverInfo = getProstglesMCPServer(serverRecord.name);\n          if (!serverInfo) {\n            console.error(\n              `Prostgles MCP server name invalid: ${serverRecord.name}`,\n            );\n          } else {\n            const { handler } = serverInfo;\n            // TODO: implement configs\n            // const { config_schema } = definition;\n            // const configs =\n            //   serverRecord.mcp_server_configs as DBSSchema[\"mcp_server_configs\"][];\n            // const firstConfig = configs[0]?.config;\n            // const validation =\n            //   config_schema &&\n            //   getJSONBSchemaValidationError(config_schema, firstConfig);\n            // if (validation?.error !== undefined) {\n            //   console.error(\n            //     `Prostgles MCP server config invalid for server ${serverRecord.name}: ${validation.error}`,\n            //   );\n            //   continue;\n            // }\n\n            void (async () => {\n              const instance = await handler.start(dbs);\n              servers.set(serverRecord.name, instance);\n            })();\n          }\n        } else if (serverInstance && !serverRecord.enabled) {\n          void serverInstance.stop();\n          servers.delete(serverRecord.name);\n        }\n      }\n    },\n  );\n\n  const getServer = (serverName: string) => {\n    const server = servers.get(serverName);\n    const serverDefinition = getProstglesMCPServer(serverName);\n\n    return {\n      server,\n      serverDefinition,\n    };\n  };\n\n  const getExpectedServer = (serverName: string) => {\n    const { server, serverDefinition } = getServer(serverName);\n    if (!server || !serverDefinition) {\n      if (!serverDefinition) {\n        throw new Error(`MCP server ${serverName} not found`);\n      }\n      throw new Error(`MCP server ${serverName} not enabled`);\n    }\n    return { server, serverDefinition };\n  };\n\n  const callTool = async (\n    serverName: string,\n    toolName: string,\n    args: unknown,\n    context: McpCallContext,\n  ): Promise<McpToolCallResponse> => {\n    const result = await tryCatchV2(async () => {\n      const { server, serverDefinition } = getExpectedServer(serverName);\n      const toolDefinition = getProperty(\n        (serverDefinition.definition as ProstglesMcpServerDefinition).tools,\n        toolName,\n      );\n      const toolMethod = getProperty(server.tools, toolName);\n      if (!toolMethod || !toolDefinition) {\n        throw new Error(`MCP server tool ${serverName}.${toolName} not found`);\n      }\n      const { schema } = toolDefinition;\n      const validation =\n        //@ts-ignore\n        schema ? getJSONBSchemaValidationError(schema, args) : undefined;\n      if (validation?.error !== undefined) {\n        throw new Error(\n          `Invalid arguments for MCP server tool ${serverName}.${toolName}: ${validation.error}`,\n        );\n      }\n      const res = await toolMethod(args, context);\n      return res;\n    });\n\n    return {\n      content: [\n        {\n          type: \"text\",\n          text:\n            result.hasError ?\n              JSON.stringify(getSerialisableError(result.error))\n            : JSON.stringify(result.data ?? {}),\n        },\n      ],\n      isError: result.hasError,\n    };\n  };\n\n  const fetchTools = async (serverName: string, context: McpCallContext) => {\n    const { server } = getExpectedServer(serverName);\n    return server.fetchTools(dbs, context);\n  };\n  const destroy = () => {\n    return sub.unsubscribe();\n  };\n\n  return { destroy, callTool, fetchTools, getServer };\n};\n\nlet inFlightInit: ReturnType<typeof init> | undefined;\nexport const getProstglesMcpHub = (dbs: DBS) => {\n  inFlightInit ??= init(dbs);\n  return inFlightInit;\n};\n\nconst getProperty = <T extends Record<string, unknown>, K extends keyof T>(\n  obj: T,\n  prop: K,\n): T[K] | undefined => {\n  if (prop in obj && includes(getKeys(obj), prop)) {\n    return obj[prop] as T[K];\n  }\n  return undefined;\n};\n"
  },
  {
    "path": "server/src/McpHub/callMCPServerTool.ts",
    "content": "import type { McpToolCallResponse } from \"@common/mcp\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport {\n  getJSONBObjectSchemaValidationError,\n  getSerialisableError,\n  tryCatchV2,\n} from \"prostgles-types\";\nimport type { DBS } from \"..\";\nimport { startMcpHub } from \"./AnthropicMcpHub/startMcpHub\";\nimport { getProstglesMCPServer } from \"./ProstglesMcpHub/ProstglesMCPServers\";\nimport { getProstglesMcpHub } from \"./ProstglesMcpHub/ProstglesMcpHub\";\nimport type { AuthClientRequest } from \"prostgles-server/dist/Auth/AuthTypes\";\n\nexport const callMCPServerTool = async (\n  user: Pick<DBSSchema[\"users\"], \"id\">,\n  chat_id: number,\n  dbs: DBS,\n  serverName: string,\n  toolName: string,\n  toolArguments: Record<string, unknown> | undefined,\n  clientReq: AuthClientRequest,\n): Promise<McpToolCallResponse> => {\n  const start = new Date();\n  const argErrors = getJSONBObjectSchemaValidationError(\n    {\n      serverName: \"string\",\n      toolName: \"string\",\n      chat_id: \"integer\",\n    },\n    {\n      serverName,\n      toolName,\n      chat_id,\n    },\n    undefined,\n    false,\n  );\n  if (argErrors.error) throw new Error(argErrors.error);\n  const result = await tryCatchV2(async () => {\n    const chat = await dbs.llm_chats.findOne({ id: chat_id, user_id: user.id });\n    if (!chat) {\n      throw new Error(\"Chat not found\");\n    }\n    const chatAllowedMCPTool = await dbs.llm_chats_allowed_mcp_tools.findOne({\n      chat_id,\n      $existsJoined: {\n        mcp_server_tools: {\n          server_name: serverName,\n          name: toolName,\n        },\n      },\n    });\n    if (!chatAllowedMCPTool) {\n      throw new Error(\"Tool invalid or not allowed for this chat\");\n    }\n\n    const prostglesMcp = getProstglesMCPServer(serverName);\n    if (prostglesMcp) {\n      const prglMcpHub = await getProstglesMcpHub(dbs);\n      return prglMcpHub.callTool(serverName, toolName, toolArguments, {\n        chat_id,\n        user_id: user.id,\n        clientReq,\n      });\n    }\n\n    const mcpHub = await startMcpHub(dbs);\n    const res = await mcpHub.callTool(\n      [serverName, chatAllowedMCPTool.server_config_id]\n        .filter(Boolean)\n        .join(\"_\"),\n      toolName,\n      toolArguments,\n    );\n    return res;\n  });\n\n  await dbs.mcp_server_tool_calls.insert({\n    duration: `${result.duration}ms` as {},\n    called: start,\n    mcp_server_name: serverName,\n    mcp_tool_name: toolName,\n    input: toolArguments,\n    output: result.data,\n    error: getSerialisableError(result.error) || null,\n    chat_id,\n    user_id: user.id,\n  });\n\n  if (result.hasError) {\n    return {\n      isError: true,\n      content: [\n        {\n          type: \"text\",\n          text:\n            result.error instanceof Error ?\n              result.error.message\n            : JSON.stringify(result.error),\n        },\n      ],\n    };\n  }\n\n  return result.data;\n};\n"
  },
  {
    "path": "server/src/McpHub/fetchMCPServerConfigs.ts",
    "content": "import { isDefined, pickKeys, type ValueOf } from \"prostgles-types\";\nimport type { DBS } from \"..\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type {\n  McpConfigWithEvents,\n  ServersConfig,\n} from \"./AnthropicMcpHub/McpTypes\";\n\nexport const fetchMCPServerConfigs = async (\n  dbs: DBS,\n  testConfig?: DBSSchema[\"mcp_server_configs\"],\n) => {\n  const mcpServers = await dbs.mcp_servers.find(\n    testConfig ?\n      { name: testConfig.server_name }\n    : { enabled: true, command: { $ne: \"prostgles-local\" } },\n    { select: { \"*\": 1, mcp_server_configs: \"*\" } },\n  );\n  const globalSettings = await dbs.global_settings.findOne();\n  if (globalSettings?.mcp_servers_disabled) {\n    return {};\n  }\n\n  const serversConfig: ServersConfig = {};\n  mcpServers.forEach((server) => {\n    const baseEnv = {\n      /** Needed for puppeteer/playwright */\n      ...(server.env_from_main_process?.length &&\n        (pickKeys(process.env, server.env_from_main_process) as Record<\n          string,\n          string\n        >)),\n      ...(server.env ?? {}),\n    };\n    const baseArgs = server.args ?? [];\n    const onLog: McpConfigWithEvents[\"onLog\"] = (type, data, log) => {\n      void dbs.mcp_server_logs.upsert(\n        { server_name: server.name },\n        type === \"stderr\" ?\n          {\n            log,\n          }\n        : {\n            error: data,\n          },\n      );\n    };\n\n    const { config_schema } = server;\n    const mcp_server_configs =\n      testConfig?.server_name === server.name ?\n        [testConfig]\n      : ((server.mcp_server_configs ??\n          []) as DBSSchema[\"mcp_server_configs\"][]);\n    if (config_schema && mcp_server_configs.length) {\n      mcp_server_configs.forEach((mcp_server_config) => {\n        const { args, env } = applyConfig(\n          {\n            args: baseArgs,\n            env: baseEnv,\n          },\n          config_schema,\n          mcp_server_config,\n        );\n        serversConfig[server.name + \"_\" + mcp_server_config.id] = {\n          ...server,\n          server_name: server.name,\n          args,\n          env,\n          stderr: undefined,\n          cwd: server.cwd ?? undefined,\n          onLog,\n        } satisfies ValueOf<ServersConfig>;\n      });\n    } else {\n      serversConfig[server.name] = {\n        ...server,\n        server_name: server.name,\n        args: baseArgs,\n        env: baseEnv,\n        stderr: undefined,\n        cwd: server.cwd ?? undefined,\n        onLog,\n      } satisfies ValueOf<ServersConfig>;\n    }\n  });\n  return serversConfig;\n};\n\nconst applyConfig = (\n  {\n    args: baseArgs,\n    env: baseEnv,\n  }: Required<Pick<McpConfigWithEvents, \"args\" | \"env\">>,\n  config_schema: NonNullable<DBSSchema[\"mcp_servers\"][\"config_schema\"]>,\n  {\n    server_name,\n    config,\n  }: Pick<DBSSchema[\"mcp_server_configs\"], \"server_name\" | \"config\">,\n) => {\n  const args = [...baseArgs];\n  const env = { ...baseEnv };\n  Object.entries({ ...config_schema }).forEach(\n    ([key, configItem], itemIndex) => {\n      if (configItem.type === \"env\") {\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n        env[key] = config[key];\n      } else {\n        const dollarArgIndexes = args\n          .map((a, i) => (a.startsWith(\"${\") ? i : undefined))\n          .filter(isDefined);\n        const argIndex = dollarArgIndexes[configItem.index ?? itemIndex];\n        if (argIndex) {\n          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n          args[argIndex] = config[key];\n        } else {\n          console.error(\n            `Invalid index for arg \"${key}\" in server \"${server_name}\"`,\n          );\n        }\n      }\n    },\n  );\n  return { args, env };\n};\n"
  },
  {
    "path": "server/src/McpHub/insertServerList.ts",
    "content": "import type { DBSSchemaForInsert } from \"@common/publishUtils\";\nimport { join } from \"path\";\nimport type { DBS } from \"..\";\nimport { getMCPDirectory } from \"./AnthropicMcpHub/installMCPServer\";\nimport { getDefaultMCPServers } from \"./DefaultMCPServers/DefaultMCPServers\";\n\nexport const insertServerList = async (dbs: DBS) => {\n  const servers = await dbs.mcp_servers.find();\n  if (!servers.length) {\n    const defaultServers = Object.entries(getDefaultMCPServers()).map(\n      ([name, { ...server }]) => {\n        return {\n          name,\n          cwd:\n            server.source ? join(getMCPDirectory(), name) : getMCPDirectory(),\n          ...server,\n        } satisfies DBSSchemaForInsert[\"mcp_servers\"];\n      },\n    );\n    await dbs.mcp_servers.insert(defaultServers);\n  }\n};\n"
  },
  {
    "path": "server/src/McpHub/reloadMcpServerTools.ts",
    "content": "import type { DBSSchemaForInsert } from \"@common/publishUtils\";\nimport { getEntries } from \"@common/utils\";\nimport { getJSONBSchemaAsJSONSchema } from \"prostgles-types\";\nimport { DBS } from \"..\";\nimport { fetchMCPToolsList } from \"./AnthropicMcpHub/fetchMCPToolsList\";\nimport type { McpHub } from \"./AnthropicMcpHub/McpHub\";\nimport { type McpTool } from \"./AnthropicMcpHub/McpTypes\";\nimport { startMcpHub } from \"./AnthropicMcpHub/startMcpHub\";\nimport { getProstglesMCPServer } from \"./ProstglesMcpHub/ProstglesMCPServers\";\nimport type { ProstglesMcpServerDefinition } from \"./ProstglesMcpHub/ProstglesMCPServerTypes\";\n\nexport const updateMcpServerTools = async (\n  dbs: DBS,\n  serverName: string,\n  mcpHub: McpHub,\n) => {\n  let tools: McpTool[] = [];\n  const prostglesMCP = getProstglesMCPServer(serverName);\n  if (prostglesMCP) {\n    tools = getEntries(\n      prostglesMCP.definition.tools as ProstglesMcpServerDefinition[\"tools\"],\n    ).map(([name, { schema, description }]) => {\n      const inputSchema =\n        !schema ? undefined : getJSONBSchemaAsJSONSchema(\"\", \"\", schema);\n      return {\n        name,\n        description,\n        inputSchema: inputSchema as unknown as McpTool[\"inputSchema\"],\n      };\n    });\n  } else {\n    const client = mcpHub.getClient(serverName);\n    if (!client) {\n      throw new Error(\n        `No connection found for MCP server: ${serverName}. Make sure it is enabled`,\n      );\n    }\n    tools = await fetchMCPToolsList(client);\n  }\n\n  await dbs.tx(async (tx) => {\n    await tx.mcp_server_tools\n      .delete({\n        server_name: serverName,\n        name: { $nin: tools.map((t) => t.name) },\n      })\n      .catch((e) => {\n        console.error(\n          `Error deleting MCP server tools for server ${serverName}:`,\n          e,\n        );\n      });\n    if (tools.length) {\n      await tx.mcp_server_tools.insert(\n        tools.map(\n          ({ name, description, inputSchema, annotations }) =>\n            ({\n              description: description ?? \"\",\n              server_name: serverName,\n              inputSchema,\n              name,\n              annotations,\n            }) satisfies DBSSchemaForInsert[\"mcp_server_tools\"],\n        ),\n        {\n          onConflict: \"DoUpdate\",\n        },\n      );\n    }\n  });\n  // const   resources = await fetchMCPResourcesList(client);\n  return tools.length;\n};\n\nexport const reloadMcpServerTools = async (dbs: DBS, serverName: string) => {\n  const mcpHub = await startMcpHub(dbs);\n  return updateMcpServerTools(dbs, serverName, mcpHub);\n};\n"
  },
  {
    "path": "server/src/McpHub/testMCPServerConfig.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { getEntries } from \"@common/utils\";\nimport type { DBS } from \"..\";\nimport { connectToMCPServer } from \"./AnthropicMcpHub/connectToMCPServer\";\nimport { fetchMCPServerConfigs } from \"./fetchMCPServerConfigs\";\n\nexport const testMCPServerConfig = async (\n  dbs: DBS,\n  config: DBSSchema[\"mcp_server_configs\"],\n) => {\n  const serversConfig = await fetchMCPServerConfigs(dbs, config);\n  const [server, ...others] = getEntries(serversConfig);\n  if (!server || others.length) {\n    throw new Error(\"Only one MCP server config can be tested at a time\");\n  }\n  const [serverName, fullConfig] = server;\n  return (\n    await connectToMCPServer({\n      name: serverName + \"_\",\n      server_name: fullConfig.server_name,\n      config: fullConfig,\n      onLog: () => {},\n      onTransportClose: () => {},\n    })\n  ).destroy();\n};\n"
  },
  {
    "path": "server/src/SecurityManager/initUsers.ts",
    "content": "import type { DB } from \"prostgles-server/dist/initProstgles\";\nimport { PRGL_PASSWORD, PRGL_USERNAME } from \"../envVars\";\nimport type { DBS, Users } from \"..\";\nimport {\n  ELECTRON_USER_AGENT,\n  PASSWORDLESS_ADMIN_USERNAME,\n} from \"@common/OAuthUtils\";\nimport { getPasswordHash } from \"../authConfig/authUtils\";\nimport { getElectronConfig } from \"../electronConfig\";\nimport { makeSession } from \"../authConfig/sessionUtils\";\nimport { YEAR } from \"@common/utils\";\n\nconst EMPTY_PASSWORD = \"\";\n\nconst NoInitialAdminPasswordProvided = Boolean(\n  !PRGL_USERNAME || !PRGL_PASSWORD,\n);\nexport const activePasswordlessAdminFilter = {\n  username: PASSWORDLESS_ADMIN_USERNAME,\n  status: \"active\",\n  passwordless_admin: true,\n} as const;\n\nexport const getPasswordlessAdmin = async (\n  db: DBS | Omit<DBS, \"tx\" | \"sql\">,\n) => {\n  if (NoInitialAdminPasswordProvided) {\n    return await db.users.findOne(activePasswordlessAdminFilter);\n  }\n  return undefined;\n};\n\n/**\n * If PRGL_USERNAME and PRGL_PASSWORD are specified then create an admin user with these credentials AND allow any IP to connect\n * Otherwise:\n * Create a passwordless admin (PASSWORDLESS_ADMIN_USERNAME, EMPTY_PASSWORD) and allow the first IP to connect\n *  then, the first user to connect must select between these options:\n *    1) Add an account with password (recommended)\n *    2) Continue to allow only the current IP\n *    3) Allow any IP to connect (not recommended)\n *\n */\nexport const initUsers = async (db: DBS, _db: DB) => {\n  let username = PRGL_USERNAME,\n    password = PRGL_PASSWORD;\n  if (NoInitialAdminPasswordProvided) {\n    username = PASSWORDLESS_ADMIN_USERNAME;\n    password = EMPTY_PASSWORD;\n  }\n\n  /**\n   * No initial admin user setup. Create a passwordless admin user is required\n   */\n  if (!(await db.users.count({ username }))) {\n    if (NoInitialAdminPasswordProvided) {\n      console.warn(\n        `PRGL_USERNAME or PRGL_PASSWORD missing. Creating a passwordless admin user: ${username}`,\n      );\n    }\n\n    try {\n      const initialAdmin = (await db.users.insert(\n        {\n          username,\n          password,\n          type: \"admin\",\n          passwordless_admin: Boolean(NoInitialAdminPasswordProvided),\n        },\n        { returning: \"*\" },\n      )) as Users | undefined;\n      if (!initialAdmin) throw \"User not inserted\";\n      await db.users.update(\n        {\n          id: initialAdmin.id,\n        },\n        {\n          password: password && getPasswordHash(initialAdmin, password),\n          status: \"active\",\n        },\n      );\n    } catch (e) {\n      console.error(e);\n    }\n\n    const addedUser = await db.users.find({ username });\n\n    console.log(\n      \"Added admin users: \",\n      addedUser.map((u) => u.username),\n    );\n  }\n\n  const electron = getElectronConfig();\n  if (electron?.isElectron) {\n    const user = await getPasswordlessAdmin(db);\n    if (!user) throw `Unexpected: Electron passwordless_admin misssing`;\n    await db.sessions.delete({});\n    await makeSession(\n      user,\n      {\n        ip_address: \"::1\",\n        user_agent: ELECTRON_USER_AGENT,\n        type: \"web\",\n        sid: electron.sidConfig.electronSid,\n      },\n      db,\n      Date.now() + 10 * YEAR,\n    );\n  }\n};\n"
  },
  {
    "path": "server/src/ServiceManager/ServiceManager.spec.ts",
    "content": "import { strict } from \"assert\";\nimport { test, describe } from \"node:test\";\nimport { getServiceManager } from \"./ServiceManager\";\n\nvoid describe(\"Service manager tests\", async () => {\n  await test(\"Enable speechToText\", async () => {\n    let logText = \"\";\n    const serviceManager = getServiceManager(undefined);\n    const res = await serviceManager.enableService(\"speechToText\", (logs) => {\n      const lastLog = logs.at(-1)?.text;\n      console.warn(lastLog);\n      logText += lastLog;\n    });\n    strict.equal(res.status, \"running\");\n    strict.equal(\n      logText.includes(\"Running on http://127.0.0.1:8000\"),\n      true,\n      \"Service did not start correctly\",\n    );\n    serviceManager.destroy();\n  });\n});\n"
  },
  {
    "path": "server/src/ServiceManager/ServiceManager.ts",
    "content": "import { type ProcessLog } from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/executeDockerCommand\";\nimport type { DBS } from \"..\";\nimport { buildService } from \"./buildService\";\nimport { enableService } from \"./enableService\";\nimport { initialiseServices } from \"./initialiseServices\";\nimport {\n  prostglesServices,\n  ServiceInstance,\n  type RunningServiceInstance,\n} from \"./ServiceManagerTypes\";\nimport { startService } from \"./startService\";\nimport { stopService } from \"./stopService\";\n\nexport class ServiceManager {\n  dbs: DBS | undefined;\n  constructor(dbs: DBS | undefined) {\n    this.dbs = dbs;\n    if (dbs) {\n      void initialiseServices(this, dbs);\n    }\n  }\n  onServiceLog = (\n    serviceName: keyof typeof prostglesServices,\n    logItems: ProcessLog[],\n  ) => {\n    const serviceStatus = this.activeServices.get(serviceName)?.status;\n    const logs = logItems\n      .slice(-100)\n      .map((l) => l.text)\n      .join(\"\");\n    void this.dbs?.services.update(\n      { name: serviceName },\n      { logs, status: serviceStatus ?? \"stopped\" },\n    );\n  };\n  activeServices: Map<string, ServiceInstance> = new Map();\n  enablingServices: Map<string, Promise<RunningServiceInstance>> = new Map();\n\n  getActiveService<Status extends ServiceInstance[\"status\"]>(\n    serviceName: keyof typeof prostglesServices,\n    expectedStatus: Status,\n  ) {\n    const activeInstance = this.activeServices.get(serviceName);\n    if (!activeInstance || activeInstance.status !== expectedStatus) {\n      throw new Error(\n        `Unexpected: service ${serviceName} is not in expected status ${expectedStatus}. Actual status: ${activeInstance?.status}`,\n      );\n    }\n    return activeInstance as Extract<ServiceInstance, { status: Status }>;\n  }\n\n  getService<\n    ServiceName extends keyof typeof prostglesServices,\n    ExistingServices extends typeof prostglesServices,\n  >(\n    serviceName: ServiceName,\n  ): ServiceInstance<ExistingServices[ServiceName]> | undefined {\n    const activeInstance = this.activeServices.get(serviceName);\n    //@ts-ignore\n    return activeInstance;\n  }\n\n  buildService = buildService.bind(this);\n\n  startService = startService.bind(this);\n\n  enableService = enableService.bind(this);\n\n  stopService = stopService.bind(this);\n\n  destroy = () => {\n    this.activeServices.forEach((service) => {\n      if (service.status === \"running\" || service.status === \"starting\") {\n        service.stop();\n      }\n    });\n    this.activeServices = new Map();\n  };\n}\n\nlet serviceManager: ServiceManager | undefined = undefined;\nexport const getServiceManager = (dbs: DBS | undefined) => {\n  serviceManager ??= new ServiceManager(dbs);\n  return serviceManager;\n};\n"
  },
  {
    "path": "server/src/ServiceManager/ServiceManagerTypes.ts",
    "content": "import type { JSONB } from \"prostgles-types\";\nimport { speechToTextService } from \"./services/speechToText/speechToText.service\";\nimport type { JSONBTypeIfDefined } from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServerTypes\";\nimport { webSearchSearxngService } from \"./services/webSearchSearxng/webSearchSearxng.service\";\nimport type {\n  ExecutionResult,\n  ProcessLog,\n} from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/executeDockerCommand\";\n\nexport type DockerGPUS = \"none\" | \"all\" | number | number[];\n\nexport type ProstglesService = {\n  icon: string;\n  label: string;\n  description: string;\n  port: number;\n  /**\n   * Defaults to port.\n   * If the port is already in use on the host, a different port will be chosen.\n   */\n  hostPort?: number;\n  env?: Record<string, string>;\n  configs?: Record<\n    string,\n    {\n      label: string;\n      description: string;\n      defaultOption: string;\n      options: Record<\n        string,\n        {\n          env: Record<string, string>;\n          buildArgs?: Record<string, string>;\n          gpus?: DockerGPUS;\n        }\n      >;\n    }\n  >;\n  gpus?: DockerGPUS;\n  healthCheck: { endpoint: string; method?: \"GET\" | \"POST\" };\n  volumes?: Record<string, string>;\n  endpoints: Record<\n    string,\n    {\n      method: \"GET\" | \"POST\";\n      description: string;\n      /* Defaults to 'body' for POST and 'query' for GET */\n      inputType?: \"body\" | \"query\";\n      inputSchema: JSONB.FieldType | undefined;\n      outputSchema: JSONB.FieldType | undefined;\n    }\n  >;\n};\n\nexport const prostglesServices = {\n  speechToText: speechToTextService,\n  webSearchSearxng: webSearchSearxngService,\n};\n\nexport type RunningServiceInstance<\n  Service extends ProstglesService = ProstglesService,\n> = {\n  status: \"running\";\n  getLogs: () => ProcessLog[];\n  stop: () => void;\n  endpoints: {\n    [endpoint in keyof Service[\"endpoints\"]]: (\n      args: JSONBTypeIfDefined<Service[\"endpoints\"][endpoint][\"inputSchema\"]>,\n      fetchOptions?: Omit<RequestInit, \"method\" | \"body\">,\n    ) => Promise<\n      JSONBTypeIfDefined<Service[\"endpoints\"][endpoint][\"outputSchema\"]>\n    >;\n  };\n};\n\nexport type ServiceInstance<\n  Service extends ProstglesService = ProstglesService,\n> =\n  | {\n      status: \"building\";\n      building: Promise<ExecutionResult>;\n      stop: () => void;\n    }\n  | {\n      status: \"building-done\";\n      buildHash: string;\n      labels: Record<string, string>;\n      labelArgs: string[];\n    }\n  | {\n      status: \"build-error\";\n      error: unknown;\n    }\n  | {\n      status: \"starting\";\n      getLogs: () => ProcessLog[];\n      stop: () => void;\n    }\n  | RunningServiceInstance<Service>\n  | {\n      status: \"error\";\n      error: unknown;\n    };\n\nexport type OnServiceLogs = (logs: ProcessLog[]) => void;\n"
  },
  {
    "path": "server/src/ServiceManager/buildService.ts",
    "content": "import { join } from \"path\";\nimport { getDockerBuildHash } from \"./getDockerBuildHash\";\nimport type { ServiceManager } from \"./ServiceManager\";\nimport {\n  OnServiceLogs,\n  prostglesServices,\n  ServiceInstance,\n} from \"./ServiceManagerTypes\";\nimport { isEqual, pickKeys } from \"prostgles-types\";\nimport { getEntries } from \"@common/utils\";\nimport { dockerInspect } from \"./dockerInspect\";\nimport { getSelectedConfigEnvs } from \"./getSelectedConfigEnvs\";\nimport {\n  executeDockerCommand,\n  type ExecutionResult,\n} from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/executeDockerCommand\";\nimport { filterArr } from \"@common/llmUtils\";\n\nexport async function buildService(\n  this: ServiceManager,\n  serviceName: keyof typeof prostglesServices,\n  onLogs: OnServiceLogs,\n): Promise<ExecutionResult[\"state\"]> {\n  const onLogsCombined: OnServiceLogs = (logs) => {\n    onLogs(logs);\n    this.onServiceLog(serviceName, logs);\n  };\n  const existingService = this.activeServices.get(serviceName);\n  if (existingService) {\n    if (existingService.status === \"building\") {\n      const res = await existingService.building;\n      return res.state;\n    }\n    if (existingService.status === \"running\") {\n      return \"close\";\n    }\n    this.activeServices.delete(serviceName);\n  }\n\n  const abortController = new AbortController();\n  const stop = () => {\n    abortController.abort();\n  };\n  const serviceCwd = join(\n    __dirname,\n    \"../../../../\",\n    \"/src/ServiceManager/services\",\n    serviceName,\n    \"src\",\n  );\n\n  const imageName = camelCaseToSkewerCase(serviceName);\n\n  const { buildArgs } = await getSelectedConfigEnvs(this.dbs, serviceName);\n  /** Only rebuild if hash differs */\n  const buildHash = await getDockerBuildHash(serviceCwd, buildArgs);\n  const buildLabels = {\n    \"prostgles-build-hash\": buildHash,\n    app: \"prostgles\",\n  };\n  const labelArgs = getEntries(buildLabels).flatMap(([key, value]) => [\n    \"--label\",\n    `${key}=${value}`,\n  ]);\n  const matchingDockerImage = await dockerInspect(imageName);\n  if (\n    matchingDockerImage &&\n    isEqual(\n      /** Some labels end up populated  (e.g.: org.opencontainers....)  */\n      pickKeys(matchingDockerImage.Config.Labels, Object.keys(buildLabels)),\n      buildLabels,\n    )\n  ) {\n    this.activeServices.set(serviceName, {\n      status: \"building-done\",\n      buildHash,\n      labels: buildLabels,\n      labelArgs,\n    });\n    return \"close\";\n  }\n\n  const instance: ServiceInstance = {\n    status: \"building\",\n    building: executeDockerCommand(\n      [\n        \"build\",\n        ...buildArgs.map((arg) => [\"--build-arg\", arg]).flat(),\n        \"-t\",\n        imageName,\n        ...labelArgs,\n        \".\",\n      ],\n      {\n        timeout: 600_000,\n        signal: abortController.signal,\n        cwd: serviceCwd,\n      },\n      onLogsCombined,\n    )\n      .then((result) => {\n        this.getActiveService(serviceName, \"building\");\n        if (result.state !== \"close\") {\n          this.activeServices.set(serviceName, {\n            status: \"build-error\",\n            error:\n              result.state === \"aborted\" ? \"aborted\"\n              : result.state === \"timed-out\" ? \"timed-out\"\n              : (filterArr(result.log, { type: \"error\" })[0] ??\n                result.log.at(-1)),\n          });\n          return result;\n        } else {\n          this.activeServices.set(serviceName, {\n            status: \"building-done\",\n            buildHash,\n            labels: buildLabels,\n            labelArgs,\n          });\n        }\n        return result;\n      })\n      .catch((err) => {\n        console.error(`Error building service ${serviceName}:`, err);\n        this.getActiveService(serviceName, \"building\");\n        this.activeServices.set(serviceName, {\n          status: \"error\",\n          error: err,\n        });\n        return Promise.reject(err);\n      }),\n    stop,\n  };\n  this.activeServices.set(serviceName, instance);\n  const { state, log } = await instance.building;\n  onLogsCombined(log);\n  return state;\n}\n\nexport const camelCaseToSkewerCase = (str: string) => {\n  return str\n    .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n    .replace(/[\\s_]+/g, \"-\")\n    .toLowerCase();\n};\n"
  },
  {
    "path": "server/src/ServiceManager/dockerInspect.ts",
    "content": "import { executeDockerCommand } from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/executeDockerCommand\";\n\nexport const dockerInspect = async (\n  containerOrImageName: string,\n): Promise<DockerInspectResult | undefined> => {\n  try {\n    const inspectData = await executeDockerCommand(\n      [\"inspect\", containerOrImageName],\n      {\n        timeout: 10_000,\n      },\n    );\n    const stdOut = inspectData.log.find((d) => d.type === \"stdout\")?.text;\n    if (!stdOut) {\n      return;\n    }\n    const [item, ...otherItems] = JSON.parse(stdOut) as DockerInspectResult[];\n    if (item && !otherItems.length) {\n      return item;\n    }\n  } catch (e) {\n    console.warn(`docker inspect failed for ${containerOrImageName}:`, e);\n  }\n  return;\n};\n\ntype DockerInspectResult = {\n  Id: string;\n  Created: string;\n  Config: {\n    Labels: Record<string, string>;\n  };\n};\n"
  },
  {
    "path": "server/src/ServiceManager/enableService.ts",
    "content": "import type { ProcessLog } from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/executeDockerCommand\";\nimport type { prostglesServices } from \"./ServiceManagerTypes\";\nimport type { ServiceManager } from \"./ServiceManager\";\n\nexport async function enableService(\n  this: ServiceManager,\n  serviceName: keyof typeof prostglesServices,\n  onLogs: (logs: ProcessLog[]) => void,\n) {\n  await this.enablingServices.get(serviceName);\n  const activeService = this.activeServices.get(serviceName);\n  if (activeService?.status === \"running\") {\n    return activeService;\n  } else {\n    this.stopService(serviceName);\n  }\n  const enabling = async () => {\n    const buildResult = await this.buildService(serviceName, onLogs);\n    if (buildResult !== \"close\") {\n      throw new Error(\n        `Service ${serviceName} build failed with state: ${buildResult}`,\n      );\n    }\n\n    const buildServiceInstance = this.activeServices.get(serviceName);\n    if (buildServiceInstance?.status === \"building-done\") {\n      await this.dbs?.services.update(\n        { name: serviceName },\n        { build_hash: buildServiceInstance.buildHash },\n      );\n    }\n\n    const startedServer = await this.startService(serviceName, onLogs);\n    return startedServer;\n  };\n  const result = enabling().finally(() => {\n    this.enablingServices.delete(serviceName);\n  });\n  this.enablingServices.set(serviceName, result);\n  return result;\n}\n"
  },
  {
    "path": "server/src/ServiceManager/getDockerBuildHash.ts",
    "content": "import crypto from \"crypto\";\nimport { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { join, relative } from \"path\";\n\nexport const getDockerBuildHash = async (\n  contextDir: string,\n  buildArgs: string[],\n): Promise<string> => {\n  if (!existsSync(join(contextDir, \"Dockerfile\"))) {\n    throw new Error(`Service Dockerfile not found in: ${contextDir}`);\n  }\n  const hashes: Map<string, string> = new Map();\n\n  const files = await getFilesRecursively(contextDir);\n  files.sort();\n\n  for (const file of files) {\n    const fullPath = join(contextDir, file);\n    hashes.set(file, calculateFileHash(fullPath));\n  }\n\n  const combinedHashInput = Array.from(hashes.entries())\n    .map(([file, hash]) => `${file}:${hash}`)\n    .join(\"\\n\");\n\n  const contextHash = crypto\n    .createHash(\"sha256\")\n    .update(combinedHashInput + JSON.stringify(buildArgs))\n    .digest(\"hex\");\n\n  return contextHash;\n};\nconst getFilesRecursively = async (\n  dir: string,\n  fileList: string[] = [],\n  baseDir = dir,\n) => {\n  const files = readdirSync(dir, { withFileTypes: true });\n\n  for (const file of files) {\n    const filePath = join(dir, file.name);\n    const relativePath = relative(baseDir, filePath);\n\n    if (file.isDirectory()) {\n      await getFilesRecursively(filePath, fileList, baseDir);\n    } else {\n      fileList.push(relativePath);\n    }\n  }\n\n  return fileList;\n};\n\nconst calculateFileHash = (filePath: string) => {\n  const content = readFileSync(filePath);\n  return crypto.createHash(\"sha256\").update(content).digest(\"hex\");\n};\n"
  },
  {
    "path": "server/src/ServiceManager/getSelectedConfigEnvs.ts",
    "content": "import type { DBS } from \"..\";\nimport {\n  prostglesServices,\n  type ProstglesService,\n} from \"./ServiceManagerTypes\";\n\nexport const getSelectedConfigEnvs = async (\n  dbs: DBS | undefined,\n  serviceName: keyof typeof prostglesServices,\n) => {\n  const serviceConfig = prostglesServices[serviceName] as ProstglesService;\n  const serviceRecord = await dbs?.services.findOne({ name: serviceName });\n  let env = serviceConfig.env || {};\n  const buildArgs: string[] = [];\n  let gpus = serviceConfig.gpus;\n  const { configs } = serviceConfig;\n  if (configs && serviceRecord?.selected_config_options) {\n    for (const [configKey, configValue] of Object.entries(\n      serviceRecord.selected_config_options,\n    )) {\n      const config = configs[configKey];\n      if (config) {\n        const option = config.options[configValue];\n        if (option) {\n          env = {\n            ...env,\n            ...option.env,\n          };\n          if (option.buildArgs) {\n            for (const [buildArgKey, buildArgValue] of Object.entries(\n              option.buildArgs,\n            )) {\n              buildArgs.push(`--build-arg`, `${buildArgKey}=${buildArgValue}`);\n            }\n          }\n          if (\"gpus\" in option) {\n            gpus = option.gpus;\n          }\n        }\n      }\n    }\n  }\n\n  return {\n    gpus,\n    env,\n    buildArgs,\n  };\n};\n"
  },
  {
    "path": "server/src/ServiceManager/getServiceEndoints.ts",
    "content": "import { getEntries } from \"@common/utils\";\nimport { getJSONBSchemaValidationError } from \"prostgles-types\";\nimport type {\n  ProstglesService,\n  RunningServiceInstance,\n} from \"./ServiceManagerTypes\";\n\nexport const getServiceEndoints = <S extends ProstglesService>({\n  serviceName,\n  endpoints,\n  baseUrl,\n}: {\n  serviceName: string;\n  baseUrl: string;\n  endpoints: ProstglesService[\"endpoints\"];\n}): RunningServiceInstance<S>[\"endpoints\"] => {\n  return Object.fromEntries(\n    getEntries(endpoints).map(\n      ([endpoint, { inputSchema, outputSchema, method, inputType }]) => [\n        endpoint,\n        async (args: unknown, fetchOptions) => {\n          if (!inputSchema && args) {\n            throw new Error(\"No input expected\");\n          }\n          const validatedInput =\n            inputSchema ?\n              getJSONBSchemaValidationError(inputSchema, args)\n            : undefined;\n          if (validatedInput?.error !== undefined) {\n            throw new Error(\n              `Invalid input for endpoint ${endpoint} of service ${serviceName}: ${validatedInput.error}`,\n            );\n          }\n\n          const { data } = validatedInput ?? {};\n          const asQueryOrBody =\n            (inputType ?? method === \"POST\") ? \"body\" : \"query\";\n          const query =\n            asQueryOrBody === \"query\" && data ?\n              `?${new URLSearchParams(\n                data as Record<string, string>,\n              ).toString()}`\n            : \"\";\n\n          const response = await fetch(`${baseUrl}${endpoint}${query}`, {\n            ...fetchOptions,\n            body: asQueryOrBody === \"body\" ? (data as string) : undefined,\n            method: method,\n          });\n          if (!response.ok) {\n            const errorText = await response.text().catch(() => \"\");\n            throw new Error(\n              `Error calling endpoint ${endpoint} of service ${serviceName}: ${response.status} ${response.statusText} ${errorText}`,\n            );\n          }\n\n          if (outputSchema) {\n            const responseData = await response.json();\n            const validatedOutput = getJSONBSchemaValidationError(\n              outputSchema,\n              responseData,\n              {\n                allowExtraProperties: true,\n              },\n            );\n            if (validatedOutput.error !== undefined) {\n              throw new Error(\n                `Invalid output from endpoint ${endpoint} of service ${serviceName}: ${validatedOutput.error}`,\n              );\n            }\n            //@ts-ignore\n            return validatedOutput.data as unknown;\n          }\n\n          return undefined;\n        },\n      ],\n    ),\n  ) as RunningServiceInstance<S>[\"endpoints\"];\n};\n"
  },
  {
    "path": "server/src/ServiceManager/initialiseServices.ts",
    "content": "import type { DBSSchemaForInsert } from \"@common/publishUtils\";\n\nimport type { DBS } from \"..\";\nimport {\n  prostglesServices,\n  type ProstglesService,\n} from \"./ServiceManagerTypes\";\nimport type { ServiceManager } from \"./ServiceManager\";\n\nexport const initialiseServices = async (\n  serviceManager: ServiceManager,\n  dbs: DBS,\n) => {\n  await dbs.services\n    .insert(\n      Object.entries(prostglesServices as Record<string, ProstglesService>).map(\n        ([name, service]) =>\n          ({\n            name,\n            label: service.label,\n            icon: service.icon,\n            default_port: service.hostPort ?? service.port,\n            description: service.description,\n            configs: service.configs,\n            status: \"stopped\",\n          }) satisfies DBSSchemaForInsert[\"services\"],\n      ),\n      {\n        onConflict: \"DoNothing\",\n      },\n    )\n    .then(() => {\n      void dbs.services.find({ status: \"running\" }).then((services) => {\n        const activeServiceNames: string[] = [];\n        services.forEach((service) => {\n          activeServiceNames.push(service.name);\n          console.log(\"Re-enabling service on startup: \", service.name);\n          serviceManager\n            .enableService(\n              service.name as keyof typeof prostglesServices,\n              () => {},\n            )\n            .catch(console.error);\n        });\n        serviceManager.activeServices.forEach((_, serviceName) => {\n          if (!activeServiceNames.includes(serviceName)) {\n            console.log(\n              \"Removing inactive service from activeServices:\",\n              serviceName,\n            );\n            serviceManager.activeServices.delete(serviceName);\n          }\n        });\n      });\n    });\n};\n"
  },
  {
    "path": "server/src/ServiceManager/services/speechToText/speechToText.service.ts",
    "content": "import type { ProstglesService } from \"../../ServiceManagerTypes\";\n\nexport const speechToTextService = {\n  icon: \"MicrophoneMessage\",\n  label: \"Speech to Text\",\n  port: 8000,\n  env: {\n    WHISPER_MODEL: \"small\",\n    MODEL_CACHE_DIR: \"/app/models\",\n    HF_HOME: \"/app/models\",\n  },\n  hostPort: 8100,\n  configs: {\n    model: {\n      label: \"Model\",\n      description: \"Select the Whisper model size to use for transcription.\",\n      defaultOption: \"small\",\n      options: Object.fromEntries(\n        [\n          \"tiny.en\",\n          \"tiny\",\n          \"base.en\",\n          \"base\",\n          \"small.en\",\n          \"small\",\n          \"medium.en\",\n          \"medium\",\n          \"large-v1\",\n          \"large-v2\",\n          \"large-v3\",\n          \"large\",\n          \"distil-large-v2\",\n          \"distil-medium.en\",\n          \"distil-small.en\",\n          \"distil-large-v3\",\n          \"distil-large-v3.5\",\n          \"large-v3-turbo\",\n          \"turbo\",\n        ].map((modelName) => [\n          modelName,\n          { env: { WHISPER_MODEL: modelName } },\n        ]),\n      ),\n    },\n    device: {\n      label: \"Device\",\n      description: \"Select the device to run the Whisper model on.\",\n      defaultOption: \"cpu\",\n      options: {\n        cpu: { env: { WHISPER_DEVICE: \"cpu\" } },\n        cuda: {\n          env: { WHISPER_DEVICE: \"cuda\" },\n          buildArgs: {\n            BASE_IMAGE: \"nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04\",\n          },\n          gpus: \"all\",\n        },\n      },\n    },\n    language: {\n      label: \"Language\",\n      description:\n        \"Select the language for transcription. 'auto' will auto-detect the language.\",\n      defaultOption: \"auto\",\n      options: {\n        auto: { env: { WHISPER_LANGUAGE: \"\" } },\n        en: { env: { WHISPER_LANGUAGE: \"en\" } },\n        es: { env: { WHISPER_LANGUAGE: \"es\" } },\n        fr: { env: { WHISPER_LANGUAGE: \"fr\" } },\n        de: { env: { WHISPER_LANGUAGE: \"de\" } },\n        zh: { env: { WHISPER_LANGUAGE: \"zh\" } },\n        ja: { env: { WHISPER_LANGUAGE: \"ja\" } },\n        ru: { env: { WHISPER_LANGUAGE: \"ru\" } },\n        ro: { env: { WHISPER_LANGUAGE: \"ro\" } },\n        it: { env: { WHISPER_LANGUAGE: \"it\" } },\n        pt: { env: { WHISPER_LANGUAGE: \"pt\" } },\n        ar: { env: { WHISPER_LANGUAGE: \"ar\" } },\n        hi: { env: { WHISPER_LANGUAGE: \"hi\" } },\n      },\n    },\n  },\n  volumes: {\n    \"whisper-models\": \"/app/models\",\n  },\n  healthCheck: { endpoint: \"/health\" },\n  description:\n    \"Speech-to-Text Service using Faster-Whisper. Used in the AI Assistant chat.\",\n  endpoints: {\n    \"/\": {\n      method: \"GET\",\n      inputSchema: undefined,\n      description: \"HTML page with voice recorder UI\",\n      outputSchema: {\n        type: \"string\",\n      },\n    },\n    \"/transcribe\": {\n      method: \"POST\",\n      description: \"Audio file upload for speech-to-text transcription\",\n      inputSchema: {\n        type: \"any\",\n        description: \"Audio file as multipart/form-data (webm, mp3, wav, etc.)\",\n      },\n      outputSchema: {\n        oneOf: [\n          {\n            type: {\n              success: {\n                type: \"boolean\",\n              },\n              transcription: {\n                type: \"string\",\n                description: \"The transcribed text from the audio\",\n              },\n              language: {\n                type: \"string\",\n                description: \"Detected language code (e.g., 'en', 'es', 'fr')\",\n              },\n              language_probability: {\n                type: \"number\",\n                description: \"Confidence score for detected language (0-1)\",\n              },\n              segments: {\n                arrayOfType: {\n                  start: {\n                    type: \"number\",\n                    description: \"Segment start time in seconds\",\n                  },\n                  end: {\n                    type: \"number\",\n                    description: \"Segment end time in seconds\",\n                  },\n                  text: {\n                    type: \"string\",\n                    description: \"Transcribed text for this segment\",\n                  },\n                },\n                description: \"Array of transcription segments with timestamps\",\n              },\n            },\n            description: \"Successful transcription response\",\n          },\n          {\n            type: {\n              error: {\n                type: \"string\",\n                description: \"Error message describing what went wrong\",\n              },\n            },\n            description: \"Error response\",\n          },\n        ],\n        description: \"Transcription result or error\",\n      },\n    },\n    \"/health\": {\n      method: \"GET\",\n      description: \"Health check response\",\n      inputSchema: undefined,\n      outputSchema: {\n        type: {\n          status: {\n            type: \"string\",\n            allowedValues: [\"healthy\"],\n            description: \"Health check status\",\n          },\n        },\n      },\n    },\n  },\n} as const satisfies ProstglesService;\n"
  },
  {
    "path": "server/src/ServiceManager/services/speechToText/src/Dockerfile",
    "content": "# FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04\nARG BASE_IMAGE=python:3.11-slim\nFROM ${BASE_IMAGE}\n\n# Set working directory\nWORKDIR /app\n\n# Install system dependencies\nRUN apt-get update -y && apt-get install -y \\\n    ffmpeg \\\n    python3-pip \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy requirements first for better caching\nCOPY requirements.txt .\n\n# Install Python dependencies\nRUN pip3 install --no-cache-dir -r requirements.txt\n\n# Copy application files\nCOPY app.py .\n\n# Expose port\nEXPOSE 8000\n\n# Set environment variables\nENV PYTHONUNBUFFERED=1\nENV NVIDIA_VISIBLE_DEVICES=all\nENV NVIDIA_DRIVER_CAPABILITIES=compute,utility\n\n# Run the application\nCMD [\"python3\", \"app.py\"]"
  },
  {
    "path": "server/src/ServiceManager/services/speechToText/src/app.py",
    "content": "import os\nimport tempfile\nimport logging\nfrom flask import Flask, request, jsonify\nfrom faster_whisper import WhisperModel\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\napp = Flask(__name__)\n\n# Configuration\n\n# Options: tiny, base, small, medium, large-v2,  large-v3, whisper-large-v3-ct2, distil-whisper-large-v3\nMODEL_SIZE = os.environ.get(\"WHISPER_MODEL\", \"base\")  \n# cpu or cuda\nDEVICE = os.environ.get(\"WHISPER_DEVICE\", \"cpu\")  \n# int8, float16\n# COMPUTE_TYPE = os.environ.get(\"WHISPER_COMPUTE_TYPE\", \"int8\")  \nCOMPUTE_TYPE = os.environ.get(\"WHISPER_COMPUTE_TYPE\", \"float16\" if DEVICE == \"cuda\" else \"int8\")\n\nLANGUAGE = os.environ.get(\"WHISPER_LANGUAGE\", None)  # e.g., \"en\" for English, None for auto-detect\n\n# Initialize the model (lazy loading)\nmodel = None\n\ndef get_model():\n    global model\n    if model is None:\n        logger.info(f\"Loading Whisper model: {MODEL_SIZE} on {DEVICE} with {COMPUTE_TYPE}. Language: {LANGUAGE}\")\n        model = WhisperModel(MODEL_SIZE, device=DEVICE, compute_type=COMPUTE_TYPE)\n        logger.info(\"Model loaded successfully\")\n    return model\n\n@app.route('/transcribe', methods=['POST'])\ndef transcribe():\n    if 'audio' not in request.files:\n        return jsonify({'error': 'No audio file provided'}), 400\n    \n    audio_file = request.files['audio']\n    \n    if audio_file.filename == '':\n        return jsonify({'error': 'No audio file selected'}), 400\n    \n\n    try:\n        # Parse user options from form data\n        task = request.form.get('task', 'transcribe')  # transcribe or translate\n        initial_prompt = request.form.get('initial_prompt', None)\n        word_timestamps = request.form.get('word_timestamps', 'false').lower() == 'true'\n\n        # Save the uploaded file temporarily\n        with tempfile.NamedTemporaryFile(delete=False, suffix='.webm') as tmp_file:\n            audio_file.save(tmp_file.name)\n            tmp_path = tmp_file.name\n        \n        logger.info(f\"Processing audio file: {tmp_path}\")\n        \n        # Get the model and transcribe\n        whisper_model = get_model()\n        segments, info = whisper_model.transcribe(\n            tmp_path, \n            language=LANGUAGE,\n            task=task,\n            beam_size=5,\n            vad_filter=True,\n            initial_prompt=initial_prompt,\n            word_timestamps=word_timestamps\n        )\n        \n        # Collect all segments\n        transcription = \"\"\n        segments_list = []\n        for segment in segments:\n            transcription += segment.text\n            segments_list.append({\n                'start': round(segment.start, 2),\n                'end': round(segment.end, 2),\n                'text': segment.text\n            })\n        \n        # Clean up temp file\n        os.unlink(tmp_path)\n        \n        logger.info(f\"Transcription complete. Detected language: {info.language}\")\n        \n        return jsonify({\n            'success': True,\n            'transcription': transcription.strip(),\n            'language': info.language,\n            'language_probability': round(info.language_probability, 2),\n            'segments': segments_list\n        })\n        \n    except Exception as e:\n        logger.error(f\"Error during transcription: {str(e)}\")\n        # Clean up temp file if it exists\n        if 'tmp_path' in locals():\n            try:\n                os.unlink(tmp_path)\n            except:\n                pass\n        return jsonify({'error': str(e)}), 500\n\n@app.route('/health')\ndef health():\n    return jsonify({'status': 'healthy'})\n\nif __name__ == '__main__':\n    # Preload model on startup (optional - comment out for faster startup)\n    logger.info(\"Starting Voice Recorder STT Server...\")\n    get_model()  # Preload the model\n    app.run(host='0.0.0.0', port=8000, debug=False)\n\n"
  },
  {
    "path": "server/src/ServiceManager/services/speechToText/src/requirements.txt",
    "content": "flask>=3.0.0\nfaster-whisper>=1.1.0\nwerkzeug>=3.0.0\n"
  },
  {
    "path": "server/src/ServiceManager/services/webSearchSearxng/src/Dockerfile",
    "content": "FROM searxng/searxng:latest\n\n# Copy custom settings file\nCOPY settings.yml /etc/searxng/settings.yml\nCOPY limiter.toml /etc/searxng/limiter.toml\n\n# Set proper permissions\nUSER root\nRUN chown -R searxng:searxng /etc/searxng/settings.yml\nRUN chown -R searxng:searxng /etc/searxng/limiter.toml\nUSER searxng"
  },
  {
    "path": "server/src/ServiceManager/services/webSearchSearxng/src/limiter.toml",
    "content": ""
  },
  {
    "path": "server/src/ServiceManager/services/webSearchSearxng/src/settings.yml",
    "content": "use_default_settings: true\n\nsearch:\n  formats:\n    - html\n    - json\n\nserver:\n  secret_key: \"changeme\"\n  limiter: false\n  image_proxy: true\n"
  },
  {
    "path": "server/src/ServiceManager/services/webSearchSearxng/webSearchSearxng.service.ts",
    "content": "import { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport type { JSONB } from \"prostgles-types\";\nimport type { ProstglesService } from \"../../ServiceManagerTypes\";\n\nconst inputSchema = {\n  type: {\n    ...PROSTGLES_MCP_SERVERS_AND_TOOLS[\"websearch\"][\"websearch\"][\"schema\"].type,\n    format: { enum: [\"json\"] },\n  },\n} as const satisfies JSONB.FieldType;\n\nexport const webSearchSearxngService = {\n  icon: \"Web\",\n  label: \"Web Search\",\n  port: 8080,\n  hostPort: 8888,\n  healthCheck: { method: \"GET\", endpoint: \"/search\" },\n  description: \"Web search using searxng. Used in the AI Assistant chat.\",\n  endpoints: {\n    \"/search\": {\n      method: \"GET\",\n      description: \"SearXNG search endpoint\",\n\n      inputSchema: inputSchema,\n      outputSchema: {\n        type: {\n          results: {\n            ...PROSTGLES_MCP_SERVERS_AND_TOOLS[\"websearch\"][\"websearch\"][\n              \"outputSchema\"\n            ],\n          },\n        },\n      },\n    },\n  },\n} as const satisfies ProstglesService;\n"
  },
  {
    "path": "server/src/ServiceManager/startService.ts",
    "content": "import { spawn } from \"child_process\";\nimport { tout } from \"..\";\nimport type { ServiceManager } from \"./ServiceManager\";\nimport {\n  prostglesServices,\n  type OnServiceLogs,\n  type ProstglesService,\n  type RunningServiceInstance,\n  type ServiceInstance,\n} from \"./ServiceManagerTypes\";\nimport { camelCaseToSkewerCase } from \"./buildService\";\nimport { getServiceEndoints } from \"./getServiceEndoints\";\nimport { getSelectedConfigEnvs } from \"./getSelectedConfigEnvs\";\nimport {\n  executeDockerCommand,\n  type ExecutionResult,\n  type ProcessLog,\n} from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/executeDockerCommand\";\nimport { getFreePort } from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/dockerMCPServerProxy/isPortFree\";\n\nexport async function startService(\n  this: ServiceManager,\n  serviceName: keyof typeof prostglesServices,\n  onLogs: OnServiceLogs,\n): Promise<Extract<ServiceInstance, { status: \"running\" }>> {\n  const onLogsCombined: OnServiceLogs = (logs) => {\n    onLogs(logs);\n    this.onServiceLog(serviceName, logs);\n  };\n  console.log(\"starting Service \" + serviceName);\n  const { labelArgs } = this.getActiveService(serviceName, \"building-done\");\n\n  const serviceConfig: ProstglesService = prostglesServices[serviceName];\n  const abortController = new AbortController();\n  let logs: ProcessLog[] = [];\n  const getLogs = () => {\n    return logs;\n  };\n  const stop = () => abortController.abort();\n  this.activeServices.set(serviceName, { getLogs, stop, status: \"starting\" });\n  const imageName = camelCaseToSkewerCase(serviceName);\n  const containerName = `prostgles-service-${imageName}`;\n\n  const cleanup = () => {\n    spawn(\"docker\", [\"stop\", \"-t\", \"0\", containerName], { stdio: \"ignore\" });\n  };\n  process.once(\"exit\", cleanup);\n  process.once(\"SIGINT\", cleanup);\n  process.once(\"SIGTERM\", cleanup);\n  process.once(\"uncaughtException\", cleanup);\n\n  const {\n    port,\n    hostPort: preferredHostPort = port,\n    volumes = {},\n    healthCheck,\n    endpoints,\n  } = serviceConfig;\n  const hostPort = await getFreePort(preferredHostPort);\n  const volumeArgs: string[] = [];\n  for (const [volumeName, containerPath] of Object.entries(volumes)) {\n    const hostPath = `prostgles-service-${imageName}-${volumeName}`;\n    await executeDockerCommand([\"volume\", \"create\", hostPath], {\n      timeout: 10_000,\n    });\n    volumeArgs.push(\"-v\", `${hostPath}:${containerPath}`);\n  }\n\n  const { env, gpus = \"none\" } = await getSelectedConfigEnvs(\n    this.dbs,\n    serviceName,\n  );\n\n  const baseHost = `127.0.0.1:${hostPort}`;\n\n  const onStopped = (\n    res: ExecutionResult | { type: \"error\"; error: unknown },\n  ) => {\n    const stopReason = \"type\" in res ? res.type : res.state;\n    const instance: ServiceInstance = {\n      status: \"error\",\n      error: new Error(\n        `Service ${serviceName} stopped unexpectedly with state: ${stopReason}`,\n      ),\n    };\n    this.activeServices.set(serviceName, instance);\n    this.onServiceLog(serviceName, logs);\n  };\n\n  executeDockerCommand(\n    [\n      \"run\",\n      \"-i\",\n      \"--rm\",\n      \"--init\",\n      ...labelArgs,\n      \"--name\",\n      containerName,\n      ...(gpus !== \"none\" ?\n        [\n          \"--gpus\",\n          Array.isArray(gpus) ? `\"device=${gpus.join(\",\")}\"` : gpus.toString(),\n        ]\n      : []),\n      \"-p\",\n      `${baseHost}:${port}`,\n      ...volumeArgs,\n      ...Object.entries(env).flatMap(([key, value]) => [\n        \"-e\",\n        `${key}=${value}`,\n      ]),\n      imageName,\n    ],\n    {\n      signal: abortController.signal,\n    },\n    (newLogs) => {\n      logs = newLogs;\n      onLogsCombined(logs);\n    },\n  )\n    .then((res) => {\n      onStopped(res);\n    })\n    .catch((error: unknown) => {\n      onStopped({ type: \"error\", error });\n    });\n\n  const baseUrl = `http://${baseHost}`;\n  while (this.activeServices.get(serviceName)?.status === \"starting\") {\n    const clientIp = \"127.0.0.1\";\n    const healthCheckResponse = await fetch(\n      `${baseUrl}${healthCheck.endpoint}`,\n      {\n        method: healthCheck.method ?? \"GET\",\n        headers: {\n          \"X-Forwarded-For\": clientIp,\n          \"X-Real-IP\": clientIp,\n        },\n      },\n    ).catch(() => null);\n    if (healthCheckResponse?.ok) {\n      break;\n    }\n    await tout(1000);\n  }\n\n  const serviceInstance = this.activeServices.get(serviceName);\n  if (serviceInstance?.status !== \"starting\") {\n    const error = new Error(\n      \"Healthcheck not finished. Service failed to start. Current status:\" +\n        serviceInstance?.status,\n    );\n    onStopped({\n      type: \"error\",\n      error,\n    });\n    return Promise.reject(error);\n  }\n\n  const runningService: RunningServiceInstance = {\n    status: \"running\",\n    getLogs,\n    stop,\n    endpoints: getServiceEndoints({ serviceName, baseUrl, endpoints }),\n  };\n  //@ts-ignore\n  this.activeServices.set(serviceName, runningService);\n  this.onServiceLog(serviceName, logs);\n\n  console.log(\"started Service \" + serviceName);\n  return runningService;\n}\n\nexport const getContainerName = (\n  serviceName: keyof typeof prostglesServices,\n) => {\n  return `prostgles-service-${camelCaseToSkewerCase(serviceName)}`;\n};\n"
  },
  {
    "path": "server/src/ServiceManager/stopService.ts",
    "content": "import { spawn } from \"node:child_process\";\nimport type { ServiceManager } from \"./ServiceManager\";\nimport type { prostglesServices } from \"./ServiceManagerTypes\";\nimport { getContainerName } from \"./startService\";\n\nexport function stopService(\n  this: ServiceManager,\n  serviceName: keyof typeof prostglesServices,\n) {\n  try {\n    const service = this.getService(serviceName);\n    if (service && \"stop\" in service) {\n      service.stop();\n    }\n  } catch {}\n  const containerName = getContainerName(serviceName);\n  spawn(\"docker\", [\"stop\", \"-t\", \"0\", containerName], { stdio: \"ignore\" });\n  this.activeServices.delete(serviceName);\n  this.onServiceLog(serviceName, []);\n}\n"
  },
  {
    "path": "server/src/authConfig/OAuthProviders/getOAuthLoginProviders.ts",
    "content": "import type { LoginWithOAuthConfig } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { getFailedTooManyTimes } from \"../startRateLimitedLoginAttempt\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\n\ntype AuthProviders = DBSSchema[\"global_settings\"][\"auth_providers\"];\n\nexport const getOAuthLoginProviders = (\n  auth_providers: AuthProviders | undefined,\n) => {\n  const OAuthProviders = auth_providers && getOAuthProviders(auth_providers);\n  if (!OAuthProviders) return;\n  const loginWithOAuth: LoginWithOAuthConfig<DBGeneratedSchema> = {\n    websiteUrl: auth_providers.website_url,\n    OAuthProviders,\n    onProviderLoginFail: async ({ clientInfo, provider }) => {\n      // await startLoginAttempt(dbo, clientInfo, {\n      //   auth_type: \"provider\",\n      //   auth_provider: provider,\n      // });\n    },\n    onProviderLoginStart: async ({ dbo, clientInfo }) => {\n      const check = await getFailedTooManyTimes(dbo, clientInfo);\n      if (\"success\" in check) return check;\n      return check.failedTooManyTimes ?\n          {\n            success: false,\n            code: \"rate-limit-exceeded\",\n            message: \"Too many failed attempts\",\n          }\n        : { success: true };\n    },\n  };\n\n  return loginWithOAuth;\n};\n\nconst getOAuthProviders = (\n  auth_providers: AuthProviders,\n): LoginWithOAuthConfig<DBSSchema>[\"OAuthProviders\"] | undefined => {\n  if (!auth_providers) return undefined;\n  const getProvider = <\n    Conf extends { clientID: string; clientSecret: string; enabled?: boolean },\n  >(\n    conf: Conf | undefined,\n    providerName: string,\n  ): Conf | undefined => {\n    if (!conf?.enabled) return undefined;\n    if (!conf.clientID || !conf.clientSecret) {\n      console.warn(\n        `OAuth provider ${providerName} is missing clientID or clientSecret`,\n      );\n      return;\n    }\n    return conf;\n  };\n  const { google, microsoft, github, facebook } = auth_providers;\n  return {\n    google: getProvider(google, \"google\"),\n    facebook: getProvider(facebook, \"google\"),\n    microsoft: getProvider(microsoft, \"microsoft\"),\n    github: getProvider(github, \"github\"),\n  };\n};\n"
  },
  {
    "path": "server/src/authConfig/OAuthProviders/loginWithProvider.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type {\n  LoginClientInfo,\n  LoginParams,\n  LoginSignupConfig,\n} from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\nimport type { SUser } from \"../sessionUtils\";\nimport { startRateLimitedLoginAttempt } from \"../startRateLimitedLoginAttempt\";\nimport { upsertSession } from \"../upsertSession\";\n\ntype LoginReturnType = ReturnType<\n  Required<LoginSignupConfig<DBGeneratedSchema, SUser>>[\"login\"]\n>;\n\nexport const loginWithProvider = async (\n  loginParams: Extract<LoginParams, { type: \"OAuth\" }>,\n  db: DBOFullyTyped<DBGeneratedSchema>,\n  clientInfo: LoginClientInfo,\n): Promise<LoginReturnType> => {\n  const { user_agent } = clientInfo;\n  const { provider, profile } = loginParams;\n  const auth_provider_user_id = profile.id;\n  if (!auth_provider_user_id) {\n    return \"server-error\";\n  }\n  const username = `${provider}-${auth_provider_user_id}`;\n  const auth_provider = provider;\n  const name =\n    profile.displayName ||\n    (loginParams.provider === \"github\" ? loginParams.profile.username\n    : loginParams.provider === \"google\" ? loginParams.profile.name?.givenName\n    : loginParams.provider === \"facebook\" ? loginParams.profile.name?.givenName\n    : loginParams.profile.name?.givenName);\n  const email = profile.emails?.[0]?.value;\n  const rateLimitInfo = await startRateLimitedLoginAttempt(db, clientInfo, {\n    auth_type: \"oauth\",\n    auth_provider: provider,\n  });\n  if (\"success\" in rateLimitInfo) return rateLimitInfo.code;\n  const { onSuccess, ip, failedTooManyTimes } = rateLimitInfo;\n  if (failedTooManyTimes) {\n    return \"rate-limit-exceeded\";\n  }\n  await onSuccess();\n\n  const matchingUser = await db.users.findOne({ auth_provider, username });\n  if (matchingUser) {\n    const session = await upsertSession({\n      user: matchingUser,\n      ip,\n      user_agent,\n      db,\n    });\n    return { session, response: { success: true } };\n  } else {\n    const globalSettings = await db.global_settings.findOne();\n    const newUser = await db.users\n      .insert(\n        {\n          username,\n          name,\n          email,\n          auth_provider,\n          auth_provider_user_id,\n          auth_provider_profile: profile,\n          registration: {\n            type: \"OAuth\",\n            provider,\n            profile,\n            user_id: auth_provider_user_id,\n          },\n          type: globalSettings?.auth_created_user_type ?? \"default\",\n          status: \"active\",\n          password: \"\",\n        },\n        { returning: \"*\" },\n      )\n      .catch((e) => {\n        console.error(e);\n        return Promise.reject(\"Could not create user\");\n      });\n    const session = await upsertSession({\n      user: newUser,\n      ip,\n      db,\n      user_agent,\n    });\n    return { session, response: { success: true } };\n  }\n};\n"
  },
  {
    "path": "server/src/authConfig/authUtils.ts",
    "content": "import { pbkdf2Sync } from \"node:crypto\";\nimport type { User } from \"../ConnectionManager/ConnectionManager\";\n\nconst ITERATIONS = 1e5;\nconst KEY_LENGTH = 512;\nexport const getPasswordHash = (\n  { id }: Pick<User, \"id\">,\n  rawPassword: string,\n): string => {\n  const salt = Buffer.from(id).toString(\"base64\");\n  const pwdHash = pbkdf2Sync(\n    rawPassword,\n    salt,\n    ITERATIONS,\n    KEY_LENGTH,\n    \"sha512\",\n  ).toString(\"hex\");\n  return pwdHash;\n};\n"
  },
  {
    "path": "server/src/authConfig/createPasswordlessAdminSessionIfNeeded.ts",
    "content": "import type {\n  AuthClientRequest,\n  LoginClientInfo,\n} from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { DBS } from \"..\";\nimport { debouncePromise, YEAR } from \"@common/utils\";\nimport { activePasswordlessAdminFilter } from \"../SecurityManager/initUsers\";\nimport type { NewRedirectSession } from \"./getUser\";\nimport { makeSession } from \"./sessionUtils\";\nimport { getIPsFromClientInfo } from \"./startRateLimitedLoginAttempt\";\nimport type { AuthSetupData } from \"./subscribeToAuthSetupChanges\";\n\nexport const createPasswordlessAdminSessionIfNeeded = debouncePromise(\n  async (\n    authSetupData: AuthSetupData,\n    dbs: DBS,\n    client: LoginClientInfo,\n    reqInfo: AuthClientRequest,\n  ): Promise<NewRedirectSession | undefined> => {\n    const { passwordlessAdmin, globalSettings } = authSetupData;\n    if (!passwordlessAdmin || !globalSettings || !reqInfo.httpReq) {\n      return;\n    }\n\n    /**\n     * Always maintain a valid session for passwordless admin\n     */\n    if (passwordlessAdmin.sessions.length) {\n      const validSession = passwordlessAdmin.sessions.find((s) => {\n        return s.active && Number(s.expires) > Date.now();\n      });\n      if (!validSession) {\n        const anyPasswordlessSession = await dbs.sessions.findOne({\n          user_id: passwordlessAdmin.id,\n        });\n        if (anyPasswordlessSession) {\n          await dbs.sessions.update(\n            { id: anyPasswordlessSession.id },\n            { active: true, expires: Date.now() + 1 * YEAR },\n            { returning: \"*\", multi: false },\n          );\n        }\n      }\n      return;\n    }\n\n    const { ip } = getIPsFromClientInfo(client, globalSettings);\n    /** Ensure multiple passwordlessAdmin sessions are not allowed */\n    const session = await dbs.tx(async (dbsTx) => {\n      const isStillActive = await dbsTx.users.findOne(\n        activePasswordlessAdminFilter,\n      );\n      if (!isStillActive) {\n        return undefined;\n      }\n      await dbsTx.sessions.delete({\n        user_id: passwordlessAdmin.id,\n      });\n      console.log(\n        \"createPasswordlessAdminSessionIfNeeded: creating new session\",\n        passwordlessAdmin.id,\n      );\n      return makeSession(\n        authSetupData.passwordlessAdmin,\n        {\n          ip_address: ip,\n          user_agent: client.user_agent || null,\n          type: \"web\",\n        },\n        dbsTx as DBS,\n        Date.now() + Number(10 * YEAR),\n      );\n    });\n\n    /** Potential race condition between authSetupData and actual data */\n    if (!session) {\n      return undefined;\n    }\n\n    return {\n      type: \"new-session\",\n      session,\n      reqInfo,\n    };\n  },\n);\n"
  },
  {
    "path": "server/src/authConfig/createPublicUserSessionIfAllowed.ts",
    "content": "import type {\n  AuthClientRequest,\n  LoginClientInfo,\n} from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { connMgr, type DBS } from \"..\";\nimport { insertUser, makeSession } from \"./sessionUtils\";\nimport { getIPsFromClientInfo } from \"./startRateLimitedLoginAttempt\";\nimport type { AuthSetupData } from \"./subscribeToAuthSetupChanges\";\nimport { DAY } from \"@common/utils\";\nimport type { NewRedirectSession } from \"./getUser\";\n\nexport const createPublicUserSessionIfAllowed = async (\n  authSetupData: AuthSetupData,\n  dbs: DBS,\n  client: LoginClientInfo,\n  reqInfo: AuthClientRequest,\n): Promise<NewRedirectSession | undefined> => {\n  const publicConnections = connMgr.getConnectionsWithPublicAccess();\n  const { globalSettings } = authSetupData;\n  if (!publicConnections.length || !globalSettings || !reqInfo.httpReq) {\n    return;\n  }\n  const { ip } = getIPsFromClientInfo(client, globalSettings);\n  const session = await dbs.tx(async (dbsTx) => {\n    const newRandomUser = await insertUser(dbsTx, {\n      username: `user-${new Date().toISOString()}_${Math.round(Math.random() * 1e8)}`,\n      password: \"\",\n      type: \"public\",\n    });\n    if (!newRandomUser) {\n      return;\n    }\n\n    return makeSession(\n      newRandomUser,\n      {\n        ip_address: ip,\n        user_agent: client.user_agent || null,\n        type: \"web\",\n      },\n      dbsTx as DBS,\n      Date.now() + Number(7 * DAY),\n    );\n  });\n\n  if (!session) {\n    return;\n  }\n  return {\n    type: \"new-session\",\n    session,\n    reqInfo,\n  };\n};\n"
  },
  {
    "path": "server/src/authConfig/emailProvider/getEmailAuthProvider.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport type { SignupWithEmail } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { DBS } from \"../..\";\nimport { getEmailSenderWithMockTest } from \"./getEmailSenderWithMockTest\";\nimport { onEmailRegistration } from \"./onEmailRegistration\";\n\nexport const getEmailAuthProvider = async (\n  {\n    auth_providers,\n    auth_created_user_type,\n  }: Pick<\n    DBSSchema[\"global_settings\"],\n    \"auth_providers\" | \"auth_created_user_type\"\n  >,\n  dbs: DBS | undefined,\n): Promise<SignupWithEmail | undefined> => {\n  const { email: emailAuthConfig, website_url } = auth_providers ?? {};\n  if (\n    !emailAuthConfig?.enabled ||\n    !dbs ||\n    emailAuthConfig.signupType !== \"withPassword\"\n  ) {\n    return undefined;\n  }\n  if (!website_url) throw \"website_url is required for email auth\";\n\n  const mailClient = await getEmailSenderWithMockTest(auth_providers);\n\n  return {\n    minPasswordLength:\n      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n      emailAuthConfig.signupType === \"withPassword\" ?\n        emailAuthConfig.minPasswordLength\n      : undefined,\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    requirePassword: emailAuthConfig.signupType === \"withPassword\",\n    onRegister: async (args) =>\n      onEmailRegistration(args, {\n        mailClient,\n        dbs,\n        websiteUrl: website_url,\n        newUserType: auth_created_user_type ?? \"default\",\n      }),\n  };\n};\n"
  },
  {
    "path": "server/src/authConfig/emailProvider/getEmailSenderWithMockTest.ts",
    "content": "import type { Email } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { getEmailSender } from \"prostgles-server/dist/Prostgles\";\nimport {\n  getMagicLinkEmailFromTemplate,\n  getVerificationEmailFromTemplate,\n  MOCK_SMTP_HOST,\n} from \"@common/OAuthUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { Unpromise } from \"../../ConnectionManager/ConnectionManager\";\nimport { tout } from \"../..\";\n\nexport const getSMTPWithTLS = (\n  smtp: NonNullable<\n    NonNullable<DBSSchema[\"global_settings\"][\"auth_providers\"]>[\"email\"]\n  >[\"smtp\"],\n) => {\n  return {\n    ...smtp,\n    ...(smtp.type === \"smtp\" &&\n      smtp.rejectUnauthorized !== undefined && {\n        tls: { rejectUnauthorized: smtp.rejectUnauthorized },\n      }),\n  };\n};\n\nexport type EmailClient = Unpromise<\n  ReturnType<typeof getEmailSenderWithMockTest>\n>;\n\nexport const getEmailSenderWithMockTest = async (\n  auth_providers: DBSSchema[\"global_settings\"][\"auth_providers\"] | undefined,\n) => {\n  const { email, website_url } = auth_providers ?? {};\n  if (!email || !website_url) return undefined;\n  const { smtp: smtpWithoutTLS, emailTemplate } = email;\n  const smtp = getSMTPWithTLS(smtpWithoutTLS);\n  let sendEmail: Awaited<ReturnType<typeof getEmailSender>>[\"sendEmail\"];\n\n  /**\n   * Mock email sending for testing\n   * TODO: identify why e2e/tests/mockSMTPServer.ts is not working\n   * from playwright tests while it works from nodejs\n   */\n  if (smtp.type === \"smtp\" && smtp.host === MOCK_SMTP_HOST) {\n    sendEmail = async (_email: Email) => {\n      console.log(\"Mock email sent\", _email);\n      await tout(100);\n    };\n  } else {\n    ({ sendEmail } = await getEmailSender(smtp, website_url));\n  }\n\n  return {\n    sendEmail,\n    sendEmailVerification: ({ to, code, verificationUrl }: EmailData) => {\n      const verificationEmail = getVerificationEmailFromTemplate({\n        code,\n        url: verificationUrl,\n        template: emailTemplate,\n      });\n      return sendEmail({\n        to,\n        from: emailTemplate.from,\n        subject: verificationEmail.subject,\n        html: verificationEmail.body,\n      });\n    },\n    sendMagicLinkEmail: ({\n      to,\n      url,\n      code,\n    }: {\n      to: string;\n      url: string;\n      code: string;\n    }) => {\n      const magicLinkEmail = getMagicLinkEmailFromTemplate({\n        template: emailTemplate,\n        url,\n        code,\n      });\n      return sendEmail({\n        to,\n        from: emailTemplate.from,\n        subject: magicLinkEmail.subject,\n        html: magicLinkEmail.body,\n      });\n    },\n  };\n};\n\ntype EmailData = {\n  code: string;\n  to: string;\n  verificationUrl: string;\n};\n"
  },
  {
    "path": "server/src/authConfig/emailProvider/onEmailRegistration.ts",
    "content": "import { randomInt } from \"crypto\";\nimport type { SignupWithEmail } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { AuthResponse } from \"prostgles-types\";\nimport type { DBS } from \"../..\";\nimport { getPasswordHash } from \"../authUtils\";\nimport type { EmailClient } from \"./getEmailSenderWithMockTest\";\nimport { startRateLimitedLoginAttempt } from \"../startRateLimitedLoginAttempt\";\nimport type { DBSSchema } from \"@common/publishUtils\";\n\nexport const onEmailRegistration = async (\n  {\n    email,\n    clientInfo,\n    password,\n    getConfirmationUrl,\n  }: Parameters<SignupWithEmail[\"onRegister\"]>[0],\n  {\n    dbs,\n    mailClient,\n    newUserType,\n    websiteUrl,\n  }: {\n    dbs: DBS;\n    mailClient: EmailClient;\n    newUserType: DBSSchema[\"users\"][\"type\"];\n    websiteUrl: string;\n  },\n): Promise<ReturnType<SignupWithEmail[\"onRegister\"]>> => {\n  const withErrorCode = (\n    code: AuthResponse.PasswordRegisterFailure[\"code\"],\n    message?: string,\n  ) =>\n    ({\n      success: false,\n      code,\n      message,\n    }) satisfies AuthResponse.PasswordRegisterFailure;\n  if (!mailClient) {\n    return withErrorCode(\"server-error\", \"Email client not found\");\n  }\n  const registrationAttempt = await startRateLimitedLoginAttempt(\n    dbs,\n    clientInfo,\n    {\n      auth_type: \"registration\",\n      username: email,\n    },\n  );\n  if (\"success\" in registrationAttempt)\n    return withErrorCode(\"server-error\", registrationAttempt.message);\n  if (registrationAttempt.failedTooManyTimes) {\n    return withErrorCode(\"rate-limit-exceeded\", \"Rate limit exceeded\");\n  }\n  const existingUser = await dbs.users.findOne({\n    username: email,\n    email,\n  });\n  const email_confirmation_code = getRandomSixDigitCode();\n  const getUserUpdate = (newUsr: { id: string }) =>\n    ({\n      registration: {\n        type: \"password-w-email-confirmation\",\n        email_confirmation: {\n          status: \"pending\",\n          confirmation_code: email_confirmation_code,\n          date: new Date().toISOString(),\n        },\n      },\n      password: getPasswordHash(newUsr, password),\n    }) as const;\n  if (existingUser) {\n    if (\n      existingUser.registration?.type !== \"password-w-email-confirmation\" ||\n      existingUser.registration.email_confirmation.status === \"confirmed\"\n    ) {\n      return withErrorCode(\"user-already-registered\");\n    }\n    await dbs.users.update(\n      { id: existingUser.id },\n      getUserUpdate(existingUser),\n    );\n  } else {\n    const newUser = await dbs.users.insert(\n      {\n        type: newUserType,\n        username: email,\n        email,\n        ...getUserUpdate({ id: \"missing\" }),\n      },\n      { returning: \"*\" },\n    );\n    await dbs.users.update({ id: newUser.id }, getUserUpdate(newUser));\n  }\n\n  await mailClient.sendEmailVerification({\n    to: email,\n    code: email_confirmation_code,\n    verificationUrl: getConfirmationUrl({\n      code: email_confirmation_code,\n      websiteUrl,\n    }),\n  });\n  return {\n    success: true,\n    code:\n      existingUser ?\n        \"already-registered-but-did-not-confirm-email\"\n      : \"email-verification-code-sent\",\n  };\n};\n\nexport const getRandomSixDigitCode = () =>\n  randomInt(0, 999999).toString().padStart(6, \"0\");\n"
  },
  {
    "path": "server/src/authConfig/getActiveSession.ts",
    "content": "import type { LoginClientInfo } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { type AuthResponse } from \"prostgles-types\";\nimport type { DBS } from \"../index\";\nimport type { Sessions } from \"./sessionUtils\";\nimport { startRateLimitedLoginAttempt } from \"./startRateLimitedLoginAttempt\";\n\ntype AuthType =\n  | {\n      type: \"session-id\";\n      filter: { id: string };\n      client: LoginClientInfo;\n    }\n  | {\n      type: \"login-success\";\n      filter: { user_id: string; type: \"web\"; user_agent: string };\n    };\n\nexport const getActiveSession = async (\n  db: DBS,\n  authType: AuthType,\n): Promise<{\n  validSession: Sessions | undefined;\n  expiredSession: Sessions | undefined;\n  failedTooManyTimes: boolean;\n  error?: AuthResponse.AuthFailure;\n}> => {\n  if (Object.values(authType.filter).some((v) => typeof v !== \"string\" || !v)) {\n    return {\n      validSession: undefined,\n      failedTooManyTimes: false,\n      expiredSession: undefined,\n      error: {\n        success: false,\n        code: \"server-error\",\n        message: \"Must provide a valid session filter\",\n      },\n    };\n  }\n  const validSession = await db.sessions.findOne(\n    getActiveSessionFilter(authType.filter),\n  );\n\n  let failedTooManyTimes = false;\n  let expiredSession: Sessions | undefined;\n  if (authType.type === \"session-id\" && !validSession) {\n    expiredSession = await db.sessions.findOne({ ...authType.filter });\n    if (!expiredSession) {\n      const { ip_address, ip_address_remote, user_agent, x_real_ip } =\n        authType.client;\n      const failedInfo = await startRateLimitedLoginAttempt(\n        db,\n        { ip_address, ip_address_remote, user_agent, x_real_ip },\n        { auth_type: \"session-id\", sid: authType.filter.id },\n      );\n      if (\"success\" in failedInfo) {\n        return {\n          validSession: undefined,\n          failedTooManyTimes: true,\n          expiredSession,\n          error: failedInfo,\n        };\n      }\n      failedTooManyTimes = failedInfo.failedTooManyTimes;\n    }\n  }\n\n  return { validSession, failedTooManyTimes, expiredSession };\n};\n\nexport const getActiveSessionFilter = (\n  filter: AuthType[\"filter\"] | { user_id: string },\n) => ({\n  ...filter,\n  \"expires.>\": Date.now(),\n  active: true,\n});\n"
  },
  {
    "path": "server/src/authConfig/getAuth.ts",
    "content": "import cors from \"cors\";\nimport type e from \"express\";\nimport type { Express } from \"express\";\nimport path from \"path\";\nimport type { AuthConfig } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { upsertNamedExpressMiddleware } from \"prostgles-server/dist/Auth/utils/upsertNamedExpressMiddleware\";\nimport type { DB } from \"prostgles-server/dist/Prostgles\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { API_ENDPOINTS, ROUTES } from \"@common/utils\";\nimport { actualRootDir } from \"../electronConfig\";\nimport type { DBS } from \"../index\";\nimport { getEmailAuthProvider } from \"./emailProvider/getEmailAuthProvider\";\nimport { getLogin } from \"./getLogin\";\nimport { getGetUser } from \"./getUser\";\nimport { getOAuthLoginProviders } from \"./OAuthProviders/getOAuthLoginProviders\";\nimport { onMagicLinkOrOTP } from \"./onMagicLinkOrOTP\";\nimport { getOnUseOrSocketConnected } from \"./onUseOrSocketConnected\";\nimport {\n  authCookieOpts,\n  parseAsBasicSession,\n  sidKeyName,\n  type SUser,\n} from \"./sessionUtils\";\nimport type { AuthSetupData } from \"./subscribeToAuthSetupChanges\";\nimport { initBackupManager } from \"@src/init/onProstglesReady\";\n\nlet globalSettings: AuthSetupData[\"globalSettings\"] | undefined;\n\nexport const withOrigin: WithOrigin = {\n  origin: (origin, cb) => {\n    cb(null, globalSettings?.allowed_origin ?? undefined);\n  },\n};\n\ntype WithOrigin = {\n  origin?: (\n    requestOrigin: string | undefined,\n    callback: (err: Error | null, origin?: string) => void,\n  ) => void;\n};\n\nconst setExpressAppOptions = (\n  app: e.Express,\n  authData: Pick<AuthSetupData, \"globalSettings\">,\n) => {\n  globalSettings = authData.globalSettings;\n\n  const corsMiddleware = cors(withOrigin);\n  upsertNamedExpressMiddleware(app, corsMiddleware, \"corsMiddleware\");\n  app.set(\"trust proxy\", globalSettings?.trust_proxy ?? false);\n};\n\nexport type GetAuthResult = Awaited<ReturnType<typeof getAuth>>;\n\nexport const getAuth = async (\n  app: Express,\n  dbs: DBS,\n  authSetupData: AuthSetupData,\n) => {\n  const { globalSettings } = authSetupData;\n  setExpressAppOptions(app, { globalSettings });\n  const { auth_providers, auth_created_user_type = null } =\n    globalSettings ?? {};\n  const auth = {\n    sidKeyName,\n    onUseOrSocketConnected: getOnUseOrSocketConnected(dbs, authSetupData),\n    getUser: getGetUser(authSetupData, dbs),\n    cacheSession: {\n      getSession: async (sid, _) => {\n        if (!sid) return undefined;\n        const s = await dbs.sessions.findOne({ id: sid });\n        if (!s) return undefined;\n        return parseAsBasicSession(s);\n      },\n    },\n\n    loginSignupConfig: {\n      app,\n\n      login: await getLogin(auth_providers),\n\n      logout: async (sid, db, _db: DB) => {\n        if (!sid) throw \"err\";\n        const s = await db.sessions.findOne({ id: sid });\n        if (!s) throw \"err\";\n        const u = await db.users.findOne({ id: s.user_id });\n\n        if (u?.passwordless_admin) {\n          throw `Passwordless admin cannot logout`;\n        }\n\n        await db.sessions.update({ id: sid }, { active: false });\n        /** Keep last 20 sessions */\n      },\n      publicRoutes: [\n        \"/manifest.json\",\n        \"/favicon.ico\",\n        \"/robots.txt\",\n        API_ENDPOINTS.WS_DB,\n      ],\n      onGetRequestOK: async (req, res, { getUser, db, dbo: dbs }) => {\n        if (req.path.startsWith(ROUTES.BACKUPS)) {\n          const userData = await getUser();\n          await (\n            await initBackupManager(db, dbs)\n          ).onRequestBackupFile(\n            res,\n            !userData.user ? undefined : userData,\n            req,\n          );\n        } else if (req.path.startsWith(ROUTES.STORAGE)) {\n          req.next?.();\n\n          /* Must be socket io reconnecting */\n        } else if (req.query.transport === \"polling\") {\n          req.next?.();\n        } else {\n          res.sendFile(\n            path.resolve(actualRootDir + \"/../client/build/index.html\"),\n          );\n        }\n      },\n      cookieOptions: authCookieOpts,\n      onMagicLinkOrOTP,\n      localLoginMode:\n        (\n          auth_providers?.email?.signupType === \"withMagicLink\" &&\n          auth_providers.email.enabled\n        ) ?\n          \"email\"\n        : \"email+password\",\n      signupWithEmail:\n        !auth_providers ? undefined : (\n          await getEmailAuthProvider(\n            { auth_providers, auth_created_user_type },\n            dbs,\n          )\n        ),\n      loginWithOAuth: getOAuthLoginProviders(auth_providers),\n    },\n  } satisfies AuthConfig<DBGeneratedSchema, SUser>;\n  return auth;\n};\n"
  },
  {
    "path": "server/src/authConfig/getLogin.ts",
    "content": "import { authenticator } from \"otplib\";\nimport {\n  getMagicLinkUrl,\n  type LoginSignupConfig,\n} from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { DB } from \"prostgles-server/dist/initProstgles\";\nimport type { Users } from \"..\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { log } from \"../index\";\nimport { getPasswordHash } from \"./authUtils\";\nimport { upsertSession } from \"./upsertSession\";\nimport { getEmailSenderWithMockTest } from \"./emailProvider/getEmailSenderWithMockTest\";\nimport { getRandomSixDigitCode } from \"./emailProvider/onEmailRegistration\";\nimport type { SUser } from \"./sessionUtils\";\nimport { loginWithProvider } from \"./OAuthProviders/loginWithProvider\";\nimport { startRateLimitedLoginAttempt } from \"./startRateLimitedLoginAttempt\";\n\nexport const getLogin = async (\n  auth_providers: DBGeneratedSchema[\"global_settings\"][\"columns\"][\"auth_providers\"],\n) => {\n  const mailClient = await getEmailSenderWithMockTest(auth_providers);\n  const { email: emailAuthConfig } = auth_providers ?? {};\n\n  const login: Required<\n    LoginSignupConfig<DBGeneratedSchema, SUser>\n  >[\"login\"] = async (loginParams, dbs, _db: DB, clientInfo) => {\n    log(\"login\");\n    const { ip_address, ip_address_remote, user_agent, x_real_ip } = clientInfo;\n\n    if (loginParams.type === \"OAuth\") {\n      return loginWithProvider(loginParams, dbs, clientInfo);\n    }\n\n    const { username, password, totp_token, totp_recovery_code } = loginParams;\n    if (!username) return \"username-missing\";\n    const authAttemptRateLimit = await startRateLimitedLoginAttempt(\n      dbs,\n      { ip_address, ip_address_remote, user_agent, x_real_ip },\n      { auth_type: \"login\", username },\n    );\n    if (\"success\" in authAttemptRateLimit) {\n      return authAttemptRateLimit.code;\n    }\n    const { onSuccess, ip, failedTooManyTimes } = authAttemptRateLimit;\n    if (failedTooManyTimes) {\n      return \"rate-limit-exceeded\";\n    }\n\n    let matchingUser: Users | undefined;\n    try {\n      const userFromUsername = await dbs.users.findOne({ username });\n\n      const magicLinkAuthEnabled =\n        emailAuthConfig?.enabled &&\n        emailAuthConfig.signupType === \"withMagicLink\";\n\n      if (\n        magicLinkAuthEnabled &&\n        (!userFromUsername ||\n          (userFromUsername.registration?.type === \"magic-link\" &&\n            !userFromUsername.password))\n      ) {\n        if (!mailClient || !auth_providers) {\n          return \"server-error\";\n        }\n        const newCode = getRandomSixDigitCode();\n\n        const newUser =\n          userFromUsername ??\n          (await dbs.users.insert(\n            {\n              username,\n              email: username,\n              type: \"default\",\n              registration: {\n                type: \"magic-link\",\n                otp_code: newCode,\n                date: new Date().toISOString(),\n              },\n              password: \"\",\n            },\n            { returning: \"*\" },\n          ));\n        // const mlink = await makeMagicLink(newUser, db, \"/\", {\n        //   session_expires: Date.now() + 1 * YEAR,\n        // });\n        await mailClient.sendMagicLinkEmail({\n          to: newUser.username,\n          code: newCode,\n          url: getMagicLinkUrl(auth_providers.website_url, {\n            type: \"otp\",\n            code: newCode,\n            email: username,\n            returnToken: false,\n          }),\n        });\n        return {\n          session: undefined,\n          response: { success: true, code: \"magic-link-sent\" },\n        };\n      }\n\n      if (!userFromUsername) {\n        return \"no-match\";\n      }\n      /** Trying to login OAuth user using password */\n      if (userFromUsername.registration?.type === \"OAuth\") {\n        return \"is-from-OAuth\";\n      }\n\n      if (userFromUsername.passwordless_admin) {\n        /** This should normally not happen because when pwdless admin is enabled login is not possible */\n        return \"server-error\";\n      }\n\n      if (!userFromUsername.password) {\n        return \"something-went-wrong\";\n      }\n\n      if (\n        userFromUsername.registration?.type ===\n          \"password-w-email-confirmation\" &&\n        userFromUsername.registration.email_confirmation.status !== \"confirmed\"\n      ) {\n        return \"email-not-confirmed\";\n      }\n\n      if (!password) {\n        // await onSuccess();\n        return \"password-missing\";\n      }\n\n      if (password.length > 400) {\n        return \"something-went-wrong\";\n      }\n      if (\n        userFromUsername.password !==\n        getPasswordHash(userFromUsername, password)\n      ) {\n        return \"no-match\";\n      }\n      matchingUser = userFromUsername;\n    } catch (e) {\n      return \"server-error\";\n    }\n\n    if (\n      matchingUser.registration?.type === \"password-w-email-confirmation\" &&\n      matchingUser.registration.email_confirmation.status !== \"confirmed\"\n    ) {\n      return \"email-not-confirmed\";\n    }\n    if (matchingUser.status !== \"active\") {\n      return \"inactive-account\";\n    }\n\n    if (matchingUser[\"2fa\"]?.enabled) {\n      if (totp_recovery_code && typeof totp_recovery_code === \"string\") {\n        const hashedRecoveryCode = getPasswordHash(\n          matchingUser,\n          totp_recovery_code.trim(),\n        );\n        const areMatching = await _db.any(\n          \"SELECT * FROM users WHERE id = ${id} AND \\\"2fa\\\"->>'recoveryCode' = ${hashedRecoveryCode} \",\n          { id: matchingUser.id, hashedRecoveryCode },\n        );\n        if (!areMatching.length) {\n          return \"invalid-totp-recovery-code\";\n        }\n      } else if (totp_token && typeof totp_token === \"string\") {\n        if (\n          !authenticator.verify({\n            secret: matchingUser[\"2fa\"].secret,\n            token: totp_token,\n          })\n        ) {\n          return \"invalid-totp-code\";\n        }\n      } else {\n        return \"totp-token-missing\";\n      }\n    }\n\n    await onSuccess();\n\n    const session = await upsertSession({\n      user: matchingUser,\n      ip,\n      db: dbs,\n      user_agent,\n    });\n    return { session, response: { success: true } };\n  };\n\n  return login;\n};\n"
  },
  {
    "path": "server/src/authConfig/getUser.ts",
    "content": "import type {\n  AuthClientRequest,\n  AuthConfig,\n  BasicSession,\n} from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { omitKeys } from \"prostgles-types\";\nimport type { DBS } from \"..\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { createPasswordlessAdminSessionIfNeeded } from \"./createPasswordlessAdminSessionIfNeeded\";\nimport { createPublicUserSessionIfAllowed } from \"./createPublicUserSessionIfAllowed\";\nimport { getActiveSession } from \"./getActiveSession\";\nimport {\n  PASSWORDLESS_ADMIN_ALREADY_EXISTS_ERROR,\n  type SUser,\n} from \"./sessionUtils\";\nimport type { AuthSetupData } from \"./subscribeToAuthSetupChanges\";\n\ntype GetUser = NonNullable<AuthConfig<DBGeneratedSchema, SUser>[\"getUser\"]>;\nexport const getGetUser = (authSetupData: AuthSetupData, dbs: DBS) => {\n  const getUser: GetUser = async (sid, _, __, client, req) => {\n    const sessionInfo =\n      sid &&\n      (await getActiveSession(dbs, {\n        type: \"session-id\",\n        client,\n        filter: { id: sid },\n      }));\n\n    if (sessionInfo) {\n      const { validSession, expiredSession, failedTooManyTimes, error } =\n        sessionInfo;\n      if (error) return error;\n      if (failedTooManyTimes) return \"rate-limit-exceeded\";\n      if (validSession) {\n        const user = await dbs.users.findOne({ id: validSession.user_id });\n        if (!user) return undefined;\n\n        const suser: SUser = {\n          sid: validSession.id,\n          user,\n          isAnonymous: user.type === \"public\",\n          clientUser: {\n            sid: validSession.id,\n            uid: user.id,\n\n            has_2fa: !!user[\"2fa\"]?.enabled,\n            ...omitKeys(user, [\"password\", \"2fa\"]),\n          },\n        };\n\n        return suser;\n      }\n      if (expiredSession) {\n        const user = await dbs.users.findOne({ id: expiredSession.user_id });\n        if (user?.status === \"active\") {\n          return {\n            preferredLogin:\n              user.registration?.type === \"OAuth\" ? user.registration.provider\n              : user.registration ? \"email\"\n              : user.password ? \"email+password\"\n              : undefined,\n          };\n        }\n      }\n    }\n\n    if (authSetupData.passwordlessAdmin?.activeSessions.length) {\n      return {\n        success: false,\n        code: \"something-went-wrong\",\n        message: PASSWORDLESS_ADMIN_ALREADY_EXISTS_ERROR,\n      };\n    }\n    /**\n     * This is to prevent a fresh setup (passwordless admin has not been assigned yet) redirecting users with existing session cookies to login\n     * */\n    const passwordlessAdminSession =\n      await createPasswordlessAdminSessionIfNeeded(\n        authSetupData,\n        dbs,\n        client,\n        req,\n      );\n\n    if (!passwordlessAdminSession) {\n      const newPublicUserSession = await createPublicUserSessionIfAllowed(\n        authSetupData,\n        dbs,\n        client,\n        req,\n      );\n      return newPublicUserSession;\n    }\n\n    return passwordlessAdminSession;\n  };\n\n  return getUser;\n};\n\nexport type NewRedirectSession = {\n  type: \"new-session\";\n  session: BasicSession;\n  reqInfo: Exclude<AuthClientRequest, { socket: any }>;\n};\n"
  },
  {
    "path": "server/src/authConfig/onMagicLinkOrOTP.ts",
    "content": "import type { LoginSignupConfig } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { makeSession, type SUser } from \"./sessionUtils\";\nimport type { AuthResponse } from \"prostgles-types\";\nimport { startRateLimitedLoginAttempt } from \"./startRateLimitedLoginAttempt\";\nimport type { Users } from \"..\";\nimport { DAY, MINUTE, YEAR } from \"@common/utils\";\n\nexport const onMagicLinkOrOTP: Required<\n  LoginSignupConfig<DBGeneratedSchema, SUser>\n>[\"onMagicLinkOrOTP\"] = async (\n  data,\n  dbs,\n  _db,\n  { ip_address, ip_address_remote, user_agent, x_real_ip },\n) => {\n  const withError = (\n    code: AuthResponse.MagicLinkAuthFailure[\"code\"],\n    message?: string,\n  ) => ({\n    response: {\n      success: false,\n      code,\n      message,\n    } satisfies AuthResponse.MagicLinkAuthFailure,\n  });\n  const rateLimitedAttempt = await startRateLimitedLoginAttempt(\n    dbs,\n    { ip_address, ip_address_remote, user_agent, x_real_ip },\n    data.type === \"magic-link\" ?\n      { auth_type: \"magic-link\", magic_link_id: data.id }\n    : { auth_type: \"otp-code\", username: data.email },\n  );\n  if (\"success\" in rateLimitedAttempt) {\n    return withError(\"server-error\", rateLimitedAttempt.message);\n  }\n  if (rateLimitedAttempt.failedTooManyTimes) {\n    return withError(\"rate-limit-exceeded\");\n  }\n\n  let user: Users | undefined;\n  let session_expires = Date.now() + YEAR;\n  if (data.type === \"magic-link\") {\n    const mlink = await dbs.magic_links.findOne({ id: data.id });\n    if (!mlink) {\n      return withError(\"no-match\");\n    }\n    if (Number(mlink.expires) < Date.now()) {\n      await rateLimitedAttempt.onSuccess();\n      return withError(\"expired-magic-link\", \"Expired magic link\");\n    }\n    if (mlink.magic_link_used) {\n      await rateLimitedAttempt.onSuccess();\n      return withError(\"expired-magic-link\", \"Magic link already used\");\n    }\n    user = await dbs.users.findOne({ id: mlink.user_id });\n    if (!user) {\n      return withError(\"no-match\", \"User from Magic link not found\");\n    }\n    if (user.status !== \"active\") {\n      return withError(\"inactive-account\", \"Account is inactive\");\n    }\n\n    /**\n     * This is done to prevent multiple logins with the same magic link\n     * even if the requests are sent at the same time\n     */\n    const usedMagicLink = await dbs.magic_links.update(\n      { id: mlink.id, magic_link_used: null },\n      { magic_link_used: new Date() },\n      { returning: \"*\" },\n    );\n    if (!usedMagicLink?.length) {\n      return withError(\"used-magic-link\", \"Magic link already used\");\n    }\n\n    session_expires = Number(mlink.session_expires);\n  } else {\n    user = await dbs.users.findOne({ email: data.email });\n\n    if (!user) {\n      return withError(\"no-match\", \"Invalid confirmation code\");\n    }\n\n    const { registration } = user;\n    if (registration?.type === \"password-w-email-confirmation\") {\n      if (\n        registration.email_confirmation.status === \"pending\" &&\n        new Date(registration.email_confirmation.date).getTime() + DAY <\n          Date.now()\n      ) {\n        return withError(\"something-went-wrong\", \"Confirmation code expired\");\n      }\n      if (\n        registration.email_confirmation.status !== \"pending\" ||\n        registration.email_confirmation.confirmation_code !== data.code\n      ) {\n        return withError(\"no-match\", \"Invalid confirmation code\");\n      }\n\n      const userId = user.id;\n      await dbs.tx(async (dbsTx) => {\n        const latestUser = await dbsTx.users.findOne({ id: userId });\n        if (\n          latestUser?.registration?.type !== \"password-w-email-confirmation\" ||\n          latestUser.registration.email_confirmation.status !== \"pending\"\n        ) {\n          throw new Error(\"Email confirmation already completed\");\n        }\n        await dbsTx.users.update(\n          { id: userId },\n          {\n            registration: {\n              $merge: [\n                {\n                  email_confirmation: {\n                    status: \"confirmed\",\n                    date: new Date().toISOString(),\n                  },\n                },\n              ],\n            },\n          },\n        );\n      });\n\n      await rateLimitedAttempt.onSuccess();\n      return {\n        response: { success: true, code: \"email-verified\" },\n        /** No session because we expect the user to login with email and password for extra security */\n        session: undefined,\n      };\n    }\n    if (user.registration?.type === \"magic-link\") {\n      if (!data.code || user.registration.otp_code !== data.code) {\n        return withError(\"invalid-magic-link\", \"Invalid code\");\n      }\n      if (\n        new Date(user.registration.date).getTime() + 5 * MINUTE <\n        Date.now()\n      ) {\n        await rateLimitedAttempt.onSuccess();\n        return withError(\"expired-magic-link\", \"Code expired\");\n      }\n      if (user.registration.used_on) {\n        await rateLimitedAttempt.onSuccess();\n        return withError(\"used-magic-link\", \"Magic link already used\");\n      }\n      await dbs.users.update(\n        { id: user.id },\n        { registration: { $merge: [{ used_on: new Date().toISOString() }] } },\n      );\n    }\n  }\n  const session = await makeSession(\n    user,\n    {\n      ip_address: rateLimitedAttempt.ip,\n      user_agent: user_agent || null,\n      type: \"web\",\n    },\n    dbs,\n    Number(session_expires),\n  );\n  await rateLimitedAttempt.onSuccess();\n  return { session };\n};\n"
  },
  {
    "path": "server/src/authConfig/onUseOrSocketConnected.ts",
    "content": "import type { AuthConfig } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { tout, type DBS } from \"..\";\nimport { checkClientIP, sidKeyName } from \"./sessionUtils\";\nimport type { AuthSetupData } from \"./subscribeToAuthSetupChanges\";\nimport { getElectronConfig } from \"../electronConfig\";\n\nexport const getOnUseOrSocketConnected = (\n  dbs: DBS,\n  authSetupData: AuthSetupData,\n) => {\n  const onUseOrSocketConnected: AuthConfig[\"onUseOrSocketConnected\"] = async (\n    sid,\n    client,\n    reqInfo,\n  ) => {\n    while (!authSetupData.globalSettings) {\n      console.warn(\n        \"Delaying user request until globalSettings is ready\",\n        reqInfo,\n      );\n      await tout(2000);\n    }\n    const { globalSettings } = authSetupData;\n\n    /** Is this needed? */\n    const electronConfig = getElectronConfig();\n    if (\n      electronConfig?.isElectron &&\n      electronConfig.sidConfig.electronSid !== sid\n    ) {\n      return {\n        httpCode: 400,\n        error: \"Not authorized. Expecting a different \" + sidKeyName,\n      };\n    }\n\n    if (globalSettings.allowed_ips_enabled) {\n      const ipCheck = await checkClientIP(dbs, reqInfo, globalSettings);\n      if (!ipCheck.isAllowed) {\n        return { error: \"Your IP is not allowed\", httpCode: 403 };\n      }\n    }\n  };\n\n  return onUseOrSocketConnected;\n};\n"
  },
  {
    "path": "server/src/authConfig/sessionUtils.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { DAY, ROUTES, YEAR } from \"@common/utils\";\nimport * as crypto from \"crypto\";\nimport type { Request } from \"express\";\nimport { getClientRequestIPsInfo } from \"prostgles-server/dist/Auth/AuthHandler\";\nimport type { BasicSession } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { PRGLIOSocket } from \"prostgles-server/dist/DboBuilder/DboBuilderTypes\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\nimport { PROSTGLES_STRICT_COOKIE } from \"../envVars\";\nimport type { DBS, Users } from \"../index\";\nimport { getPasswordlessAdmin } from \"../SecurityManager/initUsers\";\nimport { getPasswordHash } from \"./authUtils\";\nimport type { AuthSetupData } from \"./subscribeToAuthSetupChanges\";\n\nexport type Sessions = DBSSchema[\"sessions\"];\nexport const parseAsBasicSession = (s: Sessions): BasicSession => {\n  // TODO send sid and set id as hash of sid\n  return {\n    ...s,\n    sid: s.id,\n    expires: +s.expires,\n    onExpiration: s.type === \"api_token\" ? \"show_error\" : \"redirect\",\n  };\n};\n\nexport const createSessionSecret = () => {\n  return crypto.randomBytes(48).toString(\"hex\");\n};\n\nexport const makeSession = async (\n  user: Pick<Users, \"id\" | \"type\"> | undefined,\n  client: Pick<Sessions, \"user_agent\" | \"ip_address\" | \"type\"> & {\n    sid?: string;\n  },\n  dbo: DBOFullyTyped<DBGeneratedSchema>,\n  expires = 0,\n): Promise<BasicSession> => {\n  if (user) {\n    const session = await dbo.sessions.insert(\n      {\n        id: client.sid ?? createSessionSecret(),\n        user_id: user.id,\n        user_type: user.type,\n        expires,\n        type: client.type,\n        ip_address: client.ip_address,\n        user_agent: client.user_agent,\n      },\n      { returning: \"*\" },\n    );\n\n    return parseAsBasicSession(session);\n  } else {\n    throw \"Invalid user\";\n  }\n};\n\nexport type SUser = {\n  sid: string;\n  user: Users;\n  clientUser: {\n    sid: string;\n    uid: string;\n    has_2fa: boolean;\n  } & Omit<Users, \"password\" | \"2fa\">;\n  isAnonymous: boolean;\n};\nexport const sidKeyName = \"sid_token\" as const;\n\nexport const authCookieOpts =\n  process.env.PROSTGLES_STRICT_COOKIE || PROSTGLES_STRICT_COOKIE ?\n    {}\n  : {\n      secure: false,\n      sameSite: \"lax\", //  \"none\"\n    };\n\n/**\n * This is mainly used to ensure that when there is passwordless admin access external IPs cannot connect\n */\nexport const checkClientIP = async (\n  dbsOrTx: DBS,\n  args: { socket: PRGLIOSocket } | { httpReq: Request },\n  globalSettings: AuthSetupData[\"globalSettings\"],\n) => {\n  const { ip_address, ip_address_remote, x_real_ip } =\n    getClientRequestIPsInfo(args);\n  const { groupBy } = globalSettings?.login_rate_limit ?? {};\n  const ipValue =\n    groupBy === \"x-real-ip\" ? x_real_ip\n    : groupBy === \"remote_ip\" ? ip_address_remote\n    : ip_address;\n  const isAllowed = (await dbsOrTx.sql(\n    \"SELECT inet ${ip} <<= any (allowed_ips::inet[]) FROM global_settings \",\n    { ip: ipValue },\n    { returnType: \"value\" },\n  )) as boolean;\n\n  return {\n    ip: ipValue,\n    ip_address,\n    ip_address_remote,\n    x_real_ip,\n    isAllowed, //: (args.byPassedRanges || this.ipRanges).some(({ from, to }) => ip && ip >= from && ip <= to )\n  };\n};\n\nexport const getPasswordlessMagicLink = async (dbs: DBS) => {\n  /** Create session for passwordless admin */\n  const maybePasswordlessAdmin = await getPasswordlessAdmin(dbs);\n  if (maybePasswordlessAdmin) {\n    const existingMagicLink = await dbs.magic_links.findOne({\n      user_id: maybePasswordlessAdmin.id,\n    });\n    if (existingMagicLink) {\n      return {\n        state: \"magic-link-exists\",\n        wasUsed: !!existingMagicLink.magic_link_used,\n        error:\n          existingMagicLink.magic_link_used ?\n            PASSWORDLESS_ADMIN_ALREADY_EXISTS_ERROR\n          : undefined,\n      } as const;\n    }\n\n    const mlink = await makeMagicLink(maybePasswordlessAdmin, dbs, \"/\", {\n      session_expires: Date.now() + 10 * YEAR,\n    });\n\n    return {\n      state: \"magic-link-ready\" as const,\n      magicLinkUrl: mlink.magic_login_link_redirect,\n    } as const;\n  }\n\n  return {\n    state: \"no-passwordless-admin\",\n  } as const;\n};\n\nexport const PASSWORDLESS_ADMIN_ALREADY_EXISTS_ERROR =\n  \"Only 1 session is allowed for the passwordless admin. If you're seeing this then the passwordless admin session has already been assigned to a different device/browser\";\n\nexport const makeMagicLink = async (\n  user: Users,\n  dbo: DBS,\n  returnURL: string,\n  opts?: {\n    expires?: number;\n    session_expires?: number;\n  },\n) => {\n  const maxValidityDays =\n    (await dbo.global_settings.findOne())?.magic_link_validity_days ?? 2;\n  const mlink = await dbo.magic_links.insert(\n    {\n      expires: opts?.expires ?? Date.now() + DAY * maxValidityDays,\n      session_expires: opts?.session_expires ?? Date.now() + DAY * 7,\n      user_id: user.id,\n    },\n    { returning: \"*\" },\n  );\n\n  return {\n    id: user.id,\n    magicLinkId: mlink.id,\n    magic_login_link_redirect: `${ROUTES.MAGIC_LINK}/${mlink.id}?returnURL=${returnURL}`,\n  };\n};\n\nexport const insertUser = async (\n  db: Pick<DBS, \"users\">,\n  u: Parameters<typeof db.users.insert>[0] & { password: string },\n) => {\n  const user = (await db.users.insert(u, { returning: \"*\" })) as Users;\n  if (!user.id) throw \"User id missing\";\n  if (typeof user.password !== \"string\") throw \"Password missing\";\n  const hashedPassword = getPasswordHash(user, user.password);\n  // await _db.any(\n  //   \"UPDATE users SET password = ${hashedPassword} WHERE id = ${id};\",\n  //   { id: user.id, hashedPassword },\n  // );\n  await db.users.update({ id: user.id }, { password: hashedPassword });\n  return db.users.findOne({ id: user.id });\n};\n"
  },
  {
    "path": "server/src/authConfig/startRateLimitedLoginAttempt.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { HOUR } from \"@common/utils\";\nimport type { LoginClientInfo } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\nimport { type AuthResponse, isEmpty, pickKeys } from \"prostgles-types\";\nimport { waitForGlobalSettings } from \"./subscribeToAuthSetupChanges\";\n\ntype FailedAttemptsInfo =\n  | {\n      ip: string;\n      failedTooManyTimes: boolean;\n      disabled?: false;\n      matchByFilter: Pick<\n        LoginClientInfo,\n        \"ip_address\" | \"x_real_ip\" | \"ip_address_remote\"\n      >;\n    }\n  | {\n      ip: string;\n      matchByFilter?: undefined;\n      failedTooManyTimes: false;\n      disabled?: true;\n    };\nexport const getFailedTooManyTimes = async (\n  db: DBOFullyTyped<DBGeneratedSchema>,\n  clientInfo: LoginClientInfo,\n): Promise<FailedAttemptsInfo | AuthResponse.AuthFailure> => {\n  const lastHour = new Date(Date.now() - 1 * HOUR).toISOString();\n  const globalSettings = await waitForGlobalSettings();\n  const { ip, ipFromMatchByFilterKey, matchByFilterKey } = getIPsFromClientInfo(\n    clientInfo,\n    globalSettings,\n  );\n  if (!ipFromMatchByFilterKey) {\n    return {\n      success: false,\n      code: \"something-went-wrong\",\n      message: \"Invalid/empty ip\",\n    };\n  }\n  const matchByFilter = pickKeys(clientInfo, [matchByFilterKey]);\n  if (isEmpty(matchByFilter)) {\n    const message =\n      \"matchByFilter is empty \" +\n      JSON.stringify([matchByFilter, matchByFilterKey]);\n\n    return {\n      success: false,\n      code: \"something-went-wrong\",\n      message,\n    };\n  }\n  const previousFails = await db.login_attempts.find({\n    ...matchByFilter,\n    failed: true,\n    \"created.>=\": lastHour,\n  });\n  const maxAttemptsPerHour = Math.max(\n    1,\n    globalSettings.login_rate_limit.maxAttemptsPerHour,\n  );\n  if (previousFails.length >= maxAttemptsPerHour) {\n    return { ip, matchByFilter, failedTooManyTimes: true };\n  }\n\n  return { ip, matchByFilter, failedTooManyTimes: false };\n};\n\ntype AuthAttepmt =\n  | {\n      auth_type: \"login\" | \"registration\" | \"otp-code\";\n      username: string;\n    }\n  | { auth_type: \"oauth\"; auth_provider: string }\n  | { auth_type: \"magic-link\"; magic_link_id: string }\n  | { auth_type: \"session-id\"; sid: string };\n\n/**\n * Used to prevent ip addresses from authentication after too many recent failed attempts\n * Configured in global_settings.login_rate_limit found in Server settings page\n */\nexport const startRateLimitedLoginAttempt = async (\n  db: DBOFullyTyped<DBGeneratedSchema>,\n  clientInfo: LoginClientInfo,\n  authInfo: AuthAttepmt,\n) => {\n  const failedInfo = await getFailedTooManyTimes(db, clientInfo);\n  if (\"success\" in failedInfo) {\n    return failedInfo;\n  }\n  const { failedTooManyTimes, matchByFilter, ip, disabled } = failedInfo;\n  const result = {\n    ip,\n    onSuccess: async () => {},\n    disabled,\n    failedTooManyTimes,\n  };\n  if (failedTooManyTimes || disabled) {\n    return result;\n  }\n\n  /** In case of a bad sid do not log it multiple times */\n  if (authInfo.auth_type === \"session-id\") {\n    const alreadyFailedOnThisSID = await db.login_attempts.findOne(\n      { ...matchByFilter, failed: true, sid: authInfo.sid },\n      { orderBy: { created: false } },\n    );\n    if (alreadyFailedOnThisSID) {\n      return result;\n    }\n  }\n\n  const loginAttempt = await db.login_attempts.insert(\n    {\n      failed: true,\n      ...authInfo,\n      ...clientInfo,\n      user_agent: clientInfo.user_agent ?? \"\",\n      x_real_ip: clientInfo.x_real_ip ?? \"\",\n      ip_address_remote: clientInfo.ip_address_remote ?? \"\",\n    },\n    { returning: { id: 1 } },\n  );\n  return {\n    ip,\n    failedTooManyTimes,\n    loginAttemptId: loginAttempt.id,\n    onSuccess: async () => {\n      await db.login_attempts.update(\n        { id: loginAttempt.id },\n        { failed: false },\n      );\n\n      /**\n       * Upon successfully confirming an email\n       * must delete all failed attempts within last day for that email\n       * */\n      if (\n        authInfo.auth_type === \"otp-code\" ||\n        authInfo.auth_type === \"login\" ||\n        authInfo.auth_type === \"magic-link\"\n      ) {\n        const getUsername = async () => {\n          if (authInfo.auth_type === \"magic-link\") {\n            const user = await db.users.findOne({\n              $existsJoined: { magic_links: { id: authInfo.magic_link_id } },\n            });\n            return user?.username;\n          }\n          return authInfo.username;\n        };\n\n        const username = await getUsername();\n        if (!username) throw \"No username found for magic link\";\n        await db.login_attempts.delete({\n          ...matchByFilter,\n          username,\n          failed: true,\n          \"created.>=\": new Date(Date.now() - 24 * HOUR).toISOString(),\n        });\n      }\n    },\n  };\n};\n\nexport const getIPsFromClientInfo = (\n  clientInfo: LoginClientInfo,\n  globalSettings: DBSSchema[\"global_settings\"],\n) => {\n  const { ip_address } = clientInfo;\n  const {\n    login_rate_limit: { groupBy },\n    login_rate_limit_enabled,\n  } = globalSettings;\n  if (!login_rate_limit_enabled) {\n    return {\n      ip:\n        ip_address ||\n        clientInfo.ip_address_remote ||\n        clientInfo.x_real_ip ||\n        \"\",\n      failedTooManyTimes: false,\n      disabled: true,\n    };\n  }\n  const matchByFilterKey = (\n    {\n      ip: \"ip_address\",\n      \"x-real-ip\": \"x_real_ip\",\n      remote_ip: \"ip_address_remote\",\n    } as const\n  )[groupBy];\n\n  const ipFromMatchByFilterKey = clientInfo[matchByFilterKey];\n  const ip = ipFromMatchByFilterKey ?? ip_address;\n  return { ip, ipFromMatchByFilterKey, matchByFilterKey };\n};\n"
  },
  {
    "path": "server/src/authConfig/subscribeToAuthSetupChanges.ts",
    "content": "import { getKeys, isEqual } from \"prostgles-types\";\nimport { DOCKER_USER_AGENT } from \"@common/OAuthUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { tout, type DBS } from \"../index\";\nimport {\n  activePasswordlessAdminFilter,\n  getPasswordlessAdmin,\n} from \"../SecurityManager/initUsers\";\nimport { tableConfig } from \"../tableConfig/tableConfig\";\n\nexport type AuthSetupData = {\n  globalSettings: DBSSchema[\"global_settings\"] | undefined;\n  passwordlessAdmin:\n    | (Pick<DBSSchema[\"users\"], \"id\" | \"type\"> & {\n        sessions: DBSSchema[\"sessions\"][];\n        activeSessions: DBSSchema[\"sessions\"][];\n      })\n    | undefined;\n};\n\nlet authSetupData: AuthSetupData | undefined;\n\nexport type AuthSetupDataListener = Promise<{\n  context: Partial<AuthSetupData>;\n  destroy: () => Promise<void>;\n}>;\n\nexport const subscribeToAuthSetupChanges = async (\n  dbs: DBS,\n  onChange: (auth: AuthSetupData) => void | Promise<void>,\n  oldListener: AuthSetupDataListener | undefined,\n): AuthSetupDataListener => {\n  await (await oldListener)?.destroy();\n  let context: Partial<AuthSetupData> = {};\n  const totalContextKeys = getKeys({\n    globalSettings: 1,\n    passwordlessAdmin: 1,\n  } satisfies Record<keyof AuthSetupData, 1>);\n\n  const setContext = (changes: Partial<AuthSetupData>) => {\n    const oldContext = { ...context };\n    const newContext = { ...context, ...changes };\n    const newKeyCount = Object.keys(newContext).length;\n    context = { ...newContext };\n    if (\n      isEqual(oldContext, newContext) ||\n      newKeyCount !== totalContextKeys.length\n    ) {\n      return;\n    }\n    authSetupData = { ...context } as AuthSetupData;\n\n    void onChange(context as AuthSetupData);\n  };\n\n  /** Add cors config if missing */\n  await dbs.tx(async (dbsTx) => {\n    if (!(await dbsTx.global_settings.count())) {\n      await dbsTx.global_settings.insert({\n        /** Origin \"*\" is required to enable API access */\n        allowed_origin: (await getPasswordlessAdmin(dbsTx)) ? null : \"*\",\n        allowed_ips_enabled: false,\n        allowed_ips: [\"::ffff:127.0.0.1\"],\n        tableConfig,\n      });\n    }\n  });\n\n  const globalSettingSub = await dbs.global_settings.subscribeOne(\n    {},\n    {},\n    (globalSettings) => {\n      setContext({\n        globalSettings,\n      });\n    },\n  );\n  /** This is used to avoid docker-mcp session that changes frequently and causes page restart when running a docker mcp */\n  const userAgentFilter = {\n    user_agent: { $ne: DOCKER_USER_AGENT },\n  };\n  const passwordlessAdminSub = await dbs.users.subscribeOne(\n    activePasswordlessAdminFilter,\n    {\n      select: {\n        id: 1,\n        type: 1,\n        sessions: {\n          $leftJoin: \"sessions\",\n          select: \"*\",\n          filter: userAgentFilter,\n        },\n        activeSessions: {\n          $leftJoin: \"sessions\",\n          select: \"*\",\n          filter: {\n            \"expires.>\": Date.now(),\n            active: true,\n            ...userAgentFilter,\n          },\n        },\n      },\n    },\n    (passwordlessAdmin) => {\n      setContext({\n        passwordlessAdmin:\n          passwordlessAdmin as AuthSetupData[\"passwordlessAdmin\"],\n      });\n    },\n  );\n  const destroy = async () => {\n    await globalSettingSub.unsubscribe();\n    await passwordlessAdminSub.unsubscribe();\n  };\n  return { context, destroy };\n};\n\nexport const waitForGlobalSettings = async () => {\n  while (!authSetupData?.globalSettings) {\n    console.warn(\"Delaying user request until GlobalSettings area available\");\n    await tout(500);\n  }\n  return authSetupData.globalSettings;\n};\n\nexport const getAuthSetupData = () => {\n  return (\n    authSetupData ?? {\n      globalSettings: undefined,\n      passwordlessAdmin: undefined,\n    }\n  );\n};\n"
  },
  {
    "path": "server/src/authConfig/upsertSession.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { DAY } from \"@common/utils\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\nimport type { Users } from \"..\";\nimport { getActiveSession } from \"./getActiveSession\";\nimport { makeSession, parseAsBasicSession } from \"./sessionUtils\";\n\ntype CreateSessionArgs = {\n  user: Users;\n  ip: string;\n  db: DBOFullyTyped<DBGeneratedSchema>;\n  user_agent: string | undefined;\n};\nexport const upsertSession = async ({\n  db,\n  ip,\n  user,\n  user_agent,\n}: CreateSessionArgs) => {\n  const {\n    validSession: activeSession,\n    failedTooManyTimes,\n    error,\n  } = await getActiveSession(db, {\n    type: \"login-success\",\n    filter: { user_id: user.id, type: \"web\", user_agent: user_agent ?? \"\" },\n  });\n  if (error) {\n    throw error;\n  }\n  if (failedTooManyTimes) {\n    throw \"rate-limit-exceeded\";\n  }\n  if (!activeSession) {\n    const globalSettings = await db.global_settings.findOne();\n    const expires =\n      Date.now() + (globalSettings?.session_max_age_days ?? 1) * DAY;\n    return await makeSession(\n      user,\n      { ip_address: ip, user_agent: user_agent || null, type: \"web\" },\n      db,\n      expires,\n    );\n  }\n  await db.sessions.update({ id: activeSession.id }, { last_used: new Date() });\n  return parseAsBasicSession(activeSession);\n};\n"
  },
  {
    "path": "server/src/cloudClients/cloudClients.ts",
    "content": "import {\n  S3Client,\n  GetObjectCommand,\n  DeleteObjectCommand,\n} from \"@aws-sdk/client-s3\";\nimport { Upload } from \"@aws-sdk/lib-storage\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\n\nimport { Readable } from \"stream\";\nimport type {\n  CloudClient,\n  FileUploadArgs,\n  UploadedCloudFile,\n} from \"prostgles-server/dist/FileManager/FileManager\";\nimport { pickKeys } from \"prostgles-types\";\n\ntype S3Config = {\n  Bucket: string;\n  region: string;\n  endpoint?: string;\n  accessKeyId: string;\n  secretAccessKey: string;\n};\nconst getS3CloudClient = (s3Config: S3Config): CloudClient => {\n  const bucket = pickKeys(s3Config, [\"Bucket\"]);\n\n  // Initialize S3 client\n  const s3Client = new S3Client({\n    credentials: pickKeys(s3Config, [\"accessKeyId\", \"secretAccessKey\"]),\n    region: s3Config.region || \"auto\",\n    endpoint: s3Config.endpoint,\n  });\n\n  // Helper function to upload a file to S3 and track progress\n  const uploadToS3 = async (\n    bucketName: string,\n    objectKey: string,\n    file: string | Buffer | Readable,\n    contentType: string,\n    onFinish: FileUploadArgs[\"onFinish\"],\n    onProgress?: (bytesUploaded: number) => void,\n  ): Promise<void> => {\n    const stream = file instanceof Readable ? file : Readable.from(file);\n\n    // Prepare the parameters for the PutObjectCommand\n    const params = {\n      Bucket: bucketName,\n      Key: objectKey,\n      Body: stream,\n      ContentType: contentType,\n    };\n    try {\n      const parallelUploads3 = new Upload({\n        client: s3Client,\n        // tags: [...], // optional tags\n        // queueSize: 4, // optional concurrency configuration\n        leavePartsOnError: false, // optional manually handle dropped parts\n        params,\n      });\n\n      parallelUploads3.on(\"httpUploadProgress\", (progres) => {\n        onProgress?.(progres.loaded ?? 0);\n      });\n\n      await parallelUploads3.done();\n\n      // Fetch the object metadata to get etag and content length\n      const headCommand = new GetObjectCommand({ ...bucket, Key: objectKey });\n      const headResponse = await s3Client.send(headCommand);\n\n      const endpoint =\n        s3Config.endpoint || `https://${bucketName}.s3.amazonaws.com/`;\n\n      const uploadedFile: UploadedCloudFile = {\n        cloud_url: `${endpoint}${objectKey}`,\n        etag: headResponse.ETag || \"\",\n        content_length: headResponse.ContentLength || 0,\n      };\n\n      onFinish(undefined, uploadedFile);\n    } catch (error: unknown) {\n      if (error instanceof Error) {\n        onFinish(error, undefined);\n      } else {\n        console.error(\"Unknown error in uploadToS3 \", error);\n        onFinish(\n          new Error(\"Unknown error in uploadToS3. Check logs\"),\n          undefined,\n        );\n      }\n    }\n  };\n\n  return {\n    upload: (file: FileUploadArgs) =>\n      new Promise((resolve, reject) => {\n        void uploadToS3(\n          bucket.Bucket,\n          file.fileName,\n          file.file,\n          file.contentType,\n          (...args) => {\n            const [error, result] = args;\n            if (error) {\n              reject(error);\n            } else {\n              resolve();\n            }\n            file.onFinish(...args);\n          },\n          file.onProgress,\n        );\n      }),\n\n    downloadAsStream: async (name: string) => {\n      const command = new GetObjectCommand({ ...bucket, Key: name });\n      const response = await s3Client.send(command);\n      return response.Body as Readable;\n    },\n\n    delete: async (fileName: string) => {\n      const command = new DeleteObjectCommand({ ...bucket, Key: fileName });\n      await s3Client.send(command);\n    },\n\n    getSignedUrlForDownload: async (\n      fileName: string,\n      expiresInSeconds: number,\n    ) => {\n      const command = new GetObjectCommand({ ...bucket, Key: fileName });\n      const url = await getSignedUrl(s3Client, command, {\n        expiresIn: expiresInSeconds,\n      });\n      return url;\n    },\n  };\n};\n\nexport const getCloudClient = getS3CloudClient;\n"
  },
  {
    "path": "server/src/connectionUtils/getConnectionDetails.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nexport type Connections = Required<DBGeneratedSchema[\"connections\"][\"columns\"]>;\nimport { ConnectionString } from \"connection-string\";\nimport type pg from \"pg-promise/typescript/pg-subset\";\n\ntype ConnectionDetails = Required<\n  Pick<\n    pg.IConnectionParameters<pg.IClient>,\n    | \"application_name\"\n    | \"host\"\n    | \"port\"\n    | \"password\"\n    | \"user\"\n    | \"ssl\"\n    | \"database\"\n  >\n> & { password: string; connectionTimeoutMillis?: number };\n\nexport const getConnectionDetails = (c: Connections): ConnectionDetails => {\n  /**\n   * Cannot use connection uri without having ssl issues\n   * https://github.com/brianc/node-postgres/issues/2281\n   */\n  const getSSLOpts = (\n    sslmode: Connections[\"db_ssl\"],\n  ): pg.IConnectionParameters<pg.IClient>[\"ssl\"] =>\n    sslmode !== \"disable\" ?\n      {\n        ca: c.ssl_certificate ?? undefined,\n        cert: c.ssl_client_certificate ?? undefined,\n        key: c.ssl_client_certificate_key ?? undefined,\n        rejectUnauthorized:\n          c.ssl_reject_unauthorized ??\n          ((sslmode === \"require\" && !!c.ssl_certificate) ||\n            sslmode === \"verify-ca\" ||\n            sslmode === \"verify-full\"),\n      }\n    : undefined;\n\n  const default_application_name = \"\";\n\n  if (c.type === \"Connection URI\") {\n    const cs = new ConnectionString(c.db_conn);\n    const params = cs.params ?? {};\n    const {\n      sslmode,\n      application_name = default_application_name,\n      connect_timeout = 10,\n    } = params;\n    const conn = {\n      application_name,\n      host: cs.hosts![0]!.name ?? c.db_host, // fallback to db_host for pg_dump state_db\n      port: cs.hosts![0]!.port!,\n      user: cs.user!,\n      password: cs.password!,\n      database: cs.path![0]!,\n      ssl: getSSLOpts(sslmode) ?? false,\n      ...(Number.isFinite(connect_timeout) && {\n        connectionTimeoutMillis: Math.ceil(connect_timeout) * 1000,\n      }),\n    };\n    return conn;\n  }\n  const conn = {\n    application_name: default_application_name,\n    database: c.db_name,\n    user: c.db_user,\n    password: c.db_pass!,\n    host: c.db_host,\n    port: c.db_port,\n    ssl: getSSLOpts(c.db_ssl) ?? false,\n    ...(Number.isFinite(c.db_connection_timeout) && {\n      connectionTimeoutMillis: Math.ceil(c.db_connection_timeout!),\n    }),\n  };\n  return conn;\n};\n"
  },
  {
    "path": "server/src/connectionUtils/testDBConnection.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { getConnectionDetails } from \"./getConnectionDetails\";\nimport { validateConnection, type ConnectionInfo } from \"./validateConnection\";\nexport type Connections = Required<DBGeneratedSchema[\"connections\"][\"columns\"]>;\n\nimport pgPromise from \"pg-promise\";\nimport type pg from \"pg-promise/typescript/pg-subset\";\nimport { getIsSuperUser, type DBorTx } from \"prostgles-server/dist/Prostgles\";\nimport { getSerialisableError, isObject, tryCatchV2 } from \"prostgles-types\";\nconst pgpNoWarnings = pgPromise({ noWarnings: true });\nconst pgp = pgPromise();\n\nconst NO_SSL_SUPPORT_ERROR = \"The server does not support SSL connections\";\n\n/**\n * Ensures sslmode=prefer is supported\n * https://www.postgresql.org/docs/8.4/libpq-connect.html#LIBPQ-CONNECT-SSLMODE\n * https://github.com/brianc/node-postgres/issues/2720\n *\n *  disable\t    only try a non-SSL connection\n *  allow\t      first try a non-SSL connection; if that fails, try an SSL connection\n *  prefer      (default)\tfirst try an SSL connection; if that fails, try a non-SSL connection\n *  require\t    only try an SSL connection. If a root CA file is present, verify the certificate in the same way as if verify-ca was specified\n *  verify-ca\t  only try an SSL connection, and verify that the server certificate is issued by a trusted CA.\n *  verify-full\tonly try an SSL connection, verify that the server certificate is issued by a trusted CA and that the server hostname matches that in the certificate.\n */\nexport const testDBConnection = (\n  _c: ConnectionInfo,\n  expectSuperUser = false,\n  check?: (c: pgPromise.IConnected<{}, pg.IClient>) => any,\n): Promise<{\n  prostglesSchemaVersion: string | undefined;\n  connectionInfo: pg.IConnectionParameters<pg.IClient>;\n  canCreateDb: boolean | undefined;\n  isSSLModeFallBack?: boolean;\n}> => {\n  const con = validateConnection(_c);\n  if (typeof con !== \"object\" || (!(\"db_host\" in con) && !(\"db_conn\" in con))) {\n    throw (\n      \"Incorrect database connection info provided. \" +\n      \"\\nExpecting: \\\n      db_conn: string; \\\n      OR \\\n      db_user: string; db_pass: string; db_host: string; db_port: number; db_name: string, db_ssl: string\"\n    );\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n  return new Promise(async (resolve, reject) => {\n    const connOpts = getConnectionDetails(con as Connections);\n    const db = pgpNoWarnings({ ...connOpts });\n    return db\n      .connect()\n      .then(async function (c) {\n        if (expectSuperUser) {\n          const usessuper = await getIsSuperUser(c as unknown as DBorTx);\n          if (!usessuper) {\n            reject(new Error(\"Provided user must be a superuser\"));\n            return;\n          }\n        }\n        await check?.(c);\n\n        const { data: prostglesSchemaVersion } = await tryCatchV2(async () => {\n          const { version } = await c.one<{ version: string }>(\n            \"SELECT version FROM prostgles.versions\",\n          );\n          return version;\n        });\n        const { data: canCreateDb } = await tryCatchV2(async () => {\n          const { can_create_db } = await c.one<{ can_create_db: boolean }>(`\n            SELECT rolcreatedb OR rolsuper as can_create_db\n            FROM pg_catalog.pg_roles\n            WHERE rolname = \"current_user\"();\n          `);\n          return can_create_db;\n        });\n\n        resolve({\n          connectionInfo: connOpts,\n          prostglesSchemaVersion,\n          canCreateDb,\n        });\n\n        await c.done();\n      })\n      .catch((err) => {\n        const errRes = err instanceof Error ? err.message : JSON.stringify(err);\n        if (errRes === NO_SSL_SUPPORT_ERROR && _c.db_ssl === \"prefer\") {\n          console.warn(\n            `Falling back to sslmode=disable for host ${con.db_host} as sslmode=prefer is not supported by the server.`,\n          );\n          return resolve(\n            testDBConnection(\n              {\n                ..._c,\n                db_ssl: \"disable\",\n                type: \"Standard\",\n              },\n              (expectSuperUser = false),\n              check,\n            ).then((res) => ({ ...res, isSSLModeFallBack: true })),\n          );\n        } else {\n          console.error(\n            `Error connecting to database ${JSON.stringify(con.db_host)}`,\n            err,\n          );\n        }\n        const localHosts = [\n          \"host.docker.internal\",\n          \"localhost\",\n          \"127.0.0.1\",\n          \"172.17.0.1\",\n        ];\n\n        let prostgles_error_hint = \"\";\n        if (process.env.IS_DOCKER && localHosts.includes(con.db_host)) {\n          prostgles_error_hint = [\n            `\\nTo connect to a localhost database from docker you need to either use \"host\" netoworking mode or:\\n `,\n            `1) If using docker-compose.yml, uncomment extra_hosts:  `,\n            `  extra_hosts:`,\n            `    - \"host.docker.internal:host-gateway\"`,\n            `2) Ensure the target database postgresql.conf contains either:`,\n            `  listen_addresses = 'localhost,172.17.0.1'`,\n            `  OR a more permissive setting like:`,\n            `  listen_addresses = '*'`,\n            `3) Ensure the target database pg_hba.conf contains:`,\n            `  host  all   all   172.17.0.0/16  md5`,\n            `4) Restart the postgresql server to apply the changes.`,\n            `5) Ensure the user you connect with has an encrypted password. `,\n            `6) Use \"172.17.0.1\" or \"host.docker.internal\" instead of \"localhost\" in the above connection details`,\n          ].join(\"\\n\");\n        }\n        const serialisableError = getSerialisableError(err);\n        const removeUndefined = (obj: Record<string, unknown>) => {\n          return Object.fromEntries(\n            Object.entries(obj).filter(([_, v]) => v !== undefined),\n          );\n        };\n        reject(\n          removeUndefined(\n            isObject(serialisableError) ?\n              { ...serialisableError, prostgles_error_hint }\n            : { error: serialisableError, prostgles_error_hint },\n          ),\n        );\n      });\n  });\n};\n\nexport const getDbConnection = async (\n  _c: DBGeneratedSchema[\"connections\"][\"columns\"],\n  opts?: pg.IConnectionParameters<pg.IClient>,\n): Promise<pgPromise.IDatabase<{}, pg.IClient>> => {\n  const { connectionInfo } = await testDBConnection(_c);\n  const db = pgp({ ...connectionInfo, ...opts, allowExitOnIdle: true });\n  return db;\n};\n"
  },
  {
    "path": "server/src/connectionUtils/validateConnection.ts",
    "content": "import { ConnectionString } from \"connection-string\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DBSConnectionInfo } from \"../electronConfig\";\nexport type Connections = Required<DBGeneratedSchema[\"connections\"][\"columns\"]>;\nexport type ConnectionInsert = DBGeneratedSchema[\"connections\"][\"columns\"];\n\ntype ConnectionDefaults = Pick<\n  DBSConnectionInfo,\n  \"db_host\" | \"db_name\" | \"db_port\" | \"db_ssl\" | \"db_user\"\n>;\nconst getDefaults = (c: Partial<ConnectionDefaults>) =>\n  ({\n    db_host: c.db_host ?? \"localhost\",\n    db_name: c.db_name ?? \"postgres\",\n    db_user: c.db_user ?? \"postgres\",\n    db_port: c.db_port ?? 5432,\n    db_ssl: c.db_ssl ?? \"prefer\",\n  }) satisfies Required<ConnectionDefaults>;\n\ntype ValidatedConnectionDetails = Required<ConnectionDefaults> & {\n  db_conn: string;\n  db_pass?: string;\n};\n\nexport type ConnectionInfo = Partial<\n  DBSConnectionInfo | Connections | ConnectionInsert\n>;\n\nexport const validateConnection = <C extends ConnectionInfo>(\n  rawConnection: C,\n): C & ValidatedConnectionDetails => {\n  const result = { ...rawConnection };\n\n  if (rawConnection.type === \"Connection URI\") {\n    const db_conn =\n      rawConnection.db_conn ||\n      validateConnection({\n        ...result,\n        ...getDefaults(result),\n        type: \"Standard\",\n      }).db_conn;\n\n    const cs = new ConnectionString(db_conn);\n    const params = cs.params ?? {};\n    const {\n      sslmode,\n      host,\n      port = \"5432\",\n      dbname,\n      user,\n      password,\n    } = params as {\n      sslmode?: ConnectionDefaults[\"db_ssl\"];\n      host?: string;\n      port?: string;\n      dbname?: string;\n      user?: string;\n      password?: string;\n    };\n\n    const { db_host, db_port, db_user, db_name, db_ssl } = getDefaults({\n      db_host: cs.hosts?.[0]?.name || host,\n      db_port: cs.hosts?.[0]?.port || +port,\n      db_user: cs.user ?? (user || \"postgres\"),\n      db_name: cs.path?.join(\"/\") ?? dbname,\n      db_ssl: sslmode,\n    });\n\n    const validated = {\n      ...rawConnection,\n      db_conn,\n      db_host,\n      db_port,\n      db_user,\n      db_name,\n      db_ssl,\n      db_pass: cs.password ?? password,\n    };\n\n    return validated;\n  } else if (rawConnection.type === \"Standard\" || rawConnection.db_host) {\n    const { db_host, db_port, db_user, db_name, db_ssl, db_pass } = {\n      ...getDefaults(rawConnection),\n      ...rawConnection,\n    };\n    const cs = new ConnectionString(null, { protocol: \"postgres\" });\n    cs.hosts = [\n      {\n        name: db_host,\n        port: db_port,\n      },\n    ];\n    cs.password = db_pass ?? undefined;\n    cs.user = db_user;\n    cs.path = [db_name];\n    cs.params = { sslmode: rawConnection.db_ssl ?? \"prefer\" };\n    const db_conn = cs.toString();\n\n    const validated = {\n      ...rawConnection,\n      db_host,\n      db_port,\n      db_user,\n      db_name,\n      db_ssl,\n      db_conn,\n      db_pass: rawConnection.db_pass ?? \"\",\n    };\n\n    return validated;\n  } else {\n    throw \"Not supported\";\n  }\n};\n"
  },
  {
    "path": "server/src/electronConfig.ts",
    "content": "/* eslint-disable security/detect-non-literal-fs-filename */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\n\nexport type Connections = Required<DBGeneratedSchema[\"connections\"][\"columns\"]>;\nexport type DBSConnectionInfo = Pick<\n  Required<Connections>,\n  | \"db_conn\"\n  | \"db_name\"\n  | \"db_user\"\n  | \"db_pass\"\n  | \"db_host\"\n  | \"db_port\"\n  | \"db_ssl\"\n  | \"type\"\n>;\nexport type OnServerReadyCallback = (portNumber: number) => void;\n\ntype SafeStorage = {\n  decryptString(encrypted: Buffer): string;\n  encryptString(plainText: string): Buffer;\n  isEncryptionAvailable(): boolean;\n};\n\nlet port: number | undefined;\n\nconst electronConfig: {\n  isElectron: boolean;\n  electronSid: string;\n  safeStorage: SafeStorage | undefined;\n} = {\n  isElectron: false,\n  electronSid: \"\",\n  safeStorage: undefined,\n  // onReady: (actualPort: number) => {},\n};\n\nexport const actualRootDir = path.join(__dirname, \"/../../..\");\nlet rootDir = actualRootDir;\n/**\n * server root directory\n */\nexport const getRootDir = () => rootDir;\n\nexport const getElectronConfig = () => {\n  const { isElectron, safeStorage } = electronConfig;\n  if (!isElectron) return undefined;\n\n  if (\n    !safeStorage ||\n    typeof safeStorage.encryptString !== \"function\" ||\n    typeof safeStorage.decryptString !== \"function\"\n  ) {\n    throw \"Invalid safeStorage provided. encryptString or decryptString is not a function\";\n  }\n\n  const electronConfigPath = path.resolve(\n    `${getRootDir()}/.prostgles-desktop-config.json`,\n  );\n  const getCredentials = (): DBSConnectionInfo | undefined => {\n    try {\n      const file =\n        !fs.existsSync(electronConfigPath) ?\n          undefined\n        : fs.readFileSync(electronConfigPath);\n      const decrypted = file ? safeStorage.decryptString(file) : undefined;\n      if (decrypted) {\n        return JSON.parse(decrypted) as DBSConnectionInfo;\n      }\n    } catch (e) {\n      console.error(e);\n    }\n\n    return undefined;\n  };\n\n  return {\n    isElectron: true,\n    port,\n    sidConfig: electronConfig,\n    hasCredentials: () => !!getCredentials(),\n    getCredentials,\n    setCredentials: (connection?: DBSConnectionInfo) => {\n      if (!connection) {\n        if (fs.existsSync(electronConfigPath)) {\n          fs.unlinkSync(electronConfigPath);\n        }\n      } else {\n        try {\n          console.log(\"Writing auth file: \" + electronConfigPath);\n          fs.writeFileSync(\n            electronConfigPath,\n            safeStorage.encryptString(JSON.stringify(connection)),\n          );\n        } catch (err) {\n          console.error(\"Failed writing auth file: \" + electronConfigPath, err);\n          throw err;\n        }\n      }\n    },\n  };\n};\n\nexport const start = async (params: {\n  safeStorage: SafeStorage;\n  electronSid: string;\n  rootDir: string;\n  onReady: (actualPort: number) => void;\n  port?: number;\n}) => {\n  const { electronSid, onReady } = params;\n  if (!params.rootDir || typeof params.rootDir !== \"string\") {\n    throw `Must provide a valid rootDir`;\n  }\n  rootDir = params.rootDir;\n  if (\n    !electronSid ||\n    typeof electronSid !== \"string\" ||\n    typeof onReady !== \"function\"\n  ) {\n    throw \"Must provide a valid electronSid: string and onSidWasSet: () => void\";\n  }\n  electronConfig.isElectron = true;\n  electronConfig.electronSid = params.electronSid;\n  electronConfig.safeStorage = params.safeStorage;\n  const { startServer } = await import(\"./index\");\n  const startResult = await startServer(async ({ port: actualPort }) => {\n    // const [token] = prostglesTokens;\n    // if (token) {\n    //   console.log(\"Setting prostgles tokens\");\n    //   void dbs.global_settings.update(\n    //     {},\n    //     { prostgles_registration: { email: \"\", enabled: true, token } },\n    //   );\n    // }\n    await new Promise((resolve) => {\n      setTimeout(() => {\n        resolve(true);\n      }, 1000);\n    });\n    return onReady(actualPort);\n  });\n\n  return {\n    destroy: async () => {\n      return startResult.connMgr.destroy();\n    },\n  };\n};\n\n/**\n * Prostgles token granted after registration\n */\nconst prostglesTokens: string[] = [];\nexport const setProstglesToken = (token: string) => {\n  prostglesTokens.push(token);\n  setInterval(() => {\n    console.log(\"Prostgles token: \" + token);\n  }, 1000);\n};\n"
  },
  {
    "path": "server/src/envVars.ts",
    "content": "import * as dotenv from \"dotenv\";\nimport path from \"path\";\nimport type { DBSConnectionInfo } from \"./electronConfig\";\nimport { actualRootDir } from \"./electronConfig\";\nimport { DB_SSL_ENUM } from \"./tableConfig/tableConfigConnections\";\nimport { validateConnection } from \"./connectionUtils/validateConnection\";\nconst envFileVars = dotenv.config({\n  path: path.resolve(actualRootDir + \"/../.env\"),\n});\n\nexport const {\n  PRGL_USERNAME = \"\",\n  PRGL_PASSWORD = \"\",\n\n  POSTGRES_URL,\n  POSTGRES_DB,\n  POSTGRES_HOST,\n  POSTGRES_PASSWORD,\n  POSTGRES_PORT,\n  POSTGRES_USER,\n  POSTGRES_SSL,\n  PROSTGLES_STRICT_COOKIE,\n} = {\n  ...(envFileVars.parsed ?? {}),\n  ...process.env,\n} as Record<string, string>;\n\nconst db_ssl: DBSConnectionInfo[\"db_ssl\"] = //@ts-ignore\n  DB_SSL_ENUM[DB_SSL_ENUM.indexOf(POSTGRES_SSL?.trim().toLowerCase())] ??\n  \"prefer\";\nexport const DBS_CONNECTION_INFO = validateConnection({\n  name: \"Prostgles UI state\",\n  type: !POSTGRES_URL ? \"Standard\" : \"Connection URI\",\n  db_conn: POSTGRES_URL ?? null,\n  db_name: POSTGRES_DB,\n  db_user: POSTGRES_USER,\n  db_pass: POSTGRES_PASSWORD ?? null,\n  db_host: POSTGRES_HOST,\n  db_port: parseInt(POSTGRES_PORT ?? \"5432\"),\n  db_ssl,\n});\n"
  },
  {
    "path": "server/src/getPSQLQueries.ts",
    "content": "export const COMMANDS = [\n  { cmd: \"\\\\d\", opts: \"[S+]\", desc: \"list tables, views, and sequences\" },\n  {\n    cmd: \"\\\\d\",\n    opts: \"[S+]\",\n    desc: \"describe table, view, sequence, or index\",\n  },\n  { cmd: \"\\\\da\", opts: \"[S]\", desc: \"list aggregates\" },\n  { cmd: \"\\\\dA\", opts: \"[+]\", desc: \"list access methods\" },\n  { cmd: \"\\\\dAc\", opts: \"[+]\", desc: \"list operator classes\" },\n  { cmd: \"\\\\dAf\", opts: \"[+]\", desc: \"list operator families\" },\n  { cmd: \"\\\\dAo\", opts: \"[+]\", desc: \"list operators of operator families\" },\n  {\n    cmd: \"\\\\dAp\",\n    opts: \"[+]\",\n    desc: \"list support functions of operator families\",\n  },\n  { cmd: \"\\\\db\", opts: \"[+]\", desc: \"list tablespaces\" },\n  { cmd: \"\\\\dc\", opts: \"[S+]\", desc: \"list conversions\" },\n  { cmd: \"\\\\dC\", opts: \"[+]\", desc: \"list casts\" },\n  {\n    cmd: \"\\\\dd\",\n    opts: \"[S]\",\n    desc: \"show object descriptions not displayed elsewhere\",\n  },\n  { cmd: \"\\\\dD\", opts: \"[S+]\", desc: \"list domains\" },\n  { cmd: \"\\\\ddp\", desc: \"list default privileges\" },\n  { cmd: \"\\\\dE\", opts: \"[S+]\", desc: \"list foreign tables\" },\n  { cmd: \"\\\\des\", opts: \"[+]\", desc: \"list foreign servers\" },\n  { cmd: \"\\\\det\", opts: \"[+]\", desc: \"list foreign tables\" },\n  { cmd: \"\\\\deu\", opts: \"[+]\", desc: \"list user mappings\" },\n  { cmd: \"\\\\dew\", opts: \"[+]\", desc: \"list foreign-data wrappers\" },\n  {\n    cmd: \"\\\\df\",\n    opts: \"[anptw]\",\n    desc: \"list [only agg/normal/procedure/trigger/window]\",\n  },\n  { cmd: \"\\\\dF\", opts: \"[+]\", desc: \"list text search configurations\" },\n  { cmd: \"\\\\dFd\", opts: \"[+]\", desc: \"list text search dictionaries\" },\n  { cmd: \"\\\\dFp\", opts: \"[+]\", desc: \"list text search parsers\" },\n  { cmd: \"\\\\dFt\", opts: \"[+]\", desc: \"list text search templates\" },\n  { cmd: \"\\\\dg\", opts: \"[S+]\", desc: \"list roles\" },\n  { cmd: \"\\\\di\", opts: \"[S+]\", desc: \"list indexes\" },\n  { cmd: \"\\\\dl\", desc: \"list large objects, same as \\\\lo_list\" },\n  { cmd: \"\\\\dL\", opts: \"[S+]\", desc: \"list procedural languages\" },\n  { cmd: \"\\\\dm\", opts: \"[S+]\", desc: \"list materialized views\" },\n  { cmd: \"\\\\dn\", opts: \"[S+]\", desc: \"list schemas\" },\n  { cmd: \"\\\\do\", opts: \"[S+]\", desc: \"list operators\" },\n  { cmd: \"\\\\dO\", opts: \"[S+]\", desc: \"list collations\" },\n  { cmd: \"\\\\dp\", desc: \"list table, view, and sequence access privileges\" },\n  {\n    cmd: \"\\\\dP\",\n    opts: \"[itn+]\",\n    desc: \"list [only index/table] partitioned relations [n=nested]\",\n  },\n  { cmd: \"\\\\drds\", desc: \"list per-database role settings\" },\n  { cmd: \"\\\\dRp\", opts: \"[+]\", desc: \"list replication publications\" },\n  { cmd: \"\\\\dRs\", opts: \"[+]\", desc: \"list replication subscriptions\" },\n  { cmd: \"\\\\ds\", opts: \"[S+]\", desc: \"list sequences\" },\n  { cmd: \"\\\\dt\", opts: \"[S+]\", desc: \"list tables\" },\n  { cmd: \"\\\\dT\", opts: \"[S+]\", desc: \"list data types\" },\n  { cmd: \"\\\\du\", opts: \"[S+]\", desc: \"list roles\" },\n  { cmd: \"\\\\dv\", opts: \"[S+]\", desc: \"list views\" },\n  { cmd: \"\\\\dx\", opts: \"[+]\", desc: \"list extensions\" },\n  { cmd: \"\\\\dX\", desc: \"list extended statistics\" },\n  { cmd: \"\\\\dy\", opts: \"[+]\", desc: \"list event triggers\" },\n  { cmd: \"\\\\l\", opts: \"[+]\", desc: \"list databases\" },\n  // { cmd: \"\\\\sf func_name\", opts: \"[+]\",  desc: \"show a function's definition\" },\n  // { cmd: \"\\\\sv view_name\", opts: \"[+]\",  desc: \"show a view's definition\" },\n  // { cmd: \"\\\\z\",                desc: \"same as \\\\dp\" },\n] as const;\n\nimport { execSync } from \"child_process\";\nimport * as fs from \"fs\";\nimport { validateConnection } from \"./connectionUtils/validateConnection\";\nimport type { DBSConnectionInfo } from \"./electronConfig\";\nlet started = false;\nexport const getPSQLQueries = (con: DBSConnectionInfo) => {\n  if (started) return;\n  started = true;\n  const c = validateConnection(con);\n  const queries: { cmd: string; desc: string; query: string }[] = [];\n  COMMANDS.forEach((command) => {\n    /* First is empty */\n    const opts = (\" \" + (\"opts\" in command ? command.opts : \"\"))\n      .replaceAll(\"[\", \"\")\n      .replaceAll(\"]\", \"\")\n      .split(\"\");\n\n    opts.forEach((opt, i) => {\n      const cmd = `\\\\${command.cmd}${opts\n        .slice(0, i + 1)\n        .join(\"\")\n        .trim()}`;\n      if (cmd.includes(\"dRp+\")) return;\n      try {\n        const queryAndResult = execSync(\n          `psql 'postgres://${c.db_user}:${c.db_pass}@${c.db_host}/${c.db_name}' -w -E -c '${cmd}'`,\n        ).toString();\n        const query = queryAndResult\n          .split(\"********* QUERY **********\")[1]\n          ?.split(\"**************************\")[0];\n\n        if (query) {\n          queries.push({\n            ...command,\n            cmd,\n            query,\n          });\n        }\n        console.log(`psql ${cmd} ok`);\n      } catch (err) {\n        console.error(`psql ${cmd} fail:`, err);\n      }\n    });\n  });\n\n  fs.writeFileSync(\n    __dirname + `/../../../../common/psql_queries.json`,\n    JSON.stringify(queries, null, 2),\n    { encoding: \"utf-8\" },\n  );\n};\n"
  },
  {
    "path": "server/src/index.ts",
    "content": "process.on(\"unhandledRejection\", (reason, p) => {\n  console.trace(\"Unhandled Rejection at: Promise\", p, \"reason:\", reason);\n});\n\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { ProstglesState } from \"@common/electronInitTypes\";\nimport { isObject, type DBSSchema } from \"@common/publishUtils\";\nimport { SPOOF_TEST_VALUE } from \"@common/utils\";\nimport { spawn } from \"child_process\";\nimport type { NextFunction, Request, Response } from \"express\";\nimport path from \"path\";\nimport type { DBOFullyTyped } from \"prostgles-server/dist/DBSchemaBuilder/DBSchemaBuilder\";\nimport type { VoidFunction } from \"prostgles-server/dist/SchemaWatch/SchemaWatch\";\nimport { getKeys, omitKeys, type AnyObject } from \"prostgles-types\";\nimport { sidKeyName } from \"./authConfig/sessionUtils\";\nimport { getAuthSetupData } from \"./authConfig/subscribeToAuthSetupChanges\";\nimport { ConnectionManager } from \"./ConnectionManager/ConnectionManager\";\nimport { actualRootDir, getElectronConfig } from \"./electronConfig\";\nimport { initExpressAndIOServers } from \"./init/initExpressAndIOServers\";\nimport { setDBSRoutesForElectron } from \"./init/setDBSRoutesForElectron\";\nimport type {\n  InitExtra,\n  ProstglesInitStateWithDBS,\n} from \"./init/startProstgles\";\nimport {\n  getProstglesState,\n  startingProstglesResult,\n  tryStartProstgles,\n} from \"./init/tryStartProstgles\";\n\nconst { app, http, io } = initExpressAndIOServers();\n\nexport const connMgr = new ConnectionManager(http, app);\nexport const isDocker = Boolean(process.env.IS_DOCKER);\n\nconst isTestingElectron = require.main?.filename.endsWith(\"testElectron.js\");\nconst electronConfig = getElectronConfig();\nconst PORT =\n  electronConfig && !isTestingElectron ? 0 : (\n    +(process.env.PROSTGLES_UI_PORT ?? 3004)\n  );\nconst LOCALHOST = \"127.0.0.1\";\nconst HOST =\n  electronConfig ? LOCALHOST : process.env.PROSTGLES_UI_HOST || LOCALHOST;\nsetDBSRoutesForElectron(app, io, PORT, HOST);\n\n/** Make client wait for everything to load before serving page */\nexport const waitForInitialisation =\n  async (): Promise<ProstglesInitStateWithDBS> => {\n    const { initState, isElectron, electronCredsProvided } =\n      getProstglesState();\n\n    // Return immediately if not in loading state or if in Electron without credentials\n    if (\n      initState.state !== \"loading\" ||\n      (isElectron && !electronCredsProvided)\n    ) {\n      return initState;\n    }\n\n    await startingProstglesResult?.result;\n    await tout(200);\n    return waitForInitialisation();\n  };\n\n/**\n * Serve prostglesInitState\n */\napp.get(\"/dbs\", (req, res) => {\n  const prostglesState = getProstglesState();\n  const { initState } = prostglesState;\n  const nonSerialiseableOrNotNeededKeys = getKeys({\n    dbs: 1,\n  } satisfies Record<keyof InitExtra, 1>);\n  const serverState: ProstglesState = {\n    ...prostglesState,\n    initState:\n      /**\n       * Do not send full error details to the client if it's not Electron.\n       * Electron can only be accessed locally so we can safely send full error details\n       * */\n      initState.state === \"error\" && !prostglesState.isElectron ?\n        {\n          ...initState,\n          error:\n            initState.errorType === \"init\" ?\n              \"Failed to start prostgles. Check logs\"\n            : \"Could not connect to the state database. Check logs\",\n        }\n      : initState.state === \"ok\" ?\n        omitKeys(initState, nonSerialiseableOrNotNeededKeys)\n      : initState,\n  };\n\n  const electronCreds =\n    electronConfig?.isElectron && electronConfig.hasCredentials() ?\n      electronConfig.getCredentials()\n    : undefined;\n  /** Provide credentials if there is a connection error so the user can rectify it */\n  if (\n    electronCreds &&\n    serverState.initState.state === \"error\" &&\n    serverState.initState.errorType === \"connection\" &&\n    (req.cookies as AnyObject)[sidKeyName] ===\n      electronConfig?.sidConfig.electronSid\n  ) {\n    serverState.electronCreds =\n      electronCreds as typeof serverState.electronCreds;\n  }\n  /** Alert admin if x-real-ip is spoofable */\n  let xRealIpSpoofable = false;\n  const { globalSettings } = getAuthSetupData();\n  if (\n    req.headers[\"x-real-ip\"] === SPOOF_TEST_VALUE &&\n    globalSettings?.login_rate_limit_enabled &&\n    globalSettings.login_rate_limit.groupBy === \"x-real-ip\"\n  ) {\n    xRealIpSpoofable = true;\n  }\n  res.json({ ...serverState, xRealIpSpoofable });\n});\n\n/* Must provide index.html if there is an error OR prostgles is loading */\nconst serveIndexIfNoCredentialsOrInitError = async (\n  req: Request,\n  res: Response,\n  next: NextFunction,\n) => {\n  await waitForInitialisation();\n  const {\n    isElectron,\n    initState: { state },\n    electronCredsProvided,\n  } = getProstglesState();\n  if (state !== \"ok\" || (isElectron && !electronCredsProvided)) {\n    if (req.method === \"GET\" && !req.path.startsWith(\"/dbs\")) {\n      res.sendFile(path.resolve(actualRootDir + \"/../client/build/index.html\"));\n      return;\n    }\n  }\n\n  next();\n};\n\n// eslint-disable-next-line @typescript-eslint/no-misused-promises\napp.use(serveIndexIfNoCredentialsOrInitError);\n\n/** Startup procedure\n * If electron:\n *  - serve index\n *  - serve prostglesInitState\n *  - Check for any existing older prostgles schema versions AND allow deleting curr db OR connect to new db\n *  - start prostgles IF or WHEN creds provided\n *  - remove serve index after prostgles is ready\n *\n * If docker/default\n *  - serve index if loading\n *  - serve prostglesInitState\n *  - try start prostgles\n *  - If failed to connect then also serve index\n */\nconst startProstgles = ({ host, port }: { host: string; port: number }) => {\n  if (electronConfig) {\n    const creds = electronConfig.getCredentials();\n    if (creds) {\n      void tryStartProstgles({ app, io, host, port, con: creds });\n    } else {\n      console.log(\"Electron: No credentials\");\n    }\n    setDBSRoutesForElectron(app, io, port, host);\n  } else {\n    void tryStartProstgles({ app, io, host, port, con: undefined });\n  }\n};\n\nexport function restartProc(cb?: VoidFunction) {\n  console.warn(\"Restarting process\");\n  if (process.env.process_restarting) {\n    delete process.env.process_restarting;\n    // Give old process one second to shut down before continuing ...\n    setTimeout(() => {\n      cb?.();\n      restartProc();\n    }, 1000);\n    return;\n  }\n\n  // Restart process ...\n  const command = process.argv[0];\n  if (!command) {\n    throw new Error(\"No command found to restart process\");\n  }\n  const args = process.argv.slice(1);\n  spawn(command, args, {\n    env: { process_restarting: \"1\" },\n    stdio: \"ignore\",\n  }).unref();\n}\n\nexport const tout = (timeout: number) => {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(true);\n    }, timeout);\n  });\n};\n\nexport type BareConnectionDetails = Pick<\n  Connections,\n  | \"type\"\n  | \"db_conn\"\n  | \"db_host\"\n  | \"db_name\"\n  | \"db_pass\"\n  | \"db_port\"\n  | \"db_user\"\n  | \"db_ssl\"\n  | \"ssl_certificate\"\n>;\nexport type DBS = DBOFullyTyped<DBGeneratedSchema>;\nexport type Users = Required<DBGeneratedSchema[\"users\"][\"columns\"]>;\nexport type Connections = Required<DBGeneratedSchema[\"connections\"][\"columns\"]>;\nexport type DatabaseConfigs = DBSSchema[\"database_configs\"];\n\nexport const log = (msg: string, extra?: any) => {\n  console.log(\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    ...[`(server): ${new Date().toISOString()} ` + msg, extra].filter((v) => v),\n  );\n};\n\ntype OnServerReadyResult = {\n  // dbs: DBS;\n  port: number;\n};\n\nexport const startServer = async (\n  onReady?: (\n    result: OnServerReadyResult,\n    startupResult: ProstglesInitStateWithDBS,\n  ) => void | Promise<void>,\n) => {\n  const actualPort = await new Promise<number>((resolve) => {\n    const server = http.listen(PORT, HOST, () => {\n      const address = server.address();\n      const port = isObject(address) ? address.port : PORT;\n      const host = isObject(address) ? address.address : HOST;\n\n      startProstgles({ host, port });\n\n      console.log(\n        `\\n\\nexpress listening on port ${port} (${host}:${port})\\n\\n`,\n      );\n\n      resolve(port);\n    });\n  });\n\n  const startupResult = await waitForInitialisation();\n  await onReady?.({ port: actualPort }, startupResult);\n  return { connMgr };\n};\n\n/**\n * Start the server if not electron\n * Otherwise it will be started from electron/main.ts\n */\nif (require.main === module) {\n  void startServer((result, dbStartupInfo) => {\n    if (dbStartupInfo.state === \"error\") {\n      console.error(\"Failed to start prostgles\", dbStartupInfo);\n      process.exit(1);\n    }\n    console.log(\"Server started\", result);\n  });\n}\n"
  },
  {
    "path": "server/src/init/cleanupTestDatabases.ts",
    "content": "import { testDBConnection } from \"../connectionUtils/testDBConnection\";\nimport type { DBSConnectionInfo } from \"../electronConfig\";\nimport { isTesting } from \"./initExpressAndIOServers\";\n\nexport const cleanupTestDatabases = async (con: DBSConnectionInfo) => {\n  if (!isTesting) return;\n\n  await testDBConnection({ ...con, db_name: \"postgres\" }, false, async (c) => {\n    // const existingDbs: { datname: string }[] = await c.any(\n    //   \"SELECT datname FROM pg_database WHERE datistemplate = false;\",\n    // );\n    const commands = [\n      \"drop database db; \",\n      \"drop database cloud; \",\n      \"drop database crypto; \",\n      \"drop database sample_database; \",\n      \"drop database sample_db; \",\n      \"drop database my_new_db; \",\n      \"drop database db_with_owner;\",\n      \"drop user db_with_owner;\",\n      \"create database db with owner usr;\",\n    ];\n    // .filter((cmd) => {\n    //   return (\n    //     !cmd.includes(\"database\") ||\n    //     existingDbs.some((dbName) => cmd.includes(`drop database ${dbName}`))\n    //   );\n    // });\n    await Promise.all(\n      commands.map(async (cmd) => {\n        return c.result(cmd).catch(console.error);\n      }),\n    );\n  });\n};\n"
  },
  {
    "path": "server/src/init/initExpressAndIOServers.ts",
    "content": "import { logOutgoingHttpRequests } from \"./logOutgoingHttpRequests\";\nlogOutgoingHttpRequests(false);\n\nimport cookieParser from \"cookie-parser\";\nimport express, { json, urlencoded } from \"express\";\nimport helmet from \"helmet\";\nimport _http from \"http\";\nimport path from \"path\";\nimport { Server } from \"socket.io\";\nimport { API_ENDPOINTS } from \"@common/utils\";\nimport { withOrigin } from \"../authConfig/getAuth\";\nimport { sidKeyName } from \"../authConfig/sessionUtils\";\nimport { actualRootDir } from \"../electronConfig\";\n\nexport const isTesting = !!process.env.PRGL_TEST;\nexport const initExpressAndIOServers = () => {\n  const app = express();\n  app.use(\n    helmet({\n      crossOriginResourcePolicy: false,\n      referrerPolicy: false,\n    }),\n  );\n\n  if (isTesting) {\n    app.use((req, res, next) => {\n      res.on(\"finish\", () => {\n        console.log(\n          [\n            new Date().toISOString(),\n            req.headers[\"x-real-ip\"] || req.ip,\n            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n            ((req.cookies?.[sidKeyName] as string) || \"[undefined] \").slice(\n              0,\n              10,\n            ),\n            req.method,\n            res.statusCode,\n            req.url,\n            res.statusCode === 302 ? res.getHeader(\"Location\") : \"\",\n          ].join(\" \"),\n        );\n      });\n      next();\n    });\n  }\n\n  /**\n   * Required to ensure xenova/transformators works\n   */\n  const localLLMHeaders = \"\"; // `'unsafe-eval' 'wasm-unsafe-eval'`;\n  // console.error(\"REMOVE CSP\", localLLMHeaders);\n  const renderPDFinIframe = `data: blob:`;\n  app.use(json({ limit: \"100mb\" }));\n  app.use(urlencoded({ extended: true, limit: \"100mb\" }));\n  app.use(function (req, res, next) {\n    /* data import (papaparse) requires: worker-src blob: 'self' */\n    res.setHeader(\n      \"Content-Security-Policy\",\n      ` script-src 'self' ${localLLMHeaders}; frame-src 'self' ${renderPDFinIframe} ; worker-src blob: 'self';`,\n    );\n    next();\n  });\n\n  process.on(\"unhandledRejection\", (reason, p) => {\n    console.trace(\"Unhandled Rejection at: Promise\", p, \"reason:\", reason);\n  });\n\n  const http = _http.createServer(app);\n\n  app.use(\n    express.static(path.resolve(actualRootDir + \"/../client/build\"), {\n      index: false,\n      cacheControl: false,\n    }),\n  );\n  app.use(\n    express.static(path.resolve(actualRootDir + \"/../client/static\"), {\n      index: false,\n      cacheControl: false,\n    }),\n  );\n  app.use(\n    express.static(path.resolve(actualRootDir + \"/../docs\"), {\n      index: false,\n      cacheControl: false,\n    }),\n  );\n  app.use(\n    express.static(\"/icons\", {\n      cacheControl: true,\n      index: false,\n      maxAge: 31536000,\n    }),\n  );\n\n  /** Needed to load MVT tiles worker */\n  app.use(\n    express.static(\n      path.resolve(\n        actualRootDir + \"/../client/node_modules/@loaders.gl/mvt/dist/\",\n      ),\n      { index: false, extensions: [\"js\"] },\n    ),\n  );\n\n  app.use(cookieParser());\n\n  const io = new Server(http, {\n    path: API_ENDPOINTS.WS_DBS,\n    maxHttpBufferSize: 100e100,\n    cors: withOrigin,\n  });\n\n  // Log server-level events\n  io.engine.on(\"connection_error\", (err) => {\n    console.error(\"Connection error :\", err);\n  });\n  return {\n    app,\n    io,\n    http,\n  };\n};\n"
  },
  {
    "path": "server/src/init/insertStateDatabase.ts",
    "content": "import type { DB } from \"prostgles-server/dist/Prostgles\";\nimport { pickKeys, tryCatchV2 } from \"prostgles-types\";\nimport type { DBS } from \"..\";\nimport type { DBSConnectionInfo } from \"../electronConfig\";\nimport { upsertConnection } from \"../upsertConnection\";\n\n/** Add state db if missing */\nexport const insertStateDatabase = async (\n  db: DBS,\n  _db: DB,\n  con: DBSConnectionInfo,\n  isElectron: boolean,\n) => {\n  const stateConnectionCount = await db.connections.count(\n    pickKeys(con, [\"db_name\", \"db_host\", \"db_port\", \"db_user\"]),\n  );\n  if (!stateConnectionCount) {\n    const { data: state_db, error } = await tryCatchV2(async () => {\n      const { connection: state_db } = await upsertConnection(\n        {\n          ...con,\n          user_id: null,\n          name: isElectron ? \"Prostgles Desktop state\" : \"Prostgles UI state\",\n          type: !con.db_conn ? \"Standard\" : \"Connection URI\",\n          db_port: con.db_port || 5432,\n          db_ssl: con.db_ssl, // || \"disable\",\n          is_state_db: true,\n        },\n        null,\n        db,\n      );\n\n      return state_db;\n    });\n\n    if (error) {\n      console.error(\"Failed to insert state database\", error);\n      throw error;\n    } else {\n      console.log(\"Inserted state database \", state_db?.db_name);\n    }\n    if (!state_db) throw \"state_db not found\";\n  }\n};\n\n// export const createSampleDatabase = async (db: DBS, _db: DB, state_db: DBSSchema[\"connections\"]) => {\n\n//   const SAMPLE_DB_LABEL = \"Sample database\";\n//   const SAMPLE_DB_NAME = \"sample_database\";\n//   const sampleConnection = await db.connections.findOne({ name: SAMPLE_DB_LABEL, db_name: SAMPLE_DB_NAME });\n//   if(!sampleConnection){\n\n//     const databases: string[] = (await _db.any(`SELECT datname FROM pg_database WHERE datistemplate = false;`)).map(({ datname }) => datname)\n//     if(!databases.includes(SAMPLE_DB_NAME)) {\n//       await _db.any(\"CREATE DATABASE \" + SAMPLE_DB_NAME);\n//     }\n//     if(!getElectronConfig()?.isElectron){\n//       const stateCon = { ...omitKeys(state_db, [\"id\"]) };\n//       const validatedSampleDBConnection = validateConnection({\n//         ...stateCon,\n//         type: \"Standard\",\n//         name: SAMPLE_DB_LABEL,\n//         db_name: SAMPLE_DB_NAME,\n//       })\n//       const { connection: con, database_config } = await upsertConnection({\n//         ...stateCon,\n//         ...validatedSampleDBConnection,\n//         is_state_db: false,\n//         name: SAMPLE_DB_LABEL,\n//       }, null, db);\n//       console.log(\"Inserted sample connection for db \", con.db_name);\n//     }\n\n//   }\n// }\n"
  },
  {
    "path": "server/src/init/isRetryableError.ts",
    "content": "import type { AnyObject } from \"prostgles-types\";\nimport type { ProstglesInitStateWithDBS } from \"./startProstgles\";\n\nexport const isRetryableError = (\n  errorState: Extract<ProstglesInitStateWithDBS, { state: \"error\" }>,\n): boolean => {\n  // Explicitly non-retryable Prostgles init errors\n  if (errorState.errorType === \"init\") {\n    console.warn(\n      \"Non-retryable Prostgles Initialization Error detected.\",\n      errorState.error,\n    );\n    return false;\n  }\n\n  // Explicitly non-retryable connection errors (customise based on common pg error codes)\n  if (errorState.error) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n    const code = (errorState.error as AnyObject).code;\n    // Examples:\n    if (code === \"3D000\") {\n      // Database does not exist\n      console.warn(\n        `Non-retryable DB connection error: Database does not exist (Code: ${code})`,\n      );\n      return false;\n    }\n    if (code === \"28P01\") {\n      // Invalid password / Auth failed\n      console.warn(\n        `Non-retryable DB connection error: Authentication failed (Code: ${code})`,\n      );\n      return false;\n    }\n    // Add other known fatal error codes here (e.g., invalid config, syntax errors)\n    // If it's not explicitly non-retryable, assume it might be transient\n    console.warn(\n      \"Potentially retryable DB connection error encountered:\",\n      errorState.error,\n    );\n    return true; // Assume other connection errors might be temporary\n  }\n\n  // Unexpected errors during startProstgles execution (outside the expected return structure)\n  console.error(\n    \"Unexpected error during startProstgles execution, considering non-retryable:\",\n    errorState,\n  );\n  return false;\n};\n"
  },
  {
    "path": "server/src/init/logOutgoingHttpRequests.ts",
    "content": "function requestLogger(httpModule: any) {\n  const original = httpModule.request;\n  httpModule.request = function (options: any, callback: any) {\n    console.log(\n      (options.href || options.proto) + \"://\" + options.host + options.path,\n      options.method,\n    );\n    return original(options, callback);\n  };\n}\nexport const logOutgoingHttpRequests = (enable: boolean) => {\n  if (!enable) return;\n  requestLogger(require(\"http\"));\n  requestLogger(require(\"https\"));\n};\n"
  },
  {
    "path": "server/src/init/onProstglesReady.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { getProstglesMcpHub } from \"@src/McpHub/ProstglesMcpHub/ProstglesMcpHub\";\nimport { getServiceManager } from \"@src/ServiceManager/ServiceManager\";\nimport type { SUser } from \"@src/authConfig/sessionUtils\";\nimport type { DBSConnectionInfo } from \"@src/electronConfig\";\nimport type e from \"express\";\nimport type { DB } from \"prostgles-server/dist/Prostgles\";\nimport type { OnReadyCallback } from \"prostgles-server/dist/initProstgles\";\nimport { connMgr, type DBS } from \"..\";\nimport BackupManager from \"../BackupManager/BackupManager\";\nimport { setLoggerDBS } from \"../Logger\";\nimport { setupMCPServerHub } from \"../McpHub/AnthropicMcpHub/startMcpHub\";\nimport { initUsers } from \"../SecurityManager/initUsers\";\nimport { getAuth } from \"../authConfig/getAuth\";\nimport {\n  subscribeToAuthSetupChanges,\n  type AuthSetupDataListener,\n} from \"../authConfig/subscribeToAuthSetupChanges\";\nimport { setupLLM } from \"../publishMethods/askLLM/setupLLM\";\nimport { insertStateDatabase } from \"./insertStateDatabase\";\nimport { getProstglesState } from \"./tryStartProstgles\";\n\nlet authSetupDataListener: AuthSetupDataListener | undefined;\n\nlet backupManager: BackupManager | undefined;\nexport const initBackupManager = async (db: DB, dbs: DBS) => {\n  backupManager ??= await BackupManager.create(db, dbs, connMgr);\n  return backupManager;\n};\n\nexport const getBackupManager = () => backupManager;\n\nexport const onProstglesReady = async (\n  params: Parameters<OnReadyCallback<DBGeneratedSchema, SUser>>[0],\n  update: Parameters<OnReadyCallback<DBGeneratedSchema, SUser>>[1],\n  app: e.Express,\n  con: DBSConnectionInfo,\n) => {\n  await promiseCleanup(async () => {\n    const { dbo: db } = params;\n    const _db: DB = params.db;\n\n    setLoggerDBS(params.dbo);\n\n    await initUsers(db, _db);\n\n    await insertStateDatabase(db, _db, con, getProstglesState().isElectron);\n    await setupLLM(db);\n    await setupMCPServerHub(db);\n\n    await connMgr.destroy();\n    await connMgr.init(db, _db);\n    getServiceManager(db);\n\n    backupManager ??= await BackupManager.create(_db, db, connMgr);\n\n    const newAuthSetupDataListener = subscribeToAuthSetupChanges(\n      db,\n      async (authData) => {\n        const auth = await getAuth(app, db, authData);\n        void update({\n          auth,\n        });\n      },\n      authSetupDataListener,\n    );\n    authSetupDataListener = newAuthSetupDataListener;\n\n    const prostglesMCPHub = await getProstglesMcpHub(db);\n\n    return {\n      cleanup: async () => {\n        await backupManager?.destroy();\n        await prostglesMCPHub.destroy();\n      },\n    };\n  });\n};\n\ntype AsyncCleanup = () => Promise<{ cleanup: () => Promise<void> }>;\nlet oldCleanups: ReturnType<AsyncCleanup> | undefined;\nconst promiseCleanup = async (func: AsyncCleanup) => {\n  const previous = oldCleanups;\n  oldCleanups = func();\n\n  if (previous) {\n    const { cleanup } = await previous;\n    await cleanup().catch((e) => {\n      console.error(\"Error during prostgles onReady cleanup\", e);\n    });\n  }\n};\n"
  },
  {
    "path": "server/src/init/setDBSRoutesForElectron.ts",
    "content": "import { randomBytes } from \"crypto\";\nimport type { Express, RequestHandler } from \"express\";\nimport { removeExpressRoute } from \"prostgles-server/dist/Auth/AuthHandler\";\nimport {\n  assertJSONBObjectAgainstSchema,\n  getSerialisableError,\n  pickKeys,\n  tryCatchV2,\n} from \"prostgles-types\";\nimport type { Server } from \"socket.io\";\nimport { DEFAULT_ELECTRON_CONNECTION } from \"@common/electronInitTypes\";\nimport { testDBConnection } from \"../connectionUtils/testDBConnection\";\nimport { validateConnection } from \"../connectionUtils/validateConnection\";\nimport { getElectronConfig } from \"../electronConfig\";\nimport { getProstglesState, tryStartProstgles } from \"./tryStartProstgles\";\n\n/**\n * Used in Electron to set the DB connection and show any connection errors\n */\nexport const setDBSRoutesForElectron = (\n  app: Express,\n  io: Server,\n  port: number,\n  host: string,\n) => {\n  const initState = getProstglesState();\n  if (!initState.isElectron) return;\n\n  const ele = getElectronConfig();\n  if (!ele?.sidConfig.electronSid) {\n    throw \"Electron sid missing\";\n  }\n\n  removeExpressRoute(app, [\"/dbs\"], \"post\");\n  app.post(\"/dbs\", onPostDBSRequestHandler(app, io, port, host));\n};\n\nconst onPostDBSRequestHandler =\n  (app: Express, io: Server, port: number, host: string): RequestHandler =>\n  async (req, res) => {\n    const electronConfig = getElectronConfig();\n    try {\n      if (!electronConfig?.isElectron) {\n        throw \"Not an electron app\";\n      }\n      const dataRaw = pickKeys(req.body, [\"connection\", \"mode\"]);\n      // TODO: should add allow extra props to jsonb validation\n      const data = {\n        ...dataRaw,\n        connection: pickKeys(\n          dataRaw.connection || {},\n          Object.keys(connectionSchema),\n        ),\n      };\n\n      assertJSONBObjectAgainstSchema(\n        {\n          connection: {\n            type: connectionSchema,\n          },\n          mode: { enum: [\"validate\", \"quick\", \"manual\"] },\n        } as const,\n        data,\n        \"/connection\",\n      );\n\n      const { connection, mode } = data;\n\n      const initialCredentials = pickKeys(connection, [\n        \"db_conn\",\n        \"db_user\",\n        \"db_pass\",\n        \"db_host\",\n        \"db_port\",\n        \"db_name\",\n        \"db_ssl\",\n        \"type\",\n      ]);\n\n      if (mode === \"validate\") {\n        const connection = validateConnection(initialCredentials);\n        res.json({ connection });\n        return;\n      }\n\n      if (!initialCredentials.db_conn || !initialCredentials.db_host) {\n        throw \"db_conn or db_host Missing\";\n      }\n\n      const { data: validatedCreds, error } = await tryCatchV2(async () => {\n        if (mode === \"manual\") {\n          await testDBConnection(initialCredentials);\n          return initialCredentials;\n        }\n\n        const { db_user, db_name } = DEFAULT_ELECTRON_CONNECTION;\n        let db_pass = randomBytes(12).toString(\"hex\");\n        /**\n         * Quick mode = login with provided credentials to ensure DEFAULT_ELECTRON_CONNECTION db and user exist\n         * */\n        await testDBConnection(\n          { ...initialCredentials, db_name: \"postgres\" },\n          undefined,\n          async (c) => {\n            const userExists = await c.oneOrNone<{ usename: string }>(\n              `SELECT usename FROM pg_catalog.pg_user WHERE usename = $1`,\n              [db_user],\n            );\n            if (!userExists) {\n              await c.none(\n                `CREATE USER ${db_user} WITH ENCRYPTED PASSWORD $1 SUPERUSER`,\n                [db_pass],\n              );\n              /** Overwrite password only if using different username */\n            } else if (initialCredentials.db_user !== db_user) {\n              await c.none(\n                `ALTER USER ${db_user} WITH ENCRYPTED PASSWORD $1 SUPERUSER`,\n                [db_pass],\n              );\n            } else {\n              db_pass = initialCredentials.db_pass;\n            }\n            const dbExists = await c.oneOrNone<{ datname: string }>(\n              `SELECT datname FROM pg_catalog.pg_database WHERE datname = $1`,\n              [db_name],\n            );\n            if (!dbExists) {\n              await c.none(`CREATE DATABASE ${db_name} WITH OWNER ${db_user} `);\n            } else {\n              /** If db exists, ensure user has access */\n              await c.none(\n                `GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user}`,\n              );\n            }\n          },\n        );\n\n        const newConnectionDetails = validateConnection({\n          ...initialCredentials,\n          type: \"Standard\",\n          db_user,\n          db_name,\n          db_pass,\n        });\n        await testDBConnection(newConnectionDetails, true);\n        return newConnectionDetails;\n      });\n\n      if (error) {\n        throw error;\n      }\n\n      const startup = await tryStartProstgles({\n        app,\n        io,\n        con: validatedCreds,\n        port,\n        host,\n      });\n\n      if (startup.state === \"error\") {\n        throw startup;\n      }\n      electronConfig.setCredentials(validatedCreds);\n      /* Update existing state connection info from the database if changed */\n      if (validatedCreds) {\n        void startup.dbs.connections\n          .findOne({\n            db_name: validatedCreds.db_name,\n            db_user: validatedCreds.db_user,\n            db_pass: { $ne: validatedCreds.db_pass },\n          })\n          .then((stateConnection) => {\n            if (!stateConnection) {\n              return;\n            }\n            return startup.dbs.connections.update(\n              { id: stateConnection.id },\n              {\n                db_pass: validatedCreds.db_pass,\n                db_conn: validatedCreds.db_conn,\n              },\n            );\n          });\n      }\n      res.json({ msg: \"DBS changed. Restart system\" });\n      return;\n    } catch (err) {\n      res.json({ warning: getSerialisableError(err) });\n      electronConfig?.setCredentials(undefined);\n    }\n  };\n\nconst connectionSchema = {\n  type: { enum: [\"Standard\"] },\n  db_conn: { type: \"string\" },\n  db_host: { type: \"string\" },\n  db_port: { type: \"number\" },\n  db_user: { type: \"string\" },\n  db_name: { type: \"string\" },\n  db_pass: { type: \"string\" },\n  db_ssl: {\n    enum: [\"disable\", \"allow\", \"prefer\", \"require\", \"verify-ca\", \"verify-full\"],\n  },\n} as const;\n"
  },
  {
    "path": "server/src/init/startDevHotReloadNotifier.ts",
    "content": "import path from \"path\";\nimport * as fs from \"fs\";\nimport type { InitResult } from \"prostgles-server/dist/initProstgles\";\nimport { RELOAD_NOTIFICATION } from \"@common/utils\";\n\nlet showedMessage = false;\nexport const startDevHotReloadNotifier = ({\n  io,\n  port,\n  host,\n}: {\n  io: NonNullable<InitResult[\"io\"]>;\n  port: number;\n  host: string;\n}) => {\n  console.log(\n    \"startDevHotReloadNotifier. Starting dev hot reload notifier in \" +\n      process.env.NODE_ENV +\n      \" mode\",\n  );\n  const showMessage = () => {\n    if (showedMessage) return;\n    console.log(`\\n\\n${RELOAD_NOTIFICATION}:\\n\\n http://${host}:${port}`);\n    showedMessage = true;\n  };\n  if (process.env.NODE_ENV === \"development\") {\n    const lastCompiledPath = path.join(\n      __dirname,\n      \"../../../../../client/configs/last_compiled.txt\",\n    );\n\n    fs.watchFile(lastCompiledPath, { interval: 100 }, (eventType, filename) => {\n      io.emit(\"server-restart-request\");\n      showMessage();\n    });\n  } else {\n    showMessage();\n  }\n};\n"
  },
  {
    "path": "server/src/init/startProstgles.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { ProstglesInitState } from \"@common/electronInitTypes\";\nimport type { Express } from \"express\";\nimport path from \"path\";\nimport type pg from \"pg-promise/typescript/pg-subset\";\nimport prostgles from \"prostgles-server\";\nimport type { InitResult } from \"prostgles-server/dist/initProstgles\";\nimport { getSerialisableError } from \"prostgles-types\";\nimport type { Server } from \"socket.io\";\nimport type { DBS } from \"..\";\nimport { addLog } from \"../Logger\";\nimport type { SUser } from \"../authConfig/sessionUtils\";\nimport { testDBConnection } from \"../connectionUtils/testDBConnection\";\nimport type { DBSConnectionInfo } from \"../electronConfig\";\nimport { actualRootDir, getElectronConfig } from \"../electronConfig\";\nimport { DBS_CONNECTION_INFO } from \"../envVars\";\nimport { publish } from \"../publish/publish\";\nimport { publishMethods } from \"../publishMethods/publishMethods\";\nimport { tableConfig } from \"../tableConfig/tableConfig\";\nimport { tableConfigMigrations } from \"../tableConfig/tableConfigMigrations\";\nimport { onProstglesReady } from \"./onProstglesReady\";\nimport { startDevHotReloadNotifier } from \"./startDevHotReloadNotifier\";\n\ntype StartArguments = {\n  app: Express;\n  io: Server;\n  con: DBSConnectionInfo | undefined;\n  port: number;\n  host: string;\n};\n\nexport let statePrgl: InitResult<DBGeneratedSchema, SUser> | undefined;\nexport type InitExtra = {\n  dbs: DBS;\n};\nexport type ProstglesInitStateWithDBS = ProstglesInitState<InitExtra>;\n\nexport const startProstgles = async ({\n  app,\n  port,\n  host,\n  io,\n  con = DBS_CONNECTION_INFO,\n}: StartArguments): Promise<\n  Exclude<ProstglesInitStateWithDBS, { state: \"loading\" }>\n> => {\n  try {\n    if (!con.db_conn && !con.db_user && !con.db_name) {\n      const error = `\n        Make sure .env file contains superuser postgres credentials:\n          POSTGRES_URL\n          or\n          POSTGRES_DB\n          POSTGRES_USER\n\n        Example:\n          POSTGRES_USER=myusername \n          POSTGRES_PASSWORD=exampleText \n          POSTGRES_DB=mydatabase  \n          POSTGRES_HOST=exampleText \n          POSTGRES_PORT=exampleText \n\n        To create a superuser and database on linux:\n          sudo -su postgres createuser -P --superuser myusername\n          sudo -su postgres createdb mydatabase -O myusername\n\n      `;\n\n      return { state: \"error\", error, errorType: \"connection\" };\n    }\n\n    let validatedDbConnection: pg.IConnectionParameters<pg.IClient> | undefined;\n    try {\n      const tested = await testDBConnection(con, true);\n      if (tested.isSSLModeFallBack) {\n        console.warn(\"sslmode=prefer fallback. Connecting through non-ssl\");\n      }\n      validatedDbConnection = tested.connectionInfo;\n    } catch (connError) {\n      return {\n        state: \"error\",\n        error:\n          getSerialisableError(connError) ?? \"State database connection error\",\n        errorType: \"connection\",\n      };\n    }\n    const IS_PROD = process.env.NODE_ENV === \"production\";\n\n    /** Prevent electron access denied error (cannot edit files in the install directory in electron) */\n    const tsGeneratedTypesDir =\n      IS_PROD || getElectronConfig()?.isElectron ?\n        undefined\n      : path.join(actualRootDir + \"/../common/\");\n    const watchSchema = !!tsGeneratedTypesDir;\n\n    const prgl = await prostgles<DBGeneratedSchema, SUser>({\n      dbConnection: {\n        ...validatedDbConnection,\n        connectionTimeoutMillis: 10 * 1000,\n      },\n      sqlFilePath: path.join(actualRootDir + \"/src/init.sql\"),\n      io,\n      tsGeneratedTypesDir,\n      watchSchema,\n      watchSchemaType: \"DDL_trigger\",\n      transactions: true,\n      onSocketConnect: async ({ socket, dbo, getUser }) => {\n        const user = await getUser();\n        const userId = user.user?.id;\n        const sid = user.sid;\n        // await securityManager.onSocketConnected({\n        //   sid,\n        // });\n\n        // if (sid) {\n        //   const s = await dbo.sessions.findOne({ id: sid });\n        //   if (!s) {\n        //     /** Can happen to deleted sessions */\n        //     // console.log(\"onSocketConnect session missing ?!\");\n        //   } else if (Date.now() > +new Date(+s.expires)) {\n        //     console.log(\"onSocketConnect session expired ?!\", s.id, Date.now());\n        //   } else {\n        //     await dbo.sessions.update(\n        //       { id: sid },\n        //       {\n        //         last_used: new Date(),\n        //         is_connected: true,\n        //         socket_id: socket.id,\n        //       },\n        //     );\n        //   }\n        // }\n\n        if (userId) {\n          /** Delete:\n           * - deleted workspaces\n           * - deleted/closed/detached (null wsp id)  non type=sql windows and links. Must keep detached SQL windows\n           */\n          const deletedWorkspaces = await dbo.workspaces.delete(\n            { deleted: true, user_id: userId },\n            { returning: { id: 1 }, returnType: \"values\" },\n          );\n\n          const deletedWindows = await dbo.windows.delete(\n            {\n              $or: [{ deleted: true }, { closed: true, \"type.<>\": \"sql\" }],\n            },\n            {\n              returning: { id: 1 },\n              returnType: \"values\",\n            },\n          );\n          deletedWorkspaces;\n          deletedWindows;\n        }\n      },\n      onSocketDisconnect: async (params) => {\n        const { dbo, getUser } = params;\n\n        const user = await getUser();\n        const sid = user.sid;\n        if (sid) {\n          await dbo.sessions.update({ id: sid }, { is_connected: false });\n        }\n      },\n      /** Used for debugging */\n      // onQuery: (err, ctx) => {\n      //   if(err){\n      //     console.error(err, ctx?.client?.processID, ctx?.query);\n      //   }\n      // },\n      // DEBUG_MODE: true,\n      onLog: (e) => {\n        addLog(e, null);\n      },\n      tableConfig,\n      tableConfigMigrations,\n      publishRawSQL: (params) => {\n        const { user } = params;\n        return Boolean(user && user.type === \"admin\");\n      },\n      publishMethods,\n      publish,\n      joins: \"inferred\",\n      onReady: async (params, update) => {\n        await onProstglesReady(params, update, app, con);\n      },\n    });\n\n    statePrgl = prgl;\n\n    startDevHotReloadNotifier({ io, port, host });\n    return { state: \"ok\", dbs: prgl.db as DBS };\n  } catch (err) {\n    return { state: \"error\", error: err as Error, errorType: \"init\" };\n  }\n};\n"
  },
  {
    "path": "server/src/init/testDashboardTypesContent.ts",
    "content": "import { dashboardTypesContent } from \"@common/dashboardTypesContent\";\nimport { readFileSync, writeFileSync } from \"fs\";\nimport { getRootDir } from \"@src/electronConfig\";\n\nexport const testDashboardTypesContent = () => {\n  const commonDir = `${getRootDir()}/../common`;\n  const actualContent = readFileSync(`${commonDir}/DashboardTypes.ts`, \"utf-8\");\n  console.log(\"Comparing DashboardTypes.ts content...\");\n  if (actualContent.trim() !== dashboardTypesContent.trim()) {\n    const escapedContent = actualContent\n      .replace(/\\\\/g, \"\\\\\\\\\")\n      .replace(/`/g, \"\\\\`\")\n      .replace(/\\$\\{/g, \"\\\\${\");\n    const newContent = `/**\n * Generated file. Do not edit.\n * https://github.com/electron-userland/electron-builder/issues/5064\n */\nexport const dashboardTypesContent = \\`${escapedContent}\\n\\`;`;\n    writeFileSync(`${commonDir}/dashboardTypesContent.ts`, newContent);\n    throw new Error(\n      `DashboardTypes.ts content has changed. Please update the dashboardTypesContent in common/dashboardTypesContent.ts`,\n    );\n  }\n};\n"
  },
  {
    "path": "server/src/init/tryStartProstgles.ts",
    "content": "import type { Express } from \"express\";\nimport { isEqual } from \"prostgles-types\";\nimport type { Server } from \"socket.io\";\nimport { tout } from \"..\";\nimport type { ProstglesState } from \"@common/electronInitTypes\";\nimport type { DBSConnectionInfo } from \"../electronConfig\";\nimport { getElectronConfig } from \"../electronConfig\";\nimport { DBS_CONNECTION_INFO } from \"../envVars\";\nimport { cleanupTestDatabases } from \"./cleanupTestDatabases\";\nimport { isRetryableError } from \"./isRetryableError\";\nimport { setDBSRoutesForElectron } from \"./setDBSRoutesForElectron\";\nimport {\n  startProstgles,\n  type InitExtra,\n  type ProstglesInitStateWithDBS,\n} from \"./startProstgles\";\nimport { testDashboardTypesContent } from \"./testDashboardTypesContent\";\n\ntype StartArguments = {\n  app: Express;\n  io: Server;\n  con: DBSConnectionInfo | undefined;\n  port: number;\n  host: string;\n};\n\ntype FinishedState = Exclude<ProstglesInitStateWithDBS, { state: \"loading\" }>;\ntype StateListener = (state: FinishedState) => void;\n\nexport const startupState: {\n  listeners: StateListener[];\n  state: ProstglesInitStateWithDBS;\n  set: (state: FinishedState) => void;\n  onReady: (cb: StateListener) => void;\n} = {\n  listeners: [],\n  state: {\n    state: \"loading\",\n  },\n  set: (state: FinishedState) => {\n    startupState.state = state;\n    startupState.listeners.forEach((listener) => listener(state));\n    startupState.listeners = [];\n  },\n  onReady: (cb: StateListener) => {\n    if (startupState.state.state !== \"loading\") {\n      cb(startupState.state as FinishedState);\n      return;\n    }\n    startupState.listeners.push(cb);\n  },\n};\n\nconst RETRY_CONFIG = {\n  maxAttempts: 3, // Maximum number of attempts\n  initialDelayMs: 1000, // Initial delay before first retry\n  backoffFactor: 2, // Multiplier for delay (exponential backoff)\n  maxDelayMs: 30000, // Maximum delay between retries\n  jitterFactor: 0.3, // Percentage of delay to use for jitter (0 to 1)\n};\n\nexport let startingProstglesResult:\n  | {\n      args: StartArguments;\n      result: ReturnType<typeof tryStartProstgles>;\n    }\n  | undefined = undefined;\n\nconst connHistory: string[] = [];\nexport const tryStartProstgles = async (\n  args: StartArguments,\n): Promise<FinishedState> => {\n  const config = startingProstglesResult;\n  if (\n    config &&\n    (await config.result).state === \"error\" &&\n    !isEqual(args, config.args)\n  ) {\n    startingProstglesResult = undefined;\n  }\n  startingProstglesResult ??= { args, result: _tryStartProstgles(args) };\n  return await startingProstglesResult.result;\n};\n\nconst _tryStartProstgles = async ({\n  app,\n  io,\n  port,\n  host,\n  con = DBS_CONNECTION_INFO,\n}: StartArguments): Promise<FinishedState> => {\n  /** Cleanup state for local tests */\n  await cleanupTestDatabases(con);\n\n  setDBSRoutesForElectron(app, io, port, host);\n\n  let lastError:\n    | Extract<ProstglesInitStateWithDBS, { state: \"error\" }>\n    | undefined = undefined;\n  let attempt = 0;\n\n  const connHistoryItem = JSON.stringify(con);\n  if (connHistory.includes(connHistoryItem)) {\n    console.error(\"DUPLICATE UNFINISHED CONNECTION\");\n    return {\n      state: \"error\",\n      error: \"Duplicate connection attempt\",\n      errorType: \"init\",\n    };\n  }\n  connHistory.push(connHistoryItem);\n\n  while (attempt < RETRY_CONFIG.maxAttempts) {\n    attempt++;\n\n    try {\n      console.log(`Attempt ${attempt} to connect to state database...`);\n      const attemptResult = await startProstgles({\n        app,\n        io,\n        con,\n        port,\n        host,\n      });\n      if (attemptResult.state === \"ok\") {\n        startupState.set(attemptResult);\n        return attemptResult;\n      }\n\n      lastError = attemptResult;\n      if (!isRetryableError(lastError)) {\n        break;\n      }\n\n      // Calculate delay with exponential backoff and jitter\n      const exponentialDelay =\n        RETRY_CONFIG.initialDelayMs *\n        Math.pow(RETRY_CONFIG.backoffFactor, attempt - 1);\n      const delay = Math.min(exponentialDelay, RETRY_CONFIG.maxDelayMs); // Cap the delay\n      const jitter =\n        delay * RETRY_CONFIG.jitterFactor * (Math.random() - 0.5) * 2; // Apply jitter (+/-)\n      const waitTime = Math.max(0, Math.round(delay + jitter)); // Ensure non-negative delay\n\n      console.log(`Attempt ${attempt} failed. Retrying in ${waitTime}ms...`);\n      await tout(waitTime);\n    } catch (err: unknown) {\n      console.error(\"startProstgles fail: \", err);\n      lastError = {\n        state: \"error\",\n        errorType: \"init\",\n        error: err as Error,\n      };\n      break;\n    }\n  }\n\n  const erroredState = {\n    state: \"error\",\n    errorType: lastError?.errorType ?? \"init\",\n    error: lastError?.error ?? \"Failed to start prostgles. Check logs\",\n  } satisfies FinishedState;\n\n  startupState.set(erroredState);\n\n  console.error(\"Failed to start prostgles: \", lastError);\n\n  return erroredState;\n};\n\nexport const getProstglesState = (): ProstglesState<InitExtra> => {\n  const eConfig = getElectronConfig();\n  const isElectron = Boolean(eConfig?.isElectron);\n  return {\n    isElectron,\n    initState: { ...startupState.state },\n    electronCredsProvided: Boolean(eConfig?.hasCredentials()),\n  };\n};\n\nif (process.env.NODE_ENV !== \"production\") {\n  testDashboardTypesContent();\n}\n"
  },
  {
    "path": "server/src/init.sql",
    "content": "CREATE EXTENSION IF NOT EXISTS pgcrypto;"
  },
  {
    "path": "server/src/methods/getPidStats.ts",
    "content": "import type { DB } from \"prostgles-server/dist/Prostgles\";\nimport { getSerialisableError, isDefined } from \"prostgles-types\";\nimport type { DBS } from \"..\";\nimport {\n  STATUS_MONITOR_IGNORE_QUERY,\n  type ConnectionStatus,\n  type IOStats,\n  type PG_STAT_DATABASE,\n  type ServerStatus,\n} from \"@common/utils\";\nimport { bytesToSize } from \"../BackupManager/utils\";\nimport { getSuperUserCDB } from \"../ConnectionManager/ConnectionManager\";\nimport { getPidStatsFromProc } from \"./getPidStatsFromProc\";\nimport { execPSQLBash, getServerStatus } from \"./statusMonitorUtils\";\n\ntype PS_ProcInfo = {\n  pid: number;\n  cpu: number;\n  mem: number;\n  mhz: string;\n  cmd: string;\n};\n\nexport type ServerLoadStats = {\n  pidStats: PS_ProcInfo[];\n  serverStatus: ServerStatus;\n  getPidStatsErrors?: any;\n};\n\nconst parsePidStats = (\n  procInfo: Record<keyof PS_ProcInfo, string>,\n): PS_ProcInfo | undefined => {\n  const pid = +procInfo.pid;\n  const cpu = +procInfo.cpu;\n  const mem = +procInfo.mem;\n  const cmd = procInfo.cmd;\n\n  if ([pid, cpu, mem].some((v) => !Number.isFinite(v)) || !cmd) {\n    return undefined;\n  }\n\n  const result = {\n    pid: +pid,\n    cpu: +cpu,\n    mem: +mem,\n    mhz: procInfo.mhz || \"\",\n    cmd,\n  };\n\n  return result;\n};\n\nconst ioStatMethods = {\n  /**\n   * https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats\n   */\n  diskstats: async (db: DB, connId: string): Promise<IOStats[]> => {\n    const ioRows = await execPSQLBash(db, connId, `cat /proc/diskstats`);\n    const ioInfo = ioRows.map((row) => {\n      const rowParts = row.trim().replace(/  +/g, \" \").split(\" \");\n\n      return Object.fromEntries(\n        Object.entries({\n          majorNumber: \"number\",\n          minorNumber: \"number\",\n          deviceName: \"string\",\n          readsCompletedSuccessfully: \"number\",\n          readsMerged: \"number\",\n          sectorsRead: \"number\",\n          timeSpentReadingMs: \"number\",\n          writesCompleted: \"number\",\n          writesMerged: \"number\",\n          sectorsWritten: \"number\",\n          timeSpentWritingMs: \"number\",\n          IOsCurrentlyInProgress: \"number\",\n          timeSpentDoingIOms: \"number\",\n          weightedTimeSpentDoingIOms: \"number\",\n        } as const).map(([key, type], idx) => [\n          key,\n          type === \"number\" ? Number(rowParts[idx]) : rowParts[idx]!,\n        ]),\n      ) as IOStats;\n    });\n\n    const deviceRows = ioInfo.filter(\n      (r) => r.minorNumber === 0 && !r.deviceName.startsWith(\"loop\"),\n    );\n    return deviceRows;\n  },\n};\n\nconst pidStatsMethods = {\n  top: async (db: DB, connId: string) => {\n    const pidRows = await execPSQLBash(db, connId, `top -b -n1 | sed 1,7d`);\n    const pidStatsWithMhz =\n      connectionBashStatus[connId]?.available?.includes(\"ps\") ?\n        await pidStatsMethods.ps(db, connId)\n      : undefined;\n    const pidStats: PS_ProcInfo[] = pidRows\n      .map((shell) => {\n        const [\n          pid = \"-1\",\n          usr,\n          pr,\n          ni,\n          virt,\n          res,\n          shr,\n          s,\n          cpu = \"-1\",\n          mem = \"-1\",\n          time,\n          cmd = \"\",\n        ] = shell.trim().replace(/  +/g, \" \").split(\" \");\n        const mhz =\n          pidStatsWithMhz?.pidStats.find((p) => p.pid == (pid as any))?.mhz ||\n          \"\";\n        return parsePidStats({ pid, cpu, mem, cmd, mhz });\n      })\n      .filter(isDefined);\n    const serverStatus = await getServerStatus(db, connId);\n    return { pidStats, serverStatus };\n  },\n  ps: async (db: DB, connId: string) => {\n    const psRes = await execPSQLBash(db, connId, `ps -o pid,%cpu,%mem,psr,cmd`);\n    const freqs = await getCpuCoresMhz(db, connId);\n    const pidStats: PS_ProcInfo[] = psRes\n      .slice(1)\n      .map((line) => {\n        const [pid = \"-1\", cpu = \"-1\", mem = \"-1\", psr, ...cmd] = line\n          .trim()\n          .replace(/  +/g, \" \")\n          .split(\" \");\n        return parsePidStats({\n          pid,\n          cpu,\n          mem,\n          mhz: (freqs[+psr!] ?? \"-1\").toString(),\n          cmd: cmd.join(\" \"),\n        });\n      })\n      .filter(isDefined);\n\n    const serverStatus = await getServerStatus(db, connId);\n    return { pidStats, serverStatus };\n  },\n  proc: (db: DB, connId: string) => {\n    return getPidStatsFromProc(db, connId);\n  },\n} as const satisfies Record<\n  string,\n  (db: DB, connId: string) => Promise<ServerLoadStats>\n>;\n\ntype PidStatProgs = keyof typeof pidStatsMethods;\ntype PidStatMode = PidStatProgs | \"off\";\ntype ConnectionStatInfo = {\n  mode: PidStatMode;\n  available?: PidStatProgs[];\n  ioMode: \"diskstats\" | \"off\";\n  getPidStatsErrors: Partial<Record<PidStatProgs, any>>;\n};\nconst connectionBashStatus: Record<string, ConnectionStatInfo | undefined> = {};\n\nexport const getCpuCoresMhz = async (\n  db: DB,\n  connId: string,\n): Promise<number[]> => {\n  const coreFrequencies = await execPSQLBash(\n    db,\n    connId,\n    `cat /proc/cpuinfo | grep \"MHz\" | sed 's/^.*: //'`,\n  );\n  return coreFrequencies.map((mhzStr) => {\n    const mhz = Number(mhzStr);\n    return Number.isFinite(mhz) ? Math.round(mhz) : -1;\n  });\n};\n\nconst getPidStatsMode = async (\n  db: DB,\n  connId: string,\n): Promise<ConnectionStatInfo> => {\n  const [platform] = await execPSQLBash(db, connId, \"uname\");\n  const getPidStatsErrors: Partial<Record<PidStatProgs, any>> = {};\n  if (platform !== \"Linux\") {\n    getPidStatsErrors[\"proc\"] = \"uname not Linux\";\n    return { mode: \"off\", ioMode: \"off\", getPidStatsErrors };\n  }\n\n  let mode: PidStatMode | undefined;\n  const available: PidStatProgs[] = [];\n  for (const [program, method] of Object.entries(pidStatsMethods) as [\n    PidStatProgs,\n    (typeof pidStatsMethods)[keyof typeof pidStatsMethods],\n  ][]) {\n    try {\n      if (program === \"proc\") {\n        await method(db, connId);\n        mode ??= program;\n        available.push(program);\n      } else {\n        /** TODO - ensure ps & proc output same values as top for cpu */\n        const [which] = await execPSQLBash(db, connId, `which ${program}`);\n        if (which) {\n          await method(db, connId);\n          mode ??= program;\n          available.push(program);\n        }\n      }\n    } catch (e) {\n      getPidStatsErrors[program] = getSerialisableError(e);\n    }\n  }\n\n  let ioMode: \"off\" | \"diskstats\" = \"off\";\n  try {\n    await ioStatMethods.diskstats(db, connId);\n    ioMode = \"diskstats\";\n  } catch (e) {}\n\n  return {\n    mode: mode || \"off\",\n    ioMode,\n    available,\n    getPidStatsErrors,\n  };\n};\n\nexport const getPidStats = async (\n  db: DB,\n  connId: string,\n): Promise<ServerLoadStats | undefined> => {\n  connectionBashStatus[connId] ??= await getPidStatsMode(db, connId).catch(\n    (error) =>\n      Promise.resolve({\n        mode: \"off\",\n        ioMode: \"off\",\n        getPidStatsErrors: { proc: getSerialisableError(error) },\n      } as const),\n  );\n  const { mode } = connectionBashStatus[connId];\n  if (mode === \"off\") return undefined;\n  return pidStatsMethods[mode](db, connId);\n};\n\nexport const getStatus = async (connId: string, dbs: DBS) => {\n  const dbConf = await dbs.database_configs.findOne({\n    $existsJoined: { connections: { id: connId } },\n  });\n  if (!dbConf) {\n    throw new Error(`Connection with id ${connId} not found`);\n  }\n  const { db: cdb } = await getSuperUserCDB(connId, dbs);\n  const { maxConnections } = await cdb.one<{ maxConnections: number }>(`\n    SELECT setting::numeric as \"maxConnections\"\n    FROM pg_catalog.pg_settings\n    WHERE name = 'max_connections'\n  `);\n  const result: ConnectionStatus = {\n    queries: await cdb.any(\n      `\n      /* ${STATUS_MONITOR_IGNORE_QUERY} */\n      SELECT \n        datid, datname, pid, usesysid, usename, application_name, client_addr, \n        client_hostname, client_port, backend_start, xact_start, query_start, \n        state_change, wait_event_type, wait_event, state, \n        backend_xid, backend_xmin, query, backend_type, \n        pg_blocking_pids(pid) as blocked_by,\n        COALESCE(cardinality(pg_blocking_pids(pid)), 0) blocked_by_num,\n        md5(pid || query) as id_query_hash\n      FROM pg_catalog.pg_stat_activity \n    `,\n    ),\n    blockedQueries: [],\n    topQueries: [],\n    getPidStatsErrors: connectionBashStatus[connId]\n      ?.getPidStatsErrors as Partial<Record<string, any>>,\n    noBash: connectionBashStatus[connId]?.mode === \"off\",\n    connections: await cdb.any<PG_STAT_DATABASE>(`\n      /* ${STATUS_MONITOR_IGNORE_QUERY} */\n      SELECT *\n      FROM pg_stat_database\n      WHERE numbackends > 0;\n    `),\n    maxConnections,\n  };\n\n  let procInfo: ServerLoadStats | undefined;\n  try {\n    procInfo = await getPidStats(cdb, connId);\n    result.serverStatus = procInfo?.serverStatus;\n  } catch (err) {\n    console.error(err);\n  }\n\n  if (procInfo && connectionBashStatus[connId]?.ioMode === \"diskstats\") {\n    procInfo.serverStatus.ioInfo = await ioStatMethods.diskstats(cdb, connId);\n  }\n\n  const database_id = dbConf.id;\n  const sampled_at = new Date().toISOString();\n  const now = Date.now();\n  const retentionMs = 5_000;\n  await dbs.tx(async (tx) => {\n    await tx.stats.delete({\n      $or: [\n        { database_id },\n        { sampled_at: { \"<\": new Date(now - retentionMs).toISOString() } },\n      ],\n    });\n    await tx.stats.insert(\n      result.queries.map((q) => {\n        const pidInfo = procInfo?.pidStats.find((p) => p.pid === q.pid);\n\n        return {\n          ...q,\n          ...pidInfo,\n          sampled_at,\n          memPretty:\n            pidInfo &&\n            procInfo &&\n            bytesToSize(\n              (+pidInfo.mem / 100) *\n                procInfo.serverStatus.total_memoryKb *\n                1024,\n            ),\n          database_id,\n        };\n      }),\n      { onConflict: \"DoNothing\" },\n    );\n  });\n\n  return result;\n};\n"
  },
  {
    "path": "server/src/methods/getPidStatsFromProc.ts",
    "content": "import type { DB } from \"prostgles-server/dist/Prostgles\";\nimport { execPSQLBash, getServerStatus } from \"./statusMonitorUtils\";\nimport { getCpuCoresMhz, type ServerLoadStats } from \"./getPidStats\";\n\nexport const getPidStatsFromProc = async (\n  db: DB,\n  connId: string,\n): Promise<ServerLoadStats> => {\n  const serverStatus = await getServerStatus(db, connId);\n  const { clock_ticks, total_memoryKb, uptimeSeconds } = serverStatus;\n\n  const pidCommand = `\n    pid_array=\\`ls /proc | grep -E '^[0-9]+$'\\`\n    for pid in $pid_array\n      do\n        if [ -r /proc/$pid/stat ]\n        then\n          vm_rss=$( grep 'VmRSS' /proc/$pid/status  | grep -o -E \"[0-9]+\"  )\n          stat_array=$( cat /proc/$pid/stat )\n          result=\" $stat_array __$vm_rss\"\n          echo $result\n        fi\n      done\n  `;\n  const pidInfo = await execPSQLBash(db, connId, pidCommand);\n  const freqs = await getCpuCoresMhz(db, connId);\n  const pidStats = pidInfo.map((line: string) => {\n    const vm_rss_kb = Number(line.split(\"__\")[1] || \"nan\");\n\n    const name = line.split(\" \")[1];\n\n    /**\n     * https://man7.org/linux/man-pages/man5/proc.5.html\n     */\n    const numericParts = line.split(\" \").map((v) => Number(v || \"nan\"));\n\n    const pid = numericParts[0];\n\n    //           --#14 utime - CPU time spent in user code, measured in clock ticks\n    const utime = numericParts[13];\n\n    //           --#15 stime - CPU time spent in kernel code, measured in clock ticks\n    const stime = numericParts[14];\n\n    //           --#16 cutime - Waited-for children's CPU time spent in user code (in clock ticks)\n    const cutime = numericParts[15];\n\n    //           --#17 cstime - Waited-for children's CPU time spent in kernel code (in clock ticks)\n    const cstime = numericParts[16];\n\n    //           --#22 starttime - Time when the process started, measured in clock ticks\n    const starttime = numericParts[21];\n    const procId = numericParts[38];\n\n    const total_time = utime! + stime! + cutime! + cstime!; // total time spent for the process and it's children\n    const seconds = uptimeSeconds - starttime! / clock_ticks; // total elapsed time in seconds since the process started:\n\n    const cpu_usage = 100 * (total_time / clock_ticks / seconds);\n\n    const mem_usage =\n      Number.isFinite(vm_rss_kb) ?\n        (100 * vm_rss_kb) / total_memoryKb\n      : undefined;\n\n    const mhz = freqs[procId ?? -1] ?? \"\";\n    return {\n      pid,\n      name,\n      utime,\n      stime,\n      cutime,\n      cstime,\n      starttime,\n      vm_rss_kb,\n      mhz,\n      total_time,\n      seconds,\n      cpu_usage,\n      mem_usage,\n    };\n  });\n\n  return {\n    pidStats: pidStats.map((s) => ({\n      pid: s.pid!,\n      cmd: s.name!,\n      cpu: s.cpu_usage,\n      mem: s.mem_usage!,\n      mhz: s.mhz.toString(),\n    })),\n    serverStatus,\n  };\n\n  //         -- comm=( `grep -Po '^[^\\s\\/]+' /proc/$pid/comm` )\n  //         -- user_id=$( grep -Po '(?<=Uid:\\s)(\\d+)' /proc/$pid/status )\n\n  //         -- user=$( id -nu $user_id )\n  //         -- uptime=${uptime_array[0]}\n\n  //         -- state=${stat_array[2]}\n  //         -- ppid=${stat_array[3]}\n  //         -- priority=${stat_array[17]}\n  //         -- nice=${stat_array[18]}\n\n  //         -- utime=${stat_array[13]}\n  //         -- stime=${stat_array[14]}\n  //         -- cutime=${stat_array[15]}\n  //         -- cstime=${stat_array[16]}\n  //         -- num_threads=${stat_array[19]}\n  //         -- starttime=${stat_array[21]}\n\n  //         -- total_time=$(( $utime + $stime ))\n  //         -- #add $cstime - CPU time spent in user and kernel code ( can olso add $cutime - CPU time spent in user code )\n  //         -- total_time=$(( $total_time + $cstime ))\n  //         -- seconds=$( awk 'BEGIN {print ( '$uptime' - ('$starttime' / '$clock_ticks') )}' )\n  //         -- cpu_usage=$( awk 'BEGIN {print ( 100 * (('$total_time' / '$clock_ticks') / '$seconds') )}' )\n\n  //         -- resident=${statm_array[1]}\n  //         -- data_and_stack=${statm_array[5]}\n  //         -- memory_usage=$( awk 'BEGIN {print( (('$resident' + '$data_and_stack' ) * 100) / '$total_memory'  )}' )\n\n  //         -- printf \"%-6d %-6d %-10s %-4d %-5d %-4s %-4u %-7.2f %-7.2f %-18s\\n\" $pid $ppid $user $priority $nice $state $num_threads $memory_usage $cpu_usage $comm >> .data.ps\n\n  //   `\n};\n"
  },
  {
    "path": "server/src/methods/statusMonitorUtils.ts",
    "content": "import type { DB } from \"prostgles-server/dist/Prostgles\";\nimport {\n  EXCLUDE_FROM_SCHEMA_WATCH,\n  STATUS_MONITOR_IGNORE_QUERY,\n  type ServerStatus,\n} from \"@common/utils\";\nimport { getCDB } from \"../ConnectionManager/ConnectionManager\";\n\nexport const execPSQLBash = (\n  db: DB,\n  connId: string,\n  command: string,\n): Promise<string[]> => {\n  const tblName = JSON.stringify(`prostgles_shell_${connId}`);\n\n  return db\n    .any<{ shell: string }>(\n      `\n    /* ${STATUS_MONITOR_IGNORE_QUERY} */\n    /* ${EXCLUDE_FROM_SCHEMA_WATCH}  */\n    --DROP TABLE IF EXISTS ${tblName};\n    CREATE TEMP TABLE IF NOT EXISTS ${tblName} (\n      shell text\n    );\n    DELETE FROM ${tblName};\n    COPY ${tblName} FROM PROGRAM \\${command};\n    SELECT shell FROM ${tblName};\n    `,\n      { command },\n    )\n    .then((vals) => vals.map(({ shell }) => shell));\n};\n\nexport const getServerStatus = async (\n  db: DB,\n  connId: string,\n): Promise<ServerStatus> => {\n  const getBashQuery = async <Vars extends Record<string, string>>(\n    vars: Vars,\n  ): Promise<Vars> => {\n    const varDelimiter = \"__del!m!t3r__\";\n    const ObjeEntries = Object.entries(vars);\n    const command =\n      ObjeEntries.map(\n        ([varName, varCommand]) => `${varName}=$(${varCommand})`,\n      ).join(\"\\n\") +\n      `\\nprintf \"${ObjeEntries.map(([varName]) => `${varName}: \\${${varName}}`).join(varDelimiter)}\"`;\n    const resArr = (await execPSQLBash(db, connId, command))[0]?.split(\n      varDelimiter,\n    );\n    return Object.fromEntries(\n      ObjeEntries.map(([key], index) => [\n        key,\n        resArr?.[index]?.slice(key.length + 2),\n      ]),\n    ) as any;\n  };\n  const { data_directory } = await db.oneOrNone(\"show data_directory;\");\n\n  const cpu_cores_mhz_arr = await execPSQLBash(\n    db,\n    connId,\n    `cat /proc/cpuinfo | grep \"MHz\" | sed 's/^.*: //'`,\n  );\n  const cpu_cores_mhz = cpu_cores_mhz_arr.join(\"\\n\");\n  let cpu_mhz = \"\";\n  try {\n    cpu_mhz = (await execPSQLBash(db, connId, `lscpu | grep \"MHz\"`)).join(\"\\n\");\n  } catch (e) {\n    /** Set from mhz array */\n    const mhzNumArr = cpu_cores_mhz_arr.map((v) => +v).filter((v) => v);\n    if (mhzNumArr.length && mhzNumArr.every((v) => Number.isFinite(v))) {\n      cpu_mhz = [\n        \"CPU min MHz: \" + Math.max(...mhzNumArr),\n        \"CPU max MHz: \" + Math.max(...mhzNumArr),\n      ].join(\"\\n\");\n    }\n  }\n  const disk_spaceStr = (\n    await execPSQLBash(db, connId, `df -h ${data_directory} | grep \"/\"`)\n  ).join(\"\\n\");\n  const {\n    clock_ticks: clock_ticksStr,\n    total_memory: total_memoryStr,\n    uptime_array: uptimeStr,\n    free_memory: free_memoryStr,\n    cpu_model: cpu_modelStr,\n    MemAvailableStr,\n  } = await getBashQuery({\n    clock_ticks: `getconf CLK_TCK`,\n    total_memory: ` grep 'MemTotal' /proc/meminfo | grep -o -E \"[0-9]+\"`,\n    free_memory: ` grep 'MemFree' /proc/meminfo | grep -o -E \"[0-9]+\"`,\n    MemAvailableStr: ` grep 'MemAvailable' /proc/meminfo | grep -o -E \"[0-9]+\"`,\n    uptime_array: ` cat /proc/uptime`,\n    cpu_model: `lscpu | grep \"Model name\"`,\n  });\n  const [dsFilesystem, dsSize, dsUsed, dsAvail, dsUsePerc, dsMountedOn] =\n    disk_spaceStr\n      .trim()\n      .split(\" \")\n      .map((v) => v.trim())\n      .filter((v) => v);\n  const disk_space = [\n    `Filesystem: ${dsFilesystem}`,\n    `Size: ${dsSize}`,\n    `Used: ${dsUsed}`,\n    `Avail: ${dsAvail}`,\n    `Used: ${dsUsePerc}`,\n    `MountedOn: ${dsMountedOn}`,\n  ].join(\"\\n\");\n\n  const clock_ticks = +clock_ticksStr;\n  const total_memoryKb = +total_memoryStr;\n  const free_memoryKb = +free_memoryStr;\n  const [uptimeTotalSecondsStr, idleSeconds] = uptimeStr.split(\" \");\n  const uptimeSeconds = +(uptimeTotalSecondsStr ?? \"0\");\n  const memAvailable = +MemAvailableStr;\n\n  return {\n    clock_ticks,\n    total_memoryKb,\n    free_memoryKb,\n    uptimeSeconds,\n    cpu_model: cpu_modelStr.split(\"Model name:\")[1]?.trim() || cpu_modelStr,\n    cpu_mhz,\n    disk_space,\n    cpu_cores_mhz,\n    memAvailable,\n  };\n};\n\nexport const killPID = async (\n  connId: string,\n  id_query_hash: string,\n  type: \"cancel\" | \"terminate\" = \"cancel\",\n) => {\n  if (!id_query_hash) throw \"id_query_hash missing\";\n\n  const { db } = await getCDB(connId);\n  return db.any(\n    `\n    /* ${STATUS_MONITOR_IGNORE_QUERY} */\n    SELECT pid, query, pg_${type}_backend(pid) \n    FROM pg_catalog.pg_stat_activity\n    WHERE md5(pid || query) = \\${id_query_hash} `,\n    { id_query_hash },\n  );\n};\n"
  },
  {
    "path": "server/src/publish/getPublishLLM.ts",
    "content": "import type { Publish } from \"prostgles-server/dist/PublishParser/PublishParser\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { getBestLLMChatModel } from \"../publishMethods/askLLM/askLLM\";\nimport { fetchLLMResponse } from \"../publishMethods/askLLM/fetchLLMResponse\";\nimport type { Filter } from \"prostgles-server/dist/DboBuilder/DboBuilderTypes\";\nimport { testMCPServerConfig } from \"@src/McpHub/testMCPServerConfig\";\nimport { refreshModels } from \"@src/publishMethods/askLLM/refreshModels\";\nimport type { DBS } from \"..\";\n\nexport const getPublishLLM = (\n  user_id: string,\n  isAdmin: boolean,\n  accessRules: undefined | DBSSchema[\"access_control\"][],\n  dbs: DBS,\n) => {\n  const forcedData = { user_id };\n  const forcedFilter = { user_id };\n\n  const forcedFilterLLM = {\n    $existsJoined: {\n      access_control_allowed_llm: {\n        access_control_id: { $in: accessRules?.map((ac) => ac.id) ?? [] },\n      },\n    },\n  };\n\n  const userOwnsRelatedChat = {\n    $existsJoined: {\n      llm_chats: {\n        user_id,\n      },\n    },\n  } as const;\n\n  const result = {\n    llm_providers: isAdmin && {\n      select: \"*\",\n      insert: {\n        fields: \"*\",\n        requiredNestedInserts: [\n          {\n            ftable: \"llm_models\",\n          },\n        ],\n      },\n      update: \"*\",\n      delete: \"*\",\n    },\n    llm_models:\n      isAdmin ?\n        {\n          select: \"*\",\n          update: {\n            fields: { name: 0, id: 0, provider_id: 0, model_created: 0 },\n          },\n          insert: \"*\",\n          delete: \"*\",\n        }\n      : {\n          select: {\n            fields: \"*\",\n          },\n        },\n    llm_credentials: {\n      select: {\n        fields: isAdmin ? { api_key: 0 } : { id: 1, name: 1 },\n        forcedFilter: isAdmin ? undefined : forcedFilterLLM,\n      },\n      delete: isAdmin && \"*\",\n      insert: isAdmin && {\n        fields: { name: 1, provider_id: 1, api_key: 1 },\n        forcedData,\n        postValidate: async ({ row, dbx }) => {\n          const provider = await dbx.llm_providers.findOne({\n            id: row.provider_id,\n          });\n          if (!provider) throw \"Provider not found\";\n          const preferredModel = await getBestLLMChatModel(dbx, {\n            provider_id: row.provider_id,\n          });\n          await fetchLLMResponse({\n            llm_chat: {\n              extra_body: {},\n              extra_headers: {},\n            },\n            llm_model: preferredModel,\n            llm_provider: provider,\n            llm_credential: row,\n            tools: undefined,\n            messages: [\n              {\n                role: \"system\",\n                content: [{ type: \"text\", text: \"Be helpful\" }],\n              },\n              {\n                role: \"user\",\n                content: [{ type: \"text\", text: \"Hey\" }],\n              },\n            ],\n            aborter: new AbortController(),\n          });\n\n          if (row.provider_id === \"OpenRouter\") {\n            void refreshModels(dbs);\n          }\n        },\n      },\n      update: isAdmin && {\n        fields: { created: 0, provider_id: 0 },\n      },\n    },\n    llm_prompts: {\n      select:\n        isAdmin ? \"*\" : (\n          {\n            fields: { id: 1, name: 1 },\n            forcedFilter: forcedFilterLLM,\n          }\n        ),\n      delete: isAdmin && \"*\",\n      insert: isAdmin && {\n        fields: \"*\",\n        forcedData,\n      },\n      update: isAdmin && {\n        fields: \"*\",\n        forcedFilter,\n        forcedData,\n      },\n    },\n    llm_chats: {\n      select: {\n        fields: \"*\",\n        forcedFilter,\n      },\n      delete: isAdmin && \"*\",\n      insert: {\n        fields: \"*\",\n        forcedData,\n        preValidate: async ({ row, dbx }) => {\n          if (row.model) return row;\n\n          const preferredChatModel = await getBestLLMChatModel(dbx, {\n            $existsJoined: {\n              \"llm_providers.llm_credentials\": {},\n            },\n          } as Filter);\n          return {\n            ...row,\n            model: preferredChatModel.id,\n          };\n        },\n      },\n      update: {\n        fields: { created: 0, user_id: 0, connection_id: 0 },\n        forcedData,\n        forcedFilter,\n      },\n    },\n    llm_messages: {\n      select: {\n        fields: \"*\",\n        forcedFilter: userOwnsRelatedChat,\n      },\n      delete: isAdmin && \"*\",\n      insert: {\n        fields: \"*\",\n        forcedData,\n        checkFilter: userOwnsRelatedChat,\n      },\n      update: isAdmin && {\n        fields: \"*\",\n        forcedFilter: userOwnsRelatedChat,\n      },\n    },\n    mcp_servers:\n      isAdmin ?\n        {\n          select: \"*\",\n          update: {\n            fields: {\n              args: 1,\n              env: 1,\n              icon_path: 1,\n              enabled: 1,\n            },\n          },\n          insert: \"*\",\n          delete: \"*\",\n        }\n      : {\n          select: {\n            fields: \"*\",\n            forcedFilter: {\n              $exists: {\n                users: {\n                  id: user_id,\n                  type: \"admin\",\n                },\n              },\n            },\n          },\n        },\n    mcp_server_tools:\n      isAdmin ? \"*\" : (\n        {\n          select: {\n            fields: \"*\",\n          },\n        }\n      ),\n    mcp_server_configs: isAdmin && {\n      insert: {\n        fields: \"*\",\n        postValidate: async ({ row, dbx }) => {\n          await testMCPServerConfig(dbx, row);\n        },\n      },\n      update: {\n        fields: \"*\",\n        postValidate: async ({ row, dbx }) => {\n          await testMCPServerConfig(dbx, row);\n          // await startMcpHub(dbx, row);\n        },\n      },\n      select: \"*\",\n      delete: \"*\",\n    },\n    llm_chats_allowed_mcp_tools: {\n      select: {\n        fields: \"*\",\n        forcedFilter: userOwnsRelatedChat,\n      },\n      insert: {\n        fields: \"*\",\n        checkFilter: userOwnsRelatedChat,\n      },\n      update: {\n        fields: \"*\",\n        forcedFilter: userOwnsRelatedChat,\n        checkFilter: userOwnsRelatedChat,\n      },\n      delete: {\n        filterFields: \"*\",\n        forcedFilter: userOwnsRelatedChat,\n      },\n    },\n    mcp_server_tool_calls: {\n      select: {\n        fields: \"*\",\n        forcedFilter: userOwnsRelatedChat,\n      },\n    },\n    llm_chats_allowed_functions:\n      isAdmin ? \"*\" : (\n        {\n          select: {\n            fields: \"*\",\n            forcedFilter: userOwnsRelatedChat,\n          },\n          insert: {\n            fields: \"*\",\n            checkFilter: userOwnsRelatedChat,\n          },\n          delete: {\n            filterFields: \"*\",\n            forcedFilter: userOwnsRelatedChat,\n          },\n        }\n      ),\n  } satisfies Publish<DBGeneratedSchema>;\n\n  return result;\n};\n"
  },
  {
    "path": "server/src/publish/publish.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { isDefined } from \"@common/filterUtils\";\nimport {\n  getMagicLinkEmailFromTemplate,\n  getVerificationEmailFromTemplate,\n  MOCK_SMTP_HOST,\n} from \"@common/OAuthUtils\";\nimport type { SessionUser } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { verifySMTPConfig } from \"prostgles-server/dist/Prostgles\";\nimport type { Publish } from \"prostgles-server/dist/PublishParser/PublishParser\";\nimport type { ValidateUpdateRow } from \"prostgles-server/dist/PublishParser/publishTypesAndUtils\";\nimport { getKeys, type FilterItem } from \"prostgles-types\";\nimport { getPasswordHash } from \"../authConfig/authUtils\";\nimport { getSMTPWithTLS } from \"../authConfig/emailProvider/getEmailSenderWithMockTest\";\nimport { checkClientIP } from \"../authConfig/sessionUtils\";\nimport { getACRules } from \"../ConnectionManager/ConnectionManager\";\nimport { getPublishLLM } from \"./getPublishLLM\";\nimport type { DBSSchema } from \"@common/publishUtils\";\n\nexport const publish: Publish<\n  DBGeneratedSchema,\n  SessionUser<DBSSchema[\"users\"]>\n> = async (params) => {\n  const { dbo: db, user, db: _db, clientReq } = params;\n\n  if (!user || !user.id) {\n    return null;\n  }\n  const isAdmin = user.type === \"admin\";\n\n  const { id: user_id } = user;\n\n  /** This will prevent admins from seing each others published workspaces?! */\n  const accessRules = isAdmin ? undefined : await getACRules(db, user);\n\n  const createEditDashboards =\n    isAdmin ||\n    accessRules?.some(({ dbsPermissions }) => dbsPermissions?.createWorkspaces);\n\n  const publishedWspIDs =\n    accessRules\n      ?.flatMap(\n        (ac) => ac.dbsPermissions?.viewPublishedWorkspaces?.workspaceIds,\n      )\n      .filter(isDefined) || [];\n\n  const dashboardMainTables: Publish<DBGeneratedSchema> = (\n    [\"windows\", \"links\", \"workspaces\"] as const\n  ).reduce(\n    (a, tableName) => ({\n      ...a,\n      [tableName]: {\n        select: {\n          fields: \"*\",\n          forcedFilter: {\n            $or: [\n              { user_id },\n              /** User either owns the item or the item has been shared/published to the user */\n              {\n                [tableName === \"workspaces\" ? \"id\" : \"workspace_id\"]: {\n                  $in: publishedWspIDs,\n                },\n              },\n            ],\n          },\n        },\n        sync: {\n          id_fields: [\"id\"],\n          synced_field: \"last_updated\",\n        },\n        ...(createEditDashboards && {\n          update: {\n            fields: { user_id: 0 },\n            forcedData: { user_id },\n            forcedFilter: { user_id },\n          },\n          insert: {\n            fields: \"*\",\n            forcedData: { user_id },\n            /** TODO: Add workspace publish modes */\n            checkFilter:\n              tableName === \"workspaces\" ? undefined : (\n                {\n                  $existsJoined: {\n                    workspaces: {\n                      user_id: user.id,\n                    },\n                  },\n                }\n              ),\n          },\n          delete: {\n            filterFields: \"*\",\n            forcedFilter: { user_id },\n          },\n        }),\n      },\n    }),\n    {},\n  );\n\n  type User = DBGeneratedSchema[\"users\"][\"columns\"];\n  const getValidateAndHashUserPassword = (mustUpdate = false) => {\n    const validateFunc: ValidateUpdateRow<User, DBGeneratedSchema> = async ({\n      dbx,\n      filter,\n      update,\n    }) => {\n      if (\"password\" in update) {\n        //@ts-ignore\n        const [user, ...otherUsers] = await dbx.users.find(filter);\n        if (!user || otherUsers.length) {\n          throw \"Cannot update: update filter must match exactly one user\";\n        }\n        if (!update.password) {\n          throw \"Password cannot be empty\";\n        }\n        const hashedPassword = getPasswordHash(user, update.password);\n        if (typeof hashedPassword !== \"string\") throw \"Not ok\";\n        if (mustUpdate) {\n          await dbx.users.update(filter, { password: hashedPassword });\n        }\n        return {\n          ...update,\n          password: hashedPassword,\n        };\n      }\n      update.last_updated ??= Date.now().toString();\n      return update;\n    };\n    return validateFunc;\n  };\n\n  const userTypeFilter = {\n    access_control_user_types: { user_type: user.type },\n  };\n\n  const forcedData = { user_id: user.id };\n\n  // const forcedFilterLLM = {\n  //   $existsJoined: {\n  //     access_control_allowed_llm: {\n  //       access_control_id: { $in: accessRules?.map((ac) => ac.id) ?? [] },\n  //     },\n  //   },\n  // };\n\n  let dashboardTables: Publish<DBGeneratedSchema> = {\n    /* DASHBOARD */\n    ...dashboardMainTables,\n    access_control_user_types: isAdmin && \"*\",\n    published_methods:\n      isAdmin ? \"*\" : (\n        {\n          select: {\n            fields: {\n              id: 1,\n              name: 1,\n              description: 1,\n              arguments: 1,\n              connection_id: 1,\n            },\n          },\n        }\n      ),\n    credentials: isAdmin && {\n      select: {\n        fields: { key_secret: 0 },\n      },\n      delete: \"*\",\n      insert: {\n        fields: { id: 0 },\n        forcedData,\n        postValidate: ({ row }) => {\n          if (row.type !== \"AWS\" && !row.endpoint_url) {\n            throw \"Endpoint URL is required for non-AWS credentials\";\n          }\n        },\n      },\n      update: \"*\",\n    },\n    ...getPublishLLM(user_id, isAdmin, accessRules, db),\n    credential_types: isAdmin && { select: \"*\" },\n    access_control: isAdmin ? \"*\" : undefined, // { select: { fields: \"*\", forcedFilter: { $existsJoined: userTypeFilter } } },\n    database_configs:\n      isAdmin ? \"*\" : (\n        {\n          select: { fields: { id: 1 } },\n        }\n      ),\n    connections: {\n      select: {\n        fields: isAdmin ? \"*\" : { id: 1, name: 1, created: 1, is_state_db: 1 },\n        orderByFields: { db_conn: 1, created: 1 },\n        forcedFilter:\n          isAdmin ?\n            {}\n          : {\n              $and: [\n                {\n                  $existsJoined: {\n                    \"database_configs.access_control.access_control_user_types\":\n                      userTypeFilter[\"access_control_user_types\"],\n                  },\n                } as FilterItem,\n                { $existsJoined: { access_control_connections: {} } },\n              ],\n            },\n      },\n      update: isAdmin && {\n        fields: {\n          name: 1,\n          url_path: 1,\n          table_options: 1,\n          db_schema_filter: 1,\n          display_options: 1,\n        },\n        validate: async ({ update, dbx, filter }) => {\n          const row = await dbx.connections.findOne(filter);\n          if (row?.is_state_db && update.table_options) {\n            throw \"Table options are not supported yet\";\n          }\n          return update;\n        },\n      },\n    },\n    user_types: isAdmin && {\n      insert: \"*\",\n      select: {\n        fields: \"*\",\n      },\n      delete: {\n        filterFields: \"*\",\n        validate: async (filter) => {\n          const adminVal = await db.user_types.findOne({\n            $and: [filter, { id: \"admin\" }],\n          });\n          if (adminVal) throw \"Cannot delete the admin value\";\n        },\n      },\n    },\n    users:\n      isAdmin ?\n        {\n          select: { fields: { \"2fa\": 0, password: 0 } },\n          insert: {\n            fields: { created: 0, \"2fa\": 0, last_updated: 0 },\n            postValidate: async ({ row, dbx, localParams }) => {\n              await getValidateAndHashUserPassword(true)({\n                localParams,\n                update: row,\n                dbx,\n                filter: { id: row.id },\n              });\n            },\n          },\n          update: {\n            fields: {\n              options: 1,\n            },\n            validate: getValidateAndHashUserPassword(),\n            dynamicFields: [\n              {\n                /* For own user can only change these fields */\n                fields: { username: 1, password: 1, status: 1, options: 1 },\n                filter: { id: user.id },\n              },\n            ],\n          },\n          delete: {\n            filterFields: \"*\",\n            forcedFilter: { \"id.<>\": user.id }, // Cannot delete your admin user\n          },\n        }\n      : {\n          select: {\n            fields: {\n              id: 1,\n              username: 1,\n              name: 1,\n              email: 1,\n              auth_provider: 1,\n              type: 1,\n              options: 1,\n              created: 1,\n            },\n            forcedFilter: { id: user_id },\n          },\n          update: {\n            fields: { password: 1, options: 1 },\n            forcedFilter: { id: user_id },\n            validate: getValidateAndHashUserPassword(),\n          },\n        },\n    sessions: {\n      delete:\n        isAdmin ? \"*\" : (\n          {\n            filterFields: \"*\",\n            forcedFilter: { user_id },\n          }\n        ),\n      select: {\n        fields: { id: 0 },\n        forcedFilter: isAdmin ? undefined : { user_id },\n      },\n      update:\n        isAdmin ? \"*\" : (\n          {\n            fields: { active: 1 },\n            forcedFilter: { user_id, active: true },\n          }\n        ),\n    },\n    backups: {\n      select: true,\n      update: isAdmin && {\n        fields: [\"restore_status\"],\n      },\n    },\n    magic_links: isAdmin && {\n      insert: {\n        fields: { magic_link: 0, magic_link_used: 0 },\n      },\n      select: true,\n      update: true,\n      delete: true,\n    },\n\n    login_attempts: {\n      select: \"*\",\n    },\n\n    global_settings: isAdmin && {\n      select: \"*\",\n      update: {\n        fields: {\n          allowed_origin: 1,\n          allowed_ips: 1,\n          trust_proxy: 1,\n          allowed_ips_enabled: 1,\n          session_max_age_days: 1,\n          login_rate_limit: 1,\n          login_rate_limit_enabled: 1,\n          pass_process_env_vars_to_server_side_functions: 1,\n          enable_logs: 1,\n          auth_providers: 1,\n          prostgles_registration: 1,\n          mcp_servers_disabled: 1,\n        },\n        postValidate: async ({ row, dbx: dbsTX }) => {\n          if (!row.allowed_ips.length) {\n            throw \"Must include at least one allowed IP CIDR\";\n          }\n          // const ranges = await Promise.all(\n          //   row.allowed_ips?.map(\n          //     cidr => db.sql!(\n          //       getCIDRRangesQuery({ cidr, returns: [\"from\", \"to\"] }),\n          //       { cidr },\n          //       { returnType: \"row\" }\n          //     )\n          //   )\n          // )\n\n          if (row.allowed_ips_enabled) {\n            const { isAllowed, ip } = await checkClientIP(\n              dbsTX,\n              {\n                ...clientReq,\n              },\n              await dbsTX.global_settings.findOne(),\n            );\n            if (!isAllowed)\n              throw `Cannot update to a rule that will block your current IP.  \\n Must allow ${ip} within Allowed IPs`;\n          }\n\n          const { email } = row.auth_providers ?? {};\n          if (\n            email?.enabled &&\n            (email.smtp.type !== \"smtp\" || email.smtp.host !== MOCK_SMTP_HOST)\n          ) {\n            const smtp = getSMTPWithTLS(email.smtp);\n            await verifySMTPConfig(smtp);\n          }\n\n          if (email?.signupType === \"withPassword\") {\n            getVerificationEmailFromTemplate({\n              template: email.emailTemplate,\n              url: \"a\",\n              code: \"a\",\n            });\n          }\n          if (email?.signupType === \"withMagicLink\") {\n            getMagicLinkEmailFromTemplate({\n              template: email.emailTemplate,\n              url: \"a\",\n              code: \"a\",\n            });\n          }\n\n          return undefined;\n        },\n      },\n    },\n    services:\n      isAdmin ? \"*\" : (\n        {\n          select: {\n            fields: {\n              name: 1,\n              status: 1,\n              icon: 1,\n              label: 1,\n              description: 1,\n            },\n          },\n        }\n      ),\n  };\n\n  const curTables = Object.keys(dashboardTables);\n  const remainingTables = getKeys(db).filter((k) => {\n    const tableHandler = db[k];\n    return tableHandler && \"find\" in tableHandler && !curTables.includes(k);\n  });\n  const adminExtra = remainingTables.reduce((a, v) => ({ ...a, [v]: \"*\" }), {});\n  dashboardTables = {\n    ...(dashboardTables as object),\n    ...(isAdmin ? adminExtra : {}),\n  };\n\n  return dashboardTables;\n};\n"
  },
  {
    "path": "server/src/publishMethods/applySampleSchema.ts",
    "content": "import fs from \"fs\";\nimport path from \"path\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport type { DBS } from \"../index\";\n\nexport type Users = Required<DBGeneratedSchema[\"users\"][\"columns\"]>;\nexport type Connections = Required<DBGeneratedSchema[\"connections\"][\"columns\"]>;\n\nimport { isDefined } from \"@common/filterUtils\";\nimport type { SampleSchema, SampleSchemaDir } from \"@common/utils\";\nimport { getEvaledExports } from \"../ConnectionManager/connectionManagerUtils\";\nimport { actualRootDir } from \"../electronConfig\";\nimport { runConnectionQuery } from \"./publishMethods\";\n\nexport const applySampleSchema = async (\n  dbs: DBS,\n  sampleSchemaName: string,\n  connId: string,\n) => {\n  const schema = getSampleSchemas().find((s) => s.name === sampleSchemaName);\n  if (!schema) {\n    throw \"Sample schema not found: \" + sampleSchemaName;\n  }\n  if (schema.type === \"sql\") {\n    await runConnectionQuery(connId, schema.file, undefined, { dbs });\n    return;\n  }\n\n  const { tableConfigTs, onMountTs, onInitSQL, connection, databaseConfig } =\n    schema;\n  if (onInitSQL) {\n    await runConnectionQuery(connId, onInitSQL, undefined, { dbs });\n  }\n  await dbs.database_configs.update(\n    { $existsJoined: { connections: { id: connId } } },\n    { table_config_ts: tableConfigTs, ...databaseConfig },\n  );\n  await dbs.connections.update(\n    { id: connId },\n    { on_mount_ts: onMountTs, ...connection },\n  );\n};\n\nconst getFileIfExists = (path: string) => {\n  if (!fs.existsSync(path)) {\n    return undefined;\n  }\n  return fs.readFileSync(path, \"utf8\");\n};\n\nexport const getSampleSchemas = (): SampleSchema[] => {\n  const sampleSchemasDir = path.join(actualRootDir, `/sample_schemas`);\n  const files = fs\n    .readdirSync(sampleSchemasDir)\n    .filter((name) => !name.startsWith(\"_\"));\n  return files\n    .map((name) => {\n      const schemaPath = `${sampleSchemasDir}/${name}`;\n      if (fs.statSync(`${schemaPath}`).isDirectory()) {\n        return {\n          path: sampleSchemasDir,\n          name,\n          type: \"dir\" as const,\n          tableConfigTs: getFileIfExists(`${schemaPath}/tableConfig.ts`) ?? \"\",\n          onMountTs: getFileIfExists(`${schemaPath}/onMount.ts`) ?? \"\",\n          onInitSQL: getFileIfExists(`${schemaPath}/onInit.sql`) ?? \"\",\n          connection: getEvaledExports<{\n            default: SampleSchemaDir[\"connection\"];\n          }>(getFileIfExists(`${schemaPath}/connection.ts`))?.default,\n          databaseConfig: getEvaledExports<{\n            default: SampleSchemaDir[\"databaseConfig\"];\n          }>(getFileIfExists(`${schemaPath}/databaseConfig.ts`) ?? \"\")?.default,\n          workspaceConfig: getEvaledExports<SampleSchemaDir>(\n            getFileIfExists(`${schemaPath}/workspaceConfig.ts`),\n          )?.workspaceConfig,\n        } satisfies SampleSchema;\n      }\n      return {\n        name,\n        path: sampleSchemasDir,\n        type: \"sql\" as const,\n        file: getFileIfExists(schemaPath) ?? \"\",\n      };\n    })\n    .filter(isDefined);\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/LLMResponseTypes.ts",
    "content": "import type { AnyObject } from \"prostgles-types\";\n\n/**\n * Usage statistics for the completion request.\n */\ntype CompletionUsage = {\n  /**\n   * Only shown in anthropic responses.\n   */\n  cost?: number;\n\n  /**\n   * Number of tokens in the generated completion.\n   */\n  completion_tokens: number;\n\n  /**\n   * Number of tokens in the prompt.\n   */\n  prompt_tokens: number;\n\n  /**\n   * Total number of tokens used in the request (prompt + completion).\n   */\n  total_tokens: number;\n\n  /**\n   * Breakdown of tokens used in a completion.\n   */\n  completion_tokens_details?: CompletionTokensDetails;\n\n  /**\n   * Breakdown of tokens used in the prompt.\n   */\n  prompt_tokens_details?: PromptTokensDetails;\n};\n\n/**\n * Breakdown of tokens used in a completion.\n */\ntype CompletionTokensDetails = {\n  /**\n   * When using Predicted Outputs, the number of tokens in the prediction that\n   * appeared in the completion.\n   */\n  accepted_prediction_tokens?: number;\n\n  /**\n   * Audio input tokens generated by the model.\n   */\n  audio_tokens?: number;\n\n  /**\n   * Tokens generated by the model for reasoning.\n   */\n  reasoning_tokens?: number;\n\n  /**\n   * When using Predicted Outputs, the number of tokens in the prediction that did\n   * not appear in the completion. However, like reasoning tokens, these tokens are\n   * still counted in the total completion tokens for purposes of billing, output,\n   * and context window limits.\n   */\n  rejected_prediction_tokens?: number;\n};\n\n/**\n * Breakdown of tokens used in the prompt.\n */\ntype PromptTokensDetails = {\n  /**\n   * Audio input tokens present in the prompt.\n   */\n  audio_tokens?: number;\n\n  /**\n   * Cached tokens present in the prompt.\n   */\n  cached_tokens?: number;\n};\n\n/**\n * https://openrouter.ai/docs/api-reference/api-guides/overview\n */\ntype ErrorResponse = {\n  code: number; // See \"Error Handling\" section\n  message: string;\n  metadata?: Record<string, unknown>; // Contains additional error information such as provider details, the raw error message, etc.\n};\n\ntype Choice = {\n  /**\n   * The reason the model stopped generating tokens. This will be `stop` if the model\n   * hit a natural stop point or a provided stop sequence, `length` if the maximum\n   * number of tokens specified in the request was reached, `content_filter` if\n   * content was omitted due to a flag from our content filters, `tool_calls` if the\n   * model called a tool, or `function_call` (deprecated) if the model called a\n   * function.\n   */\n  finish_reason:\n    | \"stop\"\n    | \"length\"\n    | \"tool_calls\"\n    | \"content_filter\"\n    | \"function_call\";\n\n  /**\n   * The index of the choice in the list of choices.\n   */\n  index: number;\n\n  /**\n   * Log probability information for the choice.\n   */\n  logprobs: null;\n\n  /**\n   * A chat completion message generated by the model.\n   */\n  message: ChatCompletionMessage;\n\n  /**\n   * Malformed function call from Gemini models.\n   * https://github.com/google-gemini/gemini-cli/issues/4486\n   */\n  error?: ErrorResponse;\n};\n\n/**\n * A chat completion message generated by the model.\n */\ntype ChatCompletionMessage = {\n  /**\n   * The contents of the message.\n   */\n  content: string | null;\n\n  reasoning: string | null;\n\n  /**\n   * The refusal message generated by the model.\n   */\n  refusal: string | null;\n\n  /**\n   * The role of the author of this message.\n   */\n  role: \"assistant\";\n\n  /**\n   * If the audio output modality is requested, this object contains data about the\n   * audio response from the model.\n   * [Learn more](https://platform.openai.com/docs/guides/audio).\n   */\n  audio?: ChatCompletionAudio | null;\n\n  /**\n   * The tool calls generated by the model, such as function calls.\n   */\n  tool_calls?: ChatCompletionMessageToolCall[];\n};\n\ntype ChatCompletionMessageToolCall = {\n  /**\n   * The ID of the tool call.\n   */\n  id: string;\n\n  /**\n   * The function that the model called.\n   */\n  function: {\n    /**\n     * The arguments to call the function with, as generated by the model in JSON\n     * format. Note that the model does not always generate valid JSON, and may\n     * hallucinate parameters not defined by your function schema. Validate the\n     * arguments in your code before calling your function.\n     */\n    arguments: string;\n\n    /**\n     * The name of the function to call.\n     */\n    name: string;\n  };\n\n  /**\n   * The type of the tool. Currently, only `function` is supported.\n   */\n  type: \"function\";\n};\n\n/**\n * If the audio output modality is requested, this object contains data about the\n * audio response from the model.\n * [Learn more](https://platform.openai.com/docs/guides/audio).\n */\ntype ChatCompletionAudio = {\n  /**\n   * Unique identifier for this audio response.\n   */\n  id: string;\n\n  /**\n   * Base64 encoded audio bytes generated by the model, in the format specified in\n   * the request.\n   */\n  data: string;\n\n  /**\n   * The Unix timestamp (in seconds) for when this audio response will no longer be\n   * accessible on the server for use in multi-turn conversations.\n   */\n  expires_at: number;\n\n  /**\n   * Transcript of the audio generated by the model.\n   */\n  transcript: string;\n};\n\n/**\n * https://github.com/openai/openai-node/blob/master/src/resources/completions.ts\n */\nexport type OpenAIChatCompletionResponse = {\n  /**\n   * A unique identifier for the chat completion.\n   */\n  id: string;\n\n  /**\n   * A list of chat completion choices. Can be more than one if `n` is greater\n   * than 1.\n   */\n  choices: Choice[];\n\n  /**\n   * The Unix timestamp (in seconds) of when the chat completion was created.\n   */\n  created: number;\n\n  /**\n   * The model used for the chat completion.\n   */\n  model: string;\n\n  /**\n   * The object type, which is always `chat.completion`.\n   */\n  object: \"chat.completion\";\n\n  /**\n   * Usage statistics for the completion request.\n   */\n  usage?: CompletionUsage;\n};\n\ntype AnthropicTextResponse = {\n  type: \"text\";\n  text: string;\n};\ntype AnthropicToolUseResponse = {\n  type: \"tool_use\";\n  id: string;\n  name: string;\n  input: AnyObject;\n};\nexport type AnthropicChatCompletionResponse = {\n  id: string;\n  type: \"message\";\n  role: \"assistant\";\n  /**\n   * \"claude-3-5-sonnet-20240620\" |\n   */\n  model: string;\n  content: (AnthropicTextResponse | AnthropicToolUseResponse)[];\n  stop_reason: \"tool_use\";\n  stop_sequence: null;\n  usage: {\n    input_tokens: number;\n    cache_creation_input_tokens: number;\n    cache_read_input_tokens: number;\n    output_tokens: number;\n  };\n};\n\nexport type GoogleGeminiChatCompletionResponse = {\n  candidates: {\n    content: {\n      parts: (\n        | {\n            text: string;\n          }\n        | {\n            functionCall: {\n              name: string;\n              args: AnyObject | undefined;\n            };\n          }\n      )[];\n      role: \"model\";\n    };\n    finishReason: \"STOP\";\n    avgLogprobs: number;\n  }[];\n  usageMetadata: {\n    totalTokenCount: number;\n    promptTokenCount: number;\n    promptTokensDetails: [{ modality: \"TEXT\"; tokenCount: number }];\n    candidatesTokenCount: number;\n    candidatesTokensDetails: [{ modality: \"TEXT\"; tokenCount: number }];\n  };\n  modelVersion: \"gemini-1.5-flash\";\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/askLLM.ts",
    "content": "import { dashboardTypesContent } from \"@common/dashboardTypesContent\";\nimport {\n  filterArr,\n  filterArrInverse,\n  getLLMMessageText,\n  isAssistantMessageRequestingToolUse,\n  reachedMaximumNumberOfConsecutiveToolRequests,\n} from \"@common/llmUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { sliceText } from \"@common/utils\";\nimport type { Filter } from \"prostgles-server/dist/DboBuilder/DboBuilderTypes\";\nimport { HOUR } from \"prostgles-server/dist/FileManager/FileManager\";\nimport {\n  getSerialisableError,\n  isObject,\n  omitKeys,\n  tryCatchV2,\n} from \"prostgles-types\";\nimport { type DBS } from \"../..\";\nimport { checkLLMLimit } from \"./checkLLMLimit\";\nimport { fetchLLMResponse, type LLMMessageWithRole } from \"./fetchLLMResponse\";\nimport { getLLMToolsAllowedInThisChat } from \"./getLLMToolsAllowedInThisChat\";\n\nimport {\n  getMCPToolNameParts,\n  type PROSTGLES_MCP_SERVERS_AND_TOOLS,\n} from \"@common/prostglesMcp\";\nimport type { AuthClientRequest } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { checkMaxCostLimitForChat } from \"./checkMaxCostLimitForChat\";\nimport { getFullPrompt } from \"./getFullPrompt\";\nimport { runApprovedTools } from \"./runApprovedTools/runApprovedTools\";\n\nexport const getBestLLMChatModel = async (\n  dbs: DBS,\n  filter: Parameters<DBS[\"llm_models\"][\"findOne\"]>[0],\n) => {\n  const preferredChatModel = await dbs.llm_models.findOne(filter, {\n    orderBy: [{ key: \"chat_suitability_rank\", asc: true, nulls: \"last\" }],\n  });\n  if (!preferredChatModel)\n    throw \"No LLM models found for \" + JSON.stringify(filter);\n  return preferredChatModel;\n};\n\nexport type LLMMessage = DBSSchema[\"llm_messages\"][\"message\"];\n\nexport type AskLLMArgs = {\n  connectionId: string;\n  userMessage: LLMMessage;\n  schema: string;\n  chatId: number;\n  dbs: DBS;\n  user: Pick<DBSSchema[\"users\"], \"id\" | \"type\">;\n  allowedLLMCreds: DBSSchema[\"access_control_allowed_llm\"][] | undefined;\n  accessRules: DBSSchema[\"access_control\"][] | undefined;\n  clientReq: AuthClientRequest;\n  type:\n    | \"new-message\"\n    | \"approve-tool-use\"\n    | \"tool-use-result\"\n    | \"tool-use-result-with-denied\";\n  aborter: AbortController | undefined;\n};\n\nconst activeLLMFetchRequests = new Map<number, AbortController>();\n\nexport const stopAskLLM = (chatId: number) => {\n  const aborter = activeLLMFetchRequests.get(chatId);\n  if (aborter) {\n    aborter.abort();\n    activeLLMFetchRequests.delete(chatId);\n  }\n};\n\nexport const askLLM = async (args: AskLLMArgs) => {\n  const {\n    accessRules,\n    allowedLLMCreds,\n    chatId,\n    connectionId,\n    dbs,\n    user,\n    schema,\n    userMessage,\n    type,\n    clientReq,\n  } = args;\n\n  const {\n    chat,\n    prompt,\n    pastMessages,\n    promptObj,\n    getChat,\n    llm_credential,\n    llm_prompt_id,\n  } = await getValidatedAskLLMChatOptions(args);\n\n  /** It's crucial we reduce the posibility that a new user message fails to insert due to some non critical error */\n  const {\n    data: toolsWithInfo,\n    error,\n    hasError,\n  } = await tryCatchV2(\n    async () =>\n      await getLLMToolsAllowedInThisChat({\n        userType: user.type,\n        dbs,\n        chat,\n        connectionId,\n        prompt: promptObj,\n        clientReq,\n      }),\n  );\n  if (hasError) {\n    console.error(\"LLM Tools fetch error:\", error);\n  }\n  const tools = toolsWithInfo?.map(\n    ({ name, description, input_schema, auto_approve }) => {\n      return {\n        name,\n        description,\n        input_schema: omitKeys(input_schema, [\"$id\"]),\n        auto_approve,\n      };\n    },\n  );\n\n  const aborter = args.aborter ?? new AbortController();\n  activeLLMFetchRequests.set(chat.id, aborter);\n  const lastMessage = pastMessages.at(-1);\n  if (type === \"approve-tool-use\") {\n    if (!lastMessage) {\n      throw new Error(\"Last message not found for tool use approval\");\n    }\n    const toolUseMessages = filterArr(lastMessage.message, {\n      type: \"tool_use\",\n    } as const);\n\n    return runApprovedTools(\n      toolsWithInfo,\n      args,\n      chat,\n      toolUseMessages,\n      userMessage,\n      aborter,\n      clientReq,\n    );\n  }\n\n  /** If user stopped chat must add tool use responses to prevent errors */\n  const awaitingToolUseResult = pastMessages.flatMap(({ message }, index) => {\n    const result = filterArr(message, {\n      type: \"tool_use\",\n    } as const).filter(\n      (toolUse) =>\n        !pastMessages\n          .slice(index + 1)\n          .some((maybeResponse) =>\n            maybeResponse.message.some(\n              (n) => n.type === \"tool_result\" && n.tool_use_id === toolUse.id,\n            ),\n          ),\n    );\n    return result;\n  });\n  if (awaitingToolUseResult.length && args.type === \"new-message\") {\n    await dbs.llm_messages.insert({\n      user_id: user.id,\n      chat_id: chatId,\n      message: awaitingToolUseResult.map((m) => ({\n        type: \"tool_result\" as const,\n        tool_name: m.name,\n        tool_use_id: m.id,\n        content:\n          chat.status?.state === \"stopped\" ?\n            \"Tool use requests were stopped by the user\"\n          : \"Tool use requests were interrupted by the user\",\n      })),\n      llm_model_id: chat.model,\n    });\n  }\n\n  if (args.type === \"tool-use-result\" && !awaitingToolUseResult.length) {\n    // Chat might have since been stopped and other user messages were added\n    return;\n  }\n\n  await dbs.llm_messages.insert({\n    user_id: user.id,\n    chat_id: chatId,\n    message: userMessage,\n    llm_model_id: chat.model,\n  });\n  if (type === \"tool-use-result-with-denied\") {\n    return;\n  }\n\n  /** Update chat name based on first user message */\n  const isFirstUserMessage = !pastMessages.some((m) => m.user_id === user.id);\n  if (isFirstUserMessage) {\n    const questionText = getLLMMessageText({ message: userMessage });\n    const isOnlyImage =\n      !questionText && userMessage.some((m) => m.type === \"image\");\n    void dbs.llm_chats.update(\n      { id: chatId },\n      {\n        name:\n          isOnlyImage ? \"[Attached image]\" : (\n            sliceText(questionText, 25).replaceAll(\"\\n\", \" \")\n          ),\n      },\n    );\n  }\n\n  const allowedUsedCreds = allowedLLMCreds?.filter(\n    (c) =>\n      c.llm_credential_id === llm_credential.id &&\n      c.llm_prompt_id === llm_prompt_id,\n  );\n\n  // Check if usage limit reached\n  if (allowedUsedCreds) {\n    const limitReachedMessage = await checkLLMLimit(\n      dbs,\n      user,\n      allowedUsedCreds,\n      accessRules ?? [],\n    );\n    if (limitReachedMessage) {\n      await dbs.llm_chats.update(\n        { id: chatId },\n        {\n          disabled_message: limitReachedMessage,\n          disabled_until: new Date(Date.now() + 24 * HOUR),\n        },\n      );\n      return;\n    } else if (chat.disabled_message) {\n      await dbs.llm_chats.update(\n        { id: chatId },\n        {\n          disabled_message: null,\n          disabled_until: null,\n        },\n      );\n    }\n  }\n\n  const hasMessagesThatNeedsAIResponse = userMessage.some(\n    (m) =>\n      !(\n        m.type === \"tool_result\" &&\n        !m.is_error &&\n        getMCPToolNameParts(m.tool_name)?.serverName ===\n          (\"prostgles-ui\" satisfies keyof typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)\n      ),\n  );\n  if (!hasMessagesThatNeedsAIResponse) {\n    return;\n  }\n\n  await dbs.llm_chats.update(\n    { id: chatId },\n    {\n      status: { state: \"loading\", since: new Date().toISOString() },\n    },\n  );\n  const aiResponseMessagePlaceholder = await dbs.llm_messages.insert(\n    {\n      user_id: null,\n      chat_id: chatId,\n      message: [{ type: \"text\", text: \"\" }],\n      llm_model_id: chat.model,\n    },\n    { returning: \"*\" },\n  );\n  try {\n    const modelData = (await dbs.llm_models.findOne(\n      { id: chat.model },\n      {\n        select: {\n          \"*\": 1,\n          llm_providers: \"*\",\n        },\n      },\n    )) as\n      | (DBSSchema[\"llm_models\"] & {\n          llm_providers: DBSSchema[\"llm_providers\"][];\n        })\n      | undefined;\n\n    if (!modelData) throw \"Model not found\";\n\n    checkMaxCostLimitForChat(chat, modelData, pastMessages, userMessage);\n\n    const promptWithContext = getFullPrompt({\n      prompt,\n      schema,\n      dashboardTypesContent,\n    });\n\n    const {\n      llm_providers: [llm_provider],\n      ...llm_model\n    } = modelData;\n    if (!llm_provider) throw \"Provider not found\";\n\n    const gemini25BreakingChanges = llm_model.name.includes(\"gemini-2.5\");\n    const {\n      content: aiResponseMessageRaw,\n      meta,\n      cost,\n    } = await fetchLLMResponse({\n      llm_chat: chat,\n      llm_model,\n      llm_provider,\n      llm_credential,\n      tools,\n      messages: [\n        {\n          /** TODO check if this works with all providers */\n          role: \"system\",\n          content: [{ type: \"text\", text: promptWithContext }],\n        },\n        ...pastMessages\n          /**all messages must have non-empty content */\n          .filter((m) => m.message.length)\n          .map(\n            (m) =>\n              ({\n                role:\n                  m.user_id ? \"user\"\n                  : gemini25BreakingChanges ? \"model\"\n                  : \"assistant\",\n                content: m.message,\n              }) satisfies LLMMessageWithRole,\n          ),\n        {\n          role: \"user\",\n          content: userMessage,\n        } satisfies LLMMessageWithRole,\n      ],\n      aborter,\n    });\n\n    /** Move prostgles-ui tool_use messages to the end for better UX (because no tool result is expected) */\n    const prostglesUIToolUse = filterArr(aiResponseMessageRaw, {\n      type: \"tool_use\",\n    } as const).filter(\n      (m) =>\n        getMCPToolNameParts(m.name)?.serverName ===\n        (\"prostgles-ui\" satisfies keyof typeof PROSTGLES_MCP_SERVERS_AND_TOOLS),\n    );\n    const aiResponseMessage =\n      !prostglesUIToolUse.length ? aiResponseMessageRaw : (\n        [\n          ...filterArrInverse(aiResponseMessageRaw, {\n            type: \"tool_use\",\n          } as const),\n          ...prostglesUIToolUse,\n        ]\n      );\n\n    await dbs.llm_messages.update(\n      { id: aiResponseMessagePlaceholder.id },\n      {\n        message: aiResponseMessage,\n        meta,\n        cost,\n      },\n    );\n\n    const { maximum_consecutive_tool_fails } = chat;\n    if (\n      maximum_consecutive_tool_fails &&\n      args.type !== \"approve-tool-use\" &&\n      isAssistantMessageRequestingToolUse({ message: aiResponseMessage }) &&\n      reachedMaximumNumberOfConsecutiveToolRequests(\n        [...pastMessages, { message: userMessage }],\n        maximum_consecutive_tool_fails,\n        true,\n      )\n    ) {\n      throw `Maximum number (${maximum_consecutive_tool_fails}) of failed consecutive tool requests reached`;\n    }\n\n    const latestChat = await getChat();\n    if (!latestChat) throw \"Chat not found after LLM response\";\n\n    const newToolUseMessages = filterArr(aiResponseMessage, {\n      type: \"tool_use\",\n    } as const);\n    if (newToolUseMessages.length) {\n      await runApprovedTools(\n        toolsWithInfo,\n        args,\n        latestChat,\n        newToolUseMessages,\n        undefined,\n        aborter,\n        clientReq,\n      );\n    }\n  } catch (err) {\n    const isAdmin = user.type === \"admin\";\n    const errorObjOrString = getSerialisableError(err);\n    const errorIsString = typeof errorObjOrString === \"string\";\n    const errorTextOrEmpty =\n      isObject(errorObjOrString) ?\n        JSON.stringify(errorObjOrString, null, 2)\n      : errorObjOrString;\n    const errorText = isAdmin ? `${errorTextOrEmpty}` : \"\";\n    const messageText = [\n      \"🔴 Something went wrong\",\n      isObject(err) && err.name === \"AbortError\" ?\n        \"Response generation was aborted by user.\"\n      : errorIsString ? errorText\n      : [\"```json\", errorText, \"```\"].join(\"\\n\"),\n    ].join(\".\\n\");\n    await dbs.llm_messages.update(\n      { id: aiResponseMessagePlaceholder.id },\n      {\n        message: [{ type: \"text\", text: messageText }],\n      },\n    );\n  }\n\n  await dbs.llm_chats.update(\n    { id: chatId },\n    {\n      status: null,\n    },\n  );\n};\n\nconst getValidatedAskLLMChatOptions = async ({\n  userMessage,\n  type,\n  chatId,\n  dbs,\n  user,\n}: AskLLMArgs) => {\n  if (!userMessage.length && type === \"new-message\") throw \"Message is empty\";\n  if (!Number.isInteger(chatId)) throw \"chatId must be an integer\";\n  const getChat = () => dbs.llm_chats.findOne({ id: chatId, user_id: user.id });\n  let maybeChat = await getChat();\n  if (!maybeChat) throw \"Chat not found\";\n  const { llm_prompt_id } = maybeChat;\n  if (!maybeChat.model) {\n    const preferredChatModel = await getBestLLMChatModel(dbs, {\n      $existsJoined: {\n        \"llm_providers.llm_credentials\": {},\n      },\n    } as Filter);\n    await dbs.llm_chats.update(\n      { id: chatId },\n      { model: preferredChatModel.id },\n    );\n    maybeChat = await getChat();\n  }\n  if (!maybeChat?.model) throw \"Chat model not found\";\n  const chat = { ...maybeChat, model: maybeChat.model };\n  const llm_credential = await dbs.llm_credentials.findOne({\n    $existsJoined: {\n      \"llm_providers.llm_models\": {\n        id: chat.model,\n      },\n    },\n  } as Filter);\n  if (!llm_prompt_id) throw \"Chat missing prompt\";\n  if (!llm_credential) throw \"LLM credentials missing\";\n  const promptObj = await dbs.llm_prompts.findOne({ id: llm_prompt_id });\n  if (!promptObj) throw \"Prompt not found\";\n  const { prompt } = promptObj;\n  const pastMessages = await dbs.llm_messages.find(\n    { chat_id: chatId },\n    { orderBy: { created: 1 } },\n  );\n  return {\n    prompt,\n    promptObj,\n    pastMessages,\n    chat,\n    llm_credential,\n    llm_prompt_id,\n    getChat,\n  };\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/checkLLMLimit.ts",
    "content": "import { HOUR } from \"prostgles-server/dist/FileManager/FileManager\";\nimport type { DBS } from \"../..\";\nimport type { DBSSchema } from \"@common/publishUtils\";\n\nexport const checkLLMLimit = async (\n  dbs: DBS,\n  user: Pick<DBSSchema[\"users\"], \"id\" | \"type\">,\n  allowedUsedLLMCreds: DBSSchema[\"access_control_allowed_llm\"][],\n  accessRules: DBSSchema[\"access_control\"][],\n) => {\n  if (user.type === \"admin\") return;\n  if (!allowedUsedLLMCreds.length) {\n    throw \"LLM credential/prompt not allowed\";\n  }\n  if (!accessRules.length) throw \"Access rules missing for non admin user\";\n  const usedRules = accessRules.filter((r) =>\n    allowedUsedLLMCreds.some((c) => c.access_control_id === r.id),\n  );\n  const limits = usedRules.map((r) => r.llm_daily_limit);\n  if (!limits.length) throw \"No limits found\";\n  if (limits.includes(0)) return;\n  const totalLimit = limits.reduce((a, v) => a + v, 0);\n  if (totalLimit <= 0) throw \"No limit found\";\n\n  /** If normal user then check messages by user_id only */\n  let userIds: string[] = [];\n  if (user.type !== \"public\") {\n    userIds = [user.id];\n\n    /** If public user then must check all messages from the same IP */\n  } else {\n    const currentSession = await dbs.sessions.findOne({ user_id: user.id });\n    if (!currentSession) throw \"Session not found\";\n    const sameIpSessions = await dbs.sessions.find({\n      ip_address: currentSession.ip_address,\n    });\n    userIds = sameIpSessions.map((s) => s.user_id).filter(Boolean);\n  }\n\n  if (!userIds.length) throw \"User id filter empty\";\n  const _messagesCount = await dbs.llm_messages.count({\n    user_id: { $in: userIds },\n    created: { $gte: new Date(Date.now() - 24 * HOUR).toISOString() },\n  });\n  const messagesCount = +_messagesCount;\n  if (+messagesCount > totalLimit) {\n    return \"Daily limit reached\" as const;\n  }\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/checkMaxCostLimitForChat.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport { getUserMessageCost } from \"./getUserMessageCost\";\nimport type { LLMMessage } from \"./askLLM\";\n\nexport const checkMaxCostLimitForChat = (\n  chat: DBSSchema[\"llm_chats\"],\n  model: DBSSchema[\"llm_models\"],\n  pastMessages: DBSSchema[\"llm_messages\"][],\n  userMessage: LLMMessage,\n) => {\n  const { max_total_cost_usd } = chat;\n  const maxTotalCost = parseFloat(max_total_cost_usd || \"0\");\n  if (maxTotalCost && maxTotalCost > 0) {\n    const pastMessageCost = pastMessages.reduce(\n      (acc, m) => acc + parseFloat(m.cost),\n      0,\n    );\n    if (pastMessageCost > maxTotalCost) {\n      throw `Maximum total cost of the chat (${maxTotalCost}) reached. Current cost: ${pastMessageCost}`;\n    }\n    const currentMessageCost = getUserMessageCost(userMessage, model);\n    if (pastMessageCost + currentMessageCost > maxTotalCost) {\n      throw [\n        `Maximum total cost of the chat (${maxTotalCost}) will be reached after sending this message.`,\n        `Current cost: ${pastMessageCost}.`,\n        `Estimated cost of current message: ${currentMessageCost}`,\n      ].join(\"\\n\");\n    }\n  }\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/fetchLLMResponse.ts",
    "content": "import {\n  getSerialisableError,\n  isObject,\n  type AnyObject,\n} from \"prostgles-types\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { getLLMRequestBody } from \"./getLLMRequestBody\";\nimport type { MCPToolSchema } from \"./getLLMToolsAllowedInThisChat\";\nimport {\n  parseLLMResponseObject,\n  type LLMParsedResponse,\n} from \"./parseLLMResponseObject\";\nimport { readFetchStream } from \"./readFetchStream\";\nexport type LLMMessageWithRole = {\n  role: \"system\" | \"user\" | \"assistant\" | \"model\";\n  content: DBSSchema[\"llm_messages\"][\"message\"];\n};\nexport type FetchLLMResponseArgs = {\n  llm_chat: Pick<DBSSchema[\"llm_chats\"], \"extra_body\" | \"extra_headers\">;\n  llm_model: DBSSchema[\"llm_models\"];\n  llm_provider: DBSSchema[\"llm_providers\"];\n  llm_credential: DBSSchema[\"llm_credentials\"];\n  tools: undefined | (MCPToolSchema & { auto_approve: boolean })[];\n  messages: LLMMessageWithRole[];\n  aborter: AbortController;\n};\n\nexport const fetchLLMResponse = async (\n  args: FetchLLMResponseArgs,\n): Promise<LLMParsedResponse> => {\n  const { llm_provider, llm_credential, llm_model, aborter } = args;\n  const model = llm_model.name;\n  const provider = llm_provider.id;\n  const { api_key } = llm_credential;\n  const { body, headers } = getLLMRequestBody(args);\n  const api_url = llm_provider.api_url\n    .replace(\"$KEY\", api_key)\n    .replace(\"$MODEL\", model);\n  if (api_url === \"http://localhost:3004/mocked-llm\") {\n    return { content: [{ type: \"text\", text: \"Mocked response\" }], cost: 0 };\n  }\n\n  const res = await fetch(api_url, {\n    method: \"POST\",\n    headers,\n    body,\n    signal: aborter.signal,\n  }).catch((err) => {\n    const serialisableError = getSerialisableError(err);\n    return Promise.reject(serialisableError);\n  });\n  if (!res.ok) {\n    const contentType = res.headers.get(\"content-type\");\n    const errorData = {\n      statusText: res.statusText,\n      statusCode: res.status,\n      error: \"\" as unknown,\n    };\n    if (contentType?.includes(\"application/json\")) {\n      errorData.error = await res.json();\n    } else {\n      errorData.error = await res.text();\n    }\n\n    throw new Error(\n      `Failed to fetch LLM response: ${res.statusText} ${JSON.stringify(errorData)}`,\n    );\n  }\n  const responseClone = res.clone();\n\n  const responseData = (await readFetchStream(res)) as AnyObject | undefined;\n  if (!responseData) {\n    throw new Error(\"No response data from LLM\");\n  }\n\n  try {\n    return parseLLMResponseObject({\n      provider,\n      responseData,\n      model: llm_model,\n    });\n  } catch (e) {\n    console.error(\n      `Error parsing LLM response from ${provider} for model ${model}`,\n      getSerialisableError(e),\n      responseData,\n    );\n    throw new Error(\n      `Error parsing LLM response from ${provider} for model ${model}: ${JSON.stringify(getSerialisableError(e))}`,\n    );\n  }\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/getFullPrompt.ts",
    "content": "import { LLM_PROMPT_VARIABLES, wrapCode } from \"@common/llmUtils\";\nimport { getElectronConfig } from \"@src/electronConfig\";\n\nexport const getFullPrompt = ({\n  prompt,\n  schema,\n  dashboardTypesContent,\n}: {\n  prompt: string;\n  schema: string;\n  dashboardTypesContent: string;\n}) => {\n  const promptWithContext = prompt\n    .replaceAll(\n      LLM_PROMPT_VARIABLES.PROSTGLES_SOFTWARE_NAME,\n      getElectronConfig()?.isElectron ? \"Prostgles Desktop\" : \"Prostgles UI\",\n    )\n    .replace(\n      LLM_PROMPT_VARIABLES.TODAY,\n      new Date().toISOString().split(\"T\")[0]!,\n    )\n    .replace(\n      LLM_PROMPT_VARIABLES.SCHEMA,\n      schema ?\n        wrapCode(\"sql\", schema)\n      : \"Schema is empty: there are no tables or views in the database\",\n    );\n  // .replace(\n  //   LLM_PROMPT_VARIABLES.DASHBOARD_TYPES,\n  //   wrapCode(\"typescript\", dashboardTypesContent),\n  // );\n  return promptWithContext;\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/getLLMRequestBody.ts",
    "content": "import {\n  includes,\n  isDefined,\n  omitKeys,\n  tryCatchV2,\n  type AnyObject,\n} from \"prostgles-types\";\nimport { filterArr, findArr } from \"@common/llmUtils\";\nimport type { FetchLLMResponseArgs } from \"./fetchLLMResponse\";\nimport type { LLMMessage } from \"./askLLM\";\n\nexport const getLLMRequestBody = ({\n  llm_provider,\n  llm_credential,\n  messages: maybeEmptyMessages,\n  tools: maybeEmptyTools,\n  llm_model,\n  llm_chat,\n}: FetchLLMResponseArgs) => {\n  /**\n   * Sending an empty array of tools or messages produces an error in openrouter: tool_choice may only be specified while providing tools\n   */\n  const tools =\n    maybeEmptyTools && maybeEmptyTools.length ? maybeEmptyTools : undefined;\n  const nonEmptyMessages = maybeEmptyMessages\n    .map((m) => {\n      const nonEmptyMessageContent = m.content.filter(\n        (m) => m.type !== \"text\" || !(\"text\" in m) || m.text.trim(),\n      );\n      return {\n        ...m,\n        content: nonEmptyMessageContent,\n      };\n    })\n    .filter((m) => m.content.length);\n\n  const systemMessage = nonEmptyMessages.filter((m) => m.role === \"system\");\n  const [systemMessageObj, ...otherSM] = systemMessage;\n  if (otherSM.length) throw \"Multiple prompts found\";\n  const { api_key } = llm_credential;\n  const model = llm_model.name;\n  const provider = llm_provider.id as\n    | \"OpenAI\"\n    | \"Anthropic\"\n    | \"Google\"\n    | \"Prostgles\"\n    | \"OpenRouter\"\n    | \"Ollama\";\n  const messages =\n    includes([\"OpenAI\", \"OpenRouter\", \"Prostgles\", \"Ollama\"], provider) ?\n      nonEmptyMessages\n    : nonEmptyMessages.filter((m) => m.role !== \"system\");\n  const headers: RequestInit[\"headers\"] =\n    provider === \"Anthropic\" ?\n      {\n        \"content-type\": \"application/json\",\n        \"x-api-key\": api_key,\n        \"anthropic-version\": \"2023-06-01\",\n      }\n    : provider === \"Google\" ?\n      {\n        \"content-type\": \"application/json\",\n      }\n    : {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${api_key}`,\n      };\n\n  const body =\n    provider === \"Anthropic\" ?\n      {\n        model,\n        system: systemMessageObj?.content.map((c, i, arr) =>\n          i === arr.length - 1 ?\n            {\n              ...c,\n              cache_control: { type: \"ephemeral\" },\n            }\n          : c,\n        ),\n        messages: messages.map((m) => ({\n          ...m,\n          content: m.content.map((c) => {\n            return (\n              c.type === \"image\" ?\n                {\n                  type: \"image\",\n                  source: {\n                    ...c.source,\n                    data: removeBase64Prefix(c.source.data),\n                  },\n                }\n              : c.type === \"tool_result\" ? omitKeys(c, [\"tool_name\"])\n              : c\n            );\n          }),\n        })),\n        tools,\n      }\n    : provider === \"Google\" ?\n      {\n        system_instruction: systemMessageObj && {\n          parts: systemMessageObj.content\n            .map((c) => {\n              if (c.type !== \"text\" || !(\"text\" in c)) return undefined;\n              return {\n                text: c.text,\n              };\n            })\n            .filter(isDefined),\n        },\n        /**\n         * https://ai.google.dev/gemini-api/docs/text-generation?lang=rest\n         */\n        contents: messages.map((m) => ({\n          role:\n            m.content.some((c) => c.type === \"tool_result\") ? \"function\"\n            : m.content.some((c) => c.type === \"tool_use\") ? \"model\"\n            : m.role,\n          parts: m.content\n            .map((c) => {\n              // if (c.type === \"image\") {\n              //   return {\n              //     inlineData: {\n              //       mimeType: c.source.media_type,\n              //       data: removeBase64Prefix(c.source.data),\n              //     },\n              //   };\n              // }\n              if (c.type === \"text\" && \"text\" in c) return [{ text: c.text }];\n              if (c.type === \"tool_use\") {\n                return [\n                  {\n                    functionCall: {\n                      name: c.name,\n                      args: c.input,\n                    },\n                  },\n                ];\n              }\n              const toolResults = getToolResults([c]);\n              if (toolResults.length) {\n                return toolResults.map((toolResult) => {\n                  const resultText = toolResult.content;\n                  const funcName = toolResult.tool_name; //arr[i - 1]?.content\n                  // .map((c) =>\n                  //   c.type === \"tool_use\" && c.name ? c.name : undefined,\n                  // )\n                  // .find(isDefined);\n                  const resultObject = tryCatchV2(\n                    () => JSON.parse(resultText || \"{}\") as AnyObject,\n                  );\n                  return {\n                    functionResponse: {\n                      name: funcName,\n                      response: {\n                        name: funcName,\n                        content: resultObject.data ?? {\n                          parsingError: \"Could not parse tool result as JSON\",\n                          toolResult: resultText,\n                        },\n\n                        // JSON.parse(\n                        //   (resultText as string | undefined) ?? \"{}\",\n                        // ),\n                      },\n                    },\n                  };\n                });\n              }\n            })\n            .flat()\n            .filter(isDefined),\n        })),\n        ...(tools && {\n          tools: [\n            {\n              functionDeclarations: tools.map((t) => ({\n                name: t.name,\n                description: t.description,\n                parameters: omitKeys(t.input_schema, [\"$schema\", \"$id\"]),\n              })),\n            },\n          ],\n        }),\n      }\n    : /**  \"OpenAI\"  */\n      {\n        model,\n        messages: messages\n          .map((m) => {\n            const toolUseContent = filterArr(m.content, {\n              type: \"tool_use\" as const,\n            });\n            const textContent = filterArr(m.content, { type: \"text\" as const });\n\n            if (toolUseContent.length) {\n              return {\n                role: \"assistant\",\n                content: textContent.map((t) => t.text).join(\"\\n\") || \"\",\n                tool_calls: toolUseContent.map((tc) => ({\n                  id: tc.id,\n                  type: \"function\",\n                  function: {\n                    name: tc.name,\n                    arguments: JSON.stringify(tc.input),\n                  },\n                })),\n              };\n            }\n            const toolUseResults = getToolResults(m.content);\n            if (toolUseResults.length) {\n              return toolUseResults.map((toolUseResult) => {\n                return {\n                  role: \"tool\",\n                  tool_call_id: toolUseResult.tool_use_id,\n                  content: toolUseResult.content,\n                  ...(toolUseResult.is_error && {\n                    is_error: toolUseResult.is_error,\n                  }),\n                };\n              });\n            }\n            return {\n              ...m,\n              content: m.content.map((c) => {\n                if (c.type === \"image\") {\n                  return {\n                    type: \"image_url\",\n                    image_url: { url: c.source.data },\n                  };\n                }\n                if (c.type === \"tool_result\") {\n                  return {\n                    type: \"function_call_output\",\n                    call_id: c.tool_use_id,\n                    output:\n                      typeof c.content === \"string\" ?\n                        c.content\n                      : (filterArr(c.content, { type: \"text\" as const })[0]\n                          ?.text ??\n                        \"?? Internal issue in tool result parsing in prostgles ui\"),\n                  };\n                }\n                return c;\n              }),\n              ...(m.content.some((c) => c.type === \"tool_result\") && {\n                role: \"tool\",\n              }),\n            };\n          })\n          .flat(),\n        tools: tools?.map((t) => ({\n          type: \"function\",\n          function: {\n            name: t.name,\n            description: t.description,\n            parameters: omitKeys(t.input_schema, [\"$schema\", \"$id\"]),\n          },\n        })),\n        ...(provider === \"Ollama\" && {\n          stream: false,\n        }),\n      };\n\n  const bodyWithExtras = {\n    ...body,\n    ...llm_provider.extra_body,\n    ...llm_credential.extra_body,\n    ...llm_model.extra_body,\n    ...llm_chat.extra_body,\n  };\n  return {\n    body: JSON.stringify(\n      provider === \"Prostgles\" ? [bodyWithExtras] : bodyWithExtras,\n    ),\n    headers: {\n      ...headers,\n      ...llm_provider.extra_headers,\n      ...llm_credential.extra_headers,\n      ...llm_model.extra_headers,\n      ...llm_chat.extra_headers,\n    },\n  };\n};\n\nconst removeBase64Prefix = (data: string) => {\n  const base64Prefix = \"base64,\";\n  if (data.includes(base64Prefix)) {\n    return data.substring(data.indexOf(\"base64,\") + 7);\n  }\n  return data;\n};\n\nconst getToolResults = (content: LLMMessage) => {\n  const toolUseResults = filterArr(content, {\n    type: \"tool_result\" as const,\n  });\n\n  return toolUseResults.map((toolUseResult) => {\n    const { content, ...otherProps } = toolUseResult;\n    const contentText =\n      typeof content === \"string\" ? content : (\n        filterArr(content, { type: \"text\" as const })\n          .map((c) => c.text)\n          .join(\"\\n\")\n      );\n    return {\n      ...otherProps,\n      role: \"tool\",\n      tool_call_id: toolUseResult.tool_use_id,\n      content:\n        contentText ||\n        \"?? Internal issue in tool result parsing in prostgles ui\",\n    };\n  });\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/getLLMToolsAllowedInThisChat.ts",
    "content": "import { getJSONBSchemaAsJSONSchema, isDefined } from \"prostgles-types\";\nimport type { DBS } from \"../..\";\n\nimport {\n  getMCPFullToolName,\n  getMCPToolNameParts,\n  getProstglesMCPFullToolName,\n  PROSTGLES_MCP_SERVERS_AND_TOOLS,\n  type AllowedChatTool,\n} from \"@common/prostglesMcp\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { getEntries } from \"@common/utils\";\nimport type { AuthClientRequest } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { getMCPServerTools } from \"./prostglesLLMTools/getMCPServerTools\";\nimport { getProstglesLLMTools } from \"./prostglesLLMTools/getProstglesLLMTools\";\nimport { getPublishedMethodsTools } from \"./prostglesLLMTools/getPublishedMethodsTools\";\n\nexport type GetLLMToolsArgs = {\n  userType: string;\n  chat: DBSSchema[\"llm_chats\"];\n  prompt: DBSSchema[\"llm_prompts\"];\n  dbs: DBS;\n  connectionId: string;\n  clientReq: AuthClientRequest;\n};\n\nexport type MCPToolSchema = {\n  name: string;\n  description: string;\n  input_schema: ReturnType<typeof getJSONBSchemaAsJSONSchema>;\n};\n\nexport const getLLMToolsAllowedInThisChat = async ({\n  userType,\n  dbs,\n  chat,\n  connectionId,\n  prompt,\n  clientReq,\n}: GetLLMToolsArgs): Promise<undefined | AllowedChatTool[]> => {\n  const { id: chatId } = chat;\n  const { serverSideFuncTools } = await getPublishedMethodsTools(dbs, {\n    chatId,\n    connectionId,\n  });\n  const llm_chats_allowed_functions =\n    await dbs.llm_chats_allowed_functions.find({\n      chat_id: chatId,\n    });\n\n  const llm_chats_allowed_mcp_tools =\n    await dbs.llm_chats_allowed_mcp_tools.find({\n      chat_id: chatId,\n    });\n  const { mcpTools } = await getMCPServerTools(dbs, {\n    $existsJoined: {\n      llm_chats_allowed_mcp_tools: {\n        chat_id: chatId,\n      },\n    },\n  });\n  const tools: Record<string, AllowedChatTool> = {};\n  const mcpToolsWithInfo = mcpTools\n    .map(({ id, ...tool }) => {\n      const info = llm_chats_allowed_mcp_tools.find(\n        ({ tool_id }) => tool_id === id,\n      );\n      if (!info) return;\n      return {\n        type: \"mcp\" as const,\n        ...tool,\n        ...info,\n        auto_approve: Boolean(info.auto_approve),\n      } satisfies AllowedChatTool;\n    })\n    .filter(isDefined);\n\n  const { prostglesMCPTools, prostglesDBTools } = await getProstglesLLMTools({\n    userType,\n    dbs,\n    chat,\n    prompt,\n    mcpToolsWithInfo,\n    connectionId,\n    clientReq,\n  });\n\n  /** Check for name collisions */\n  [\n    ...prostglesMCPTools.map((t) => {\n      const toolNameParts = getMCPToolNameParts(t.name);\n      if (!toolNameParts) {\n        throw new Error(`Could not parse tool name parts for ${t.name}`);\n      }\n      return {\n        ...t,\n        tool_name: toolNameParts.toolName,\n        server_name: toolNameParts.serverName,\n      } satisfies AllowedChatTool;\n    }),\n    ...serverSideFuncTools\n      .map(({ id, ...t }) => {\n        const info = llm_chats_allowed_functions.find(\n          ({ server_function_id }) => server_function_id === id,\n        );\n        if (!info) return;\n        return {\n          type: \"prostgles-db-methods\" as const,\n          ...t,\n          ...info,\n          server_name: \"prostgles-db-methods\",\n          auto_approve: Boolean(info.auto_approve),\n        } satisfies AllowedChatTool;\n      })\n      .filter(isDefined),\n    ...prostglesDBTools.map((t) => {\n      return {\n        ...t,\n      } satisfies AllowedChatTool;\n    }),\n  ].forEach((tool) => {\n    const { name } = tool;\n    if (tools[name]) {\n      throw new Error(\n        `Tool name collision: ${name} is used by both MCP tool and/or other function`,\n      );\n    }\n    tools[name] = tool;\n  });\n  const toolList = Object.values(tools);\n\n  return toolList;\n};\n\nexport const getAllToolNames = async (dbs: DBS): Promise<string[]> => {\n  const mcpTools = await dbs.mcp_server_tools.find();\n  const publishedMethods = await dbs.published_methods.find();\n\n  return [\n    ...mcpTools.map((t) => getMCPFullToolName(t.server_name, t.name)),\n    ...publishedMethods.map((t) =>\n      getProstglesMCPFullToolName(\"prostgles-db-methods\", t.name),\n    ),\n    ...getEntries(PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-db\"]).map(\n      ([toolName]) => getProstglesMCPFullToolName(\"prostgles-db\", toolName),\n    ),\n    ...getEntries(PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-ui\"]).map(\n      ([toolName]) => getProstglesMCPFullToolName(\"prostgles-ui\", toolName),\n    ),\n  ];\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/getLLMUsageCost.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport type {\n  AnthropicChatCompletionResponse,\n  GoogleGeminiChatCompletionResponse,\n  OpenAIChatCompletionResponse,\n} from \"./LLMResponseTypes\";\n\nexport const getLLMUsageCost = (\n  model: DBSSchema[\"llm_models\"],\n  meta:\n    | { type: \"OpenAI\"; meta: Pick<OpenAIChatCompletionResponse, \"usage\"> }\n    | {\n        type: \"Gemini\";\n        meta: Pick<GoogleGeminiChatCompletionResponse, \"usageMetadata\">;\n      }\n    | {\n        type: \"Anthropic\";\n        meta: Pick<AnthropicChatCompletionResponse, \"usage\">;\n      },\n) => {\n  const cacheReadTokens =\n    meta.type === \"OpenAI\" ?\n      (meta.meta.usage?.prompt_tokens_details?.cached_tokens ?? 0)\n    : meta.type === \"Anthropic\" ? meta.meta.usage.cache_read_input_tokens\n    : 0;\n  const cacheWriteTokens =\n    meta.type === \"Anthropic\" ? meta.meta.usage.cache_creation_input_tokens : 0;\n\n  const inputTokens =\n    meta.type === \"Gemini\" ? meta.meta.usageMetadata.promptTokenCount\n      /**\n       * https://community.openai.com/t/will-cached-prompt-be-charged-in-each-api-call/977999/2\n       */\n    : meta.type === \"OpenAI\" ?\n      (meta.meta.usage?.prompt_tokens ?? 0) - cacheReadTokens\n    : meta.meta.usage.input_tokens;\n  const outputTokens =\n    meta.type === \"Gemini\" ? meta.meta.usageMetadata.candidatesTokenCount\n    : meta.type === \"OpenAI\" ? (meta.meta.usage?.completion_tokens ?? 0)\n    : meta.meta.usage.output_tokens;\n\n  return getLlmMessageCost(model, {\n    cacheReadTokens,\n    cacheWriteTokens,\n    inputTokens,\n    outputTokens,\n  });\n};\n\nexport const getLlmMessageCost = (\n  model: DBSSchema[\"llm_models\"],\n  message: {\n    cacheReadTokens: number;\n    cacheWriteTokens: number;\n    inputTokens: number;\n    outputTokens: number;\n  },\n) => {\n  if (!model.pricing_info) return;\n  const {\n    input,\n    output,\n    threshold,\n    cachedInput = 0,\n    cachedOutput = 0,\n  } = model.pricing_info;\n\n  const { cacheReadTokens, cacheWriteTokens, inputTokens, outputTokens } =\n    message;\n\n  const inputPrice =\n    threshold && inputTokens > threshold.tokenLimit ? threshold.input : input;\n  const outputPrice =\n    threshold && outputTokens > threshold.tokenLimit ?\n      threshold.output\n    : output;\n\n  const cachePrice =\n    (cacheReadTokens / 1e6) * cachedInput +\n    (cacheWriteTokens / 1e6) * cachedOutput;\n\n  return (\n    cachePrice +\n    (inputPrice / 1e6) * inputTokens +\n    (outputPrice / 1e6) * outputTokens\n  );\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/getUserMessageCost.ts",
    "content": "import type { DBSSchema } from \"@common/publishUtils\";\nimport type { LLMMessage } from \"./askLLM\";\nimport { getLlmMessageCost } from \"./getLLMUsageCost\";\n\nconst TOKENS_PER_CHARACTER = 0.25;\n\nexport const getUserMessageCost = (\n  userMessage: LLMMessage,\n  model: DBSSchema[\"llm_models\"],\n): number => {\n  const messageTextLength = JSON.stringify(userMessage).length;\n\n  return (\n    getLlmMessageCost(model, {\n      cacheReadTokens: 0,\n      cacheWriteTokens: 0,\n      inputTokens: messageTextLength * TOKENS_PER_CHARACTER,\n      outputTokens: 0,\n    }) ?? 0\n  );\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/parseLLMResponseObject.ts",
    "content": "import {\n  type AnyObject,\n  getSerialisableError,\n  isDefined,\n  omitKeys,\n} from \"prostgles-types\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { LLMMessageWithRole } from \"./fetchLLMResponse\";\nimport { getLLMUsageCost } from \"./getLLMUsageCost\";\nimport type {\n  AnthropicChatCompletionResponse,\n  GoogleGeminiChatCompletionResponse,\n  OpenAIChatCompletionResponse,\n} from \"./LLMResponseTypes\";\n\nexport type LLMResponseParser<T = AnyObject> = (args: {\n  provider: string;\n  responseData: T;\n  model: DBSSchema[\"llm_models\"];\n}) => LLMParsedResponse;\n\nexport type LLMParsedResponse = Pick<LLMMessageWithRole, \"content\"> & {\n  meta?: AnyObject | null;\n  cost: number | undefined;\n};\n\nexport const parseLLMResponseObject: LLMResponseParser = ({\n  provider,\n  responseData,\n  model,\n}) => {\n  if (provider === \"Google\") {\n    const { candidates, ...meta } =\n      responseData as GoogleGeminiChatCompletionResponse;\n    const content = candidates.flatMap((c) => {\n      return c.content.parts.map((p) => {\n        if (\"text\" in p) {\n          return {\n            type: \"text\",\n            text: p.text,\n          } satisfies LLMMessageWithRole[\"content\"][number];\n        }\n        if (\"functionCall\" in p) {\n          return {\n            type: \"tool_use\",\n            id: `${Date.now()}-${Math.random()}`,\n            name: p.functionCall.name,\n            input: p.functionCall.args,\n          } satisfies LLMMessageWithRole[\"content\"][number];\n        }\n        return {\n          type: \"text\",\n          text: \"INTERNAL ERROR: Unexpected response from LLM\",\n        } satisfies LLMMessageWithRole[\"content\"][number];\n      });\n    });\n    return {\n      content,\n      meta: {\n        ...meta,\n        finishReason: candidates[0]?.finishReason,\n      },\n      cost: getLLMUsageCost(model, { type: \"Gemini\", meta }),\n    };\n  }\n  if (provider === \"Anthropic\") {\n    const { content: rawContent, ...meta } =\n      responseData as AnthropicChatCompletionResponse;\n    const content = rawContent\n      .map((c) => {\n        const contentItem: LLMMessageWithRole[\"content\"][number] | undefined =\n          c.type === \"text\" && c.text ?\n            {\n              type: \"text\",\n              text: c.text,\n            }\n          : c.type === \"tool_use\" ?\n            ({\n              type: \"tool_use\",\n              id: c.id,\n              name: c.name,\n              input: c.input,\n            } satisfies LLMMessageWithRole[\"content\"][number])\n          : undefined;\n\n        return contentItem;\n      })\n      .filter(isDefined);\n    return {\n      content,\n      meta,\n      cost: getLLMUsageCost(model, { type: \"Anthropic\", meta }),\n    };\n  } else if (\n    provider === \"OpenAI\" ||\n    provider === \"OpenRouter\" ||\n    provider === \"Ollama\" ||\n    provider === \"Prostgles\"\n  ) {\n    const { choices, ...meta } = responseData as OpenAIChatCompletionResponse;\n    const content: LLMMessageWithRole[\"content\"] = choices\n      .flatMap((c) => {\n        const toolCalls =\n          c.message.tool_calls?.map((toolCall) => {\n            let input: unknown = {};\n            if (toolCall.function.arguments) {\n              try {\n                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n                input = JSON.parse(toolCall.function.arguments);\n              } catch (_e) {\n                const error = new Error(\n                  `Could not parse tool arguments as JSON: ${toolCall.function.arguments}. ` +\n                    JSON.stringify(getSerialisableError(_e)),\n                );\n                error.name = \"ToolArgumentsParsingError\";\n                throw error;\n              }\n            }\n\n            return {\n              type: \"tool_use\",\n              id: toolCall.id,\n              name: toolCall.function.name,\n              input,\n            } satisfies LLMMessageWithRole[\"content\"][number];\n          }) ?? [];\n        return [\n          typeof c.message.content === \"string\" ?\n            ({\n              type: \"text\",\n              text: c.message.content,\n              reasoning: c.message.reasoning || undefined,\n            } satisfies LLMMessageWithRole[\"content\"][number])\n          : undefined,\n          c.error ?\n            ({\n              type: \"text\",\n              text: `🔴 Something went wrong! Error received from from LLM Provider: \\n\\`\\`\\`json\\n${JSON.stringify(c.error, null, 2)}\\n\\`\\`\\``,\n            } satisfies LLMMessageWithRole[\"content\"][number])\n          : undefined,\n          ...toolCalls,\n        ];\n      })\n      .filter(isDefined);\n    const metaCost = meta.usage?.cost;\n    return {\n      content,\n      meta: {\n        ...meta,\n        finish_reason: choices[0]?.finish_reason,\n      },\n      cost:\n        Number.isFinite(metaCost) ? metaCost : (\n          getLLMUsageCost(model, { type: \"OpenAI\", meta })\n        ),\n    };\n  } else {\n    const path = [\"choices\", 0, \"message\", \"content\"];\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n    const messageText = parsePath(responseData, path);\n    if (typeof messageText !== \"string\") {\n      throw \"Unexpected response from LLM. Expecting string\";\n    }\n    const firstPathItem = path[0];\n    const meta =\n      !isDefined(firstPathItem) ? null\n      : Array.isArray(responseData) ?\n        responseData.filter((_, i) => i !== firstPathItem)\n      : omitKeys(responseData, [firstPathItem.toString()]);\n    return {\n      content: [{ type: \"text\", text: messageText }],\n      meta,\n      cost: undefined,\n    };\n  }\n};\n\nconst parsePath = (obj: AnyObject, path: (string | number)[]): any => {\n  let val: AnyObject | undefined = obj;\n  for (const key of path) {\n    if (val === undefined) return undefined;\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n    val = val[key];\n  }\n  return val;\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/prostglesLLMTools/getMCPServerTools.ts",
    "content": "import { getMCPFullToolName } from \"@common/prostglesMcp\";\nimport type { DBS } from \"@src/index\";\nimport type { MCPToolSchema } from \"../getLLMToolsAllowedInThisChat\";\n\nexport const getMCPServerTools = async (\n  dbs: DBS,\n  filter: Parameters<typeof dbs.mcp_server_tools.find>[0],\n) => {\n  const mcp_server_tools = await dbs.mcp_server_tools.find(filter);\n  const mcpTools = mcp_server_tools.map((t) => {\n    return {\n      id: t.id,\n      tool_name: t.name,\n      server_name: t.server_name,\n      name: getMCPFullToolName(t.server_name, t.name),\n      description: t.description,\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n      input_schema: t.inputSchema,\n    } satisfies MCPToolSchema & {\n      id: number;\n      tool_name: string;\n      server_name: string;\n    };\n  });\n  return { mcpTools, mcp_server_tools };\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/prostglesLLMTools/getProstglesDBTools.ts",
    "content": "import { isDefined, type JSONB } from \"prostgles-types\";\nimport {\n  getProstglesMCPFullToolName,\n  PROSTGLES_MCP_SERVERS_AND_TOOLS,\n  type ProstglesMcpTool,\n} from \"@common/prostglesMcp\";\nimport { getEntries } from \"@common/utils\";\nimport type { ChatPermissions } from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/dockerMCPServerProxy/dockerMCPServerProxy\";\n\nexport type DBTool = Extract<ProstglesMcpTool, { type: \"prostgles-db\" }> & {\n  name: string;\n  description: string;\n  auto_approve: boolean;\n  schema: JSONB.ObjectType;\n};\n\nexport const getProstglesDBTools = (\n  chat: ChatPermissions | undefined,\n): DBTool[] => {\n  const chatDBAccess = chat?.db_data_permissions;\n  if (!chatDBAccess || chatDBAccess.Mode === \"None\") {\n    return [];\n  }\n  if (chatDBAccess.Mode === \"Custom\") {\n    const allowedCommands: Map<string, true> = new Map();\n    chatDBAccess.tables.forEach((tableRule) => {\n      for (const actionName of COMMANDS) {\n        if (tableRule[actionName]) {\n          allowedCommands.set(actionName, true);\n          if (allowedCommands.size === COMMANDS.length) {\n            break;\n          }\n        }\n      }\n    });\n    const tableTools = getEntries(\n      PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-db\"],\n    )\n      .map(([toolName, { description, schema }]) => {\n        if (!allowedCommands.has(toolName)) return;\n        return {\n          name: getProstglesMCPFullToolName(\"prostgles-db\", toolName),\n          type: \"prostgles-db\",\n          tool_name: toolName,\n          description,\n          auto_approve: Boolean(chatDBAccess.auto_approve),\n          schema,\n        } satisfies DBTool;\n      })\n      .filter(isDefined);\n    return Object.values(tableTools);\n  }\n\n  const sqlTools = getEntries(PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-db\"])\n    .map(([toolName, { description, schema }]) => {\n      if (\n        toolName === \"execute_sql_with_rollback\" ||\n        toolName === \"execute_sql_with_commit\"\n      ) {\n        const tool: DBTool = {\n          name: getProstglesMCPFullToolName(\"prostgles-db\", toolName),\n          type: \"prostgles-db\",\n          tool_name: toolName,\n          description,\n          auto_approve: Boolean(chatDBAccess.auto_approve),\n          schema,\n        };\n        const isAllowed =\n          chatDBAccess.Mode === \"Run commited SQL\" ||\n          toolName === \"execute_sql_with_rollback\";\n        if (isAllowed) {\n          return tool;\n        }\n      }\n    })\n    .filter(isDefined);\n\n  return sqlTools;\n};\nconst COMMANDS = [\"select\", \"update\", \"insert\", \"delete\"] as const;\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/prostglesLLMTools/getProstglesLLMTools.ts",
    "content": "import { getJSONBSchemaAsJSONSchema, isDefined } from \"prostgles-types\";\n\nimport {\n  getMCPFullToolName,\n  getMCPToolNameParts,\n  getProstglesMCPFullToolName,\n  type AllowedChatTool,\n} from \"@common/prostglesMcp\";\n\nimport { getProstglesMcpHub } from \"@src/McpHub/ProstglesMcpHub/ProstglesMcpHub\";\nimport type { AuthClientRequest } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport {\n  type GetLLMToolsArgs,\n  type MCPToolSchema,\n} from \"../getLLMToolsAllowedInThisChat\";\nimport { getMCPServerTools } from \"./getMCPServerTools\";\nimport { getProstglesDBTools } from \"./getProstglesDBTools\";\nimport { getPublishedMethodsTools } from \"./getPublishedMethodsTools\";\nimport {\n  getAddTaskTools,\n  getAddWorkflowTools,\n  suggestDashboardsTool,\n} from \"./prostglesMcpTools\";\n\nexport const getProstglesLLMTools = async ({\n  userType,\n  dbs,\n  chat,\n  prompt,\n  mcpToolsWithInfo,\n  connectionId,\n  clientReq,\n}: Omit<GetLLMToolsArgs, \"connectionId\"> & {\n  mcpToolsWithInfo: {\n    input_schema: any;\n    description: string;\n    auto_approve: boolean;\n    chat_id: number;\n    tool_id: number;\n    name: `${string}--${string}`;\n    type: \"mcp\";\n  }[];\n  connectionId: string;\n  clientReq: AuthClientRequest;\n}) => {\n  const isAdmin = userType === \"admin\";\n  const { prompt_type } = prompt.options ?? {};\n\n  let taskTool:\n    | {\n        tool_name: \"suggest_tools_and_prompt\" | \"suggest_agent_workflow\";\n        mcpSchema: MCPToolSchema;\n      }\n    | undefined = undefined;\n  if (prompt_type === \"tasks\" || prompt_type === \"agent_workflow\") {\n    if (!isAdmin) {\n      throw new Error(\"Only admins can use task creation tools\");\n    }\n    const { published_methods } = await getPublishedMethodsTools(dbs, {\n      chatId: chat.id,\n      connectionId,\n    });\n    const { mcp_server_tools } = await getMCPServerTools(dbs, {});\n    const [tool_name, getMCPSchema] =\n      prompt_type === \"tasks\" ?\n        ([\"suggest_tools_and_prompt\", getAddTaskTools] as const)\n      : ([\"suggest_agent_workflow\", getAddWorkflowTools] as const);\n\n    const mcpSchema = getMCPSchema({\n      availableMCPTools: mcp_server_tools.map((t) => ({\n        ...t,\n        name: getMCPFullToolName(t.server_name, t.name),\n      })),\n      availableDBTools: published_methods.map((t) => ({\n        ...t,\n        name: getProstglesMCPFullToolName(\"prostgles-db-methods\", t.name),\n      })),\n    });\n    taskTool = {\n      tool_name,\n      mcpSchema,\n    };\n  }\n\n  const dbTools = getProstglesDBTools(chat).map((tool) => {\n    return {\n      ...tool,\n      server_name: tool.type,\n      input_schema: getJSONBSchemaAsJSONSchema(\"\", \"\", tool.schema),\n    } satisfies AllowedChatTool;\n  });\n\n  const uiTools: AllowedChatTool[] = [\n    prompt_type === \"dashboards\" ?\n      ({\n        ...suggestDashboardsTool,\n        auto_approve: true,\n        type: \"prostgles-ui\",\n        server_name: \"prostgles-ui\",\n        tool_name: \"suggest_dashboards\",\n      } satisfies AllowedChatTool)\n    : undefined,\n    taskTool &&\n      ({\n        ...taskTool.mcpSchema,\n        auto_approve: true,\n        type: \"prostgles-ui\",\n        server_name: \"prostgles-ui\",\n        tool_name: taskTool.tool_name,\n      } satisfies AllowedChatTool),\n  ].filter(isDefined);\n  const prostglesDBTools: AllowedChatTool[] = [...dbTools, ...uiTools].filter(\n    isDefined,\n  );\n\n  const prostglesMCPHub = await getProstglesMcpHub(dbs);\n  const prostglesMCPTools = await Promise.all(\n    mcpToolsWithInfo\n      .map(async (tool) => {\n        const toolNameParts = getMCPToolNameParts(tool.name);\n        const prostglesMcpServer =\n          toolNameParts &&\n          prostglesMCPHub.getServer(toolNameParts.serverName).server;\n        if (toolNameParts && prostglesMcpServer) {\n          const prostglesMCPTools = await prostglesMcpServer.fetchTools(dbs, {\n            chat_id: chat.id,\n            user_id: chat.user_id,\n            clientReq,\n          });\n          const matchingTool = prostglesMCPTools.find(\n            (ts) => ts.name === toolNameParts.toolName,\n          );\n          if (!matchingTool) {\n            throw new Error(`Tool ${tool.name} not found in Docker MCP tools`);\n          }\n          return {\n            ...tool,\n            input_schema: matchingTool.inputSchema,\n            description: matchingTool.description,\n          };\n        }\n        return tool;\n      })\n      .filter(isDefined),\n  );\n\n  return { prostglesMCPTools, prostglesDBTools };\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/prostglesLLMTools/getPublishedMethodsTools.ts",
    "content": "import { getProstglesMCPFullToolName } from \"@common/prostglesMcp\";\nimport type { DBS } from \"@src/index\";\nimport { getJSONBSchemaAsJSONSchema, type JSONB } from \"prostgles-types\";\nimport type { MCPToolSchema } from \"../getLLMToolsAllowedInThisChat\";\n\nexport const getPublishedMethodsTools = async (\n  dbs: DBS,\n  { chatId, connectionId }: { chatId: number; connectionId: string },\n) => {\n  const published_methods = await dbs.published_methods.find({\n    connection_id: connectionId,\n    $existsJoined: {\n      llm_chats_allowed_functions: {\n        chat_id: chatId,\n      },\n    },\n  });\n  const serverSideFuncTools = published_methods.map((m) => {\n    const { name, description, arguments: _arguments, id } = m;\n    const properties = _arguments.reduce(\n      (acc, arg) => ({\n        ...acc,\n        [arg.name]:\n          (\n            arg.type === \"JsonbSchema\" ||\n            arg.type === \"Lookup\" ||\n            arg.type === \"Lookup[]\"\n          ) ?\n            \"any\"\n          : arg.type,\n      }),\n      {} as JSONB.ObjectType[\"type\"],\n    );\n    return {\n      id,\n      tool_name: name,\n      name: getProstglesMCPFullToolName(\"prostgles-db-methods\", name),\n      description,\n      input_schema: getJSONBSchemaAsJSONSchema(\n        \"published_methods\",\n        \"arguments\",\n        {\n          type: properties,\n        },\n      ),\n    } satisfies MCPToolSchema & { id: number; tool_name: string };\n  });\n  return { serverSideFuncTools, published_methods };\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/prostglesLLMTools/prostglesMcpTools.ts",
    "content": "import {\n  getJSONBSchemaAsJSONSchema,\n  getJSONBSchemaTSTypes,\n} from \"prostgles-types\";\nimport { dashboardTypesContent } from \"@common/dashboardTypesContent\";\nimport {\n  getProstglesMCPFullToolName,\n  PROSTGLES_MCP_SERVERS_AND_TOOLS,\n} from \"@common/prostglesMcp\";\nimport { fixIndent } from \"@common/utils\";\n\nexport const executeSQLToolWithRollback = {\n  name: getProstglesMCPFullToolName(\n    \"prostgles-db\",\n    \"execute_sql_with_rollback\",\n  ),\n  description:\n    PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-db\"][\"execute_sql_with_rollback\"]\n      .description,\n  input_schema: getJSONBSchemaAsJSONSchema(\n    \"\",\n    \"\",\n    PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-db\"][\"execute_sql_with_rollback\"]\n      .schema,\n  ),\n};\n\nexport const executeSQLToolWithCommit = {\n  name: getProstglesMCPFullToolName(\"prostgles-db\", \"execute_sql_with_commit\"),\n  description:\n    PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-db\"][\"execute_sql_with_commit\"]\n      .description,\n  input_schema: getJSONBSchemaAsJSONSchema(\n    \"\",\n    \"\",\n    PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-db\"][\"execute_sql_with_commit\"]\n      .schema,\n  ),\n};\n\nconst taskToolName = \"suggest_tools_and_prompt\" as const;\nexport const getAddTaskTools = ({\n  availableDBTools = [],\n  availableMCPTools = [],\n}: {\n  availableMCPTools?: { name: string; description: string }[];\n  availableDBTools?: { name: string; description: string }[];\n} = {}) => ({\n  name: getProstglesMCPFullToolName(\"prostgles-ui\", taskToolName),\n  description: fixIndent(`\n    This tool will update the user chat context with suggests tools and prompt.\n    The input will be shown to the user for confirmation.\n    \n    Available MCP tools: \n    ${!availableMCPTools.length ? \"None\" : availableMCPTools.map((t) => `  - ${t.name}: ${t.description}`).join(\"\\n\")}\n\n    Available database tools:\n    ${!availableDBTools.length ? \"None\" : availableDBTools.map((t) => `  - ${t.name}: ${t.description}`).join(\"\\n\")}\n\n    If access to the database is needed, an access type can be specified. \n    Use the most restrictive access type that is needed to complete the task (type custom with specific tables and allowed commands).\n\n    This tool input_schema must satisfy this typescript type:\n    \\`\\`\\`typescript\n    ${getJSONBSchemaTSTypes(\n      PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-ui\"][taskToolName].schema,\n      {},\n      undefined,\n      [],\n    )}\n    \\`\\`\\`\n  `),\n  input_schema: {\n    description: getJSONBSchemaTSTypes(\n      PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-ui\"][taskToolName].schema,\n      {},\n      undefined,\n      [],\n    ),\n  },\n});\n\nconst workflowToolName = \"suggest_agent_workflow\" as const;\nexport const getAddWorkflowTools = ({\n  availableDBTools = [],\n  availableMCPTools = [],\n}: {\n  availableMCPTools?: { name: string; description: string }[];\n  availableDBTools?: { name: string; description: string }[];\n} = {}) => ({\n  name: getProstglesMCPFullToolName(\"prostgles-ui\", workflowToolName),\n  description: fixIndent(`\n    This tool will allow the user to create and start an agent workflow with suggested tools and prompt.\n    The input will be shown to the user for confirmation.\n    \n    ## Available MCP tools: \n    ${!availableMCPTools.length ? \"None\" : availableMCPTools.map((t) => JSON.stringify(t.name)).join(\", \")}\n\n    ## Available database tools:\n    ${!availableDBTools.length ? \"None\" : availableDBTools.map((t) => JSON.stringify(t.name)).join(\", \")}\n\n    ## Database Access\n    If access to the database is needed, an access type can be specified. \n    Use the most restrictive access type that is needed to complete the task (type custom with specific tables and allowed commands).\n\n    Provide a json input for this tool that satisfies this typescript type:\n    \\`\\`\\`typescript\n    ${getJSONBSchemaTSTypes(\n      PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-ui\"][workflowToolName].schema,\n      {},\n      undefined,\n      [],\n    )}\n    \\`\\`\\`\n\n    Input tool schema details: \n    \\`\\`\\`json\n    ${JSON.stringify(PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-ui\"][workflowToolName].schema, null, 2)}\n    \\`\\`\\`\n  `),\n  input_schema: {},\n});\n\nexport const suggestDashboardsTool = {\n  name: getProstglesMCPFullToolName(\"prostgles-ui\", \"suggest_dashboards\"),\n  description: [\n    \"Suggests dashboards based on the provided task description\",\n\n    \"\",\n    \"Using dashboard structure below create workspaces with useful views my current schema.\",\n    \"Return a json of this format: `{ prostglesWorkspaces: WorkspaceInsertModel[] }`\",\n    \"Do not return more than 3 workspaces, each with no more than 5 views.\",\n    \"\",\n    \"```typescript\",\n    dashboardTypesContent,\n    \"```\",\n  ].join(\"\\n\"),\n  input_schema: getJSONBSchemaAsJSONSchema(\n    \"\",\n    \"\",\n    PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-ui\"][\"suggest_dashboards\"]\n      .schema,\n  ),\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/prostglesLLMTools/runProstglesDBTool.ts",
    "content": "import { PROSTGLES_MCP_SERVERS_AND_TOOLS } from \"@common/prostglesMcp\";\nimport type { AuthClientRequest } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport {\n  getJSONBObjectSchemaValidationError,\n  type JSONB,\n  type TableHandler,\n} from \"prostgles-types\";\nimport { connMgr } from \"../../../index\";\nimport { getProstglesDBTools } from \"./getProstglesDBTools\";\nimport type { ChatPermissions } from \"@src/McpHub/ProstglesMcpHub/ProstglesMCPServers/DockerSandbox/dockerMCPServerProxy/dockerMCPServerProxy\";\nexport const runProstglesDBTool = async (\n  chat: ChatPermissions,\n  clientReq: AuthClientRequest,\n  args: unknown,\n  name: string,\n) => {\n  const tools = getProstglesDBTools(chat);\n  const tool = tools.find((t) => t.tool_name === name);\n  if (!tool) {\n    throw new Error(`Tool \"${name}\" not found`);\n  }\n\n  const { clientDb } = await getClientDBHandlersForChat(chat, clientReq);\n\n  type DbToolsInfo = (typeof PROSTGLES_MCP_SERVERS_AND_TOOLS)[\"prostgles-db\"];\n\n  const validatedInput = getJSONBObjectSchemaValidationError(\n    tool.schema.type,\n    args,\n    \"\",\n  );\n  if (validatedInput.error !== undefined) {\n    throw new Error(`Input validation error: ${validatedInput.error}`);\n  }\n  const { data: validatedData } = validatedInput;\n  if (\n    tool.tool_name === \"execute_sql_with_commit\" ||\n    tool.tool_name === \"execute_sql_with_rollback\"\n  ) {\n    const {\n      sql,\n      query_timeout = 30,\n      query_params,\n    } = validatedData as unknown as JSONB.GetObjectType<\n      DbToolsInfo[\"execute_sql_with_commit\"][\"schema\"][\"type\"]\n    >;\n\n    if (!(clientDb.sql as unknown)) {\n      throw new Error(\"Executing SQL not allowed to this user\");\n    }\n\n    const queryWithTimeout =\n      query_timeout && Number.isInteger(query_timeout) ?\n        [`SET LOCAL statement_timeout to '${query_timeout}s'`, sql].join(\";\\n\")\n      : sql;\n    const result = await clientDb.sql(queryWithTimeout, query_params, {\n      returnType:\n        tool.tool_name === \"execute_sql_with_rollback\" ?\n          \"default-with-rollback\"\n        : \"rows\",\n    });\n    if (tool.tool_name === \"execute_sql_with_commit\") {\n      return result;\n    } else {\n      return result.rows;\n    }\n  }\n\n  const getTableHandler = (tableName: string) => {\n    const tableHandler = clientDb[tableName] as TableHandler | undefined;\n    if (!tableHandler) {\n      throw new Error(\n        `Table \"${tableName}\" is invalid or not allowed to the user`,\n      );\n    }\n\n    return tableHandler;\n  };\n\n  if (tool.tool_name === \"select\") {\n    //@ts-ignore\n    const { tableName, filter, limit } = validatedData as JSONB.GetObjectType<\n      DbToolsInfo[typeof tool.tool_name][\"schema\"][\"type\"]\n    >;\n    const tableHandler = getTableHandler(tableName);\n    return tableHandler.find(filter, { limit });\n  } else if (tool.tool_name === \"insert\") {\n    const { tableName, data } = validatedData as JSONB.GetObjectType<\n      DbToolsInfo[typeof tool.tool_name][\"schema\"][\"type\"]\n    >;\n    const tableHandler = getTableHandler(tableName);\n    const rows = await tableHandler.insert(data, { returning: \"*\" });\n    return `rows inserted: ${rows.length}`;\n  } else if (tool.tool_name === \"update\") {\n    const { tableName, data, filter } = validatedData as JSONB.GetObjectType<\n      DbToolsInfo[typeof tool.tool_name][\"schema\"][\"type\"]\n    >;\n    const tableHandler = getTableHandler(tableName);\n    const rows = await tableHandler.update(filter, data, { returning: \"*\" });\n    return `rows updated: ${rows?.length ?? 0}`;\n  } else {\n    const { tableName, filter } = validatedData as JSONB.GetObjectType<\n      DbToolsInfo[typeof tool.tool_name][\"schema\"][\"type\"]\n    >;\n    const tableHandler = getTableHandler(tableName);\n    const rows = await tableHandler.delete(filter, { returning: \"*\" });\n    return `rows deleted: ${rows?.length ?? 0}`;\n  }\n};\n\nexport const getClientDBHandlersForChat = async (\n  chat: ChatPermissions,\n  clientReq: AuthClientRequest,\n) => {\n  const chatDBPermissions = chat.db_data_permissions;\n  const { connection_id } = chat;\n  if (!connection_id) {\n    throw new Error(\"Chat does not have a connection_id\");\n  }\n  const tables =\n    chatDBPermissions?.Mode === \"Custom\" ?\n      Object.fromEntries(\n        chatDBPermissions.tables.map(({ tableName, ...rules }) => [\n          tableName,\n          rules,\n        ]),\n      )\n    : undefined;\n  const connection = connMgr.getConnection(connection_id);\n  const handlers = await connection.prgl.getClientDBHandlers(clientReq, {\n    tables,\n    sql:\n      chatDBPermissions?.Mode === \"Run commited SQL\" ? \"commited\"\n      : chatDBPermissions?.Mode === \"Run readonly SQL\" ? \"rolledback\"\n      : undefined,\n  });\n  return handlers;\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/readFetchStream.ts",
    "content": "export const readFetchStream = async (response: Response) => {\n  const contentType = response.headers.get(\"content-type\");\n\n  /**\n   * Openrouter sends chunked responses\n   */\n  const isStream = response.headers.get(\"transfer-encoding\") === \"chunked\";\n  if (!isStream && contentType?.includes(\"application/json\")) {\n    const result = await response.json();\n    return result;\n  }\n  const reader = response.body?.getReader();\n  if (!reader && !isStream) {\n    // If direct JSON parsing fails, clone the response\n    const responseClone = response.clone();\n\n    try {\n      return await response.json();\n    } catch (jsonError) {\n      // Read as text and try to parse manually\n      const text = await responseClone.text();\n      try {\n        return JSON.parse(text);\n      } catch (parseError: any) {\n        throw new Error(text);\n      }\n    }\n  }\n  if (!reader) throw new Error(\"No reader\");\n  const decoder = new TextDecoder();\n  let buffer = \"\";\n  try {\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition\n    while (true) {\n      const { done, value } = await reader.read();\n      if (done) break;\n      const decodedChunk = decoder.decode(value, { stream: true });\n      buffer += decodedChunk;\n    }\n  } finally {\n    await reader.cancel();\n  }\n\n  if (\n    response.headers.get(\"content-type\")?.includes(\"text/html\") &&\n    buffer.trim().startsWith(\"<\")\n  ) {\n    return {\n      error: \"HTML response from LLM\",\n      statusCode: response.status,\n      html: buffer,\n    };\n  }\n  return JSON.parse(buffer);\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/refreshModels.ts",
    "content": "import { isDefined } from \"prostgles-types\";\nimport type { DBS } from \"../..\";\nimport type { DBSSchemaForInsert } from \"@common/publishUtils\";\n\nexport const refreshModels = async (dbs: DBS) => {\n  /**\n   * https://openrouter.ai/docs/overview/models\n   */\n  const models: ModelInfo[] = (await fetch(\n    \"https://openrouter.ai/api/v1/models\",\n  )\n    .then((res) => res.json() as Promise<{ data: ModelInfo[] }>)\n    .then(({ data }) => data)\n    .catch((err) => {\n      console.error(\"Failed to fetch models:\", err);\n      return [];\n    })) as [];\n\n  const insertData = models\n    .map((m) => {\n      const provider_id =\n        LLM_PROVIDERS.find(\n          (p) => p.toLowerCase() === m.canonical_slug.split(\"/\")[0],\n        ) || \"OpenRouter\";\n\n      const { prompt, completion, input_cache_read, input_cache_write } =\n        m.pricing;\n      return {\n        name: m.canonical_slug,\n        pricing_info: {\n          input: Number(prompt || \"0\") * 1e6,\n          output: Number(completion || \"0\") * 1e6,\n          cachedInput: Number(input_cache_read || \"0\") * 1e6,\n          cachedOutput: Number(input_cache_write || \"0\") * 1e6,\n          // No threshold pricing info available from OpenRouter\n        },\n        architecture: m.architecture,\n        supported_parameters: m.supported_parameters,\n        context_length: m.context_length,\n        mcp_tool_support: m.supported_parameters.includes(\"tools\"),\n        provider_id,\n      } satisfies DBSSchemaForInsert[\"llm_models\"];\n    })\n    .filter(isDefined)\n    /** Remove duplicates */\n    .reduce(\n      (acc, model) => {\n        const existingModel = acc.find((m) => m.name === model.name);\n        if (!existingModel) {\n          acc.push(model);\n        }\n        return acc;\n      },\n      [] as DBSSchemaForInsert[\"llm_models\"][],\n    );\n\n  await dbs.tx(async (dbTx) => {\n    const existingModels = await dbTx.llm_models.find();\n    const nonOpenRouterModels = insertData\n      .filter((m) => m.provider_id !== \"OpenRouter\")\n      .map((m) => ({\n        ...m,\n        name: m.name.split(\"/\")[1] || m.name,\n      }));\n\n    const newModels = [\n      ...nonOpenRouterModels,\n      ...insertData.map((d) => ({\n        ...d,\n        provider_id: \"OpenRouter\",\n      })),\n    ].filter(\n      (m) =>\n        !existingModels.some(\n          (em) => em.name === m.name && em.provider_id === m.provider_id,\n        ),\n    );\n    if (newModels.length) {\n      await dbTx.llm_models.insert(newModels, { onConflict: \"DoNothing\" });\n    }\n  });\n};\n\nconst LLM_PROVIDERS = [\"OpenAI\", \"Anthropic\", \"Google\"];\n\ntype ModelInfo = {\n  id: string;\n  canonical_slug: string; // Permanent slug for the model that never changes\n  hugging_face_id: string | null;\n  name: string;\n  created: number; // Unix timestamp of when the model was added to OpenRouter\n  description: string;\n  context_length: number; // Maximum context window size in tokens\n  architecture: {\n    modality: string; // Input modality (e.g., \"text+image->text\")\n    input_modalities: string[]; // Supported input types: [\"file\", \"image\", \"text\"]\n    output_modalities: string[]; // Supported output types: [\"text\"]\n    tokenizer: string; // Tokenization method used\n    instruct_type: string | null; // Instruction format type (null if not applicable)\n  };\n  pricing: {\n    prompt: string; // Cost per input token\n    completion: string; // Cost per output token\n    request: string; // Fixed cost per API request\n    image: string; // Cost per image input\n    web_search: string; // Cost per web search operation\n    internal_reasoning: string; // Cost for internal reasoning tokens\n    input_cache_read: string; // Cost per cached input token read\n    input_cache_write: string; // Cost per cached input token write\n  };\n  top_provider: {\n    context_length: number; // Provider-specific context limit\n    max_completion_tokens: number; // Maximum tokens in response\n    is_moderated: boolean; // Whether content moderation is applied\n  };\n  per_request_limits: string | null;\n  supported_parameters: string[]; // Array of supported API parameters for this model\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/runApprovedTools/runApprovedTools.ts",
    "content": "import {\n  getMCPToolNameParts,\n  PROSTGLES_MCP_SERVERS_AND_TOOLS,\n  type AllowedChatTool,\n} from \"@common/prostglesMcp\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport type { AuthClientRequest } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport {\n  getJSONBObjectSchemaValidationError,\n  getSerialisableError,\n} from \"prostgles-types\";\nimport { callMCPServerTool } from \"../../../McpHub/callMCPServerTool\";\nimport { askLLM, type AskLLMArgs, type LLMMessage } from \"../askLLM\";\nimport {\n  getAllToolNames,\n  type getLLMToolsAllowedInThisChat,\n} from \"../getLLMToolsAllowedInThisChat\";\nimport {\n  getClientDBHandlersForChat,\n  runProstglesDBTool,\n} from \"../prostglesLLMTools/runProstglesDBTool\";\nimport { validateLastMessageToolUseRequests } from \"./validateLastMessageToolUseRequests\";\n\nexport type ToolUseMessage = Extract<LLMMessage[number], { type: \"tool_use\" }>;\ntype ToolUseMessageWithInfo =\n  | (ToolUseMessage & {\n      tool: AllowedChatTool;\n      state: \"approved\";\n    })\n  | (ToolUseMessage & {\n      tool: AllowedChatTool;\n      state: \"needs-approval\";\n    })\n  | (ToolUseMessage & {\n      tool: AllowedChatTool;\n      state: \"denied\";\n    })\n  | (ToolUseMessage & {\n      tool: undefined;\n      state: \"tool-missing\";\n    });\ntype ToolResultMessage = Extract<LLMMessage[number], { type: \"tool_result\" }>;\n\nexport const runApprovedTools = async (\n  allowedTools: Awaited<ReturnType<typeof getLLMToolsAllowedInThisChat>>,\n  args: Omit<AskLLMArgs, \"userMessage\" | \"type\">,\n  chat: DBSSchema[\"llm_chats\"],\n  toolUseRequestMessages: ToolUseMessage[],\n  userApprovals: LLMMessage | undefined,\n  aborter: AbortController,\n  clientReq: AuthClientRequest,\n) => {\n  const { user, chatId, dbs } = args;\n  if (!toolUseRequestMessages.length) {\n    return;\n  }\n\n  /**\n   * Here we expect the user to return a list of approved tools. Anything not in this list that is not auto-approved means denied.\n   */\n  if (userApprovals) {\n    validateLastMessageToolUseRequests({\n      toolUseMessages: toolUseRequestMessages,\n      userToolUseApprovals: userApprovals,\n    });\n  }\n  const toolUseRequests = toolUseRequestMessages.map((toolUse) => {\n    const tool = allowedTools?.find((t) => t.name === toolUse.name);\n    if (!tool) {\n      return {\n        ...toolUse,\n        tool: undefined,\n        state: \"tool-missing\",\n      } satisfies ToolUseMessageWithInfo;\n    }\n    const wasApprovedByUser = userApprovals?.some(\n      (m) =>\n        m.type === \"tool_use\" && m.id === toolUse.id && m.name === toolUse.name,\n    );\n    return {\n      ...toolUse,\n      tool,\n      state:\n        tool.auto_approve || tool.type === \"prostgles-ui\" || wasApprovedByUser ?\n          \"approved\"\n        : userApprovals ? \"denied\"\n        : \"needs-approval\",\n    } satisfies ToolUseMessageWithInfo;\n  });\n\n  /** Wait for user to approve/deny all pending requests */\n  if (toolUseRequests.some((tr) => tr.state === \"needs-approval\")) {\n    return;\n  }\n\n  const toolResults: ToolResultMessage[] = await Promise.all(\n    toolUseRequests.map(async (toolUseRequest) => {\n      const toolUseInfo = {\n        type: \"tool_result\",\n        tool_use_id: toolUseRequest.id,\n        tool_name: toolUseRequest.name,\n      } as const;\n      const asResponse = (\n        content: ToolResultMessage[\"content\"],\n        is_error = false,\n      ) => {\n        return {\n          ...toolUseInfo,\n          content,\n          is_error,\n        } satisfies ToolResultMessage;\n      };\n      const tool = toolUseRequest.tool;\n      if (!tool) {\n        const allToolNames = await getAllToolNames(dbs);\n        const { serverName } = getMCPToolNameParts(toolUseRequest.name) ?? {};\n        const matchedTool = allToolNames.includes(toolUseRequest.name);\n        const matchedMCPServer =\n          matchedTool || !serverName ? undefined : (\n            await dbs.mcp_servers.findOne({\n              name: serverName,\n            })\n          );\n        return asResponse(\n          `Tool name \"${toolUseRequest.name}\" ${\n            matchedTool ?\n              \"is not allowed. Must enable it for this chat\"\n            : \"is invalid.\" +\n              (matchedMCPServer ?\n                ` Try enabling and reloading the tools for ${JSON.stringify(serverName)} MCP Server`\n              : \"\")\n          }`,\n          true,\n        );\n      }\n\n      if (toolUseRequest.state === \"denied\") {\n        return asResponse(\n          `Tool use request for \"${toolUseRequest.name}\" was denied by user`,\n          true,\n        );\n      }\n\n      if (tool.type === \"prostgles-ui\") {\n        const needsValidation =\n          tool.tool_name === \"suggest_tools_and_prompt\" ||\n          tool.tool_name === \"suggest_agent_workflow\";\n        if (needsValidation) {\n          const validation = getJSONBObjectSchemaValidationError(\n            PROSTGLES_MCP_SERVERS_AND_TOOLS[\"prostgles-ui\"][tool.tool_name]\n              .schema.type,\n            toolUseRequest.input,\n            \"\",\n          );\n          if (validation.error !== undefined) {\n            return asResponse(\n              `Input validation error: ${validation.error}`,\n              true,\n            );\n          }\n        }\n\n        return asResponse(\"Done\");\n      }\n\n      if (aborter.signal.aborted) {\n        return asResponse(`Operation was aborted by user.`, true);\n      }\n      if (tool.type === \"mcp\") {\n        const toolNameParts = getMCPToolNameParts(toolUseRequest.name);\n        if (!toolNameParts) {\n          return asResponse(\n            `Tool name \"${toolUseRequest.name}\" is invalid`,\n            true,\n          );\n        }\n        const { serverName, toolName } = toolNameParts;\n        const { content, isError } = await callMCPServerTool(\n          user,\n          chatId,\n          dbs,\n          serverName,\n          toolName,\n          // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n          toolUseRequest.input,\n          clientReq,\n        ).catch((e) => ({\n          content: e instanceof Error ? e.message : JSON.stringify(e),\n          isError: true,\n        }));\n\n        return asResponse(content, isError);\n      }\n\n      const { clientMethods } = await getClientDBHandlersForChat(\n        chat,\n        args.clientReq,\n      );\n      if (tool.type === \"prostgles-db-methods\") {\n        const { content, is_error } = await parseToolResultToMessage(\n          async () => {\n            const method = clientMethods[tool.tool_name];\n            if (!method) {\n              throw new Error(\n                `Invalid or disallowed method: \"${tool.tool_name}\"`,\n              );\n            }\n            const methodFunc =\n              typeof method === \"function\" ? method : method.run;\n            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n            const res = await methodFunc(toolUseRequest.input);\n            return JSON.stringify(res ?? \"\");\n          },\n        );\n        return asResponse(content, is_error);\n      }\n\n      if (tool.type !== \"prostgles-db\") {\n        return asResponse(\n          `Tool name \"${toolUseRequest.name}\" is invalid`,\n          true,\n        );\n      }\n\n      const { content, is_error } = await parseToolResultToMessage(async () => {\n        const result = await runProstglesDBTool(\n          chat,\n          args.clientReq,\n          toolUseRequest.input,\n          tool.tool_name,\n        );\n\n        return typeof result === \"string\" ? result : JSON.stringify(result);\n      });\n      return asResponse(content, is_error);\n    }),\n  );\n\n  const denied = toolUseRequests.some((tr) => tr.state === \"denied\");\n  if (toolResults.length) {\n    await askLLM({\n      ...args,\n      type: denied ? \"tool-use-result-with-denied\" : \"tool-use-result\",\n      userMessage: toolResults,\n      aborter,\n    });\n  }\n};\n\nconst parseToolResultToMessage = (\n  func: () => Promise<string | undefined>,\n): Promise<\n  | { content: string; is_error?: undefined }\n  | { content: string; is_error: boolean }\n> => {\n  return func()\n    .then((content: string | undefined) => ({ content: content ?? \"\" }))\n    .catch((e) => ({\n      content: JSON.stringify(getSerialisableError(e)),\n      is_error: true as const,\n    }));\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/runApprovedTools/validateLastMessageToolUseRequests.ts",
    "content": "import type { LLMMessage } from \"../askLLM\";\nimport type { ToolUseMessage } from \"./runApprovedTools\";\n\nexport const validateLastMessageToolUseRequests = ({\n  toolUseMessages,\n  userToolUseApprovals,\n}: {\n  toolUseMessages: ToolUseMessage[];\n  userToolUseApprovals: LLMMessage;\n}) => {\n  if (!toolUseMessages.length) {\n    throw new Error(\n      \"Last message does not contain any tool use requests to approve\",\n    );\n  }\n  const invalidUserApprovals = userToolUseApprovals.filter(\n    (m) =>\n      m.type !== \"tool_use\" ||\n      !toolUseMessages.some((lm) => lm.id === m.id && lm.name === m.name),\n  );\n  if (invalidUserApprovals.length) {\n    throw new Error(\n      `Invalid tool use requests in user approvals: ${JSON.stringify(\n        invalidUserApprovals,\n      )}`,\n    );\n  }\n};\n"
  },
  {
    "path": "server/src/publishMethods/askLLM/setupLLM.ts",
    "content": "import type { DBS } from \"../..\";\nimport { LLM_PROMPT_VARIABLES } from \"@common/llmUtils\";\nimport type { DBSSchemaForInsert } from \"@common/publishUtils\";\nexport const setupLLM = async (dbs: DBS) => {\n  /** In case of stale schema update */\n  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n  if (dbs.llm_prompts && !(await dbs.llm_prompts.findOne())) {\n    const adminUser = await dbs.users.findOne({ passwordless_admin: true });\n    const user_id = adminUser?.id;\n    const firstLine = [\n      `You are an assistant for a software called ${LLM_PROMPT_VARIABLES.PROSTGLES_SOFTWARE_NAME}.`,\n      `It allows managing and exploring data within Postgres databases as well as creating internal tools. \\n`,\n      `Today is ${LLM_PROMPT_VARIABLES.TODAY}.`,\n      `DO NOT USE HARDCODED SAMPLE DATA UNLESS THE USER ASKS FOR IT.`,\n    ].join(\"\\n\");\n    await dbs.llm_prompts.insert([\n      {\n        name: \"Chat\",\n        description: \"Default chat. Includes schema (if allowed)\",\n        user_id,\n        prompt: [\n          firstLine,\n          \"Assist user with any queries they might have. Do not add empty lines in your sql response.\",\n          \"Reply with a full and concise answer that does not require further clarification or revisions.\",\n          \"Below is the database schema they're currently working with:\",\n          \"When asked to add or generate data DO NOT CREATE IT YOURSELF. \",\n          \"USE PUBLIC SOURCES OR GENERATE IT THORUGH TOOLS. NEVER PROVIDE THE VALUES YOURSELF UNLESS SPECIFICALLY ASKED.\",\n          \"\",\n          LLM_PROMPT_VARIABLES.SCHEMA,\n        ].join(\"\\n\"),\n      },\n      {\n        name: \"Create dashboards\",\n        description:\n          \"Includes database schema and dashboard view structure. Claude Sonnet recommended\",\n        user_id,\n        options: {\n          prompt_type: \"dashboards\",\n        },\n        prompt: [\n          firstLine,\n          \"Assist user with any queries they might have about creating dashboards.\",\n          \"Below is the database schema they're currently working with:\",\n          \"\",\n          LLM_PROMPT_VARIABLES.SCHEMA,\n          \"\",\n        ].join(\"\\n\"),\n      },\n      {\n        name: \"Create task\",\n        description:\n          \"Includes database schema and full tools list. Will suggest database access type and tools required to completed the task. Claude Sonnet recommended\",\n        user_id,\n        options: {\n          prompt_type: \"tasks\",\n        },\n        prompt: [\n          firstLine,\n          \"Assist the user with any queries they might have in their current task mode.\",\n          \"They expect you to look at the schema and the tools available to them and return a list of tools are best suited for accomplishing their task.\",\n          \"Ask the user for more information if you are not sure.\",\n          \"When suggesting a prompt make sure you add a ${today} placeholder that will be replaced with today's date.\",\n          \"\",\n          \"\",\n          \"Below is the database schema they're currently working with:\",\n          \"\",\n          LLM_PROMPT_VARIABLES.SCHEMA,\n          \"\",\n        ].join(\"\\n\"),\n      },\n      {\n        name: \"Create workflow\",\n        description:\n          \"Includes database schema and full tools list. Will suggest database access type, tools and workflow logic required to completed the task. Claude Sonnet recommended\",\n        user_id,\n        options: {\n          prompt_type: \"agent_workflow\",\n        },\n        prompt: [\n          firstLine,\n          \"Assist the user in creating a workflow.\",\n          \"They expect you to look at the schema and tools available to them and return the best suited tools, database schema and workflow logic for accomplishing their task.\",\n          \"Ask the user for more information if you are not sure.\",\n          \"\",\n          \"\",\n          \"Below is the database schema they're currently working with:\",\n          \"\",\n          LLM_PROMPT_VARIABLES.SCHEMA,\n          \"\",\n        ].join(\"\\n\"),\n      },\n      {\n        name: \"Empty\",\n        description: \"Empty prompt\",\n        user_id,\n        prompt: \"\",\n      },\n    ]);\n\n    const addedPrompts = await dbs.llm_prompts.find();\n    console.warn(\n      \"Inserted default prompts\",\n      addedPrompts.map((p) => p.name),\n    );\n  }\n  /** In case of stale schema update */\n  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n  if (dbs.llm_providers && !(await dbs.llm_providers.findOne())) {\n    await dbs.llm_providers.insert([\n      {\n        id: \"OpenAI\",\n        api_pricing_url: \"https://platform.openai.com/docs/pricing\",\n        api_docs_url: \"https://platform.openai.com/docs/api-reference\",\n        api_url: \"https://api.openai.com/v1/chat/completions\",\n        logo_url: \"/logos/openai.svg\",\n        llm_models: [\n          {\n            name: \"o1\",\n            pricing_info: {\n              input: 15,\n              cachedInput: 7.5,\n              output: 60,\n            },\n          },\n          {\n            name: \"o1-mini-2024-09-12\",\n            pricing_info: {\n              input: 1.1,\n              cachedInput: 0.55,\n              output: 4.4,\n            },\n          },\n          {\n            name: \"o3-mini-2025-01-31\",\n            pricing_info: {\n              input: 1.1,\n              cachedInput: 0.55,\n              output: 4.4,\n            },\n          },\n          {\n            name: \"gpt-4.5-preview-2025-02-27\",\n            pricing_info: {\n              input: 75,\n              cachedInput: 37.5,\n              output: 150,\n            },\n          },\n          {\n            name: \"gpt-4o-2024-08-06\",\n            pricing_info: {\n              input: 2.5,\n              cachedInput: 1.25,\n              output: 10,\n            },\n            chat_suitability_rank: \"2\",\n          },\n          {\n            name: \"gpt-4o-mini-2024-07-18\",\n            pricing_info: {\n              input: 0.15,\n              cachedInput: 0.075,\n              output: 0.6,\n            },\n          },\n        ],\n      },\n      {\n        id: \"Anthropic\",\n        api_url: \"https://api.anthropic.com/v1/messages\",\n        api_docs_url: \"https://docs.anthropic.com/en/api/getting-started\",\n        api_pricing_url: \"https://www.anthropic.com/pricing#api\",\n        logo_url: \"/logos/anthropic.svg\",\n        extra_body: {\n          max_tokens: 16_000,\n        },\n        llm_models: [\n          {\n            name: \"claude-sonnet-4-20250514\",\n            pricing_info: {\n              input: 3,\n              output: 15,\n              cachedInput: 3.75,\n              cachedOutput: 0.3,\n            },\n            mcp_tool_support: true,\n          },\n          {\n            name: \"claude-3-7-sonnet-20250219\",\n            pricing_info: {\n              input: 3,\n              output: 15,\n              cachedInput: 3.75,\n              cachedOutput: 0.3,\n            },\n            mcp_tool_support: true,\n          },\n          {\n            name: \"claude-3-5-sonnet-20241022\",\n            pricing_info: {\n              input: 3,\n              output: 15,\n              cachedInput: 1,\n              cachedOutput: 0.08,\n            },\n            chat_suitability_rank: \"1\",\n            mcp_tool_support: true,\n          },\n          {\n            name: \"claude-3-5-sonnet-20240620\",\n            pricing_info: {\n              input: 3,\n              output: 15,\n              cachedInput: 1,\n              cachedOutput: 0.08,\n            },\n            mcp_tool_support: true,\n          },\n          {\n            name: \"claude-3-sonnet-20240229\",\n            pricing_info: { input: 3, output: 15 },\n          },\n          {\n            name: \"claude-3-5-haiku-20241022\",\n            pricing_info: {\n              input: 0.8,\n              output: 4,\n              cachedInput: 1,\n              cachedOutput: 0.08,\n            },\n          },\n          {\n            name: \"claude-3-opus-20240229\",\n            pricing_info: {\n              input: 15,\n              output: 75,\n              cachedInput: 18.75,\n              cachedOutput: 1.5,\n            },\n          },\n        ],\n      },\n      {\n        id: \"Google\",\n        api_pricing_url: \"https://ai.google.dev/gemini-api/docs/pricing\",\n        api_docs_url: \"https://ai.google.dev/gemini-api/docs\",\n        api_url:\n          \"https://generativelanguage.googleapis.com/v1beta/models/$MODEL:generateContent?key=$KEY\",\n        logo_url: \"/logos/google.svg\",\n        llm_models: [\n          {\n            name: \"gemini-2.5-pro-exp-03-25\",\n            pricing_info: {\n              input: 1.25,\n              output: 10,\n              threshold: { tokenLimit: 200_000, input: 2.5, output: 15 },\n            },\n            chat_suitability_rank: \"3\",\n          },\n          {\n            name: \"gemini-2.5-pro-preview-03-25\",\n            pricing_info: {\n              input: 1.25,\n              output: 10,\n              threshold: { tokenLimit: 200_000, input: 2.5, output: 15 },\n            },\n            chat_suitability_rank: \"3\",\n          },\n          {\n            name: \"gemini-2.0-flash\",\n            pricing_info: {\n              input: 0.1,\n              output: 0.4,\n              cachedInput: 1,\n              cachedOutput: 0.025,\n            },\n            chat_suitability_rank: \"3\",\n          },\n          {\n            name: \"gemini-2.0-flash-lite\",\n            pricing_info: { input: 0.075, output: 0.3 },\n            chat_suitability_rank: \"7\",\n          },\n          {\n            name: \"gemini-1.5-flash\",\n            pricing_info: {\n              input: 0.075,\n              output: 0.3,\n              threshold: {\n                tokenLimit: 128_000,\n                input: 0.15,\n                output: 0.6,\n              },\n            },\n          },\n          {\n            name: \"gemini-1.5-flash-8b\",\n            pricing_info: { input: 0.0375, output: 0.15 },\n          },\n          { name: \"gemini-1.5-pro\", pricing_info: { input: 1.25, output: 5 } },\n        ],\n      },\n      {\n        id: \"Ollama\",\n        api_url: \"http://localhost:11434/v1/chat/completions\",\n        api_docs_url: \"https://github.com/ollama/ollama/blob/main/docs/api.md\",\n        logo_url: \"/logos/ollama.svg\",\n        llm_models: [\n          {\n            name: \"qwen2.5-coder:7b-instruct\",\n            context_length: 128_000,\n            mcp_tool_support: true,\n          },\n          {\n            name: \"deepseek-r1:8b\",\n            context_length: 128_000,\n          },\n          {\n            name: \"llama3.1:8b\",\n            context_length: 128_000,\n            mcp_tool_support: true,\n          },\n          {\n            name: \"gemma3\",\n            context_length: 128_000,\n            mcp_tool_support: true,\n          },\n          {\n            name: \"qwen2.5vl\",\n            context_length: 128_000,\n            mcp_tool_support: true,\n          },\n        ],\n        extra_body: {\n          think: false,\n          stream: false,\n        },\n      },\n      {\n        id: \"OpenRouter\",\n        api_url: \"https://openrouter.ai/api/v1/chat/completions\",\n        api_docs_url: \"https://openrouter.ai/docs/quickstart\",\n        api_pricing_url:\n          \"https://openrouter.ai/docs/api-reference/list-available-models\",\n        extra_body: {\n          max_tokens: 16_000,\n        },\n        logo_url: \"/logos/openrouter.svg\",\n        llm_models: [\n          {\n            name: \"deepseek/deepseek-r1:free\",\n            pricing_info: null,\n            model_created: \"2025-03-07 12:19:04.913961\",\n            chat_suitability_rank: \"5\",\n          },\n          {\n            name: \"anthropic/claude-sonnet-4\",\n            pricing_info: {\n              input: 3,\n              output: 15,\n              cachedInput: 1,\n              cachedOutput: 0.08,\n            },\n            model_created: \"2024-10-22 12:00:00\",\n            chat_suitability_rank: \"1\",\n          },\n        ],\n      },\n      {\n        id: \"Prostgles\",\n        api_url: \"https://cloud.prostgles.com/api/v1\",\n        logo_url: \"/v2.svg\",\n        llm_models: [\n          {\n            name: \"anthropic/claude-sonnet-4\",\n            pricing_info: {\n              input: 3,\n              output: 15,\n              cachedInput: 1,\n              cachedOutput: 0.08,\n            },\n            model_created: \"2024-10-22 12:00:00\",\n            chat_suitability_rank: \"1\",\n          },\n        ],\n      },\n      {\n        id: \"Custom\",\n        api_url: \"\",\n        api_docs_url: \"\",\n        api_pricing_url: \"\",\n        logo_url: \"/icons/CloudQuestionOutline.svg\",\n        llm_models: [],\n      },\n    ] satisfies (DBSSchemaForInsert[\"llm_providers\"] & {\n      llm_models: Omit<DBSSchemaForInsert[\"llm_models\"], \"provider_id\">[];\n    })[]);\n  }\n};\n\n// type OpenAIModel = {\n//   id: string;\n//   created: number;\n//   object: \"model\";\n//   owned_by: string;\n// };\n// const fetchOpenAIModels = async (bearerToken: string) => {\n//   try {\n//     const response = await fetch(\"https://api.openai.com/v1/models\", {\n//       method: \"GET\",\n//       headers: {\n//         Authorization: `Bearer ${bearerToken}`,\n//         \"Content-Type\": \"application/json\",\n//       },\n//     });\n\n//     if (!response.ok) {\n//       throw new Error(\n//         `OpenAI API error: ${response.status} ${response.statusText}`,\n//       );\n//     }\n\n//     const data = await response.json();\n//     const models = data.data as OpenAIModel[];\n//     return models.sort((b, a) => +a.created - +b.created);\n//   } catch (error) {\n//     console.error(\"Error fetching OpenAI models:\", error);\n//     throw error;\n//   }\n// };\n\ntype ModelInfo = {\n  id: string;\n  /**\n   * Prices are per 1M tokens\n   */\n  input: number;\n  output: number;\n  cachedInput?: number;\n} & (\n  | {\n      maxInputPrice?: undefined;\n      maxOutputPrice?: undefined;\n      tokenLimit?: undefined;\n    }\n  | {\n      maxInputPrice: number;\n      maxOutputPrice: number;\n      tokenLimit: number;\n    }\n);\n\n/**\n * https://www.anthropic.com/pricing#api\n */\nexport const AnthropicModels = [\n  { id: \"claude-3-7-sonnet-20250219\", input: 3, output: 15 },\n  { id: \"claude-3-5-sonnet-20241022\", input: 3, output: 15 },\n  { id: \"claude-3-5-sonnet-20240620\", input: 3, output: 15 },\n  { id: \"claude-3-sonnet-20240229\", input: 3, output: 15 },\n  { id: \"claude-3-5-haiku-20241022\", input: 0.8, output: 4 },\n  { id: \"claude-3-opus-20240229\", input: 15, output: 75 },\n] as const satisfies ModelInfo[];\n\n/**\n * https://ai.google.dev/gemini-api/docs/pricing\n */\nexport const GoogleModels = [\n  { id: \"gemini-2.0-flash\", input: 0.1, output: 0.4 },\n  {\n    id: \"gemini-1.5-flash\",\n    input: 0.075,\n    output: 0.3,\n    maxInputPrice: 0.15,\n    maxOutputPrice: 0.6,\n    tokenLimit: 128_000,\n  },\n  { id: \"gemini-1.5-flash-8b\", input: 0.0375, output: 0.15 },\n  { id: \"gemini-1.5-pro\", input: 1.25, output: 5 },\n] as const satisfies ModelInfo[];\n\n/**\n * https://platform.openai.com/docs/pricing\n */\nexport const OpenAIModels = [\n  {\n    id: \"o1\",\n    input: 15,\n    cachedInput: 7.5,\n    output: 60,\n  },\n  {\n    id: \"o1-mini-2024-09-12\",\n    input: 1.1,\n    cachedInput: 0.55,\n    output: 4.4,\n  },\n  {\n    id: \"o3-mini-2025-01-31\",\n    input: 1.1,\n    cachedInput: 0.55,\n    output: 4.4,\n  },\n  {\n    id: \"gpt-4.5-preview-2025-02-27\",\n    input: 75,\n    cachedInput: 37.5,\n    output: 150,\n  },\n  {\n    id: \"gpt-4o-2024-08-06\",\n    input: 2.5,\n    cachedInput: 1.25,\n    output: 10,\n  },\n  {\n    id: \"gpt-4o-mini-2024-07-18\",\n    input: 0.15,\n    cachedInput: 0.075,\n    output: 0.6,\n  },\n] as const satisfies ModelInfo[];\n"
  },
  {
    "path": "server/src/publishMethods/deleteConnection.ts",
    "content": "import { getCDB } from \"@src/ConnectionManager/ConnectionManager\";\nimport { connMgr, type DBS } from \"..\";\nimport type BackupManager from \"@src/BackupManager/BackupManager\";\n\nexport const deleteConnection = async (\n  dbs: DBS,\n  bkpManager: BackupManager,\n  id: string,\n  opts?: { keepBackups: boolean; dropDatabase: boolean },\n) => {\n  try {\n    return dbs.tx(async (t) => {\n      const con = await t.connections.findOne({ id });\n      if (con?.is_state_db)\n        throw \"Cannot delete a prostgles state database connection\";\n      connMgr.prglConnections[id]?.methodRunner?.destroy();\n      connMgr.prglConnections[id]?.onMountRunner?.destroy();\n      connMgr.prglConnections[id]?.tableConfigRunner?.destroy();\n      if (opts?.dropDatabase) {\n        if (!con?.db_name) throw \"Unexpected: Database name missing\";\n        const { db: cdb, destroy: destroyCdb } = await getCDB(\n          con.id,\n          undefined,\n          true,\n        );\n        const anotherDatabaseNames: { datname: string }[] = await cdb.any(`\n              SELECT * \n              FROM pg_catalog.pg_database \n              WHERE datname <> current_database() \n              AND NOT datistemplate\n              ORDER BY datname = 'postgres' DESC\n            `);\n        const _superUsers: { usename: string }[] = await cdb.any(\n          `\n              SELECT usename \n              FROM pg_user WHERE usesuper = true\n              `,\n          {},\n        );\n        const superUsers = _superUsers.map((u) => u.usename);\n        await destroyCdb();\n        const [anotherDatabaseName] = anotherDatabaseNames;\n        if (!anotherDatabaseName) throw \"Could not find another database\";\n        if (anotherDatabaseName.datname === con.db_name) {\n          throw \"Not expected: Another database is the same as the one being deleted\";\n        }\n\n        let superUser: { user: string; password: string } | undefined;\n        if (!superUsers.includes(con.db_user)) {\n          const conWithSuperUsers = await t.connections.findOne({\n            db_user: { $in: superUsers },\n            db_host: con.db_host,\n            db_port: con.db_port,\n            db_pass: { \"<>\": null },\n          });\n          if (conWithSuperUsers) {\n            superUser = {\n              user: conWithSuperUsers.db_user,\n              password: conWithSuperUsers.db_pass!,\n            };\n          }\n        }\n        const { db: acdb } = await getCDB(\n          con.id,\n          { database: anotherDatabaseName.datname, ...superUser },\n          true,\n        );\n        await connMgr.disconnect(con.id);\n        const killDbConnections = () => {\n          return acdb.manyOrNone(\n            `\n                SELECT pg_terminate_backend(pid) \n                FROM pg_stat_activity \n                WHERE datname = \\${db_name}\n                AND pid <> pg_backend_pid();\n              `,\n            con,\n          );\n        };\n        await killDbConnections();\n        await killDbConnections();\n        await acdb.any(\n          `\n              DROP DATABASE \\${db_name:name};\n            `,\n          con,\n        );\n      }\n      const conFilter = { connection_id: id };\n      await t.workspaces.delete(conFilter);\n\n      if (opts?.keepBackups) {\n        await t.backups.update(conFilter, { connection_id: null });\n      } else {\n        const bkps = await t.backups.find(conFilter);\n        for (const b of bkps) {\n          await bkpManager.bkpDelete(b.id, true);\n        }\n        await t.backups.delete(conFilter);\n      }\n\n      const result = await t.connections.delete({ id }, { returning: \"*\" });\n\n      /** delete orphaned database_configs */\n      await t.database_configs.delete({\n        $notExistsJoined: { connections: {} },\n      });\n      return result;\n    });\n  } catch (err) {\n    return Promise.reject(err);\n  }\n};\n"
  },
  {
    "path": "server/src/publishMethods/getConnectionAndDatabaseConfig.ts",
    "content": "import { getDatabaseConfigFilter } from \"@src/ConnectionManager/connectionManagerUtils\";\nimport { assertJSONBObjectAgainstSchema } from \"prostgles-types\";\nimport { connMgr, type DBS } from \"..\";\n\nexport const getConnectionAndDatabaseConfig = async (\n  dbs: DBS,\n  arg0: unknown,\n) => {\n  const arg = { connId: arg0 };\n  assertJSONBObjectAgainstSchema(\n    {\n      connId: \"string\",\n    },\n    arg,\n    \"connId\",\n    false,\n  );\n  const { connId } = arg;\n  const c = await dbs.connections.findOne({ id: connId });\n  if (!c) throw \"Connection not found\";\n  const dbConf = await dbs.database_configs.findOne(getDatabaseConfigFilter(c));\n  if (!dbConf) throw \"Connection database_config not found\";\n  const db = connMgr.getConnectionDb(connId);\n  if (!db) throw \"db missing\";\n\n  return { c, dbConf, db };\n};\n"
  },
  {
    "path": "server/src/publishMethods/getNodeTypes.ts",
    "content": "import * as ts from \"typescript\";\nimport * as path from \"path\";\n\nexport const getNodeTypes = () => {\n  const pathToProject = path.resolve(__dirname + \"/../../../..\");\n  const files = extractInstalledPackageTypes(pathToProject);\n  return files.map((file) => ({\n    ...file,\n    filePath: file.filePath.split(pathToProject).pop(),\n  }));\n};\n\ninterface TypeFile {\n  content: string;\n  filePath: string;\n}\n\n/**\n * Extracts type definitions for installed packages as specified by their package.json\n * \"types\" (or \"typings\") field, including any referenced declaration files.\n *\n * @param projectDir - Absolute path to your project root.\n */\nexport function extractInstalledPackageTypes(projectDir: string): TypeFile[] {\n  // 1. Read the project's package.json.\n  const projectPkgPath = path.join(projectDir, \"package.json\");\n  if (!ts.sys.fileExists(projectPkgPath)) {\n    throw new Error(`Cannot find project package.json at ${projectPkgPath}`);\n  }\n  const projectPkgContent = ts.sys.readFile(projectPkgPath);\n  if (!projectPkgContent) {\n    throw new Error(`Failed to read ${projectPkgPath}`);\n  }\n  let projectPkg;\n  try {\n    projectPkg = JSON.parse(projectPkgContent);\n  } catch (err) {\n    throw new Error(`Failed to parse ${projectPkgPath}: ${err}`);\n  }\n\n  // 2. Gather dependency names from \"dependencies\" and \"devDependencies\".\n  const deps = {\n    ...(projectPkg.dependencies || {}),\n    /** Include node types */\n    ...(projectPkg.devDependencies || {}),\n  };\n  const depNames = Object.keys(deps).filter(\n    (depName) => !depName.includes(\"prostgles-server\"),\n  );\n\n  // 3. For each dependency, locate its package.json and check for a \"types\" or \"typings\" field.\n  const rootTypeFiles: { pkgName: string; typesFilePath: string }[] = [];\n  for (const depName of depNames) {\n    // Assume package is at projectDir/node_modules/<depName>\n    const depDir = path.join(projectDir, \"node_modules\", depName);\n    const depPkgPath = path.join(depDir, \"package.json\");\n    if (!ts.sys.fileExists(depPkgPath)) continue;\n    const depPkgContent = ts.sys.readFile(depPkgPath);\n    if (!depPkgContent) continue;\n    let depPkg;\n    try {\n      depPkg = JSON.parse(depPkgContent);\n    } catch {\n      continue;\n    }\n    const typesField = depPkg.types || depPkg.typings;\n    if (!typesField) continue;\n    const typesFilePath = path.join(depDir, typesField);\n    if (!ts.sys.fileExists(typesFilePath)) continue;\n    const pkgName = depName.startsWith(\"@types/\") ? depName.slice(7) : depName;\n    rootTypeFiles.push({ pkgName, typesFilePath });\n  }\n\n  // If no package provides a types entry point, return an empty array.\n  if (rootTypeFiles.length === 0) {\n    return [];\n  }\n\n  // 4. Create a TS program using the type entry files as roots.\n  const compilerOptions: ts.CompilerOptions = {\n    allowJs: false,\n    moduleResolution: ts.ModuleResolutionKind.NodeJs,\n    baseUrl: projectDir,\n  };\n  const program = ts.createProgram(\n    rootTypeFiles.map((rtp) => rtp.typesFilePath),\n    compilerOptions,\n  );\n\n  // 5. Recursively collect declaration files from each root.\n  const collected = new Map<string, TypeFile>();\n\n  function collectSourceFile(\n    sf: ts.SourceFile,\n    packageName?: string,\n    parentPkgName?: string,\n  ): void {\n    if (collected.has(sf.fileName)) return;\n\n    // Only include declaration files.\n    if (sf.isDeclarationFile) {\n      collected.set(sf.fileName, wrapIfNeeded(sf, packageName));\n    }\n\n    // Process triple‑slash reference directives.\n    for (const ref of sf.referencedFiles) {\n      const refPath = path.resolve(path.dirname(sf.fileName), ref.fileName);\n      const refSource = program.getSourceFile(refPath);\n      if (refSource) {\n        collectSourceFile(refSource);\n      }\n    }\n\n    // Also check for module imports (or exports) in the AST.\n    ts.forEachChild(sf, (node) => {\n      if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {\n        if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {\n          const moduleName = node.moduleSpecifier.text;\n          const resolved = ts.resolveModuleName(\n            moduleName,\n            sf.fileName,\n            compilerOptions,\n            ts.sys,\n          );\n          if (\n            resolved.resolvedModule &&\n            resolved.resolvedModule.resolvedFileName\n          ) {\n            const modSource = program.getSourceFile(\n              resolved.resolvedModule.resolvedFileName,\n            );\n            if (modSource) {\n              const modFileNameForImport =\n                !parentPkgName ? undefined : (\n                  parentPkgName +\n                  modSource.fileName\n                    .split(`${projectDir}/node_modules/${parentPkgName}`)[1]\n                    ?.split(\".d.ts\")[0]\n                );\n              if (modSource.fileName.includes(\"prostgles-server\")) {\n                console.log(\n                  projectDir,\n                  modSource.fileName,\n                  modFileNameForImport,\n                );\n              }\n              collectSourceFile(modSource, modFileNameForImport, parentPkgName);\n            }\n          }\n        }\n      }\n    });\n  }\n\n  // For each package's root type file, collect its source and any referenced files.\n  for (const rootFile of rootTypeFiles) {\n    const sf = program.getSourceFile(rootFile.typesFilePath);\n    if (sf) {\n      collectSourceFile(sf, rootFile.pkgName, rootFile.pkgName);\n    }\n  }\n\n  return Array.from(collected.values());\n}\n\nfunction shouldWrapFile(sourceFile: ts.SourceFile): boolean {\n  // If the file is already an external module, it has top-level imports/exports.\n  // if (ts.isExternalModule(sourceFile)) {\n  //   return false;\n  // }\n\n  // Optionally, check if the file already starts with a 'declare module' statement.\n  const [firstStmt] = sourceFile.statements;\n  if (firstStmt && ts.isModuleDeclaration(firstStmt)) {\n    return false;\n  }\n\n  // Otherwise, it's ambient (global) and likely needs to be wrapped.\n  return true;\n}\n\nfunction wrapIfNeeded(\n  sourceFile: ts.SourceFile,\n  packageName: string | undefined,\n): { content: string; filePath: string } {\n  const fileContent = sourceFile.getFullText();\n  if (packageName && shouldWrapFile(sourceFile)) {\n    return {\n      filePath: sourceFile.fileName,\n      content: `declare module '${packageName}' {\\n${fileContent}\\n}`,\n    };\n  } else {\n    return {\n      filePath: sourceFile.fileName,\n      content: fileContent,\n    };\n  }\n}\n"
  },
  {
    "path": "server/src/publishMethods/prostglesSignup.ts",
    "content": "import { PROSTGLES_CLOUD_URL, ROUTES } from \"@common/utils\";\nimport { isTesting } from \"../init/initExpressAndIOServers\";\n\nexport const prostglesSignup = async (email: string, code: string) => {\n  const host =\n    isTesting || true ? \"http://localhost:3004\" : PROSTGLES_CLOUD_URL;\n  const path = code ? ROUTES.MAGIC_LINK : ROUTES.LOGIN;\n  const url = `${host}${path}`;\n  const rawResp = await fetch(url, {\n    method: \"POST\",\n    headers: {\n      Accept: \"application/json\",\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify(\n      code ? { email, code, returnToken: true } : { username: email },\n    ),\n  });\n  if (!rawResp.ok) {\n    const error = await rawResp\n      .json()\n      .catch(() => rawResp.text())\n      .catch(() => rawResp.statusText);\n    return { error, hasError: true };\n  }\n  const { token } = (await rawResp.json()) as { token: string };\n  return { token, host };\n};\n"
  },
  {
    "path": "server/src/publishMethods/publishMethods.ts",
    "content": "import type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { authenticator } from \"@otplib/preset-default\";\nimport * as crypto from \"crypto\";\nimport fs from \"fs\";\nimport * as os from \"os\";\nimport path, { join } from \"path\";\nimport type { PublishMethods } from \"prostgles-server/dist/PublishParser/PublishParser\";\nimport type { DBS } from \"../index\";\nimport { connMgr } from \"../index\";\n\nexport type Users = Required<DBGeneratedSchema[\"users\"][\"columns\"]>;\nexport type Connections = Required<DBGeneratedSchema[\"connections\"][\"columns\"]>;\n\nimport type { LLMMessage } from \"@common/llmUtils\";\nimport type { DBSSchema } from \"@common/publishUtils\";\nimport { reloadMcpServerTools } from \"@src/McpHub/reloadMcpServerTools\";\nimport { getServiceManager } from \"@src/ServiceManager/ServiceManager\";\nimport { prostglesServices } from \"@src/ServiceManager/ServiceManagerTypes\";\nimport type { SessionUser } from \"prostgles-server/dist/Auth/AuthTypes\";\nimport { getIsSuperUser } from \"prostgles-server/dist/Prostgles\";\nimport type { AnyObject } from \"prostgles-types\";\nimport { getKeys, includes, isEmpty } from \"prostgles-types\";\nimport { getPasswordHash } from \"../authConfig/authUtils\";\nimport { checkClientIP, createSessionSecret } from \"../authConfig/sessionUtils\";\nimport type { Backups } from \"../BackupManager/BackupManager\";\nimport { getInstalledPsqlVersions } from \"../BackupManager/getInstalledPrograms\";\nimport type { ConnectionTableConfig } from \"../ConnectionManager/ConnectionManager\";\nimport {\n  getACRules,\n  getCDB,\n  getSuperUserCDB,\n} from \"../ConnectionManager/ConnectionManager\";\nimport { getCompiledTS } from \"../ConnectionManager/connectionManagerUtils\";\nimport { testDBConnection } from \"../connectionUtils/testDBConnection\";\nimport { validateConnection } from \"../connectionUtils/validateConnection\";\nimport { getElectronConfig } from \"../electronConfig\";\nimport { callMCPServerTool } from \"../McpHub/callMCPServerTool\";\nimport {\n  getMcpHostInfo,\n  getMCPServersStatus,\n  installMCPServer,\n} from \"../McpHub/AnthropicMcpHub/installMCPServer\";\nimport { getStatus } from \"../methods/getPidStats\";\nimport { killPID } from \"../methods/statusMonitorUtils\";\nimport { getPasswordlessAdmin } from \"../SecurityManager/initUsers\";\nimport { upsertConnection } from \"../upsertConnection\";\nimport { getSampleSchemas } from \"./applySampleSchema\";\nimport { askLLM, stopAskLLM } from \"./askLLM/askLLM\";\nimport { getFullPrompt } from \"./askLLM/getFullPrompt\";\nimport { getLLMToolsAllowedInThisChat } from \"./askLLM/getLLMToolsAllowedInThisChat\";\nimport { refreshModels } from \"./askLLM/refreshModels\";\nimport { deleteConnection } from \"./deleteConnection\";\nimport { getConnectionAndDatabaseConfig } from \"./getConnectionAndDatabaseConfig\";\nimport { getNodeTypes } from \"./getNodeTypes\";\nimport { prostglesSignup } from \"./prostglesSignup\";\nimport { setFileStorage } from \"./setFileStorage\";\nimport { initBackupManager } from \"@src/init/onProstglesReady\";\nimport { statePrgl } from \"@src/init/startProstgles\";\nimport { glob } from \"glob\";\nimport { mkdir } from \"fs/promises\";\nimport type { AllowedChatTool } from \"@common/prostglesMcp\";\n\nexport const publishMethods: PublishMethods<\n  DBGeneratedSchema,\n  SessionUser<Users, Users>\n> = async (params) => {\n  const { dbo: dbs, clientReq, db: _dbs, user } = params;\n  const { socket } = clientReq;\n  const bkpManager = await initBackupManager(_dbs, dbs);\n  if (!user || !user.id) {\n    return {};\n  }\n\n  const servicesManager = getServiceManager(dbs);\n\n  const adminMethods: ReturnType<PublishMethods> = {\n    mkdir: async (path: string, folderName: string) => {\n      if (!path) throw \"Path is required\";\n      if (!folderName) throw \"Folder name is required\";\n      const fullPath = join(path, folderName);\n      await mkdir(fullPath);\n      return fullPath;\n    },\n    glob: async (path?: string, timeout: number = 10_000) => {\n      const currentPath = os.homedir();\n      const pattern = join(path || currentPath, \"*\");\n      if (timeout <= 0 || timeout > 120_000) {\n        throw \"Timeout must be between 1 and 120 seconds\";\n      }\n      // pass in a signal to cancel the glob walk\n      const resultItems = await glob(pattern, {\n        signal: AbortSignal.timeout(timeout),\n        withFileTypes: true,\n      });\n      const result = Array.from(resultItems).map((r) => ({\n        path: r.fullpath(),\n        name: r.name,\n        type:\n          r.isDirectory() ? \"directory\"\n          : r.isBlockDevice() ? \"block\"\n          : \"file\",\n        size: r.size,\n        lastModified: r.mtimeMs,\n        created: r.ctimeMs,\n      }));\n      return {\n        pattern,\n        path: currentPath,\n        result,\n      };\n    },\n    disablePasswordless: async (newAdmin: {\n      username: string;\n      password: string;\n    }) => {\n      const noPwdAdmin = await getPasswordlessAdmin(dbs);\n      if (!noPwdAdmin) throw \"No passwordless admin found\";\n      if (noPwdAdmin.id !== user.id) {\n        throw \"Only the passwordless admin can disable passwordless access\";\n      }\n\n      /** Change current passwordless user to normal admin to ensure old user data is accessible */\n      await dbs.users.update(\n        { id: noPwdAdmin.id },\n        {\n          username: newAdmin.username,\n          password: getPasswordHash(user, newAdmin.password),\n          type: \"admin\",\n          passwordless_admin: false,\n        },\n      );\n\n      /** Ensure passwordless_admin is setup and disabled */\n      await dbs.users.insert({\n        passwordless_admin: true,\n        type: noPwdAdmin.type,\n        username: noPwdAdmin.username,\n        password: noPwdAdmin.password,\n        created: noPwdAdmin.created,\n        status: \"disabled\",\n      });\n\n      /** Terminate all sessions */\n      await dbs.sessions.delete({});\n    },\n    getConnectionDBTypes: (conId: string | undefined) => {\n      if (!statePrgl) throw \"statePrgl missing\";\n      /** No connection id = state connection */\n      if (!conId) {\n        return statePrgl.getTSSchema();\n      }\n      const c = connMgr.getConnection(conId);\n      return c.prgl.getTSSchema();\n    },\n    getMyIP: async () => {\n      if (!socket) throw \"Socket missing\";\n      return checkClientIP(\n        dbs,\n        { socket },\n        await dbs.global_settings.findOne(),\n      );\n    },\n    getConnectedIds: () => {\n      return Object.keys(connMgr.getConnections());\n    },\n    toggleService: async (serviceName: string, enable: boolean) => {\n      const serviceManager = getServiceManager(dbs);\n      if (!includes(getKeys(prostglesServices), serviceName)) {\n        throw \"Service not found\";\n      }\n      if (enable) {\n        return serviceManager.enableService(serviceName, () => {});\n      } else {\n        return serviceManager.stopService(serviceName);\n      }\n    },\n    getDBSize: async (conId: string) => {\n      const c = connMgr.getConnection(conId);\n      const size = (await c.prgl.db.sql(\n        \"SELECT pg_size_pretty( pg_database_size(current_database()) ) \",\n        {},\n        { returnType: \"value\" },\n      )) as string;\n      return size;\n    },\n    getIsSuperUser: async (conId: string) => {\n      const c = connMgr.getConnection(conId);\n      return getIsSuperUser(c.prgl._db);\n    },\n    getFileFolderSizeInBytes: (conId?: string) => {\n      const dirSize = (directory: string): number => {\n        if (!fs.existsSync(directory)) return 0;\n        const files = fs.readdirSync(directory);\n        const stats = files.flatMap((file) => {\n          const fileOrPathDir = path.join(directory, file);\n          const stat = fs.statSync(fileOrPathDir);\n          if (stat.isDirectory()) {\n            return dirSize(fileOrPathDir);\n          }\n          return stat.size;\n        });\n\n        return stats.reduce((accumulator, size) => accumulator + size, 0);\n      };\n\n      if (conId && typeof conId !== \"string\") {\n        throw \"Invalid/Inexisting connection id provided\";\n      }\n      const dir = connMgr.getFileFolderPath(conId);\n      return dirSize(dir);\n    },\n    testDBConnection,\n    validateConnection: (c: Connections) => {\n      const connection = validateConnection(c);\n      return { connection, warn: \"\" };\n    },\n    getInstalledPsqlVersions: () => {\n      return getInstalledPsqlVersions(_dbs);\n    },\n    createConnection: async (con: Connections, sampleSchemaName?: string) => {\n      const res = await upsertConnection(con, user.id, dbs, sampleSchemaName);\n      const el = getElectronConfig();\n      if (res.connection.is_state_db && el?.isElectron) {\n        el.setCredentials(res.connection);\n      }\n      return res;\n    },\n    refreshModels: () => refreshModels(dbs),\n    reloadSchema: async (conId: string) => {\n      const conn = connMgr.getConnection(conId);\n      if (conId && typeof conId !== \"string\") {\n        throw \"Invalid/Inexisting connection id provided\";\n      }\n      await conn.prgl.restart();\n    },\n    deleteConnection: (\n      id: string,\n      opts?: { keepBackups: boolean; dropDatabase: boolean },\n    ) => deleteConnection(dbs, bkpManager, id, opts),\n    disconnect: async (conId: string) => {\n      return connMgr.disconnect(conId);\n    },\n    pgDump: bkpManager.pgDump,\n    pgRestore: async (arg1: { bkpId: string; connId?: string }, opts?: any) =>\n      bkpManager.pgRestore(arg1, undefined, opts),\n    bkpDelete: bkpManager.bkpDelete,\n\n    streamBackupFile: async (\n      c: \"start\" | \"chunk\" | \"end\",\n      id: null | string,\n      conId: string | null,\n      chunk: string | undefined,\n      sizeBytes: number | undefined,\n      restore_options: Backups[\"restore_options\"],\n    ) => {\n      if (c === \"start\" && id && conId && sizeBytes) {\n        const stream = bkpManager.getTempFileStream(id, user.id);\n        await bkpManager.pgRestoreStream(\n          id,\n          conId,\n          stream.stream,\n          sizeBytes,\n          restore_options,\n        );\n\n        return stream.streamId;\n      } else if (c === \"chunk\" && id && chunk) {\n        return new Promise((resolve, reject) => {\n          bkpManager.pushToStream(id, chunk, (err) => {\n            if (err) {\n              reject(err);\n            } else {\n              resolve(1);\n            }\n          });\n        });\n      } else if (c === \"end\" && id) {\n        bkpManager.closeStream(id);\n      } else throw new Error(\"Not expected\");\n    },\n    setFileStorage: (\n      connId: string,\n      tableConfig?: ConnectionTableConfig,\n      opts?: { keepS3Data?: boolean; keepFileTable?: boolean },\n    ) => setFileStorage(dbs, connId, tableConfig, opts),\n    getStatus: (connId: string) => getStatus(connId, dbs),\n    runConnectionQuery,\n    getSampleSchemas,\n    getCompiledTS: (ts: string) => {\n      return getCompiledTS(ts);\n    },\n    killPID,\n    setOnMount: async (\n      connId: string,\n      changes: Partial<\n        Pick<DBSSchema[\"connections\"], \"on_mount_ts\" | \"on_mount_ts_disabled\">\n      >,\n    ) => {\n      if (isEmpty(changes)) {\n        throw \"No changes provided\";\n      }\n      const { c } = await getConnectionAndDatabaseConfig(dbs, connId);\n      const newConn = await dbs.connections.update({ id: c.id }, changes, {\n        returning: \"*\",\n        multi: false,\n      });\n      if (!newConn) throw \"Unexpected: newConn missing\";\n      await connMgr.setOnMount(\n        connId,\n        newConn.on_mount_ts,\n        newConn.on_mount_ts_disabled,\n      );\n    },\n    setTableConfig: async (\n      connId: string,\n      changes: Partial<\n        Pick<\n          DBSSchema[\"database_configs\"],\n          \"table_config_ts\" | \"table_config_ts_disabled\"\n        >\n      >,\n    ) => {\n      if (isEmpty(changes)) {\n        throw \"No changes provided\";\n      }\n      const { dbConf } = await getConnectionAndDatabaseConfig(dbs, connId);\n      const newDbConf = await dbs.database_configs.update(\n        { id: dbConf.id },\n        changes,\n        { returning: \"*\", multi: false },\n      );\n      if (!newDbConf) throw \"Unexpected: newDbConf missing\";\n      await connMgr.setTableConfig(\n        connId,\n        newDbConf.table_config_ts,\n        newDbConf.table_config_ts_disabled,\n      );\n    },\n    getForkedProcStats: async (connId: string) => {\n      const prgl = connMgr.getConnection(connId);\n      const res = {\n        server: {\n          // cpu: os.cpus(),\n          mem: os.totalmem(),\n          freemem: os.freemem(),\n        },\n        methodRunner: await prgl.methodRunner?.getProcStats(),\n        onMountRunner: await prgl.onMountRunner?.getProcStats(),\n        tableConfigRunner: await prgl.tableConfigRunner?.getProcStats(),\n      };\n      return res;\n    },\n    getNodeTypes,\n    installMCPServer: async (name: string) => {\n      return installMCPServer(dbs, name);\n    },\n    getMCPServersStatus: (serverName: string) =>\n      getMCPServersStatus(dbs, serverName),\n    callMCPServerTool: async (\n      chatId: number,\n      serverName: string,\n      toolName: string,\n      args: Record<string, unknown> | undefined,\n    ) => {\n      const res = await callMCPServerTool(\n        user,\n        chatId,\n        dbs,\n        serverName,\n        toolName,\n        args,\n        clientReq,\n      );\n      return res;\n    },\n    reloadMcpServerTools: async (serverName: string) =>\n      reloadMcpServerTools(dbs, serverName),\n    getMcpHostInfo,\n    transcribeAudio: async (audioBlob: Blob) => {\n      const speechToTextService = servicesManager.getService(\"speechToText\");\n      if (speechToTextService?.status !== \"running\") {\n        throw \"Speech to Text service is not enabled/running\";\n      }\n      const formData = new FormData();\n      const audioBlobWithMime = new Blob([audioBlob], { type: \"audio/webm\" });\n\n      formData.append(\"audio\", audioBlobWithMime, \"recording.webm\");\n      const result =\n        await speechToTextService.endpoints[\"/transcribe\"](formData);\n\n      return result;\n    },\n  };\n\n  const isAdmin = user.type === \"admin\";\n  const accessRules = isAdmin ? undefined : await getACRules(dbs, user);\n  const allowedLLMCreds =\n    isAdmin ? undefined\n    : !accessRules?.length ? undefined\n    : await dbs.access_control_allowed_llm.find({\n        access_control_id: { $in: accessRules.map((ac) => ac.id) },\n      });\n  const userMethods = {\n    ...((allowedLLMCreds || isAdmin) && {\n      askLLM: async (\n        connectionId: string,\n        userMessage: LLMMessage[\"message\"],\n        schema: string,\n        chatId: number,\n        type: \"new-message\" | \"approve-tool-use\",\n      ) => {\n        await askLLM({\n          connectionId,\n          userMessage,\n          schema,\n          chatId,\n          dbs,\n          user,\n          allowedLLMCreds,\n          accessRules,\n          clientReq,\n          type,\n          aborter: undefined,\n        });\n      },\n    }),\n    getFullPrompt,\n    stopAskLLM: async (chatId: number) => {\n      if (!chatId) throw \"Chat ID is required\";\n      const chat = await dbs.llm_chats.findOne({ id: chatId });\n      if (!chat) throw \"Chat not found\";\n      if (chat.user_id !== user.id && user.type !== \"admin\") {\n        throw \"You are not allowed to stop this chat\";\n      }\n      stopAskLLM(chatId);\n      await dbs.llm_chats.update({ id: chatId }, { status: null });\n    },\n    sendFeedback: async ({\n      details,\n      email,\n    }: {\n      details: string;\n      email?: string;\n    }) => {\n      await fetch(\"https://prostgles.com/feedback\", {\n        method: \"POST\",\n        headers: {\n          Accept: \"application/json\",\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({ details, email }),\n      });\n    },\n    prostglesSignup,\n    generateToken: async (days: number) => {\n      if (!Number.isInteger(days)) {\n        throw \"Expecting an integer days but got: \" + days;\n      }\n\n      if (!socket) throw \"Socket missing\";\n      const ip_address = (socket as any).conn.remoteAddress as string;\n      const session = await dbs.sessions.insert(\n        {\n          expires: Date.now() + days * 24 * 3600 * 1000,\n          user_id: user.id,\n          user_type: user.type,\n          type: \"api_token\",\n          ip_address,\n          id: createSessionSecret(),\n        },\n        { returning: \"*\" },\n      );\n\n      return session.id;\n    },\n    create2FA: async () => {\n      const userName = user.username;\n      const service = \"Prostgles UI\";\n      const secret = authenticator.generateSecret();\n      const otpauth = authenticator.keyuri(userName, service, secret);\n\n      const recoveryCode = crypto.randomBytes(26).toString(\"hex\");\n      const hashedRecoveryCode = getPasswordHash(user, recoveryCode);\n      await dbs.users.update(\n        { id: user.id },\n        { \"2fa\": { secret, recoveryCode: hashedRecoveryCode, enabled: false } },\n      );\n      return {\n        url: otpauth,\n        secret,\n        recoveryCode,\n      };\n    },\n    enable2FA: async (token: string) => {\n      const latestUser = await dbs.users.findOne({ id: user.id });\n      const secret = latestUser?.[\"2fa\"]?.secret;\n      if (!secret) throw \"Secret not found\";\n\n      //totp.verify({ secret, token }) -> Does not work.\n      const isValid = authenticator.check(token, secret);\n\n      if (!isValid) throw \"Invalid code\";\n      await dbs.users.update(\n        { id: user.id },\n        { \"2fa\": { ...latestUser[\"2fa\"]!, enabled: true } },\n      );\n\n      /** Log out all web sessions after enabling 2fa */\n      await dbs.sessions.update(\n        {\n          user_id: user.id,\n          type: \"web\",\n        },\n        { type: \"web\", active: false },\n      );\n      return \"ok\";\n    },\n    disable2FA: () => {\n      return dbs.users.update({ id: user.id }, { \"2fa\": null });\n    },\n    changePassword: async (oldPassword: string, newPassword: string) => {\n      const hashedCurrentPassword = getPasswordHash(user, oldPassword);\n      if (user.password !== hashedCurrentPassword)\n        throw \"Old password is incorrect\";\n      const hashedNewPassword = getPasswordHash(user, newPassword);\n      await dbs.users.update({ id: user.id }, { password: hashedNewPassword });\n    },\n    getLLMAllowedChatTools: async (\n      chatId: number,\n    ): Promise<AllowedChatTool[] | undefined> => {\n      const chat = await dbs.llm_chats.findOne({ id: chatId });\n      if (!chat || chat.user_id !== user.id) throw \"Invalid chat\";\n      const connectionId = chat.connection_id;\n      if (!connectionId) throw \"Chat connection_id not found\";\n      if (!chat.llm_prompt_id) throw \"Chat prompt_id not found\";\n      const prompt = await dbs.llm_prompts.findOne({ id: chat.llm_prompt_id });\n      if (!prompt) throw \"Chat prompt not found\";\n      const allowedTools = await getLLMToolsAllowedInThisChat({\n        chat,\n        userType: user.type,\n        dbs,\n        prompt,\n        connectionId,\n        clientReq,\n      });\n      return allowedTools;\n    },\n  };\n\n  return {\n    ...userMethods,\n    ...(user.type === \"admin\" ? adminMethods : undefined),\n    startConnection: async (con_id: string) => {\n      try {\n        const socketPath = await connMgr.startConnection(\n          con_id,\n          dbs,\n          _dbs,\n          socket,\n        );\n        return socketPath;\n      } catch (error) {\n        console.error(\"Could not start connection \" + con_id, error);\n        // Used to prevent data leak to client\n        if (user.type === \"admin\") {\n          throw error;\n        } else {\n          throw `Something went wrong when connecting to ${con_id}`;\n        }\n      }\n    },\n  };\n};\n\nexport const runConnectionQuery = async (\n  connId: string,\n  query: string,\n  args?: AnyObject | any[],\n  asAdminOpts?: { dbs: DBS },\n): Promise<AnyObject[]> => {\n  const { db } =\n    asAdminOpts ?\n      await getSuperUserCDB(connId, asAdminOpts.dbs)\n    : await getCDB(connId);\n  return db.any(query, args);\n};\n"
  },
  {
    "path": "server/src/publishMethods/setFileStorage.ts",
    "content": "import type { ConnectionTableConfig } from \"@src/ConnectionManager/ConnectionManager\";\nimport { assertJSONBObjectAgainstSchema, pickKeys } from \"prostgles-types\";\nimport { connMgr, type DBS } from \"..\";\nimport { getConnectionAndDatabaseConfig } from \"./getConnectionAndDatabaseConfig\";\n\nexport const setFileStorage = async (\n  dbs: DBS,\n  connId: string,\n  tableConfig?: ConnectionTableConfig,\n  opts?: { keepS3Data?: boolean; keepFileTable?: boolean },\n) => {\n  const { db, dbConf } = await getConnectionAndDatabaseConfig(dbs, connId);\n\n  let newTableConfig: ConnectionTableConfig | null =\n    tableConfig ?\n      {\n        ...tableConfig,\n      }\n    : null;\n\n  /** Enable file storage */\n  if (tableConfig) {\n    if (typeof tableConfig.referencedTables !== \"undefined\") {\n      assertJSONBObjectAgainstSchema(\n        { referencedTables: { record: { values: \"any\", partial: true } } },\n        tableConfig,\n        \"referencedTables\",\n        false,\n      );\n    }\n    if (tableConfig.referencedTables && Object.keys(tableConfig).length === 1) {\n      if (!dbConf.file_table_config) throw \"Must enable file storage first\";\n      newTableConfig = { ...dbConf.file_table_config, ...tableConfig };\n    } else {\n      assertJSONBObjectAgainstSchema(\n        {\n          fileTable: \"string\",\n          storageType: {\n            oneOfType: [\n              {\n                type: { enum: [\"S3\"] },\n                credential_id: \"number\",\n              },\n              {\n                type: { enum: [\"local\"] },\n              },\n            ],\n          },\n        },\n        tableConfig as any,\n        \"tableConfig\",\n        false,\n      );\n      const { storageType } = tableConfig;\n\n      if (storageType.type === \"S3\") {\n        if (\n          !(await dbs.credentials.findOne({\n            id: storageType.credential_id,\n          }))\n        ) {\n          throw \"Invalid credential_id provided\";\n        }\n      }\n      const KEYS = [\"fileTable\", \"storageType\"] as const;\n      if (\n        dbConf.file_table_config &&\n        JSON.stringify(pickKeys(dbConf.file_table_config, KEYS.slice(0))) !==\n          JSON.stringify(pickKeys(tableConfig, KEYS.slice(0)))\n      ) {\n        throw \"Cannot update \" + KEYS.join(\"or\");\n      }\n\n      newTableConfig = tableConfig;\n    }\n\n    /** Disable current file storage */\n  } else {\n    const fileTable = dbConf.file_table_config?.fileTable;\n    if (!fileTable) throw \"Unexpected: fileTable already disabled\";\n    await db.tx(async (dbTX) => {\n      const fileTableHandler = dbTX[fileTable];\n      if (!fileTableHandler)\n        throw \"Unexpected: fileTable table handler missing\";\n      if (\n        dbConf.file_table_config?.fileTable &&\n        (dbConf.file_table_config.storageType.type === \"local\" ||\n          !opts?.keepS3Data)\n      ) {\n        if (!fileTable || !fileTableHandler.delete) {\n          throw \"Unexpected error. fileTable handler not found\";\n        }\n\n        await fileTableHandler.delete({});\n      }\n      if (!opts?.keepFileTable) {\n        await dbTX.sql!(\"DROP TABLE ${fileTable:name} CASCADE\", {\n          fileTable,\n        });\n      }\n    });\n    newTableConfig = null;\n  }\n  const con = await dbs.connections.findOne({ id: connId });\n  if (!con) throw \"Connection not found\";\n  await dbs\n    .tx(async (t) => {\n      await connMgr.setFileTable(con, newTableConfig);\n      await t.database_configs.update(\n        { id: dbConf.id },\n        { file_table_config: newTableConfig },\n      );\n    })\n    .catch((err) => {\n      console.log({ err });\n      return Promise.reject(err);\n    });\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfig.ts",
    "content": "import { CONNECTION_CONFIG_SECTIONS } from \"@common/utils\";\nimport type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport type { JSONB } from \"prostgles-types\";\nimport { loggerTableConfig } from \"../Logger\";\nimport { tableConfigAccessControl } from \"./tableConfigAccessControl\";\nimport { DUMP_OPTIONS_SCHEMA, tableConfigBackups } from \"./tableConfigBackups\";\nimport { tableConfigConnections } from \"./tableConfigConnections\";\nimport { tableConfigGlobalSettings } from \"./tableConfigGlobalSettings\";\nimport { tableConfigLinks } from \"./tableConfigLinks\";\nimport { tableConfigLLM } from \"./tableConfigLlm/tableConfigLlm\";\nimport { tableConfigMCPServers } from \"./tableConfigMCPServers\";\nimport { tableConfigPublishedMethods } from \"./tableConfigPublishedMethods\";\nimport { tableConfigUsers } from \"./tableConfigUsers\";\nimport { tableConfigWindows } from \"./tableConfigWindows\";\nimport { tableConfigWorkspaces } from \"./tableConfigWorkspaces\";\n\nexport const UNIQUE_DB_COLS = [\"db_name\", \"db_host\", \"db_port\"] as const;\nconst UNIQUE_DB_FIELDLIST = UNIQUE_DB_COLS.join(\", \");\n\nconst tableConfigSchema: JSONB.JSONBSchema = {\n  record: {\n    values: {\n      oneOfType: [\n        {\n          isLookupTable: {\n            type: {\n              values: {\n                record: { values: { type: \"string\", optional: true } },\n              },\n            },\n          },\n        },\n        {\n          columns: {\n            description: \"Column definitions and hints\",\n            record: {\n              values: {\n                oneOf: [\n                  \"string\",\n                  {\n                    type: {\n                      hint: { type: \"string\", optional: true },\n                      nullable: { type: \"boolean\", optional: true },\n                      isText: { type: \"boolean\", optional: true },\n                      trimmed: { type: \"boolean\", optional: true },\n                      defaultValue: { type: \"any\", optional: true },\n                    },\n                  },\n                  {\n                    type: {\n                      jsonbSchema: {\n                        oneOfType: [\n                          {\n                            type: {\n                              enum: [\n                                \"string\",\n                                \"number\",\n                                \"boolean\",\n                                \"Date\",\n                                \"time\",\n                                \"timestamp\",\n                                \"string[]\",\n                                \"number[]\",\n                                \"boolean[]\",\n                                \"Date[]\",\n                                \"time[]\",\n                                \"timestamp[]\",\n                              ],\n                            },\n                            optional: { type: \"boolean\", optional: true },\n                            description: { type: \"string\", optional: true },\n                          },\n                          {\n                            type: {\n                              enum: [\"Lookup\", \"Lookup[]\"],\n                            },\n                            optional: { type: \"boolean\", optional: true },\n                            description: { type: \"string\", optional: true },\n                          },\n                          {\n                            type: {\n                              enum: [\"object\"],\n                            },\n                            optional: { type: \"boolean\", optional: true },\n                            description: { type: \"string\", optional: true },\n                          },\n                        ],\n                      },\n                    },\n                  },\n                ],\n              },\n            },\n          },\n        },\n      ],\n    },\n  },\n};\n\nconst SESSION_TYPE = {\n  enum: [\"web\", \"api_token\", \"mobile\"],\n  defaultValue: \"web\",\n  nullable: false,\n} as const;\n\nexport const tableConfig: TableConfig<{ en: 1 }> = {\n  user_types: {\n    isLookupTable: {\n      values: {\n        admin: {\n          description: \"Highest access level\",\n        },\n        public: {\n          description:\n            \"Public user. Account created on login and deleted on logout\",\n        },\n        default: {},\n      },\n    },\n    triggers: {\n      atLeastOneAdminAndPublic: {\n        actions: [\"delete\", \"update\"],\n        type: \"after\",\n        forEach: \"statement\",\n        query: ` \n          BEGIN\n            IF NOT EXISTS(SELECT * FROM user_types WHERE id = 'admin') \n              OR NOT EXISTS(SELECT * FROM user_types WHERE id = 'public')\n            THEN\n              RAISE EXCEPTION 'admin and public user types cannot be deleted/modified';\n            END IF;\n  \n            RETURN NULL;\n          END;\n        `,\n      },\n    },\n  },\n  user_statuses: {\n    isLookupTable: {\n      values: { active: {}, disabled: {}, public: {} },\n    },\n  },\n\n  ...tableConfigUsers,\n\n  session_types: {\n    isLookupTable: {\n      values: { web: {}, api_token: {}, mobile: {} },\n    },\n  },\n\n  sessions: {\n    columns: {\n      id: `TEXT UNIQUE NOT NULL`,\n      id_num: `SERIAL PRIMARY KEY`,\n      user_id: `UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE`,\n      name: `TEXT`,\n      socket_id: `TEXT`,\n      user_type: `TEXT NOT NULL`,\n      is_mobile: `BOOLEAN DEFAULT FALSE`,\n      is_connected: `BOOLEAN DEFAULT FALSE`,\n      active: `BOOLEAN DEFAULT TRUE`,\n      project_id: `TEXT`,\n      ip_address: `INET NOT NULL`,\n      type: `TEXT NOT NULL REFERENCES session_types`,\n      user_agent: \"TEXT\",\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n      last_used: `TIMESTAMPTZ DEFAULT NOW()`,\n      expires: `BIGINT NOT NULL`,\n    },\n  },\n\n  services: {\n    columns: {\n      name: `TEXT PRIMARY KEY`,\n      label: `TEXT NOT NULL UNIQUE`,\n      description: `TEXT`,\n      icon: `TEXT NOT NULL`,\n      default_port: `INTEGER NOT NULL`,\n      build_hash: `TEXT`,\n      status: {\n        enum: [\n          \"stopped\",\n          \"starting\",\n          \"running\",\n          \"error\",\n          \"building\",\n          \"building-done\",\n          \"build-error\",\n        ],\n      },\n      configs: {\n        nullable: true,\n        jsonbSchema: {\n          record: {\n            values: {\n              type: {\n                label: \"string\",\n                description: \"string\",\n                defaultOption: \"string\",\n                options: {\n                  record: {\n                    values: {\n                      type: {\n                        label: { type: \"string\", optional: true },\n                        env: { record: { values: \"string\" } },\n                      },\n                    },\n                  },\n                },\n              },\n            },\n          },\n        },\n      },\n      selected_config_options: {\n        nullable: true,\n        jsonbSchema: {\n          record: {\n            values: { type: \"string\" },\n          },\n        },\n      },\n      logs: `TEXT`,\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n    },\n  },\n\n  login_attempts: {\n    // dropIfExists: true,\n    columns: {\n      id: `BIGSERIAL PRIMARY KEY`,\n      type: SESSION_TYPE,\n      auth_type: {\n        enum: [\n          \"session-id\",\n          \"registration\",\n          \"email-confirmation\",\n          \"magic-link-registration\",\n          \"magic-link\",\n          \"otp-code\",\n          \"login\",\n          \"oauth\",\n        ],\n      },\n      username: \"TEXT\",\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n      failed: \"BOOLEAN\",\n      magic_link_id: \"TEXT\",\n      sid: \"TEXT\",\n      auth_provider:\n        \"TEXT CHECK(auth_type <> 'oauth' OR auth_provider IS NOT NULL)\",\n      ip_address: `INET NOT NULL`,\n      ip_address_remote: \"TEXT NOT NULL\",\n      x_real_ip: \"TEXT NOT NULL\",\n      user_agent: \"TEXT NOT NULL\",\n      info: \"TEXT\",\n    },\n  },\n  database_configs: {\n    constraints: {\n      uniqueDatabase: { type: \"UNIQUE\", content: UNIQUE_DB_FIELDLIST },\n    },\n    columns: {\n      id: `SERIAL PRIMARY KEY`,\n      db_name: `TEXT NOT NULL`,\n      db_host: `TEXT NOT NULL`,\n      db_port: `INTEGER NOT NULL`,\n      rest_api_enabled: `BOOLEAN DEFAULT FALSE`,\n      sync_users: `BOOLEAN DEFAULT FALSE`,\n      table_config: {\n        info: { hint: `Table configurations` },\n        nullable: true,\n        jsonbSchema: tableConfigSchema,\n      },\n      table_config_ts: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Table configurations from typescript. Must export const tableConfig`,\n        },\n      },\n      table_config_ts_disabled: {\n        sqlDefinition: \"BOOLEAN\",\n        info: {\n          hint: `If true then Table configurations will not be executed`,\n        },\n      },\n      table_schema_positions: {\n        nullable: true,\n        jsonbSchema: {\n          record: {\n            partial: true,\n            values: {\n              type: {\n                x: \"number\",\n                y: \"number\",\n              },\n            },\n          },\n        },\n      },\n      table_schema_transform: {\n        nullable: true,\n        jsonbSchemaType: {\n          translate: {\n            type: {\n              x: \"number\",\n              y: \"number\",\n            },\n          },\n          scale: \"number\",\n        },\n      },\n      file_table_config: {\n        info: { hint: `File storage configurations` },\n        nullable: true,\n        jsonbSchemaType: {\n          fileTable: { type: \"string\", optional: true },\n          storageType: {\n            oneOfType: [\n              { type: { enum: [\"local\"] } },\n              {\n                type: { enum: [\"S3\"] },\n                credential_id: { type: \"number\" },\n              },\n            ],\n          },\n          referencedTables: { type: \"any\", optional: true },\n          delayedDelete: {\n            optional: true,\n            type: {\n              /**\n               * Minimum amount of time measured in days for which the files will not be deleted after requesting delete\n               */\n              deleteAfterNDays: { type: \"number\" },\n              /**\n               * How freuquently the files will be checked for deletion delay\n               */\n              checkIntervalHours: { type: \"number\", optional: true },\n            },\n          },\n        },\n      },\n\n      backups_config: {\n        nullable: true,\n        info: { hint: `Automatic backups configurations` },\n        jsonbSchemaType: {\n          enabled: { type: \"boolean\", optional: true },\n          cloudConfig: {\n            nullable: true,\n            type: {\n              /**\n               * If not provided then save to current server\n               */\n              credential_id: { type: \"number\", nullable: true, optional: true },\n            },\n          },\n          frequency: { enum: [\"daily\", \"monthly\", \"weekly\", \"hourly\"] },\n\n          /**\n           * If provided then will do the backup during that hour (24 hour format). Unless the backup frequency is less than a day\n           */\n          hour: { type: \"integer\", optional: true },\n\n          /**\n           * If provided then will do the backup during that hour (1-7: Mon to Sun). Unless the backup frequency is less than a day\n           */\n          dayOfWeek: { type: \"integer\", optional: true },\n\n          /**\n           * If provided then will do the backup during that day (1-31) or earlier if the month is shorter. Unless the backup frequency is not monthly\n           */\n          dayOfMonth: { type: \"integer\", optional: true },\n\n          /**\n           * If provided then will keep the latest N backups and delete the older ones\n           */\n          keepLast: { type: \"integer\", optional: true },\n\n          /**\n           * If not enough space will show this error\n           */\n          err: { type: \"string\", optional: true, nullable: true },\n\n          dump_options: DUMP_OPTIONS_SCHEMA.jsonbSchema,\n        },\n      },\n    },\n  },\n  database_config_logs: {\n    columns: {\n      id: `SERIAL PRIMARY KEY REFERENCES database_configs (id) ON DELETE CASCADE`,\n      on_mount_logs: {\n        sqlDefinition: \"TEXT\",\n        info: { hint: `On mount logs` },\n      },\n      table_config_logs: {\n        sqlDefinition: \"TEXT\",\n        info: { hint: `On mount logs` },\n      },\n      on_run_logs: {\n        sqlDefinition: \"TEXT\",\n        info: { hint: `On mount logs` },\n      },\n    },\n  },\n\n  ...tableConfigConnections,\n\n  alerts: {\n    columns: {\n      id: `BIGSERIAL PRIMARY KEY`,\n      title: \"TEXT\",\n      message: \"TEXT\",\n      severity: { enum: [\"info\", \"warning\", \"error\"] },\n      database_config_id:\n        \"INTEGER REFERENCES database_configs(id) ON DELETE SET NULL\",\n      connection_id: \"UUID REFERENCES connections(id) ON DELETE SET NULL\",\n      section: { enum: CONNECTION_CONFIG_SECTIONS, nullable: true },\n      data: \"JSONB\",\n      created: \"TIMESTAMPTZ DEFAULT NOW()\",\n    },\n  },\n  alert_viewed_by: {\n    columns: {\n      id: `BIGSERIAL PRIMARY KEY`,\n      alert_id: \"BIGINT REFERENCES alerts(id) ON DELETE CASCADE\",\n      user_id: \"UUID REFERENCES users(id) ON DELETE CASCADE\",\n      viewed: \"TIMESTAMPTZ DEFAULT NOW()\",\n    },\n  },\n\n  ...tableConfigPublishedMethods,\n\n  ...tableConfigAccessControl,\n\n  magic_links: {\n    // dropIfExistsCascade: true,\n    columns: {\n      id: `TEXT PRIMARY KEY DEFAULT gen_random_uuid()`,\n      user_id: `UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE`,\n      magic_link: `TEXT`,\n      magic_link_used: `TIMESTAMPTZ`,\n      expires: `BIGINT NOT NULL`,\n      session_expires: `BIGINT NOT NULL DEFAULT 0`,\n    },\n  },\n\n  credential_types: {\n    // dropIfExistsCascade: true,\n    isLookupTable: {\n      values: {\n        AWS: {\n          description: \"S3\",\n        },\n        Cloudflare: {\n          description: \"R2\",\n        },\n      },\n    },\n  },\n\n  credentials: {\n    // dropIfExists: true,\n    columns: {\n      id: `SERIAL PRIMARY KEY`,\n      name: { sqlDefinition: `TEXT`, info: { hint: \"optional\" } },\n      user_id: `UUID REFERENCES users(id) ON DELETE SET NULL`,\n      type: {\n        label: \"Provider\",\n        sqlDefinition: `TEXT NOT NULL REFERENCES credential_types(id) `,\n      },\n      key_id: `TEXT NOT NULL`,\n      key_secret: `TEXT NOT NULL`,\n      endpoint_url: `TEXT NOT NULL DEFAULT ''`,\n      bucket: { sqlDefinition: `TEXT` },\n      region: { sqlDefinition: `TEXT`, info: { hint: \"e.g. auto, us-east-1\" } },\n    },\n  },\n\n  ...tableConfigBackups,\n\n  ...tableConfigWorkspaces,\n\n  ...tableConfigWindows,\n\n  ...tableConfigGlobalSettings,\n\n  ...tableConfigLinks,\n\n  stats: {\n    columns: {\n      database_id: `INTEGER NOT NULL REFERENCES database_configs(id) ON DELETE CASCADE`,\n\n      datid: \"INTEGER\",\n      datname: \"TEXT\",\n      pid: \"INTEGER NOT NULL\",\n      usesysid: \"INTEGER\",\n      usename: {\n        sqlDefinition: \"TEXT\",\n        info: { hint: `Name of the user logged into this backend` },\n      },\n      application_name: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Name of the application that is connected to this backend`,\n        },\n      },\n      client_addr: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `IP address of the client connected to this backend. If this field is null, it indicates either that the client is connected via a Unix socket on the server machine or that this is an internal process such as autovacuum.`,\n        },\n      },\n      client_hostname: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Host name of the connected client, as reported by a reverse DNS lookup of client_addr. This field will only be non-null for IP connections, and only when log_hostname is enabled.`,\n        },\n      },\n      client_port: {\n        sqlDefinition: \"INTEGER\",\n        info: {\n          hint: `TCP port number that the client is using for communication with this backend, or -1 if a Unix socket is used. If this field is null, it indicates that this is an internal server process.`,\n        },\n      },\n      backend_start: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Time when this process was started. For client backends, this is the time the client connected to the server.`,\n        },\n      },\n      xact_start: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Time when this process' current transaction was started, or null if no transaction is active. If the current query is the first of its transaction, this column is equal to the query_start column.`,\n        },\n      },\n      query_start: {\n        sqlDefinition: \"TIMESTAMPTZ\",\n        info: {\n          hint: `Time when the currently active query was started, or if state is not active, when the last query was started`,\n        },\n      },\n      state_change: {\n        sqlDefinition: \"TEXT\",\n        info: { hint: `Time when the state was last changed` },\n      },\n      wait_event_type: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `The type of event for which the backend is waiting, if any; otherwise NULL. See Table 28.4.`,\n        },\n      },\n      wait_event: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Wait event name if backend is currently waiting, otherwise NULL. See Table 28.5 through Table 28.13.`,\n        },\n      },\n      state: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Current overall state of this backend. Possible values are: active: The backend is executing a query. idle: The backend is waiting for a new client command. idle in transaction: The backend is in a transaction, but is not currently executing a query. idle in transaction (aborted): This state is similar to idle in transaction, except one of the statements in the transaction caused an error. fastpath function call: The backend is executing a fast-path function. disabled: This state is reported if track_activities is disabled in this backend.`,\n        },\n      },\n      backend_xid: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Top-level transaction identifier of this backend, if any.`,\n        },\n      },\n      backend_xmin: {\n        sqlDefinition: \"TEXT\",\n        info: { hint: `The current backend's xmin horizon.` },\n      },\n      query: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Text of this backend's most recent query. If state is active this field shows the currently executing query. In all other states, it shows the last query that was executed. By default the query text is truncated at 1024 bytes; this value can be changed via the parameter track_activity_query_size.`,\n        },\n      },\n      backend_type: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Type of current backend. Possible types are autovacuum launcher, autovacuum worker, logical replication launcher, logical replication worker, parallel worker, background writer, client backend, checkpointer, archiver, startup, walreceiver, walsender and walwriter. In addition, background workers registered by extensions may have additional types.`,\n        },\n      },\n      blocked_by: {\n        sqlDefinition: \"INTEGER[]\",\n        info: {\n          hint: `Process ID(s) of the sessions that are blocking the server process with the specified process ID from acquiring a lock. One server process blocks another if it either holds a lock that conflicts with the blocked process's lock request (hard block), or is waiting for a lock that would conflict with the blocked process's lock request and is ahead of it in the wait queue (soft block). When using parallel queries the result always lists client-visible process IDs (that is, pg_backend_pid results) even if the actual lock is held or awaited by a child worker process. As a result of that, there may be duplicated PIDs in the result. Also note that when a prepared transaction holds a conflicting lock, it will be represented by a zero process ID.`,\n        },\n      },\n      blocked_by_num: \"INTEGER NOT NULL DEFAULT 0\",\n      id_query_hash: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `Computed query identifier (md5(pid || query)) used in stopping queries`,\n        },\n      },\n\n      cpu: {\n        sqlDefinition: \"NUMERIC\",\n        info: {\n          hint: `CPU Utilisation. CPU time used divided by the time the process has been running. It will not add up to 100% unless you are lucky`,\n        },\n      },\n      mem: {\n        sqlDefinition: \"NUMERIC\",\n        info: {\n          hint: `Ratio of the process's resident set size  to the physical memory on the machine, expressed as a percentage`,\n        },\n      },\n      memPretty: {\n        sqlDefinition: \"TEXT\",\n        info: { hint: `mem value as string` },\n      },\n      mhz: { sqlDefinition: \"TEXT\", info: { hint: `Core MHz value` } },\n      cmd: {\n        sqlDefinition: \"TEXT\",\n        info: { hint: `Command with all its arguments as a string` },\n      },\n      sampled_at: {\n        sqlDefinition: \"TIMESTAMPTZ NOT NULL DEFAULT NOW()\",\n        info: { hint: `When the statistics were collected` },\n      },\n    },\n    constraints: {\n      stats_pkey: \"PRIMARY KEY(pid, database_id)\",\n    },\n  },\n  ...tableConfigLLM,\n  ...loggerTableConfig,\n  ...tableConfigMCPServers,\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigAccessControl.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport type { JSONB } from \"prostgles-types\";\n\nconst FieldFilterSchema = {\n  oneOf: [\n    \"string[]\",\n    { enum: [\"*\", \"\"] },\n    {\n      record: {\n        values: { enum: [1, true] },\n      },\n    },\n    {\n      record: {\n        values: { enum: [0, false] },\n      },\n    },\n  ],\n} satisfies JSONB.FieldType;\n\nexport const tableConfigAccessControl: TableConfig<{ en: 1 }> = {\n  access_control: {\n    // dropIfExistsCascade: true,\n    columns: {\n      id: `SERIAL PRIMARY KEY`,\n      name: \"TEXT\",\n      database_id: `INTEGER NOT NULL REFERENCES database_configs(id) ON DELETE CASCADE`,\n      llm_daily_limit: {\n        sqlDefinition: `INTEGER NOT NULL DEFAULT 0 CHECK(llm_daily_limit >= 0)`,\n        info: { hint: \"Maximum amount of queires per user/ip per 24hours\" },\n      },\n      dbsPermissions: {\n        info: { hint: \"Permission types and rules for the state database\" },\n        nullable: true,\n        jsonbSchemaType: {\n          createWorkspaces: { type: \"boolean\", optional: true },\n          viewPublishedWorkspaces: {\n            optional: true,\n            type: {\n              workspaceIds: \"string[]\",\n            },\n          },\n        },\n      },\n      dbPermissions: {\n        info: {\n          hint: \"Permission types and rules for this (connection_id) database\",\n        },\n        jsonbSchema: {\n          oneOfType: [\n            {\n              type: {\n                enum: [\"Run SQL\"],\n                description: \"Allows complete access to the database\",\n              },\n              allowSQL: { type: \"boolean\", optional: true },\n            },\n            {\n              type: {\n                enum: [\"All views/tables\"],\n                description: \"Custom access (View/Edit/Remove) to all tables\",\n              },\n              allowAllTables: {\n                type: \"string[]\",\n                allowedValues: [\"select\", \"insert\", \"update\", \"delete\"],\n              },\n            },\n            {\n              type: {\n                enum: [\"Custom\"],\n                description: \"Fine grained access to specific tables\",\n              },\n              customTables: {\n                arrayOfType: {\n                  tableName: \"string\",\n                  select: {\n                    optional: true,\n                    description: \"Allows viewing data\",\n                    oneOf: [\n                      \"boolean\",\n                      {\n                        type: {\n                          fields: FieldFilterSchema,\n                          forcedFilterDetailed: { optional: true, type: \"any\" },\n                          subscribe: {\n                            optional: true,\n                            type: {\n                              throttle: { optional: true, type: \"integer\" },\n                            },\n                          },\n                          filterFields: {\n                            optional: true,\n                            ...FieldFilterSchema,\n                          },\n                          orderByFields: {\n                            optional: true,\n                            ...FieldFilterSchema,\n                          },\n                        },\n                      },\n                    ],\n                  },\n                  update: {\n                    optional: true,\n                    oneOf: [\n                      \"boolean\",\n                      {\n                        type: {\n                          fields: FieldFilterSchema,\n                          forcedFilterDetailed: { optional: true, type: \"any\" },\n                          checkFilterDetailed: { optional: true, type: \"any\" },\n                          filterFields: {\n                            optional: true,\n                            ...FieldFilterSchema,\n                          },\n                          orderByFields: {\n                            optional: true,\n                            ...FieldFilterSchema,\n                          },\n                          forcedDataDetail: { optional: true, type: \"any[]\" },\n                          dynamicFields: {\n                            optional: true,\n                            arrayOfType: {\n                              filterDetailed: \"any\",\n                              fields: FieldFilterSchema,\n                            },\n                          },\n                        },\n                      },\n                    ],\n                  },\n                  insert: {\n                    optional: true,\n                    oneOf: [\n                      \"boolean\",\n                      {\n                        type: {\n                          fields: FieldFilterSchema,\n                          forcedDataDetail: { optional: true, type: \"any[]\" },\n                          checkFilterDetailed: { optional: true, type: \"any\" },\n                        },\n                      },\n                    ],\n                  },\n                  delete: {\n                    optional: true,\n                    oneOf: [\n                      \"boolean\",\n                      {\n                        type: {\n                          filterFields: FieldFilterSchema,\n                          forcedFilterDetailed: { optional: true, type: \"any\" },\n                        },\n                      },\n                    ],\n                  },\n                  sync: {\n                    optional: true,\n                    type: {\n                      id_fields: { type: \"string[]\" },\n                      synced_field: { type: \"string\" },\n                      throttle: { optional: true, type: \"integer\" },\n                      allow_delete: { type: \"boolean\", optional: true },\n                    },\n                  },\n                },\n              },\n            },\n          ],\n        },\n      },\n\n      created: { sqlDefinition: `TIMESTAMPTZ DEFAULT NOW()` },\n    },\n  },\n\n  access_control_user_types: {\n    columns: {\n      access_control_id: `INTEGER NOT NULL REFERENCES access_control(id)  ON DELETE CASCADE`,\n      user_type: `TEXT NOT NULL REFERENCES user_types(id)  ON DELETE CASCADE`,\n    },\n    constraints: {\n      NoDupes: \"UNIQUE(access_control_id, user_type)\",\n    },\n  },\n\n  access_control_methods: {\n    // dropIfExistsCascade: true,\n    columns: {\n      published_method_id: `INTEGER NOT NULL REFERENCES published_methods  ON DELETE CASCADE`,\n      access_control_id: `INTEGER NOT NULL REFERENCES access_control  ON DELETE CASCADE`,\n    },\n    constraints: {\n      pkey: {\n        type: \"PRIMARY KEY\",\n        content: \"published_method_id, access_control_id\",\n      },\n    },\n  },\n  access_control_connections: {\n    columns: {\n      connection_id: `UUID NOT NULL REFERENCES connections(id) ON DELETE CASCADE`,\n      access_control_id: `INTEGER NOT NULL REFERENCES access_control  ON DELETE CASCADE`,\n    },\n    indexes: {\n      unique_connection_id: {\n        unique: true,\n        columns: \"connection_id, access_control_id\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigBackups.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\n\nexport const DUMP_OPTIONS_SCHEMA = {\n  jsonbSchema: {\n    oneOfType: [\n      {\n        command: { enum: [\"pg_dumpall\"] },\n        clean: { type: \"boolean\" },\n        dataOnly: { type: \"boolean\", optional: true },\n        globalsOnly: { type: \"boolean\", optional: true },\n        rolesOnly: { type: \"boolean\", optional: true },\n        schemaOnly: { type: \"boolean\", optional: true },\n        ifExists: { type: \"boolean\", optional: true },\n        encoding: { type: \"string\", optional: true },\n        keepLogs: { type: \"boolean\", optional: true },\n      },\n      {\n        command: { enum: [\"pg_dump\"] },\n        format: { enum: [\"p\", \"t\", \"c\"] },\n        dataOnly: { type: \"boolean\", optional: true },\n        clean: { type: \"boolean\", optional: true },\n        create: { type: \"boolean\", optional: true },\n        encoding: { type: \"string\", optional: true },\n        numberOfJobs: { type: \"integer\", optional: true },\n        noOwner: { type: \"boolean\", optional: true },\n\n        compressionLevel: { type: \"integer\", optional: true },\n        ifExists: { type: \"boolean\", optional: true },\n\n        keepLogs: { type: \"boolean\", optional: true },\n        excludeSchema: { type: \"string\", optional: true },\n        schemaOnly: { type: \"boolean\", optional: true },\n      },\n    ],\n  },\n} as const;\n\nexport const tableConfigBackups: TableConfig<{ en: 1 }> = {\n  backups: {\n    columns: {\n      id: {\n        sqlDefinition: `TEXT PRIMARY KEY DEFAULT gen_random_uuid()`,\n        info: { hint: \"Format: dbname_datetime_uuid\" },\n      },\n      name: {\n        sqlDefinition: `TEXT`,\n        info: { hint: \"Name of the backup\" },\n      },\n      connection_id: {\n        sqlDefinition: `UUID REFERENCES connections(id) ON DELETE SET NULL`,\n        info: { hint: \"If null then connection was deleted\" },\n      },\n      connection_details: {\n        sqlDefinition: `TEXT NOT NULL DEFAULT 'unknown connection' `,\n      },\n      credential_id: {\n        sqlDefinition: `INTEGER REFERENCES credentials(id) `,\n        info: { hint: \"If null then uploaded locally\" },\n      },\n      destination: {\n        enum: [\"Local\", \"Cloud\", \"None (temp stream)\"],\n        nullable: false,\n      },\n      dump_command: { sqlDefinition: `TEXT NOT NULL` },\n      restore_command: { sqlDefinition: `TEXT` },\n      local_filepath: { sqlDefinition: `TEXT` },\n      content_type: {\n        sqlDefinition: `TEXT NOT NULL DEFAULT 'application/gzip'`,\n      },\n      initiator: { sqlDefinition: `TEXT` },\n      details: { sqlDefinition: `JSONB` },\n      status: {\n        jsonbSchema: {\n          oneOfType: [\n            { ok: { type: \"string\" } },\n            { err: { type: \"string\" } },\n            // { cancelled: { type: \"number\" } },\n            {\n              loading: {\n                optional: true,\n                type: {\n                  loaded: { type: \"number\" },\n                  total: { type: \"number\", optional: true },\n                },\n              },\n            },\n          ],\n        },\n      },\n      uploaded: { sqlDefinition: `TIMESTAMPTZ` },\n      restore_status: {\n        nullable: true,\n        jsonbSchema: {\n          oneOfType: [\n            { ok: { type: \"string\" } },\n            { err: { type: \"string\" } },\n            {\n              loading: {\n                type: {\n                  loaded: { type: \"number\" },\n                  total: { type: \"number\" },\n                },\n              },\n            },\n          ],\n        },\n      },\n      restore_start: { sqlDefinition: `TIMESTAMPTZ` },\n      restore_end: { sqlDefinition: `TIMESTAMPTZ` },\n      restore_logs: { sqlDefinition: `TEXT` },\n      dump_logs: { sqlDefinition: `TEXT` },\n      dbSizeInBytes: {\n        sqlDefinition: `BIGINT NOT NULL`,\n        label: \"Database size on disk\",\n      },\n      sizeInBytes: { sqlDefinition: `BIGINT`, label: \"Backup file size\" },\n      created: { sqlDefinition: `TIMESTAMPTZ NOT NULL DEFAULT NOW()` },\n      last_updated: { sqlDefinition: `TIMESTAMPTZ NOT NULL DEFAULT NOW()` },\n      options: DUMP_OPTIONS_SCHEMA,\n      restore_options: {\n        jsonbSchemaType: {\n          command: { enum: [\"pg_restore\", \"psql\"] },\n          format: { enum: [\"p\", \"t\", \"c\"] },\n          clean: { type: \"boolean\" },\n          excludeSchema: { type: \"string\", optional: true },\n          newDbName: { type: \"string\", optional: true },\n          create: { type: \"boolean\", optional: true },\n          dataOnly: { type: \"boolean\", optional: true },\n          noOwner: { type: \"boolean\", optional: true },\n          numberOfJobs: { type: \"integer\", optional: true },\n\n          ifExists: { type: \"boolean\", optional: true },\n\n          keepLogs: { type: \"boolean\", optional: true },\n        },\n        defaultValue: `{ \"clean\": true, \"format\": \"c\", \"command\": \"pg_restore\" }`,\n      },\n    },\n    indexes: {\n      unique_name_per_connection: {\n        columns: \"name, connection_id\",\n        unique: true,\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigConnections.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\n\nconst UNIQUE_DB_COLS = [\"db_name\", \"db_host\", \"db_port\"] as const;\nconst UNIQUE_DB_FIELDLIST = UNIQUE_DB_COLS.join(\", \");\n\nexport const DB_SSL_ENUM = [\n  \"disable\",\n  \"allow\",\n  \"prefer\",\n  \"require\",\n  \"verify-ca\",\n  \"verify-full\",\n] as const;\n\nexport const tableConfigConnections: TableConfig<{ en: 1 }> = {\n  connections: {\n    columns: {\n      id: `UUID PRIMARY KEY DEFAULT gen_random_uuid()`,\n      url_path: {\n        sqlDefinition: `TEXT CHECK(LENGTH(url_path) = 0 OR url_path ~ '^[a-z_0-9-]+$')`,\n        info: {\n          hint: `URL path to be used instead of the connection uuid`,\n        },\n      },\n      user_id: `UUID REFERENCES users(id) ON DELETE CASCADE`,\n      name: `TEXT NOT NULL CHECK(LENGTH(name) > 0)`,\n      db_name: `TEXT NOT NULL CHECK(LENGTH(db_name) > 0)`,\n      db_host: `TEXT NOT NULL DEFAULT 'localhost'`,\n      db_port: `INTEGER NOT NULL DEFAULT 5432`,\n      db_user: `TEXT NOT NULL DEFAULT ''`,\n      db_pass: `TEXT DEFAULT ''`,\n      db_connection_timeout: `INTEGER CHECK(db_connection_timeout > 0)`,\n      db_schema_filter: {\n        jsonbSchema: {\n          oneOf: [\n            { record: { values: { enum: [1] } } },\n            { record: { values: { enum: [0] } } },\n          ],\n        },\n        nullable: true,\n      },\n      db_ssl: { enum: DB_SSL_ENUM, nullable: false, defaultValue: \"disable\" },\n      ssl_certificate: { sqlDefinition: `TEXT` },\n      ssl_client_certificate: { sqlDefinition: `TEXT` },\n      ssl_client_certificate_key: { sqlDefinition: `TEXT` },\n      ssl_reject_unauthorized: {\n        sqlDefinition: `BOOLEAN`,\n        info: {\n          hint: `If true, the server certificate is verified against the list of supplied CAs. \\nAn error event is emitted if verification fails`,\n        },\n      },\n      db_conn: { sqlDefinition: `TEXT DEFAULT ''` },\n      db_watch_shema: { sqlDefinition: `BOOLEAN DEFAULT TRUE` },\n      disable_realtime: {\n        sqlDefinition: `BOOLEAN DEFAULT FALSE`,\n        info: {\n          hint: `If true then subscriptions and syncs will not work. Used to ensure prostgles schema is not created and nothing is changed in the database`,\n        },\n      },\n      prgl_url: { sqlDefinition: `TEXT` },\n      prgl_params: { sqlDefinition: `JSONB` },\n      type: {\n        enum: [\"Standard\", \"Connection URI\", \"Prostgles\"],\n        nullable: false,\n      },\n      is_state_db: {\n        sqlDefinition: `BOOLEAN`,\n        info: { hint: `If true then this DB is used to run the dashboard` },\n      },\n      on_mount_ts: {\n        sqlDefinition: \"TEXT\",\n        info: {\n          hint: `On mount typescript function. Must export const onMount`,\n        },\n      },\n      on_mount_ts_disabled: {\n        sqlDefinition: \"BOOLEAN\",\n        info: { hint: `If true then On mount typescript will not be executed` },\n      },\n      info: {\n        jsonbSchemaType: {\n          canCreateDb: {\n            type: \"boolean\",\n            optional: true,\n            description:\n              \"True if postgres user is allowed to create databases. Never gets updated\",\n          },\n        },\n        nullable: true,\n      },\n      table_options: {\n        nullable: true,\n        jsonbSchema: {\n          record: {\n            partial: true,\n            values: {\n              type: {\n                icon: { type: \"string\", optional: true },\n                label: { type: \"string\", optional: true },\n                rowIconColumn: { type: \"string\", optional: true },\n                columns: {\n                  optional: true,\n                  record: {\n                    partial: true,\n                    values: {\n                      type: {\n                        icon: { type: \"string\", optional: true },\n                      },\n                    },\n                  },\n                },\n                card: {\n                  optional: true,\n                  type: {\n                    headerColumn: { type: \"string\", optional: true },\n                  },\n                },\n              },\n            },\n          },\n        },\n      },\n      display_options: {\n        nullable: true,\n        jsonbSchemaType: {\n          prettyTableAndColumnNames: {\n            type: \"boolean\",\n            title: \"Pretty table and column names\",\n            description:\n              \"Defaults to true. Whether to show pretty table names by converting underscores to spaces and capitalising words\",\n          },\n        },\n      },\n      config: {\n        jsonbSchemaType: { enabled: \"boolean\", path: \"string\" },\n        nullable: true,\n      },\n      created: { sqlDefinition: `TIMESTAMPTZ DEFAULT NOW()` },\n      last_updated: { sqlDefinition: `BIGINT NOT NULL DEFAULT 0` },\n    },\n    constraints: {\n      unique_connection_url_path: `UNIQUE(url_path)`,\n      uniqueConName: `UNIQUE(name, user_id)`,\n      \"Check connection type\": `CHECK (\n            type IN ('Standard', 'Connection URI', 'Prostgles') \n            AND (type <> 'Connection URI' OR length(db_conn) > 1) \n            AND (type <> 'Standard' OR length(db_host) > 1) \n            AND (type <> 'Prostgles' OR length(prgl_url) > 0)\n          )`,\n      database_config_fkey: `FOREIGN KEY (${UNIQUE_DB_FIELDLIST}) REFERENCES database_configs( ${UNIQUE_DB_FIELDLIST} )`,\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigGlobalSettings.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport type { JSONB } from \"prostgles-types\";\nimport { OAuthProviderOptions } from \"@common/OAuthUtils\";\n\nconst commonAuthSchema = {\n  enabled: { type: \"boolean\", optional: true },\n  clientID: { type: \"string\" },\n  clientSecret: { type: \"string\" },\n} satisfies JSONB.FieldTypeObj[\"type\"];\n\nconst EmailTemplateConfig = {\n  title:\n    \"Email template used for sending auth emails. Must contain placeholders for the url: ${url}\",\n  type: {\n    from: \"string\",\n    subject: \"string\",\n    body: \"string\",\n  },\n} as const satisfies JSONB.FieldTypeObj;\nconst SMTPConfig = {\n  oneOfType: [\n    {\n      type: { enum: [\"smtp\"] },\n      host: { type: \"string\" },\n      port: { type: \"number\" },\n      secure: { type: \"boolean\", optional: true },\n      rejectUnauthorized: { type: \"boolean\", optional: true },\n      user: { type: \"string\" },\n      pass: { type: \"string\" },\n    },\n    {\n      type: { enum: [\"aws-ses\"] },\n      region: { type: \"string\" },\n      accessKeyId: { type: \"string\" },\n      secretAccessKey: { type: \"string\" },\n      /**\n       * Sending rate per second\n       * Defaults to 1\n       */\n      sendingRate: { type: \"integer\", optional: true },\n    },\n  ],\n} as const satisfies JSONB.FieldTypeObj;\n\nexport const tableConfigGlobalSettings: TableConfig<{ en: 1 }> = {\n  global_settings: {\n    // dropIfExistsCascade: true,\n    columns: {\n      id: \"INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY\",\n      allowed_origin: {\n        sqlDefinition: \"TEXT\",\n        label: \"Allow-Origin\",\n        info: {\n          hint: \"Specifies which domains can access this app in a cross-origin manner. \\nSets the Access-Control-Allow-Origin header. \\nUse '*' or a specific URL to allow API access\",\n        },\n      },\n      allowed_ips: {\n        sqlDefinition: `cidr[] NOT NULL DEFAULT '{}'`,\n        label: \"Allowed IPs and subnets\",\n        info: { hint: \"List of allowed IP addresses in ipv4 or ipv6 format\" },\n      },\n      allowed_ips_enabled: {\n        sqlDefinition: `BOOLEAN NOT NULL DEFAULT FALSE CHECK(allowed_ips_enabled = FALSE OR cardinality(allowed_ips) > 0)`,\n        info: { hint: \"If enabled then only allowed IPs can connect\" },\n      },\n      trust_proxy: {\n        sqlDefinition: `boolean NOT NULL DEFAULT FALSE`,\n        info: {\n          hint: \"If true then will use the IP from 'X-Forwarded-For' header\",\n        },\n      },\n      enable_logs: {\n        sqlDefinition: `boolean NOT NULL DEFAULT FALSE`,\n        info: {\n          hint: \"Logs are saved in the logs table from the state database\",\n        },\n        label: \"Enable logs (experimental)\",\n      },\n      session_max_age_days: {\n        sqlDefinition: `INTEGER NOT NULL DEFAULT 14 CHECK(session_max_age_days > 0)`,\n        info: {\n          hint: \"Number of days a user will stay logged in\",\n          min: 1,\n          max: Number.MAX_SAFE_INTEGER,\n        },\n      },\n      magic_link_validity_days: {\n        sqlDefinition: `INTEGER NOT NULL DEFAULT 1 CHECK(magic_link_validity_days > 0)`,\n        info: {\n          hint: \"Number of days a magic link can be used to log in\",\n          min: 1,\n          max: Number.MAX_SAFE_INTEGER,\n        },\n      },\n      updated_by: {\n        enum: [\"user\", \"app\"],\n        defaultValue: \"app\",\n      },\n      updated_at: {\n        sqlDefinition: \"TIMESTAMPTZ NOT NULL DEFAULT now()\",\n      },\n      pass_process_env_vars_to_server_side_functions: {\n        sqlDefinition: `BOOLEAN NOT NULL DEFAULT FALSE`,\n        info: {\n          hint: \"If true then all environment variables will be passed to the server side function nodejs. Use at your own risk\",\n        },\n      },\n      login_rate_limit_enabled: {\n        sqlDefinition: `BOOLEAN NOT NULL DEFAULT TRUE`,\n        info: {\n          hint: \"If enabled then each client defined by <groupBy> that fails <maxAttemptsPerHour> in an hour will not be able to login for the rest of the hour\",\n        },\n        label: \"Enable failed login rate limit\",\n      },\n      login_rate_limit: {\n        defaultValue: {\n          maxAttemptsPerHour: 5,\n          groupBy: \"ip\",\n        },\n        jsonbSchemaType: {\n          maxAttemptsPerHour: {\n            type: \"integer\",\n            description: \"Maximum number of login attempts allowed per hour\",\n          },\n          groupBy: {\n            description: \"The IP address used to group login attempts\",\n            enum: [\"x-real-ip\", \"remote_ip\", \"ip\"],\n          },\n        },\n        label: \"Failed login rate limit options\",\n        info: { hint: \"List of allowed IP addresses in ipv4 or ipv6 format\" },\n      },\n      auth_created_user_type: {\n        info: {\n          hint: \"User type assigned to new users. Defaults to 'default'\",\n        },\n        sqlDefinition: `TEXT REFERENCES user_types`,\n      },\n      auth_providers: {\n        info: {\n          hint: \"The provided credentials will allow users to register and sign in. The redirect uri format is {website_url}/auth/{providerName}/callback\",\n        },\n        nullable: true,\n        jsonbSchemaType: {\n          website_url: { type: \"string\", title: \"Website URL\" },\n          email: {\n            optional: true,\n            oneOfType: [\n              {\n                signupType: { enum: [\"withMagicLink\"] },\n                enabled: { type: \"boolean\", optional: true },\n                smtp: SMTPConfig,\n                emailTemplate: EmailTemplateConfig,\n                emailConfirmationEnabled: {\n                  type: \"boolean\",\n                  optional: true,\n                  title: \"Enable email confirmation\",\n                },\n              },\n              {\n                signupType: { enum: [\"withPassword\"] },\n                enabled: { type: \"boolean\", optional: true },\n                minPasswordLength: {\n                  optional: true,\n                  type: \"integer\",\n                  title: \"Minimum password length\",\n                },\n                smtp: SMTPConfig,\n                emailTemplate: EmailTemplateConfig,\n                emailConfirmationEnabled: {\n                  type: \"boolean\",\n                  optional: true,\n                  title: \"Enable email confirmation\",\n                },\n              },\n            ],\n          },\n          google: {\n            optional: true,\n            type: {\n              ...commonAuthSchema,\n              authOpts: {\n                optional: true,\n                type: {\n                  scope: {\n                    type: \"string[]\",\n                    allowedValues: OAuthProviderOptions.google.scopes.map(\n                      (s) => s.key,\n                    ),\n                  },\n                },\n              },\n            },\n          },\n          github: {\n            optional: true,\n            type: {\n              ...commonAuthSchema,\n              authOpts: {\n                optional: true,\n                type: {\n                  scope: {\n                    type: \"string[]\",\n                    allowedValues: OAuthProviderOptions.github.scopes.map(\n                      (s) => s.key,\n                    ),\n                  },\n                },\n              },\n            },\n          },\n          microsoft: {\n            optional: true,\n            type: {\n              ...commonAuthSchema,\n              authOpts: {\n                optional: true,\n                type: {\n                  prompt: {\n                    enum: OAuthProviderOptions.microsoft.prompts.map(\n                      (s) => s.key,\n                    ),\n                  },\n                  scope: {\n                    type: \"string[]\",\n                    allowedValues: OAuthProviderOptions.microsoft.scopes.map(\n                      (s) => s.key,\n                    ),\n                  },\n                },\n              },\n            },\n          },\n          facebook: {\n            optional: true,\n            type: {\n              ...commonAuthSchema,\n              authOpts: {\n                optional: true,\n                type: {\n                  scope: {\n                    type: \"string[]\",\n                    allowedValues: OAuthProviderOptions.facebook.scopes.map(\n                      (s) => s.key,\n                    ),\n                  },\n                },\n              },\n            },\n          },\n          customOAuth: {\n            optional: true,\n            type: {\n              ...commonAuthSchema,\n              displayName: { type: \"string\" },\n              displayIconPath: { type: \"string\", optional: true },\n              authorizationURL: { type: \"string\" },\n              tokenURL: { type: \"string\" },\n              authOpts: {\n                optional: true,\n                type: {\n                  scope: {\n                    type: \"string[]\",\n                  },\n                },\n              },\n            },\n          },\n        },\n      },\n      tableConfig: {\n        info: { hint: \"Schema used to create prostgles-ui\" },\n        sqlDefinition: \"JSONB\",\n      },\n      prostgles_registration: {\n        info: { hint: \"Registration options\" },\n        nullable: true,\n        jsonbSchemaType: {\n          enabled: { type: \"boolean\" },\n          email: { type: \"string\" },\n          token: { type: \"string\" },\n        },\n      },\n      mcp_servers_disabled: \"BOOLEAN NOT NULL DEFAULT FALSE\",\n    },\n    triggers: {\n      \"Update updated_at\": {\n        actions: [\"update\"],\n        type: \"before\",\n        forEach: \"row\",\n        query: `\n          BEGIN\n            NEW.updated_at = now();\n            RETURN NEW;\n          END;\n        `,\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigLinks.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport type { JSONB } from \"prostgles-types\";\n\nconst CommonLinkOpts = {\n  colorArr: { type: \"number[]\", optional: true },\n} as const;\n\nconst filter = {\n  oneOfType: [{ $and: \"any[]\" }, { $or: \"any[]\" }],\n  optional: true,\n} as const;\n\nconst joinPath = {\n  description:\n    \"When adding a chart this allows showing data from a table that joins to the current table\",\n  arrayOfType: {\n    table: \"string\",\n    on: { arrayOf: { record: { values: \"any\" } } },\n  },\n  optional: true,\n} as const satisfies JSONB.FieldTypeObj;\n\nconst CommonChartLinkOpts = {\n  dataSource: {\n    optional: true,\n    oneOfType: [\n      {\n        type: {\n          enum: [\"sql\"],\n          description:\n            \"Shows data from an SQL query within an editor. Will not reflect latest changes to that query (must be re-added)\",\n        },\n        sql: \"string\",\n        withStatement: \"string\",\n      },\n      {\n        type: {\n          enum: [\"table\"],\n          description:\n            \"Shows data from an opened table window. Any filters from that table will apply to the chart as well\",\n        },\n        tableName: \"string\",\n        joinPath,\n      },\n      {\n        type: {\n          enum: [\"local-table\"],\n          description:\n            \"Shows data from a table not connected to any window (w1_id === w2_id === current chart window). Custom filters can be added\",\n        },\n        localTableName: {\n          type: \"string\",\n          description: \"Local layer (w1_id === w2_id === current chart window)\",\n        },\n        smartGroupFilter: filter,\n      },\n    ],\n  },\n  // joinPath,\n  // localTableName: {\n  //   type: \"string\",\n  //   optional: true,\n  //   description:\n  //     \"If provided then this is a local layer (w1_id === w2_id === current chart window)\",\n  // },\n  // sql: {\n  //   description: \"Defined if chart links to SQL statement\",\n  //   optional: true,\n  //   type: \"string\",\n  // },\n  title: { type: \"string\", optional: true },\n} as const satisfies JSONB.ObjectType[\"type\"];\n\nexport const tableConfigLinks: TableConfig<{ en: 1 }> = {\n  links: {\n    columns: {\n      id: `UUID PRIMARY KEY DEFAULT gen_random_uuid()`,\n      user_id: `UUID NOT NULL REFERENCES users(id)  ON DELETE CASCADE`,\n      w1_id: `UUID NOT NULL REFERENCES windows(id)  ON DELETE CASCADE`,\n      w2_id: `UUID NOT NULL REFERENCES windows(id)  ON DELETE CASCADE`,\n      workspace_id: `UUID REFERENCES workspaces(id) ON DELETE SET NULL`,\n      disabled: \"boolean\",\n      options: {\n        jsonbSchema: {\n          oneOfType: [\n            {\n              type: { enum: [\"table\"], description: \"Table cross-filter link\" },\n              ...CommonLinkOpts,\n              tablePath: {\n                ...joinPath,\n                optional: false,\n                description:\n                  \"Table join path from w1.table_name to w2.table_name\",\n              },\n            },\n            {\n              type: { enum: [\"map\"], description: \"Map layer link\" },\n              ...CommonChartLinkOpts,\n              dataSource: {\n                ...CommonChartLinkOpts.dataSource,\n                oneOfType: [\n                  ...CommonChartLinkOpts.dataSource.oneOfType,\n                  {\n                    type: {\n                      enum: [\"osm\"],\n                      description: \"Shows data from OpenStreetMap. Map only\",\n                    },\n                    osmLayerQuery: {\n                      type: \"string\",\n                      description:\n                        \"OSM layer (w1_id === w2_id === current chart window)\",\n                    },\n                  },\n                ],\n              },\n              mapIcons: {\n                optional: true,\n                oneOfType: [\n                  {\n                    type: { enum: [\"fixed\"] },\n                    display: { enum: [\"icon\", \"icon+circle\"], optional: true },\n                    iconPath: \"string\",\n                  },\n                  {\n                    type: { enum: [\"conditional\"] },\n                    display: { enum: [\"icon\", \"icon+circle\"], optional: true },\n                    columnName: \"string\",\n                    conditions: {\n                      arrayOfType: {\n                        value: \"any\",\n                        iconPath: \"string\",\n                      },\n                    },\n                  },\n                ],\n              },\n              mapColorMode: {\n                optional: true,\n                oneOfType: [\n                  {\n                    type: { enum: [\"fixed\"] },\n                    colorArr: \"number[]\",\n                  },\n                  {\n                    type: { enum: [\"scale\"] },\n                    columnName: \"string\",\n                    min: \"number\",\n                    max: \"number\",\n                    minColorArr: \"number[]\",\n                    maxColorArr: \"number[]\",\n                  },\n                  {\n                    type: { enum: [\"conditional\"] },\n                    columnName: \"string\",\n                    conditions: {\n                      arrayOfType: {\n                        value: \"any\",\n                        colorArr: \"number[]\",\n                      },\n                    },\n                  },\n                ],\n              },\n              mapShowText: {\n                optional: true,\n                type: {\n                  columnName: { type: \"string\" },\n                },\n              },\n              columns: {\n                arrayOfType: {\n                  name: {\n                    type: \"string\",\n                    description: \"Geometry/Geography column\",\n                  },\n                  colorArr: \"number[]\",\n                },\n              },\n            },\n            {\n              type: { enum: [\"timechart\"] },\n              ...CommonChartLinkOpts,\n              groupByColumn: {\n                type: \"string\",\n                optional: true,\n                description: \"Used by timechart\",\n              },\n              groupByColumnColors: {\n                optional: true,\n                arrayOfType: {\n                  value: \"any\",\n                  color: \"string\",\n                },\n              },\n              otherColumns: {\n                arrayOfType: {\n                  name: \"string\",\n                  label: { type: \"string\", optional: true },\n                  udt_name: \"string\",\n                  is_pkey: { type: \"boolean\", optional: true },\n                },\n                optional: true,\n              },\n              columns: {\n                arrayOfType: {\n                  name: { type: \"string\", description: \"Date column\" },\n                  colorArr: \"number[]\",\n                  statType: {\n                    optional: true,\n                    type: {\n                      funcName: {\n                        enum: [\n                          \"$min\",\n                          \"$max\",\n                          \"$countAll\",\n                          \"$avg\",\n                          \"$sum\",\n                          \"$count\",\n                        ],\n                      },\n                      numericColumn: \"string\",\n                    },\n                  },\n                },\n              },\n            },\n            {\n              type: { enum: [\"barchart\"] },\n              ...CommonChartLinkOpts,\n              statType: {\n                optional: true,\n                type: {\n                  funcName: {\n                    enum: [\n                      \"$min\",\n                      \"$max\",\n                      \"$count\",\n                      \"$countAll\",\n                      \"$avg\",\n                      \"$sum\",\n                    ],\n                  },\n                  numericColumn: {\n                    type: \"string\",\n                    description:\n                      \"Numeric column. Required for all but $countAll\",\n                  },\n                },\n              },\n              columns: {\n                arrayOfType: {\n                  name: {\n                    type: \"string\",\n                    description: \"Label columns. Usually a text column\",\n                  },\n                  colorArr: \"number[]\",\n                },\n              },\n            },\n          ],\n        },\n      },\n      closed: `BOOLEAN DEFAULT FALSE`,\n      deleted: `BOOLEAN DEFAULT FALSE`,\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n      last_updated: `BIGINT NOT NULL`,\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigLlm/tableConfigLlm.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport type { JSONB } from \"prostgles-types\";\nimport { tableConfigLlmChats } from \"./tableConfigLlmChats\";\nimport { extraRequestData } from \"./tableConfigLlmExtraRequestData\";\n\nconst toolUseContent: JSONB.FieldType = {\n  oneOf: [\n    \"string\",\n    {\n      arrayOf: {\n        oneOf: [\n          {\n            type: {\n              type: {\n                enum: [\"text\"],\n              },\n              text: \"string\",\n            },\n          },\n          {\n            type: {\n              type: {\n                enum: [\"image\", \"audio\"],\n              },\n              mimeType: \"string\",\n              data: \"string\",\n            },\n          },\n          {\n            type: {\n              type: { enum: [\"resource\"] },\n              resource: {\n                type: {\n                  uri: \"string\",\n                  mimeType: { type: \"string\", optional: true },\n                  text: { type: \"string\", optional: true },\n                  blob: { type: \"string\", optional: true },\n                },\n              },\n            },\n          },\n          {\n            type: {\n              type: { enum: [\"resource_link\"] },\n              uri: \"string\",\n              name: \"string\",\n              mimeType: { type: \"string\", optional: true },\n              description: { type: \"string\", optional: true },\n            },\n          },\n        ],\n      },\n    },\n  ],\n};\n\nexport const tableConfigLLM: TableConfig<{ en: 1 }> = {\n  llm_providers: {\n    columns: {\n      id: `TEXT PRIMARY KEY`,\n      api_url: `TEXT NOT NULL`,\n      api_docs_url: `TEXT`,\n      api_pricing_url: `TEXT`,\n      logo_url: `TEXT`,\n      ...extraRequestData,\n    },\n  },\n  llm_models: {\n    columns: {\n      id: `INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY`,\n      name: `TEXT NOT NULL`,\n      provider_id: `TEXT NOT NULL REFERENCES llm_providers(id) ON DELETE CASCADE`,\n      pricing_info: {\n        info: {\n          hint: \"Prices are per 1M tokens\",\n        },\n        nullable: true,\n        jsonbSchemaType: {\n          input: \"number\",\n          output: \"number\",\n          cachedInput: {\n            type: \"number\",\n            description: \"Prompt caching write\",\n            optional: true,\n          },\n          cachedOutput: {\n            type: \"number\",\n            description: \"Prompt caching read\",\n            optional: true,\n          },\n          threshold: {\n            description:\n              \"Some providers charge more for tokens above a certain limit\",\n            optional: true,\n            type: { tokenLimit: \"number\", input: \"number\", output: \"number\" },\n          },\n        },\n      },\n      chat_suitability_rank: {\n        sqlDefinition: `NUMERIC`,\n        info: { hint: \"Lowest number is used in new chats\" },\n      },\n      model_created: `TIMESTAMPTZ DEFAULT NOW()`,\n      mcp_tool_support: `BOOLEAN DEFAULT FALSE`,\n      context_length: ` INTEGER NOT NULL DEFAULT 0`,\n      architecture: {\n        nullable: true,\n        jsonbSchemaType: {\n          modality: {\n            optional: true,\n            type: \"string\",\n            description:\n              \"Input modality (e.g., 'text+image->text'). Indicates how input is processed and output is generated.\",\n          },\n          input_modalities: \"string[]\", // Supported input types: [\"file\", \"image\", \"text\"]\n          output_modalities: \"string[]\", // Supported output types: [\"text\"]\n          tokenizer: \"string\", // Tokenization method used\n          instruct_type: { oneOf: [\"string\"], nullable: true }, // Instruction format type (null if not applicable)\n        },\n      },\n      supported_parameters: {\n        nullable: true,\n        jsonbSchema: {\n          arrayOf: {\n            type: \"string\",\n            /**\n             * https://openrouter.ai/docs/overview/models\n             */\n            // undocumented values keep on being added so disabled for now.\n            // allowedValues: [\n            //   \"tools\", // - Function calling capabilities\n            //   \"tool_choice\", // - Tool selection control\n            //   \"max_tokens\", // - Response length limiting\n            //   \"temperature\", // - Randomness control\n            //   \"top_p\", // - Nucleus sampling\n            //   \"top_k\",\n            //   \"reasoning\", // - Internal reasoning mode\n            //   \"include_reasoning\", // - Include reasoning in response\n            //   \"structured_outputs\", // - JSON schema enforcement\n            //   \"response_format\", // - Output format specification\n            //   \"stop\", // - Custom stop sequences\n            //   \"frequency_penalty\", // - Repetition reduction\n            //   \"presence_penalty\", // - Topic diversity\n            //   \"seed\", // - Deterministic outputstools - Function calling capabilities\n            // ],\n          },\n        },\n      },\n      ...extraRequestData,\n    },\n    indexes: {\n      unique_model_name: {\n        unique: true,\n        columns: \"name, provider_id\",\n      },\n    },\n  },\n  llm_credentials: {\n    columns: {\n      id: `INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY`,\n      user_id: `UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE`,\n      provider_id: {\n        label: \"Provider\",\n        sqlDefinition: `TEXT NOT NULL REFERENCES llm_providers(id) ON DELETE CASCADE`,\n      },\n      api_key: `TEXT NOT NULL DEFAULT ''`,\n      name: { sqlDefinition: `TEXT UNIQUE`, info: { hint: \"Optional\" } },\n      ...extraRequestData,\n      is_default: {\n        sqlDefinition: `BOOLEAN NOT NULL DEFAULT FALSE`,\n        info: {\n          hint: \"If true then this is the default credential used in new AI Assistant chats\",\n        },\n      },\n      created: {\n        sqlDefinition: `TIMESTAMPTZ DEFAULT NOW()`,\n      },\n    },\n    indexes: {\n      unique_default: {\n        unique: true,\n        columns: \"is_default\",\n        where: \"is_default = TRUE\",\n      },\n    },\n  },\n  llm_prompts: {\n    columns: {\n      id: `INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY`,\n      name: `TEXT NOT NULL DEFAULT 'New prompt'`,\n      description: `TEXT DEFAULT ''`,\n      user_id: `UUID REFERENCES users(id) ON DELETE SET NULL`,\n      prompt: `TEXT NOT NULL DEFAULT ''`,\n      options: {\n        nullable: true,\n        jsonbSchemaType: {\n          prompt_type: {\n            enum: [\"dashboards\", \"tasks\", \"agent_workflow\"],\n            optional: true,\n            description:\n              \"Internal prompt type used in controlling chat context. Some tools may not be available for all types\",\n          },\n        },\n      },\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n    },\n    indexes: {\n      unique_llm_prompt: {\n        unique: true,\n        columns: \"name, user_id, prompt\",\n      },\n    },\n  },\n  ...tableConfigLlmChats,\n  llm_messages: {\n    columns: {\n      id: `int8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY`,\n      chat_id: `INTEGER NOT NULL REFERENCES llm_chats(id) ON DELETE CASCADE`,\n      user_id: `UUID REFERENCES users(id) ON DELETE CASCADE`,\n      llm_model_id: `INTEGER REFERENCES llm_models(id) ON DELETE SET NULL`,\n      cost: `NUMERIC NOT NULL DEFAULT 0`,\n      message: {\n        jsonbSchema: {\n          arrayOf: {\n            oneOf: [\n              {\n                type: {\n                  type: {\n                    enum: [\"text\"],\n                  },\n                  text: \"string\",\n                  reasoning: {\n                    type: \"string\",\n                    optional: true,\n                    description:\n                      \"Internal reasoning message used by the model to explain its thought process\",\n                  },\n                },\n              },\n              {\n                type: {\n                  type: {\n                    enum: [\"image\", \"audio\", \"video\", \"application\", \"text\"],\n                  },\n                  source: {\n                    type: {\n                      type: { enum: [\"base64\"] },\n                      media_type: \"string\",\n                      data: \"string\",\n                    },\n                  },\n                },\n              },\n              {\n                type: {\n                  type: { enum: [\"tool_result\"] },\n                  tool_use_id: \"string\",\n                  tool_name: { type: \"string\" },\n                  content: toolUseContent,\n                  is_error: { optional: true, type: \"boolean\" },\n                },\n              },\n              {\n                type: {\n                  type: { enum: [\"tool_use\"] },\n                  id: \"string\",\n                  name: \"string\",\n                  input: \"any\",\n                },\n              },\n            ],\n          },\n        },\n      },\n      meta: \"JSONB\",\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n    },\n  },\n  llm_chats_allowed_functions: {\n    info: {\n      label: \"Allowed functions\",\n    },\n    columns: {\n      chat_id: `INTEGER NOT NULL REFERENCES llm_chats(id) ON DELETE CASCADE`,\n      connection_id: `UUID NOT NULL, FOREIGN KEY(chat_id, connection_id) REFERENCES llm_chats(id, connection_id) ON DELETE CASCADE`,\n      server_function_id: `INTEGER NOT NULL, FOREIGN KEY(server_function_id, connection_id) REFERENCES published_methods(id, connection_id) ON UPDATE CASCADE ON DELETE CASCADE`,\n      auto_approve: {\n        info: {\n          hint: \"If true then the function call request is automatically approved\",\n        },\n        sqlDefinition: `BOOLEAN DEFAULT FALSE`,\n      },\n    },\n    indexes: {\n      unique_chat_tool: {\n        unique: true,\n        columns: \"chat_id, server_function_id\",\n      },\n    },\n  },\n  access_control_allowed_llm: {\n    columns: {\n      access_control_id: `INTEGER NOT NULL REFERENCES access_control(id)`,\n      llm_credential_id: `INTEGER NOT NULL REFERENCES llm_credentials(id)`,\n      llm_prompt_id: `INTEGER NOT NULL REFERENCES llm_prompts(id)`,\n    },\n    indexes: {\n      unique: {\n        unique: true,\n        columns: \"access_control_id, llm_credential_id, llm_prompt_id\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigLlm/tableConfigLlmChats.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport type { JSONB } from \"prostgles-types\";\nimport { extraRequestData } from \"./tableConfigLlmExtraRequestData\";\n\nconst commonrunSQLOpts = {\n  query_timeout: {\n    type: \"integer\",\n    title: \"Query timeout (s)\",\n    optional: true,\n    description: \"Timeout in seconds for the queries.\",\n  },\n  auto_approve: {\n    type: \"boolean\",\n    title: \"Auto approve\",\n    optional: true,\n    description:\n      \"If true then the assistant can run queries without asking for approval\",\n  },\n} satisfies JSONB.ObjectType[\"type\"];\n\nexport const tableConfigLlmChats: TableConfig<{ en: 1 }> = {\n  llm_chats: {\n    columns: {\n      id: `INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY`,\n      parent_chat_id: {\n        sqlDefinition: `INTEGER REFERENCES llm_chats(id) ON DELETE SET NULL`,\n        info: {\n          hint: \"Agentic chats can have a parent chat\",\n        },\n      },\n      name: `TEXT NOT NULL DEFAULT 'New chat'`,\n      user_id: `UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE`,\n      connection_id: `UUID REFERENCES connections(id) ON DELETE CASCADE`,\n      model: `INTEGER  REFERENCES llm_models(id)`,\n      llm_prompt_id: {\n        label: \"Prompt\",\n        sqlDefinition: `INTEGER REFERENCES llm_prompts(id) ON DELETE SET NULL`,\n      },\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n      disabled_message: {\n        sqlDefinition: `TEXT`,\n        info: { hint: \"Message shown when chat is disabled\" },\n      },\n      disabled_until: {\n        sqlDefinition: `TIMESTAMPTZ`,\n        info: { hint: \"If set then chat is disabled until this time\" },\n      },\n      status: {\n        nullable: true,\n        jsonbSchema: {\n          oneOf: [\n            { type: { state: { enum: [\"stopped\"] } } },\n            {\n              type: {\n                state: { enum: [\"loading\"] },\n                /** Timestamp since started waiting for LLM response */\n                since: \"Date\",\n              },\n            },\n          ],\n        },\n      },\n      db_schema_permissions: {\n        label: \"Schema read access\",\n        nullable: true,\n        info: {\n          hint: \"Controls which table and column definitions are used in the prompt\",\n        },\n        defaultValue: { type: \"Full\" },\n        jsonbSchema: {\n          oneOfType: [\n            {\n              type: {\n                enum: [\"None\"],\n                title: \"Type\",\n                description: \"No schema information is provided\",\n              },\n            },\n            {\n              type: {\n                enum: [\"Full\"],\n                title: \"Type\",\n                description: \"All tables, columns and constraints\",\n              },\n            },\n            {\n              type: {\n                enum: [\"Custom\"],\n                title: \"Type\",\n                description:\n                  \"Specific tables and their columns and constraints\",\n              },\n              tables: {\n                title: \"Tables\",\n                type: \"Lookup[]\",\n                lookup: {\n                  type: \"schema\",\n                  object: \"table\",\n                  isArray: true,\n                },\n              },\n            },\n          ],\n        },\n      },\n      db_data_permissions: {\n        label: \"Data access\",\n        nullable: true,\n        info: {\n          hint: \"Controls how the assistant is allowed to view/interact with the data found in the database. \\nSame connection and permissions are used as for the current user\",\n        },\n        jsonbSchema: {\n          oneOfType: [\n            {\n              Mode: {\n                description:\n                  \"Cannot interact with any data from the database. This excludes the schema read access which is controlled separately\",\n                enum: [\"None\"],\n              },\n            },\n            {\n              Mode: {\n                enum: [\"Run readonly SQL\"],\n                description:\n                  \"Can run readonly SQL queries (if the current user is allowed)\",\n              },\n              ...commonrunSQLOpts,\n            },\n            {\n              Mode: {\n                enum: [\"Run commited SQL\"],\n                description:\n                  \"Can run SQL queries that will be commited (if the current user is allowed). Use with caution\",\n              },\n              ...commonrunSQLOpts,\n            },\n            {\n              Mode: {\n                enum: [\"Custom\"],\n                description:\n                  \"Can only access specific tables on behalf of the user\",\n              },\n              auto_approve: commonrunSQLOpts.auto_approve,\n              tables: {\n                title: \"Tables\",\n                description: \"Tables the assistant can access\",\n                arrayOfType: {\n                  tableName: {\n                    title: \"Table name\",\n                    type: \"Lookup\",\n                    lookup: {\n                      type: \"schema\",\n                      object: \"table\",\n                    },\n                  },\n                  /** TODO: this must re-use access control data and UI */\n                  select: { type: \"boolean\", optional: true },\n                  update: { type: \"boolean\", optional: true },\n                  insert: { type: \"boolean\", optional: true },\n                  delete: { type: \"boolean\", optional: true },\n                  // columns: {\n                  //   optional: true,\n                  //   type: \"Lookup[]\",\n                  //   lookup: {\n                  //     type: \"schema\",\n                  //     object: \"column\",\n                  //   },\n                  //   description:\n                  //     \"Columns the assistant can access in the table\",\n                  // },\n                },\n              },\n            },\n          ],\n        },\n      },\n      maximum_consecutive_tool_fails: {\n        sqlDefinition: `INTEGER NOT NULL DEFAULT 5`,\n        info: {\n          hint: \"Maximum number of consecutive tool call fails before the chat stops automatically approving tool calls. Useful to prevent infinite loops\",\n        },\n      },\n      max_total_cost_usd: {\n        sqlDefinition: `NUMERIC NOT NULL DEFAULT 5`,\n        info: {\n          hint: \"Maximum total cost of the chat in USD. If set to 0 then no limit is applied\",\n        },\n      },\n      currently_typed_message: {\n        sqlDefinition: `TEXT`,\n      },\n      ...extraRequestData,\n    },\n    indexes: {\n      unique_chat_for_connection: {\n        columns: \"id, connection_id\",\n        unique: true,\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigLlm/tableConfigLlmExtraRequestData.ts",
    "content": "import type { ColumnConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\n\nexport const extraRequestData = {\n  extra_headers: {\n    nullable: true,\n    jsonbSchema: {\n      record: { values: \"string\" },\n    },\n  },\n  extra_body: {\n    nullable: true,\n    jsonbSchemaType: {\n      temperature: { type: \"number\", optional: true },\n      frequency_penalty: { type: \"number\", optional: true },\n      max_completion_tokens: { type: \"integer\", optional: true },\n      max_tokens: { type: \"integer\", optional: true },\n      presence_penalty: { type: \"number\", optional: true },\n      response_format: {\n        enum: [\"json\", \"text\", \"srt\", \"verbose_json\", \"vtt\"],\n        optional: true,\n      },\n      think: { type: \"boolean\", optional: true },\n      /* OpenRouter */\n      reasoning: {\n        optional: true,\n        oneOfType: [\n          {\n            effort: {\n              enum: [\"high\", \"medium\", \"low\"],\n              description: 'Can be \"high\", \"medium\", or \"low\" (OpenAI-style)',\n            },\n          },\n          {\n            max_tokens: {\n              type: \"integer\",\n              optional: true,\n              description: \"Specific token limit (Anthropic-style)\",\n            },\n          },\n        ],\n      },\n      stream: { type: \"boolean\", optional: true },\n    },\n  },\n} as const satisfies Record<\n  string,\n  ColumnConfig<{\n    en: 1;\n  }>\n>;\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigMCPServers.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\n\nexport const tableConfigMCPServers: TableConfig<{ en: 1 }> = {\n  mcp_servers: {\n    columns: {\n      name: `TEXT PRIMARY KEY`,\n      info: `TEXT`,\n      icon_path: `TEXT`,\n      source: {\n        nullable: true,\n        jsonbSchema: {\n          oneOfType: [\n            {\n              type: { enum: [\"github\"] },\n              name: \"string\",\n              repoUrl: \"string\",\n              installationCommands: {\n                arrayOfType: {\n                  command: \"string\",\n                  args: { type: \"string[]\", optional: true },\n                },\n                optional: true,\n              },\n            },\n            {\n              type: { enum: [\"code\"] },\n              packageJson: \"string\",\n              tsconfigJson: \"string\",\n              files: {\n                record: { values: \"string\" },\n              },\n            },\n          ],\n        },\n      },\n      command: {\n        enum: [\"npx\", \"npm\", \"uvx\", \"uv\", \"docker\", \"prostgles-local\"],\n      },\n      config_schema: {\n        jsonbSchema: {\n          record: {\n            values: {\n              oneOfType: [\n                {\n                  type: { enum: [\"env\"] },\n                  renderWithComponent: { type: \"string\", optional: true },\n                  title: { type: \"string\", optional: true },\n                  optional: { type: \"boolean\", optional: true },\n                  description: { type: \"string\", optional: true },\n                },\n                {\n                  type: { enum: [\"arg\"] },\n                  renderWithComponent: { type: \"string\", optional: true },\n                  title: { type: \"string\", optional: true },\n                  optional: { type: \"boolean\", optional: true },\n                  description: { type: \"string\", optional: true },\n                  index: { type: \"integer\", optional: true },\n                },\n              ],\n            },\n          },\n        },\n        nullable: true,\n      },\n      cwd: `TEXT`,\n      args: `TEXT[]`,\n      stderr: \"TEXT\",\n      env: {\n        nullable: true,\n        jsonbSchema: {\n          record: {\n            values: \"string\",\n          },\n        },\n      },\n      env_from_main_process: `TEXT[]`,\n      enabled: `BOOLEAN NOT NULL DEFAULT FALSE`,\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n      installed: `TIMESTAMPTZ`,\n    },\n  },\n  mcp_server_configs: {\n    columns: {\n      id: `SERIAL PRIMARY KEY`,\n      server_name: `TEXT NOT NULL REFERENCES mcp_servers(name) ON DELETE CASCADE`,\n      config: { jsonbSchema: { record: { values: \"any\" } } },\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n      last_updated: `TIMESTAMPTZ DEFAULT NOW()`,\n    },\n    constraints: {\n      unique_server_and_config: {\n        type: \"UNIQUE\",\n        content: \"server_name, config\",\n      },\n      unique_server_and_id: {\n        type: \"UNIQUE\",\n        content: \"server_name, id\",\n      },\n    },\n  },\n  mcp_server_tools: {\n    columns: {\n      id: `SERIAL PRIMARY KEY`,\n      name: `TEXT NOT NULL`,\n      description: `TEXT NOT NULL`,\n      server_name: `TEXT NOT NULL REFERENCES mcp_servers(name) ON DELETE CASCADE`,\n      inputSchema: `JSONB`,\n      annotations: {\n        jsonbSchemaType: {\n          title: {\n            type: \"string\",\n            optional: true,\n            title: \"Human-readable title for the tool\",\n          },\n          readOnlyHint: {\n            type: \"boolean\",\n            optional: true,\n            title:\n              \"If true, tool does not modify its environment (read-only). \",\n          },\n          openWorldHint: {\n            type: \"boolean\",\n            optional: true,\n            title: \"If true, tool interacts with external entities\",\n          },\n          idempotentHint: {\n            type: \"boolean\",\n            optional: true,\n            title:\n              \"If true, repeated calls with same args have no additional effect\",\n          },\n          destructiveHint: {\n            type: \"boolean\",\n            optional: true,\n            title: \"If true, the tool may perform destructive updates\",\n          },\n        },\n        nullable: true,\n      },\n      autoApprove: `BOOLEAN DEFAULT FALSE`,\n    },\n    indexes: {\n      unique_server_name_tool_name: {\n        unique: true,\n        columns: \"server_name, name\",\n      },\n    },\n  },\n  mcp_server_logs: {\n    columns: {\n      id: `SERIAL PRIMARY KEY`,\n      server_name: `TEXT NOT NULL REFERENCES mcp_servers(name) ON DELETE CASCADE`,\n      log: `TEXT NOT NULL DEFAULT ''`,\n      error: `TEXT`,\n      install_log: `TEXT`,\n      install_error: `TEXT`,\n      last_updated: `TIMESTAMPTZ DEFAULT NOW()`,\n    },\n  },\n  mcp_server_tool_calls: {\n    columns: {\n      id: `SERIAL PRIMARY KEY `,\n      chat_id: `INTEGER REFERENCES llm_chats(id) ON DELETE SET NULL`,\n      user_id: `UUID REFERENCES users(id) ON DELETE SET NULL`,\n      mcp_server_name: `TEXT REFERENCES mcp_servers(name) ON DELETE SET NULL`,\n      mcp_tool_name: `TEXT NOT NULL`,\n      mcp_server_config_id: `INTEGER`,\n      input: `JSONB`,\n      output: `JSONB`,\n      error: `JSON`,\n      called: `TIMESTAMPTZ DEFAULT NOW()`,\n      duration: `INTERVAL NOT NULL`,\n    },\n    constraints: {\n      mcp_tool_name_server_name_fk:\n        \"FOREIGN KEY (mcp_server_name, mcp_tool_name) REFERENCES mcp_server_tools(server_name, name) ON DELETE SET NULL\",\n      mcp_server_config_id_fk:\n        \"FOREIGN KEY (mcp_server_name, mcp_server_config_id) REFERENCES mcp_server_configs(server_name, id) ON DELETE SET NULL\",\n    },\n  },\n  llm_chats_allowed_mcp_tools: {\n    info: {\n      label: \"Allowed MCP tools\",\n    },\n    columns: {\n      chat_id: `INTEGER NOT NULL REFERENCES llm_chats(id) ON DELETE CASCADE`,\n      server_name: `TEXT NOT NULL REFERENCES mcp_servers ON DELETE CASCADE`,\n      tool_id: `INTEGER NOT NULL REFERENCES mcp_server_tools(id) ON DELETE CASCADE`,\n      server_config_id: `INTEGER REFERENCES mcp_server_configs ON DELETE CASCADE`,\n      auto_approve: `BOOLEAN DEFAULT FALSE`,\n    },\n    indexes: {\n      unique_chat_allowed_tool: {\n        unique: true,\n        columns: \"chat_id, tool_id\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigMigrations.ts",
    "content": "import type { TableConfigMigrations } from \"prostgles-server/dist/ProstglesTypes\";\n\nexport const tableConfigMigrations = {\n  silentFail: false,\n  version: 5,\n  onMigrate: async ({ db, oldVersion }) => {\n    console.warn(\"Migrating from version: \", oldVersion);\n    if (oldVersion === 3) {\n      await db.any(` \n              UPDATE login_attempts \n                SET ip_address_remote = COALESCE(ip_address_remote, ''),\n                  user_agent = COALESCE(user_agent, ''),\n                  x_real_ip = COALESCE(x_real_ip, '')\n              WHERE ip_address_remote IS NULL \n              OR user_agent IS NULL \n              OR x_real_ip IS NULL\n            `);\n    } else if (oldVersion === 4) {\n      await db.any(` \n              UPDATE llm_messages\n              SET message = jsonb_build_array(jsonb_build_object('type', 'text', 'text', message)) \n              WHERE message IS NOT NULL\n            `);\n    }\n  },\n} satisfies TableConfigMigrations;\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigPublishedMethods.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport { DATA_TYPES, type JSONB } from \"prostgles-types\";\n\nconst primitiveJsonbType = {\n  type: { enum: DATA_TYPES },\n  optional: { type: \"boolean\", optional: true },\n  nullable: { type: \"boolean\", optional: true },\n  description: { type: \"string\", optional: true },\n  title: { type: \"string\", optional: true },\n  defaultValue: { type: \"any\", optional: true },\n} as const satisfies JSONB.ObjectType[\"type\"];\n\nexport const tableConfigPublishedMethods: TableConfig<{ en: 1 }> = {\n  published_methods: {\n    columns: {\n      id: `SERIAL PRIMARY KEY`,\n      name: `TEXT NOT NULL DEFAULT 'Method name'`,\n      description: `TEXT NOT NULL DEFAULT 'Method description'`,\n      connection_id: {\n        sqlDefinition: `UUID REFERENCES connections(id) ON DELETE SET NULL ON UPDATE SET NULL`,\n        info: { hint: \"If null then connection was deleted\" },\n      },\n      arguments: {\n        nullable: false,\n        defaultValue: \"[]\",\n        jsonbSchema: {\n          title: \"Arguments\",\n          arrayOf: {\n            oneOfType: [\n              {\n                name: { title: \"Argument name\", type: \"string\" },\n                type: {\n                  title: \"Data type\",\n                  enum: [\n                    \"any\",\n                    \"string\",\n                    \"number\",\n                    \"boolean\",\n                    \"Date\",\n                    \"time\",\n                    \"timestamp\",\n                    \"string[]\",\n                    \"number[]\",\n                    \"boolean[]\",\n                    \"Date[]\",\n                    \"time[]\",\n                    \"timestamp[]\",\n                  ],\n                },\n                defaultValue: { type: \"string\", optional: true },\n                optional: {\n                  optional: true,\n                  type: \"boolean\",\n                  title: \"Optional\",\n                },\n                allowedValues: {\n                  title: \"Allowed values\",\n                  optional: true,\n                  type: \"string[]\",\n                },\n              },\n              {\n                name: { title: \"Argument name\", type: \"string\" },\n                type: { title: \"Data type\", enum: [\"Lookup\", \"Lookup[]\"] },\n                defaultValue: { type: \"any\", optional: true },\n                optional: { optional: true, type: \"boolean\" },\n                lookup: {\n                  title: \"Table column\",\n                  lookup: {\n                    type: \"schema\",\n                    // column: \"\",\n                    // table: \"\",\n                    object: \"column\",\n                  },\n                },\n              },\n              {\n                name: { title: \"Argument name\", type: \"string\" },\n                type: { title: \"Data type\", enum: [\"JsonbSchema\"] },\n                defaultValue: { type: \"any\", optional: true },\n                optional: { optional: true, type: \"boolean\" },\n                schema: {\n                  title: \"Jsonb schema\",\n                  // record: {\n                  //   values: { type: primitiveJsonbType },\n                  // },\n                  oneOfType: [\n                    primitiveJsonbType,\n                    {\n                      ...primitiveJsonbType,\n                      type: { enum: [\"object\", \"object[]\"] },\n                      properties: {\n                        record: {\n                          values: {\n                            type: primitiveJsonbType,\n                          },\n                        },\n                      },\n                    },\n                  ],\n                },\n              },\n            ],\n          },\n        },\n      },\n      run: \"TEXT NOT NULL DEFAULT 'export const run: ProstglesMethod = async (args, { db, dbo, user, callMCPServerTool }) => {\\n  \\n}'\",\n      tsconfig: \"JSONB\",\n      package: \"JSONB\",\n      outputTable: `TEXT`,\n    },\n    indexes: {\n      unique_name: { unique: true, columns: \"connection_id, name\" },\n      unique_func_per_connection: {\n        unique: true,\n        columns: \"id, connection_id\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigUsers.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\nimport {\n  OAuthProviderOptions,\n  PASSWORDLESS_ADMIN_USERNAME,\n} from \"@common/OAuthUtils\";\n\nexport const tableConfigUsers = {\n  users: {\n    columns: {\n      id: { sqlDefinition: `UUID PRIMARY KEY DEFAULT gen_random_uuid()` },\n      status: {\n        sqlDefinition: `TEXT NOT NULL DEFAULT 'active' REFERENCES user_statuses (id)`,\n        info: { hint: \"Only active users can access the system\" },\n      },\n      username: {\n        sqlDefinition: `TEXT NOT NULL UNIQUE CHECK(length(username) > 0)`,\n      },\n      name: {\n        sqlDefinition: `TEXT`,\n        info: { hint: \"Display name, if empty username will be shown\" },\n      },\n      email: { sqlDefinition: `TEXT` },\n      registration: {\n        nullable: true,\n        jsonbSchema: {\n          oneOfType: [\n            {\n              type: { enum: [\"password-w-email-confirmation\"] },\n              email_confirmation: {\n                oneOfType: [\n                  {\n                    status: { enum: [\"confirmed\"] },\n                    date: \"Date\",\n                  },\n                  {\n                    status: { enum: [\"pending\"] },\n                    confirmation_code: { type: \"string\" },\n                    date: \"Date\",\n                  },\n                ],\n              },\n            },\n            {\n              type: { enum: [\"magic-link\"] },\n              otp_code: { type: \"string\" },\n              date: \"Date\",\n              used_on: { type: \"Date\", optional: true },\n            },\n            {\n              type: { enum: [\"OAuth\"] },\n              provider: {\n                enum: Object.keys(OAuthProviderOptions),\n                description: \"OAuth provider name. E.g.: google, github\",\n              },\n              user_id: \"string\",\n              profile: \"any\",\n            },\n          ],\n        },\n      },\n      auth_provider: {\n        sqlDefinition: `TEXT`,\n        info: { hint: \"OAuth provider name. E.g.: google, github\" },\n      },\n      auth_provider_user_id: {\n        sqlDefinition: `TEXT`,\n        info: { hint: \"User id\" },\n      },\n      auth_provider_profile: {\n        sqlDefinition: `JSONB`,\n        info: { hint: \"OAuth provider profile data\" },\n      }, //  CHECK(auth_provider IS NOT NULL AND auth_provider_profile IS NOT NULL)\n      password: {\n        sqlDefinition: `TEXT NOT NULL`, // DEFAULT gen_random_uuid()`,\n        info: { hint: \"Hashed with the user id on insert/update\" },\n      },\n      type: {\n        sqlDefinition: `TEXT NOT NULL DEFAULT 'default' REFERENCES user_types (id)`,\n      },\n      passwordless_admin: {\n        sqlDefinition: `BOOLEAN`,\n        info: {\n          hint: \"If true and status is active: enables passwordless access for default install. First connected client will have perpetual admin access and no other users are allowed \",\n        },\n      },\n      created: { sqlDefinition: `TIMESTAMPTZ DEFAULT NOW()` },\n      last_updated: {\n        sqlDefinition: `BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) * 1000`,\n      },\n      options: {\n        nullable: true,\n        jsonbSchemaType: {\n          showStateDB: {\n            type: \"boolean\",\n            optional: true,\n            description: \"Show the prostgles database in the connections list\",\n          },\n          hideNonSSLWarning: {\n            type: \"boolean\",\n            optional: true,\n            description:\n              \"Hides the top warning when accessing the website over an insecure connection (non-HTTPS)\",\n          },\n          viewedSQLTips: {\n            type: \"boolean\",\n            optional: true,\n            description: \"Will hide SQL tips if true\",\n          },\n          viewedAccessInfo: {\n            type: \"boolean\",\n            optional: true,\n            description: \"Will hide passwordless user tips if true\",\n          },\n          theme: { enum: [\"dark\", \"light\", \"from-system\"], optional: true },\n          speech_mode: {\n            optional: true,\n            enum: [\"off\", \"stt-local\", \"stt-web\", \"audio\"],\n          },\n          speech_send_mode: {\n            optional: true,\n            enum: [\"manual\", \"auto\"],\n          },\n        },\n      },\n      \"2fa\": {\n        nullable: true,\n        jsonbSchemaType: {\n          secret: { type: \"string\" },\n          recoveryCode: { type: \"string\" },\n          enabled: { type: \"boolean\" },\n        },\n      },\n      has_2fa_enabled: `BOOLEAN GENERATED ALWAYS AS ( (\"2fa\"->>'enabled')::BOOLEAN ) STORED`,\n    },\n    constraints: {\n      [`passwordless_admin type AND username CHECK`]: `CHECK(COALESCE(passwordless_admin, false) = FALSE OR type = 'admin' AND username = '${PASSWORDLESS_ADMIN_USERNAME}')`,\n    },\n    indexes: {\n      \"Only one passwordless_admin admin account allowed\": {\n        unique: true,\n        columns: `passwordless_admin`,\n        where: `passwordless_admin = true`,\n      },\n    },\n    triggers: {\n      atLeastOneActiveAdmin: {\n        actions: [\"delete\", \"update\"],\n        type: \"after\",\n        forEach: \"statement\",\n        query: `\n          BEGIN\n            IF NOT EXISTS(SELECT * FROM users WHERE type = 'admin' AND status = 'active') THEN\n              RAISE EXCEPTION 'Must have at least one active admin user';\n            END IF;\n\n            RETURN NULL;\n          END;\n        `,\n      },\n    },\n  },\n} as const satisfies TableConfig<{ en: 1 }>;\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigWindows.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\n\nexport const tableConfigWindows = {\n  windows: {\n    columns: {\n      id: `UUID PRIMARY KEY DEFAULT gen_random_uuid()`,\n      parent_window_id: {\n        sqlDefinition: `UUID REFERENCES windows(id) ON DELETE CASCADE`,\n        info: {\n          hint: \"If defined then this is a chart for another window and will be rendered within that parent window\",\n        },\n      },\n      user_id: `UUID NOT NULL REFERENCES users(id)  ON DELETE CASCADE`,\n      /*   ON DELETE SET NULL is used to ensure we don't delete saved SQL queries */\n      workspace_id: `UUID REFERENCES workspaces(id) ON DELETE SET NULL`,\n      // type: `TEXT NOT NULL CHECK(type IN ('map', 'sql', 'table', 'timechart', 'card', 'method'))`,\n      type: {\n        nullable: true,\n        enum: [\n          \"map\",\n          \"sql\",\n          \"table\",\n          \"timechart\",\n          \"card\",\n          \"method\",\n          \"barchart\",\n        ],\n      },\n      table_name: `TEXT`,\n      method_name: `TEXT`,\n      table_oid: `INTEGER`,\n      sql: `TEXT NOT NULL DEFAULT ''`,\n      selected_sql: `TEXT NOT NULL DEFAULT ''`,\n      name: `TEXT`,\n      title: {\n        sqlDefinition: `TEXT`,\n        info: {\n          hint: \"Override name. Accepts ${rowCount} variable\",\n        },\n      }, // Hacky way to set a fixed title\n      limit: `INTEGER DEFAULT 1000 CHECK(\"limit\" > -1 AND \"limit\" < 100000)`,\n      closed: `BOOLEAN DEFAULT FALSE`,\n      deleted: `BOOLEAN DEFAULT FALSE CHECK(NOT (type = 'sql' AND deleted = TRUE AND (options->>'sqlWasSaved')::boolean = true))`,\n      show_menu: `BOOLEAN DEFAULT FALSE`,\n      minimised: {\n        info: { hint: \"Used for attached charts to hide them\" },\n        sqlDefinition: `BOOLEAN DEFAULT FALSE`,\n      },\n      fullscreen: `BOOLEAN DEFAULT TRUE`,\n      sort: \"JSONB DEFAULT '[]'::jsonb\",\n      filter: `JSONB NOT NULL DEFAULT '[]'::jsonb`,\n      having: `JSONB NOT NULL DEFAULT '[]'::jsonb`,\n      options: `JSONB NOT NULL DEFAULT '{}'::jsonb`,\n      sql_options: {\n        defaultValue: {\n          executeOptions: \"block\",\n          errorMessageDisplay: \"both\",\n          tabSize: 2,\n        },\n        jsonbSchemaType: {\n          executeOptions: {\n            optional: true,\n            description:\n              \"Behaviour of execute (ALT + E). Defaults to 'block' \\nfull = run entire sql   \\nblock = run code block where the cursor is\",\n            enum: [\"full\", \"block\", \"smallest-block\"],\n          },\n          errorMessageDisplay: {\n            optional: true,\n            description:\n              \"Error display locations. Defaults to 'both' \\ntooltip = show within tooltip only   \\nbottom = show in bottom control bar only   \\nboth = show in both locations\",\n            enum: [\"tooltip\", \"bottom\", \"both\"],\n          },\n          tabSize: {\n            type: \"integer\",\n            optional: true,\n          },\n          lineNumbers: {\n            optional: true,\n            enum: [\"on\", \"off\"],\n          },\n          renderMode: {\n            optional: true,\n            description: \"Show query results in a table or a JSON\",\n            enum: [\"table\", \"csv\", \"JSON\"],\n          },\n          minimap: {\n            optional: true,\n            description: \"Shows a vertical code minimap to the right\",\n            type: { enabled: { type: \"boolean\" } },\n          },\n          acceptSuggestionOnEnter: {\n            description: \"Insert suggestions on Enter. Tab is the default key\",\n            optional: true,\n            enum: [\"on\", \"smart\", \"off\"],\n          },\n          expandSuggestionDocs: {\n            optional: true,\n            description:\n              \"Toggle suggestions documentation tab. Requires page refresh. Enabled by default\",\n            type: \"boolean\",\n          },\n          maxCharsPerCell: {\n            type: \"integer\",\n            optional: true,\n            description:\n              \"Defaults to 1000. Maximum number of characters to display for each cell. Useful in improving performance\",\n          },\n          theme: {\n            optional: true,\n            enum: [\"vs\", \"vs-dark\", \"hc-black\", \"hc-light\"],\n          },\n          showRunningQueryStats: {\n            optional: true,\n            description:\n              \"(Experimental) Display running query stats (CPU and Memory usage) in the bottom bar\",\n            type: \"boolean\",\n          },\n        },\n      },\n      columns: `JSONB`,\n      nested_tables: `JSONB`,\n      created: `TIMESTAMPTZ NOT NULL DEFAULT NOW()`,\n      last_updated: `BIGINT NOT NULL`,\n    },\n  },\n} satisfies TableConfig<{ en: 1 }>;\n"
  },
  {
    "path": "server/src/tableConfig/tableConfigWorkspaces.ts",
    "content": "import type { TableConfig } from \"prostgles-server/dist/TableConfig/TableConfig\";\n\nexport const tableConfigWorkspaces: TableConfig<{ en: 1 }> = {\n  workspace_layout_modes: {\n    isLookupTable: {\n      values: {\n        fixed: {\n          en: \"Fixed\",\n          description: \"The workspace layout is fixed. Only admins can edit\",\n        },\n        editable: {\n          en: \"Editable\",\n          description:\n            \"The workspace will be cloned for each user to allow editing\",\n        },\n      },\n    },\n  },\n\n  workspaces: {\n    columns: {\n      id: `UUID PRIMARY KEY DEFAULT gen_random_uuid()`,\n      parent_workspace_id: `UUID REFERENCES workspaces(id) ON DELETE SET NULL`,\n      user_id: `UUID NOT NULL REFERENCES users(id)  ON DELETE CASCADE`,\n      connection_id: `UUID NOT NULL REFERENCES connections(id)  ON DELETE CASCADE`,\n      name: `TEXT NOT NULL DEFAULT 'default workspace' CHECK(length(BTRIM(name)) > 0)`,\n      created: `TIMESTAMPTZ DEFAULT NOW()`,\n      active_row: `JSONB DEFAULT '{}'::jsonb`,\n      layout: `JSONB`,\n      icon: `TEXT`,\n      options: {\n        defaultValue: {\n          defaultLayoutType: \"tab\",\n          tableListEndInfo: \"size\",\n          tableListSortBy: \"extraInfo\",\n          hideCounts: false,\n          pinnedMenu: true,\n        },\n        jsonbSchemaType: {\n          hideCounts: {\n            optional: true,\n            type: \"boolean\",\n          },\n          tableListEndInfo: {\n            optional: true,\n            enum: [\"none\", \"count\", \"size\"],\n          },\n          tableListSortBy: {\n            optional: true,\n            enum: [\"name\", \"extraInfo\"],\n          },\n          showAllMyQueries: {\n            optional: true,\n            type: \"boolean\",\n          },\n          defaultLayoutType: {\n            optional: true,\n            enum: [\"row\", \"tab\", \"col\"],\n          },\n          pinnedMenu: {\n            optional: true,\n            type: \"boolean\",\n          },\n          pinnedMenuWidth: {\n            optional: true,\n            type: \"number\",\n          },\n        },\n      },\n      last_updated: `BIGINT NOT NULL`,\n      last_used: `TIMESTAMPTZ NOT NULL DEFAULT now()`,\n      deleted: `BOOLEAN NOT NULL DEFAULT FALSE`,\n      url_path: `TEXT`,\n      published: {\n        sqlDefinition: `BOOLEAN NOT NULL DEFAULT FALSE, CHECK(parent_workspace_id IS NULL OR published = FALSE)`,\n        info: {\n          hint: \"If true then this workspace can be shared with other users through Access Control\",\n        },\n      },\n      layout_mode: {\n        nullable: true,\n        references: { tableName: \"workspace_layout_modes\" },\n      },\n      source: {\n        nullable: true,\n        jsonbSchemaType: {\n          tool_use_id: \"string\",\n        },\n      },\n    },\n    constraints: {\n      unique_url_path: `UNIQUE(url_path)`,\n      unique_name_per_user_perCon: `UNIQUE(connection_id, user_id, name)`,\n    },\n  },\n};\n"
  },
  {
    "path": "server/src/testElectron.ts",
    "content": "import { join } from \"node:path\";\nimport { start } from \"./electronConfig\";\n\nconst safeStorage = {\n  encryptString: (v: any) => Buffer.from(v),\n  decryptString: (v: Buffer) => v.toString(),\n  isEncryptionAvailable: () => true,\n};\n\nconst actualRootDir = join(__dirname, \"/../../..\");\n\n/** Must set this manually in cookies */\nconst electronSid =\n  \"1d1b8188-b199-435a-8c93-cf307d42cfe01d1b8188-b199-435a-8c93-cf307d42cfe0\";\nvoid start({\n  safeStorage,\n  electronSid,\n  rootDir: actualRootDir,\n  port: 3004, // For testing convenience\n  onReady: (actualPort) => {\n    console.log(`http://localhost:${actualPort}?electronSid=${electronSid}`);\n  },\n});\n"
  },
  {
    "path": "server/src/upsertConnection.ts",
    "content": "import { omitKeys, pickKeys, type ProstglesError } from \"prostgles-types\";\nimport type { Connections, DBS, Users } from \".\";\nimport type { DBGeneratedSchema } from \"@common/DBGeneratedSchema\";\nimport { testDBConnection } from \"./connectionUtils/testDBConnection\";\nimport { validateConnection } from \"./connectionUtils/validateConnection\";\nimport { applySampleSchema } from \"./publishMethods/applySampleSchema\";\n\nexport const upsertConnection = async (\n  con: DBGeneratedSchema[\"connections\"][\"columns\"],\n  user_id: Users[\"id\"] | null,\n  dbs: DBS,\n  sampleSchemaName?: string,\n) => {\n  const c = validateConnection({\n    ...con,\n    name: con.name || con.db_name,\n    user_id,\n    last_updated: Date.now().toString(),\n  });\n  const { canCreateDb } = await testDBConnection(con);\n  try {\n    let connection: Connections | undefined;\n    if (con.id) {\n      if (!(await dbs.connections.findOne({ id: con.id }))) {\n        throw \"Connection not found: \" + con.id;\n      }\n      connection = await dbs.connections.update(\n        { id: con.id },\n        omitKeys(c as any, [\"id\"]),\n        { returning: \"*\", multi: false },\n      );\n    } else {\n      await dbs.database_configs.insert(\n        pickKeys({ ...c }, [\"db_host\", \"db_name\", \"db_port\"]),\n        {\n          removeDisallowedFields: true,\n          onConflict: \"DoNothing\",\n        },\n      );\n      connection = await dbs.connections.insert(\n        { ...c, info: { canCreateDb } },\n        { returning: \"*\" },\n      );\n    }\n\n    if (!connection) {\n      throw \"Could not create connection\";\n    }\n    if (sampleSchemaName) {\n      await applySampleSchema(dbs, sampleSchemaName, connection.id);\n    }\n    const database_config = await dbs.database_configs.findOne({\n      $existsJoined: { connections: { id: connection.id } },\n    });\n    if (!database_config) {\n      throw \"Could not create database_config\";\n    }\n    return { connection, database_config };\n  } catch (_e: any) {\n    const e = _e as ProstglesError | undefined;\n    console.error(e);\n    if (e && e.code === \"23502\") {\n      throw { err_msg: ` ${e.column} cannot be empty` };\n    }\n    throw e;\n  }\n};\n"
  },
  {
    "path": "server/tsconfig.json",
    "content": "{\n\t\"files\": [\"./src/index.ts\", \"./src/testElectron.ts\"],\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"dist\",\n\t\t\"target\": \"ES2022\",\n\t\t\"lib\": [ \"ES2017\", \"es2019\", \"ES2021.String\", \"ES2022\" ],\n\t\t\"esModuleInterop\" : true,\n\t\t\"allowSyntheticDefaultImports\": true,\n\t\t\"allowJs\": true,\n\t\t\"module\": \"commonjs\",\n\t\t\"sourceMap\": true,\n\t\t\"moduleResolution\": \"node\",\n\t\t\"declaration\": true,\n\t\t\"declarationMap\": true,\n\t\t\"ignoreDeprecations\": \"5.0\",\n\t\t\"strict\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"noUncheckedIndexedAccess\": true,\n\t\t\"baseUrl\": \".\",\n\t\t\"noImplicitAny\": false,\n\t\t\"paths\": {\n\t\t\t\"@common/*\": [\n\t\t\t\t\"../common/*\"\n\t\t\t],\n\t\t\t\"@src/*\": [\n\t\t\t\t\"src/*\"\n\t\t\t],\n\t\t},\n\t},\n\t\"exclude\": [\n\t\t\"dist\",\n\t\t\"DBoGenerated.ts\",\n\t\t\"*.conf\",\n\t\t\"prostgles_mcp\"\n\t],\n\t\"include\": [\n\t\t\"src\",\n\t\t\"**/*.spec.ts\"\n\t]\n}"
  },
  {
    "path": "server/tslint.json",
    "content": "{\n  \"linterOptions\": {\n    \"exclude\": [\n      \"*.json\",\n      \"**/*.json\"\n    ]\n  }\n}"
  }
]